An asyncio-based chat server

The asyncio Standard Library module is new in Python 3.4 and it is an effort at bringing some standardization around asynchronous I/O into the Standard Library. The asyncio library uses a co-routine based style of programming. It provides a powerful loop class, which our programs can submit prepared tasks, called co-routines, to, for asynchronous execution. The event loop handles the scheduling of the tasks and optimization of performance around blocking I/O calls.

It has built-in support for socket-based networking, which makes building a basic server a straightforward task. Let's see how this can be done. Create a new file called 5.1-chat_server-asyncio.py and save the following code in it:

import asyncio
import tincanchat

HOST = tincanchat.HOST
PORT = tincanchat.PORT
clients = []

class ChatServerProtocol(asyncio.Protocol):
  """ Each instance of class represents a client and the socket 
       connection to it. """

    def connection_made(self, transport):
        """ Called on instantiation, when new client connects """
           self.transport = transport
        self.addr = transport.get_extra_info('peername')
        self._rest = b''
        clients.append(self)
        print('Connection from {}'.format(self.addr))

    def data_received(self, data):
        """ Handle data as it's received. Broadcast complete
        messages to all other clients """
        data = self._rest + data
        (msgs, rest) = tincanchat.parse_recvd_data(data)
        self._rest = rest
        for msg in msgs:
            msg = msg.decode('utf-8')
            msg = '{}: {}'.format(self.addr, msg)
            print(msg)
            msg = tincanchat.prep_msg(msg)
            for client in clients:
                client.transport.write(msg)  # <-- non-blocking

    def connection_lost(self, ex):
        """ Called on client disconnect. Clean up client state """
        print('Client {} disconnected'.format(self.addr))
        clients.remove(self)

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    # Create server and initialize on the event loop
    coroutine = loop.create_server(ChatServerProtocol,
                                  host=HOST,
                                  port=PORT)
    server = loop.run_until_complete(coroutine)
    # print listening socket info
    for socket in server.sockets:
        addr = socket.getsockname()
        print('Listening on {}'.format(addr))
    # Run the loop to process client connections
    loop.run_forever()

Again, we can test this with our multithreaded client to make sure that it works as we expect it to.

Let's step through the code, as it's quite different from our previous servers. We begin by defining our server behavior in a subclass of the asyncio.Protocol abstract class. We're required to override the three methods connection_made(), data_received(), and connection_lost(). By using this class we can instantiate a new server scheduled on the event loop, which will listen on a socket and behave according to the contents of these three methods. We perform this instantiation in the main section further down with the loop.create_server() call.

The connection_made() method is called when a new client connects to our socket, which is equivalent to socket.accept() receiving a connection. The transport argument that it receives is a writable stream object, that is, it is an asyncio.WriteTransport instance. We will use this to write data to the socket, so we hang on to it by assigning it to the self.transport attribute. We also grab the client's host and port by using transport.get_extra_info('peername'). This is the transport's equivalent of socket.getpeername(). We then set up a rest attribute to hold the leftover data from tincanchat.parse_recvd_data() calls, and then we add our instance to the global clients list so that the other clients can broadcast to it.

The data_received() method is where the action happens. This function is called every time the Protocol instance's socket receives any data. This is equivalent to poll.poll() returning a POLLIN event, and then us performing a recv() on the socket. When called, this method is passed the data that is received from the socket as the data argument, which we then parse using tincanchat.parse_recvd_data(), as we have done before.

We then iterate over any received messages, and for each one, send it to every client in the clients list by calling the write() method on the clients' transport objects. The important thing to note here is that the Transport.write() call is non-blocking and so returns immediately. The send just gets submitted to the event loop, to be scheduled for completion soon. Hence the broadcast itself completes quickly.

The connection_lost() method is called when the client disconnects or the connection is lost, which is equivalent to a socket.recv() returning an empty result, or a ConnectionError. Here, we just remove the client from the clients global list.

In the main module code we acquire an event loop, and then create an instance of our Protocol server. The call to loop.run_until_complete() runs the initialization phase of our server on the event loop, setting up the listening socket. Then we call loop.run_forever(), which starts our server listening for incoming connections.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.69.163