Asyncio Socket Servers

March 19, 2024 Python Asyncio

We can create an asyncio server to accept and manage client socket connections.

An asyncio server is not created directly, instead, we can use a factory function to configure, create, and start a socket server. We can then use the server or accept client connections forever.

In this tutorial, you will discover how to create and use asyncio servers to accept client connections.

After completing this tutorial, you will know:

Let's get started.

What is an Asyncio Server

An asyncio server accepts incoming TCP client connections.

It is represented in asyncio Python programs via the asyncio.Server class.

The create_server() method returns a Server instance, which wraps the sockets (or other network objects) used to accept requests.

-- PEP 3156 – Asynchronous IO Support Rebooted: the “asyncio” Module

We use an asyncio.Server in our asyncio programs when we want to accept connections from other programs.

How to Create An Asyncio Server

We should not create an asyncio.Server instance directly.

Do not instantiate the Server class directly.

-- Event Loop, Asyncio API Documentation.

Instead, we can use helper functions to create an asyncio server and return an asyncio.Server instance.

These functions include:

These are the preferred ways to create asyncio servers via the high-level asyncio API.

Server objects are created by loop.create_server(), loop.create_unix_server(), start_server(), and start_unix_server() functions.

-- Event Loop, Asyncio API Documentation.

Two additional ways to create an asyncio server are via methods directly on the asyncio event loop, they are:

These approaches to creating a server are not recommended for asyncio applications as they are part of the low-level asyncio API and therefore are intended for library developers.

Nevertheless, these methods are helpful to know, as their documentation better describes the arguments for the asyncio.start_server() and asyncio.start_unix_server() module functions.

The most common general way to create an asyncio server is via the asyncio.start_server() module function.

The server requires that a handler coroutine is specified that will handle incoming client connections.

The handler must take two arguments, a asyncio.StreamReader for reading data from the client and an asyncio.StreamWriter for writing data to the client.

The client_connected_cb callback is called whenever a new client connection is established. It receives a (reader, writer) pair as two arguments, instances of the StreamReader and StreamWriter classes.

-- Streams, Asyncio API Documentation.

A handler coroutine may look as follows:

# handler for client connections
async def handler(reader, writer):
    pass

The asyncio.start_server() function takes the name of the handler as an argument, and is typically configured with a default address and port number.

For example:

...
# create an asyncio server
server = await asyncio.start_server(handler, '127.0.0.1', port=8888)

By default, the server will begin listening and accepting client connections immediately. This can be disabled by setting the "start_serving" argument to False.

The example below creates a server and reports its details.

# SuperFastPython.com
# example of creating a server
import asyncio

# handler for client connections
async def handler(reader, writer):
    pass

# main coroutine
async def main():
    # create an asyncio server
    server = await asyncio.start_server(handler, '127.0.0.1', port=8888)
    # report the details of the server
    print(server)

# start the asyncio event loop
asyncio.run(main())

Running the program we can see that an asyncio server was created with the specified port number.

<Server sockets=(<asyncio.TransportSocket fd=6, family=2, type=1, proto=6, laddr=('127.0.0.1', 8888)>,)>

How to Start Serving From An Asyncio Server

An asyncio.Server will begin accepting client connections by default as soon as it is created.

For example:

...
# create an asyncio server
server = await asyncio.start_server(handler, port=8888)

Alternatively, we can create an asyncio.Server that does not start serving by setting the "start_serving" argument to False, then manually start serving when we are ready.

This can be achieved via the start_serving() method.

For example:

...
# create an asyncio server
server = await asyncio.start_server(handler, '127.0.0.1', port=8888, start_serving=False)
# start serving
await server.start_serving()

We need a way to ensure that the coroutine remains running so that we can continue to accept client connections for the duration of our program.

This can be achieved via the serve_forever() coroutine.

Start accepting connections until the coroutine is cancelled. Cancellation of serve_forever task causes the server to be closed. This method can be called if the server is already accepting connections. Only one serve_forever task can exist per one Server object.

-- Streams, Asyncio API Documentation.

This will allow the server to begin accepting client connections, if not already configured to do so, and will never return, allowing the program to accept client connections for as long as the program runs.

For example:

...
# create an asyncio server
server = await asyncio.start_server(handler, '127.0.0.1', port=8888)
# accept client connections forever
await server.serve_forever()

The example below demonstrates this.

# SuperFastPython.com
# example of creating a server and serving forever
import asyncio

# handler for client connections
async def handler(reader, writer):
    pass

# main coroutine
async def main():
    # create an asyncio server
    server = await asyncio.start_server(handler, '127.0.0.1', port=8888)
    # report the details of the server
    print(server)
    # accept client connections forever (kill via control-c)
    await server.serve_forever()

# start the asyncio event loop
asyncio.run(main())

Running the program creates the server and reports its details.

The program then begins serving and waiting for client connections forever.

The program must be stopped manually, such as via the Control-C key combination.

<Server sockets=(<asyncio.TransportSocket fd=6, family=2, type=1, proto=6, laddr=('127.0.0.1', 8888)>,)>

How to Check if an Asyncio Server is Serving

We can check if an asyncio.Server is currently serving or not via the is_serving() method.

For example:

...
# check if it is serving
serving = server.is_serving()

An asyncio.Server may not be serving for a number of reasons, such as:

We can explore an example of this.

The example below creates an asyncio server that is not serving, checks its status, starts serving, then checks its status again.

# SuperFastPython.com
# example of creating a server and checking if it is serving
import asyncio

# handler for client connections
async def handler(reader, writer):
    pass

# main coroutine
async def main():
    # create an asyncio server
    server = await asyncio.start_server(handler, '127.0.0.1', port=8888, start_serving=False)
    # report the details of the server
    print(server)
    # check if it is serving
    print(f'Serving: {server.is_serving()}')
    # start serving
    await server.start_serving()
    # check if it is serving
    print(f'Serving: {server.is_serving()}')

# start the asyncio event loop
asyncio.run(main())

Running the example first creates the server and reports its details.

The status is checked and we can see that it is not serving, as we configured.

The server is then configured to start serving and the status is checked again, confirming that it is indeed now serving.

<Server sockets=(<asyncio.TransportSocket fd=6, family=2, type=1, proto=6, laddr=('127.0.0.1', 8888)>,)>
Serving: False
Serving: True

How to Access a Client Connections in An Asyncio Server

A single asyncio.Server may manage many client connections.

There could be hundreds or even thousands of clients connected to a single server.

The asyncio.Server instance provides access to the client socket connections if needed.

This is provided via the "sockets" attribute that provides a list of asyncio.trsock.TransportSocket instances.

For example:

...
# access list of all client sockets
sockets = server.sockets

This might be helpful if we need to report the number of currently connected clients.

For example:

...
# report current client connections
print(f'There are {len(server.sockets)} clients connected')

How to Close an Asyncio Server

Once we are finished with the asyncio.Server in our program, we can close it.

This can be achieved by calling the close() method manually.

For example:

...
# close the server
server.close()

This will mean that the server is no longer accepting client connections, although it may still have open client connections.

We can then call the wait_closed() to suspend the caller until the server is closed completely. This means that all client connections are now closed.

The idiom for manually closing the asyncio.Server may look as follows:

...
# request that the server close
server.close()
# wait for the server to complete closing
await server.wait_closed()

The example below creates a server and starts serving, waits a moment, then closes the server again.

# SuperFastPython.com
# example of creating a server and closing it again
import asyncio

# handler for client connections
async def handler(reader, writer):
    pass

# main coroutine
async def main():
    # create an asyncio server
    server = await asyncio.start_server(handler, '127.0.0.1', port=8888)
    # report the details of the server
    print(server)
    # wait a moment
    await asyncio.sleep(2)
    # close the server
    server.close()
    # wait for the server to close
    await server.wait_closed()
    # report the details of the server
    print(server)

# start the asyncio event loop
asyncio.run(main())

Running the program first starts a server that accepts client connections.

The program then suspends for two seconds.

Finally, the server is requested to close, then suspends and waits for all client connections to close. There are no client connections so it closes quickly.

Reporting the final status of the server, we can see that it is no longer listening for incoming connections.

<Server sockets=(<asyncio.TransportSocket fd=6, family=2, type=1, proto=6, laddr=('127.0.0.1', 8888)>,)>
<Server sockets=()>

We can also close the server automatically.

This can be achieved by the context manager interface implemented by the asyncio.Server class.

Server objects are asynchronous context managers. When used in an async with statement, it's guaranteed that the Server object is closed and not accepting new connections when the async with statement is completed

-- Event Loop, Asyncio API Documentation.

For example:

...
# use the server
async with server:
	# ...
	# close automatically

This is helpful to ensure the server is closed if some unexpected error occurs within the body of the "async with" block.

We can demonstrate this with a worked example, listed below.

# SuperFastPython.com
# example of creating a server and closing it via context manager
import asyncio

# handler for client connections
async def handler(reader, writer):
    pass

# main coroutine
async def main():
    # create an asyncio server
    server = await asyncio.start_server(handler, '127.0.0.1', port=8888)
    # report the details of the server
    print(server)
    # start using the server
    async with server:
        # wait a moment
        await asyncio.sleep(2)
        # server is closed automatically
    # report the details of the server
    print(server)
    # check if it is serving
    print(f'Serving: {server.is_serving()}')

# start the asyncio event loop
asyncio.run(main())

Running the example first creates the server that starts serving.

It is then used within the context manager, blocking for two seconds.

The context manager is exited and the server is closed and is no longer serving.

<Server sockets=(<asyncio.TransportSocket fd=6, family=2, type=1, proto=6, laddr=('127.0.0.1', 8888)>,)>
<Server sockets=()>
Serving: False

We can use the context manager interface with the server_forever() method to ensure that the server is closed correctly if there is an error serving or if the server_forver() task is canceled.

For example:

...
# use the server
async with server:
    # accept client connections forever (kill via control-c)
    await server.serve_forever()
	# close automatically

Takeaways

You now know how to create and use asyncio servers in Python.