../_images/thumbnail-server.png

9.3. Asynchronous Network Server Programming

Note

The material in this section realates to chapter 22 from The Text Book.

As they say on Monty Python’s Flying Circus show:

And now, for something completely different ...

Asynchronous servers use a totally different approach than multi-threaded servers. Special operating system system calls are used to allow sockets to send and receive data without blocking.

Note

socket.send() and socket.recv() are normally blocking system calls. That is to say, they don’t return until some potentially slow network activity completes. The delay with socket.recv() can be especially long as it waits for the client to send a message.

  • We define asynchronous server technology as an approach that allows multiple sockets to be simultaneously managed within one thread. At the cornerstone of the asynchronous approach are system calls such as select.select() and select.poll that allows multiple sockets to be monitored at the same time.
  • Setting a socket to be in non-blocking mode means that when we attempt to perform a socket.recv() operation, it will return immediately with or without data. Several non-blocking sockets can simultaneously monitored for activity by the (select.poll.poll() or select.select()) functions. When these functions detect data available on the sockets, they return and the appropriate socket can be read knowing that it will return with data immediately.

Note

The select.poll.poll() and select.select() functions are blocking functions. However, this is acceptable because they can monitor all of the sockets being used together. When the server is not processing a client request, we want the server to be blocked so that it does not unnecessarily use the computer’s CPU resources.

socket.setblocking(0)

Put a socket into non-blocking mode. socket.setblocking(1) puts the socket back into blocking mode, which is the initial mode for new sockets.

  • Since we can monitor all the client sockets at once, we do not need to create multiple processes or threads and hence we do not need the synchronization tools and all the tricky details associated with using them correctly.
  • Asynchronous servers may NOT have any slow or blocking operations – quick in and out type applications only – No databases!
  • The asynchronous server must track the state of each client, which can make the problem hard if the data or finite state machine model for each client is complex. Note: Asynchronous programming frameworks mitigate some of the complexity associated with keeping track of each client. See Twisted – Easy Asynchronous Communication.
  • According to The Text Book, and any Unix documentation you may find, select.poll is preferred over select.select() because it is more robust.
  • select.poll is not available on Windows, only select.select()
  • See Asynchronous Servers for a discussion on the use of asynchronous network programming in developing a chat server.

9.3.1. select.select() and select.poll

  • Usage of select.select() and select.poll is almost the same

  • They both are given a list of sockets to watch for activity (Unix allows file descriptors also):

    • Sockets having received data
    • Sockets ready to send data
    • Sockets in an error state
  • A Detailed select.poll example may be found in The Text Book. However, since most students enrolled in this class use Windows, rather than Unix, I’ll leave the discussion of select.poll to the book and I’ll focus on select.select().

select.select(rlist, wlist, xlist[, timeout])

Wait until one or more socket (or file descriptor, in Unix) is ready for some kind of I/O. The first three arguments passed to the constructor are lists of sockets to be waited for.

Parameters:
  • rlist – list of sockets to watch for receiving data
  • wlist – list of sockets to watch for sending data
  • xlist – list of sockets to watch for an exceptional condition
Return type:

tuple of three lists, which are a subset of the sockets in rlist, wlist and xlist.

class select.poll

Returns a polling object, which supports registering and unregistering file descriptors, and then polling them for I/O events

register(fd)

Register a file descriptor with the polling object. Future calls to the poll.poll() method will then check whether the file descriptor has any pending I/O events.

poll([timeout])

Polls the set of registered file descriptors, and returns a possibly-empty list containing (fd, event) 2-tuples for the descriptors that have events or errors to report.

Note that, as a practical matter, the reading list of sockets is often the only one used. If we want to listen to a socket for incoming connections and a set of sockets for data, we might use the following minimal model as a starting point for developing an asynchronous server. The handle_request() in this example is a function (not defined here), which would determine what the client needs; take the appropriate actions; and, most likely, send a message back to the client.

import select
import socket

port = 5000
server =  socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('',port))
server.listen(1)
server.setblocking(0)

rlist = [server]
clients = {}

while True:
    try:
        in_list, out, excpt = select.select(rlist, [], [])
    except select.error, e:
        break
    except socket.error, e:
        break

    for sock in in_list:
        if sock == server:
            client_sock, address = self.server.accept()
            client_sock.setblocking(0)
            rlist.append(client_sock)
            clients[client_sock] = address
        else:
            message = sock.recv()
            if len(message):
                handle_request(message, sock, clients[sock])
            else:
                del clients[sock]
                rlist.remove(sock)
                sock.close()
#--
server.close()
  • Here is a detailed select.select() example: chatSelectServer.py

  • When select.select() returns with a socket in the receive list, then a socket.recv() call will return data immediately.

  • The ready-to-send socket list is often omitted or ignored.  A socket in this list just means that the socket is in a state where it can send data immediately.

  • The select.poll example in the book (echoserve.py) uses socket bit-masks and the select.poll.register() function to force a socket into the ready-to-send list as soon as it’s available. That functionality is not available with select.select().

9.3.2. Twisted – Easy Asynchronous Communication

  • The twisted framework implements the more challenging part of asynchronous communication – you just handle the processing of received data, sending data, and the application specific processing.
  • Based on the call-back model. This means that you define the code to execute for certain events and then let twisted go into it’s own processing loop. Your code will be called when those events occur.
  • Generally, you need to write two classes, which are sub-classes of twisted.protocol and twisted.factory.
factory
  • Use your factory class to hold any client specific data or any other application specific data or methods. You can add, and name, any methods to this class which will be needed for the application.
  • Only one object of your factory class will be instantiated.
  • Only one identifier in your factory class is pre-determined by twisted. Your factory class must have a static class variable called protocol, which is to be assigned to the name of your protocol class.
protocol
  • Your protocol class is to be a sub-class of one of the predefined protocols in twisted.
  • The method names which are to be in this class are defined by twisted. These methods are the entry points from twisted into your code. Any code from your factory class, or any other code you write, should be invoked from these methods.
  • A new object instance of your protocol class will be instantiated for each client that connects to the server.
  • The instances of the protocol class have a class variable (self.transport), which is a socket like object with a write() function for sending data to the client.
  • The instances of the protocol class have a class variable (self.factory), which is your factory class.
  • A third class from twisted called reactor, is imported and used to get everything started.

Here is an example Twisted program taken from The Text Book. This example fairly well illustrates the above points:

#!/usr/bin/env python
# Asynchronous Chat Server with Twisted - Chapter 22
# twistedchatserver.py
# Twisted 1.1.1 or above required for this example
#    -- download from www.twistedmatrix.com

from twisted.internet.protocol import Factory
from twisted.protocols.basic import LineOnlyReceiver
from twisted.internet import reactor

class Chat(LineOnlyReceiver):
    def lineReceived(self, data):
        self.factory.sendAll("%s: %s" % (self.getId(), data))

    def getId(self):
        return str(self.transport.getPeer())

    def connectionMade(self):
        print "New connection from", self.getId()
        self.transport.write("Welcome to the chat server, %s\n" %\
                                self.getId())
        self.factory.addClient(self)

    def connectionLost(self, reason):
        self.factory.delClient(self)

class ChatFactory(Factory):
    protocol = Chat

    def __init__(self):
        self.clients = []

    def addClient(self, newclient):
        self.clients.append(newclient)

    def delClient(self, client):
        self.clients.remove(client)

    def sendAll(self, message):
        for proto in self.clients:
            proto.transport.write(message + "\n")

reactor.listenTCP(51423, ChatFactory())
reactor.run()