Open Source

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:

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.

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:

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.

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.

Video

This month's Dr. Dobb's Journal

This month,
Dr. Dobb's Journal is devoted to mobile programming. We introduce you to Apple's new Swift programming language, discuss the perils of being the third-most-popular mobile platform, revisit SQLite on Android
, and much more!