Chapter 7 ■ Server arChiteCture
127
Incoming data: keep receiving until we see the suffix.
elif event & select.POLLIN:
more_data = sock.recv(4096)
if not more_data: # end-of-file
sock.close() # next poll() will POLLNVAL, and thus clean up
continue
data = bytes_received.pop(sock, b'') + more_data
if data.endswith(b'?'):
bytes_to_send[sock] = zen_utils.get_answer(data)
poll_object.modify(sock, select.POLLOUT)
else:
bytes_received[sock] = data
Socket ready to send: keep sending until all bytes are delivered.
elif event & select.POLLOUT:
data = bytes_to_send.pop(sock)
n = sock.send(data)
if n < len(data):
bytes_to_send[sock] = data[n:]
else:
poll_object.modify(sock, select.POLLIN)
if name == 'main':
address = zen_utils.parse_command_line('low-level async server')
listener = zen_utils.create_srv_socket(address)
serve(listener)
The essence of this event loop is that it takes charge of maintaining the state of each client conversation in its
own data structures instead of relying on the operating system to switch contexts when activity turns from one client
to another. The server is actually two loops deep: a while loop to call poll() over and over and then an inner loop to
process each event that poll() returns since it can return many events per call. You hide these two levels of iteration
inside a generator to prevent the main server loop from being buried needlessly two levels of indentation deep.
A dictionary of sockets is maintained so that when poll() tells you that file descriptor n is ready for more
activity, you can find the corresponding Python socket. You also remember the addresses of your sockets so that you
can print diagnostic messages with the correct remote address, even after the socket has closed and the operating
system will no longer remind you of the endpoint to which it was connected.
But the real core of the asynchronous server are its buffers: the bytes_received dictionary where you stuff
incoming data while waiting for a request to complete and the bytes_to_send dictionary where outgoing bytes wait
until the operating system can schedule them for transmission. Together with the event for which you tell poll() you
are waiting on each socket, these data structures form a complete state machine for handling a client conversation
one tiny step at a time.
- A client ready to connect manifests itself first as activity on the listening server socket, which
you leave permanently in the POLLIN (“poll input”) state. You respond to such activity by
running accept(), squirreling away the socket and its address in your dictionaries and
telling the poll object you are ready to receive data from the new client socket.