Channels ▼
RSS

Design

The New asyncio in Python 3.4: Servers, Protocols, and Transports


In a previous article on the new asyncio module introduced in Python 3.4, I explained simple use of event loop functions that register, execute, and delay or cancel calls. In this article, I demonstrate more-advanced examples that explore asyncio's support for server and client programming, protocols, and transports.

Working with a Simple Protocol

The asyncio.BaseProtocol class is the common base class for protocol interfaces in the asyncio module. The asyncio.Protocol class inherits from asyncio.BaseProtocol and provides an interface for stream protocols. The following lines show a simple example of an implementation of the asyncio.Protocol interface that works like an echo server and also displays some information on the Python console. The SimpleEchoProtocol class inherits from asyncio.Protocol and implements three methods: connection_made, data_received, and connection_lost:

import asyncio

class SimpleEchoProtocol(asyncio.Protocol):
    def connection_made(self, transport):
        """
        Called when a connection is made.
        The argument is the transport representing the pipe connection.
        To receive data, wait for data_received() calls.
        When the connection is closed, connection_lost() is called.
        """
        print("Connection received!")
        self.transport = transport

    def data_received(self, data):
        """
        Called when some data is received.
        The argument is a bytes object.
        """
        print(data)
        self.transport.write(b'echo:')
        self.transport.write(data)

    def connection_lost(self, exc):
        """
        Called when the connection is lost or closed.
        The argument is an exception object or None (the latter
        meaning a regular EOF is received or the connection was
        aborted or closed).
        """
        print("Connection lost! Closing server...")
        server.close()

loop = asyncio.get_event_loop()
server = loop.run_until_complete(loop.create_server(SimpleEchoProtocol, 'localhost', 2222))
loop.run_until_complete(server.wait_closed())

You can test the echo server by running a telnet client and connecting to localhost on port 2222. If you are already using this port number, you can change the number to any available port number. If you leave the default values, you can run the previous lines in a Python console and execute telnet localhost 2222 in a command prompt or Terminal window. You will see a Connection received! message displayed on the Python console. Then, after you type any character, you will see echo: followed by the character in the telnet console, and the Python console will display a new message with the entered character. When you quit the telnet console, you will see a Connection lost! Closing server... message displayed on the Python console.

For example, if you type abc after you start telnet, you will see the following text on the telnet window:

  echo:abecho:bcecho:c

In addition, the Python console will display the following messages:

  Connection received!
  b'a'
  b'b'
  b'c'
  Connection lost! Closing server...

After creating an event loop named loop, the code makes a call to loop.run_until_complete to run the loop.create_server coroutine. This coroutine creates a TCP server bound to the specified host and port with the protocol factory class (in this case, localhost on port 2222 with SimpleEchoProtocol as the protocol factory class) and returns a Server object that can be used to stop the service. The code assigns this instance to the server variable. This way, when a client establishes a connection, there is going to be a new instance of SimpleEchoProtocol and the overloaded methods in this class will be executed.

When a connection is made successfully, the code within the connection_made method prints a message and assigns the transport received as an argument to the transport member variable for its later use in another method.

When some data is received, the code within the data_received method prints the data bytes received from the transport and sends back echo: and the received data by making two calls to the self.transport.write method. Of course, the same effect would have been achieved by making just one call to self.transport.write with all the data to be sent, but I wanted to have a clear separation of the line that sent echo: and the line that sends back the received data in the code.

When the connection is either closed or lost, the code in the connection_lost method prints a message and calls server.close(); hence, the loop that was running until the server was closed stops its execution.

Working with Clients and Servers

In the previous example, telnet was the client. The asyncio module provides coroutines that allow you to easily code both a server and a client with stream readers and writers. The following lines show the code for a simple echo server that starts a socket server in localhost on port 2222. You can run the code on a Python console and then execute the code for the Python client on another Python console.

import asyncio

@asyncio.coroutine
def simple_echo_server():
    # Start a socket server, call back for each client connected.
    # The client_connected_handler coroutine will be automatically converted to a Task
    yield from asyncio.start_server(client_connected_handler, 'localhost', 2222)

@asyncio.coroutine
def client_connected_handler(client_reader, client_writer):
    # Runs for each client connected
    # client_reader is a StreamReader object
    # client_writer is a StreamWriter object
    print("Connection received!")
    while True:
        data = yield from client_reader.read(8192)
        if not data:
            break
        print(data)
        client_writer.write(data)

loop = asyncio.get_event_loop()
loop.run_until_complete(simple_echo_server())
try:
    loop.run_forever()
finally:
    loop.close()

The following lines show the code for the client that opens a connection to localhost on port 2222 and writes a few lines using an asyncio.StreamWriter object, then reads the lines returned from the server with an asyncio.StreamWriter object.

import asyncio

LASTLINE = b'Last line.\n'

@asyncio.coroutine
 def simple_echo_client():
    # Open a connection and write a few lines by using the StreamWriter object
    reader, writer = yield from asyncio.open_connection('localhost', 2222)
    # reader is a StreamReader object
    # writer is a StreamWriter object
    writer.write(b'First line.\n')
    writer.write(b'Second line.\n')
    writer.write(b'Third line.\n')
    writer.write(LASTLINE)

    # Now, read a few lines by using the StreamReader object
    print("Lines received")
    while True:
        line = yield from reader.readline()
        print(line)
        if line == LASTLINE or not line:
            break
    writer.close()

loop = asyncio.get_event_loop()
loop.run_until_complete(simple_echo_client())

You can execute the client code on a different Python console. If the server is running, the console will display the following lines:

Lines received
b'First line.\n'
b'Second line.\n'
b'Third line.\n'
b'Last line.\n'

The Python console that is executing the server will display the following lines:

 Connection received!
 b'First line.\nSecond line.\nThird line.\nLast line.\n'

First, let's focus on the server code. After creating an event loop named loop, the code makes a call to loop.run_until_complete to run the simple_echo_server coroutine. This coroutine calls the asyncio.start_server coroutine that starts a socket server bound to the specified host and port, then executes the callback specified as an argument for each client connected, client_connected_handler. In this case, client_connected_handler is another couroutine and it will be automatically converted to a Task. It is also possible to specify a plain callback function instead of a coroutine.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video