Server Modules

Python provides modules to setup a server quickly.

The SocketServer Module

With the SocketServer module we do not need to import the socket module for the server script.

Follow these steps:

  1. from socketserver import StreamRequestHandler

    from socketserver import TCPServer

  2. Inheriting from StreamRequestHandler define a request handler class. Override handle().

    The handle() processes incoming requests.

  3. Instantiate TCPServer with (address, port) and an instance of the request handler class. This returns a server object.

  4. Apply the method handle_request() or serve_forever() to the server object.

To illustrate the above process, we will setup a server to tell the time to its clients. The window running the server is shown below.

$ python clockserver.py
server is listening to 12091
connected at ('127.0.0.1', 49142)
read "What is the time?        " from client
writing "Sun Apr  4 18:16:14 2010" to client

The window running the client shows

$ python clockclient.py
client is connected
Sun Apr  4 18:16:14 2010

The code for the client in file clockclient.py is listed below.

from socket import socket as Socket
from socket import AF_INET, SOCK_STREAM

HOSTNAME = 'localhost'  # on same host
PORTNUMBER = 12091      # same port number
BUFFER = 25             # size of the buffer

SERVER_ADDRESS = (HOSTNAME, PORTNUMBER)
CLIENT = Socket(AF_INET, SOCK_STREAM)
CLIENT.connect(SERVER_ADDRESS)

print('client is connected')
QUESTION = 'What is the time?'
DATA = QUESTION + (BUFFER-len(QUESTION))*' '
CLIENT.send(DATA.encode())
DATA = CLIENT.recv(BUFFER)
print(DATA.decode())

CLIENT.close()

The code for the server in the file clockserver.py has the following skeleton.

from socketserver import StreamRequestHandler
from socketserver import TCPServer
from time import ctime

PORT = 12091

class ServerClock(StreamRequestHandler):
    """
    The server tells the clients the time.
    """
    def handle(self):
        """
        Handler sends time to client.
        """

def main():
    """
    Starts the server and serves requests.
    """

The code for the handler defines the handling of a request.

def handle(self):
    """
    Handler sends time to client.
    """
    print("connected at", self.client_address)
    message = self.rfile.read(25)
    data = message.decode()
    print('read \"' + data + '\" from client')
    now = ctime()
    print('writing \"' + now + '\" to client')
    self.wfile.write(now.encode())

The code for the main function in the server script is below.

def main():
    """
    Starts the server and serves requests.
    """
    ss = TCPServer(('', PORT), ServerClock)
    print('server is listening to', PORT)
    try:
        print('press ctrl c to stop server')
        ss.serve_forever()
    except KeyboardInterrupt:
        print(' ctrl c pressed, closing server')
        ss.socket.close()

if __name__ == "__main__":
    main()

To use the attributes rfile and wfile in the class StreamRequestHandler we have to know the following:

  • rfile contains the input stream to read data from the client, for example:

    data = self.rfile.read(25)
    

    The client must send exactly 25 characters!

  • wfile contains the output stream to write data to the client, for example:

    self.wfile.write(data)
    

    All data are strings of characters!

We end this example, listing some alternative constructions. Instead of StreamRequestHandler, we can use DatagramRequestHandler. Instead of TCPServer, we can use UDPServer, if we want UDP instead of TCP protocol. On Unix (instead of TCPServer), we can use UnixStreamServer or UnixDatagramServer.

There is the choice between

  1. handle_request() to handle one single request, or
  2. serve_forever() to handle indefinitely many requests.

With serve_forever(), we serve indefinitely many requests, simultaneously from multiple clients.

A Forking Server

Instead of handler threads, we may use processes to handle requests. Threads in Python are not mapped to cores. For a computationally intensive request, we want to spawn a new process.

>>> import os
>>> help(os.fork)
Help on built-in function fork in module posix:

fork(...)
    fork() -> pid

    Fork a child process.
    Return 0 to child process
      and PID of child to parent process.

Our first illustration will just be yet another hello world. The child process will just print hello.

import os

def child():
    """
    The code executed by the forked process.
    """
    print('hello from child', os.getpid())
    os._exit(0) # go back to parent loop

The code for the parent() function is below.

def parent():
    """
    Code executed by the forking process.
    Type q to quit this process.
    """
    while True:
        newpid = os.fork()
        if newpid == 0:
            child()
        else:
            print('hello from parent',\
                os.getpid(), newpid)
        if input() == 'q':
            break

The running of the script fork.py is illustrated below.

$ python fork.py
hello from parent 854 855
hello from child 855

In another terminal window:

$ ps -e | grep "Python"
  854 ttys000    0:00.03 /Library/Frameworks/Python.framework/Versions/2.6/Resources/Python.app/Contents/MacOS/Python fork.py
  855 ttys000    0:00.00 (Python)
  895 ttys001    0:00.00 grep Python

Then we type q in the first terminal window to quit the parent process.

Consider the following simulation:

  • Any number of clients connect from time to time and they ask for the current time. Are we there yet?!
  • For every request, the server forks a process. The child process exits when the client stops.

Two advantages of forking processes over threads:

  • We have parallelism, as long as there are enough cores.
  • Unlike threads, processes can be killed explicitly.

The script clockforkclient.py starts in the same manner as as the script clockclient.py.

print('client is connected')
data = 'What is the time?'

while True:
    message = data + (buffer-len(data))*' '
    client.send(message.encode())
    data = client.recv(buffer).decode()
    print(data)
    nbr = randint(3, 10)
    print('client sleeps for %d seconds' % nbr)
    sleep(nbr)

client.close()

The process to handle a client is defined below.

def handle_client(sck):
    """
    Handling a client via the socket sck.
    """
    print("client is blocked for ten seconds ...")
    sleep(10)
    print("handling a client ...")
    while True:
        data = sck.recv(buffer).decode()
        if not data:
            break
        print('received \"' + data + '\" from client')
        now = ctime()
        print('sending \"' + now + '\" to client')
        sck.send(now.encode())
    print('closing client socket, exiting child process')
    sck.close()
    os._exit(0)

With the os module, we can kill a process, once with have its process id.

import os

active_processes = []

def kill_processes():
    """
    kills handler processes
    """
    while len(active_processes) > 0:
        pid = active_processes.pop(0)
        print('-> killing process %d' % pid)
        os.system('kill -9 %d' % pid)

The main() in the server is the defined below.

def main():
    """
    Listen for connecting clients.
    """
    try:
        print('press ctrl c to stop server')
        while True:
            client, address = server.accept()
            print('server connected at', address)
            child_pid = os.fork()
            if child_pid == 0:
                handle_client(client)
            else:
                print('appending PID', child_pid)
                active_processes.append(child_pid)
    except:
        print('ctrl c pressed, closing server')
        print('active processes :', active_processes)
        kill_processes()
        server.close()

In the exception handler, before closing the server socket, all active child processes are killed.

The BaseHTTPServer Module

We can create a very simple HTTP server. The client is the web browser. Working offline, we point the browser to http://localhost:8000/ as shown in Fig. 95.

_images/figourwebserver.png

Fig. 95 Testing the our simple web server at localhost.

This is the default page displayed in response to a GET request.

For now, our server does not make files available. If a user requests a file, e.g.: test, then the server answers, as shown in Fig. 96.

_images/figourwebsvtest.png

Fig. 96 Testing our simple web server.

Recall the script myserver.py which allowed us to do server side Python scripting without Apache. We can also serve html pages without Apache:

$ python3 ourwebserver.py
welcome to our web server
press ctrl c to stop server
127.0.0.1 - - [04/Apr/2016 09:20:55] "GET / HTTP/1.1" 200 -
^C ctrl c pressed, shutting down
$

subsection{code for the simple HTTP web server}

begin{frame}{The BaseHTTPServer Module}{writing code for a web server}

Using the BaseHTTPServer module is similar to using SocketServer. Execute these steps:

  1. Import the following:

    from BaseHTTPServer import BaseHTTPRequestHandler

    from BaseHTTPServer import HTTPServer

  2. Inheriting from BaseHTTPRequestHandler define request handler class. Override do_GET(). The do_GET() defines how to serve GET requests.

  3. Instantiate HTTPServer with (address, port) and an instance of the request handler class. This returns a server object.

  4. Apply serve_forever() to the server object.

The first part of the script ourwebserver.py is below.

from http.server import BaseHTTPRequestHandler
from http.server import HTTPServer

dynhtml = """
<HTML>
<HEAD><TITLE>My Home Page</TITLE></HEAD>
<BODY> <CENTER>
<H1> hello client </H1>
</CENTER> </BODY>
</HTML>"""

This defines the HTML code we display. The second part of ourwebserver.py continues below.

class WebServer(BaseHTTPRequestHandler):
    """
    Illustration to set up a web server.
    """
    def do_GET(self):
        """
        Defines what server must do when
        it receives a GET request.
        """
        if self.path == '/':
            self.send_response(200)
            self.send_header('Content-type','text/html')
            self.end_headers()
            self.wfile.write(dynhtml.encode())
        else:
            message = self.path + ' not found'
            self.wfile.write(message.encode())

The main() in ourwebserver.py is below.

def main():
    """
    a simple web server
    """
    try:
        ws = HTTPServer(('', 8000), WebServer)
        print('welcome to our web server')
        print('press ctrl c to stop server')
        ws.serve_forever()
    except KeyboardInterrupt:
        print(' ctrl c pressed, shutting down')
        ws.socket.close()

Exercises

  1. Use the SocketServer module to implement a server to swap one data string between two clients. Clients A and B send a string to the server, client B receives what A sent and A receives what B sent.
  2. Implement a server which generates a secret number. Clients connect to the server sending their guess for the secret. In response, the server sends one of these three messages: (1) wrong, (2) right, or (3) secret found. If a client has sent the right answer, all future clients must get reply (3).
  3. Consider the previous exercise and set up a simple web server to guess a secret word. The word is the name typed in after localhost:8000/ in the URL.