Sling Academy
Home/Python/Python Steam: How to start a socket server with asyncio.start_server()

Python Steam: How to start a socket server with asyncio.start_server()

Last updated: February 12, 2024

Overview

Asynchronous programming in Python has seen a significant rise in popularity and usage, thanks to the simplicity and efficiency it brings to the table, especially when dealing with I/O-bound tasks. One common use case in asynchronous programming is creating a socket server that can handle multiple client connections concurrently without blocking. This tutorial aims to guide you through the process of starting an asynchronous socket server in Python using asyncio.start_server().

Understanding asyncio.start_server()

The asyncio module provides the infrastructure for writing single-threaded concurrent code using coroutines, multiplexing I/O access over sockets and other resources, running network clients and servers, and other related primitives. asyncio.start_server() is a high-level function for creating a socket server that kindly handles client connections.

Before diving into the code, make sure you have Python 3.7 or higher installed on your system, as asyncio has seen significant changes and improvements in recent versions.

Basic AsyncIO Socket Server Example

To begin, let’s create a simple server that accepts connections and prints messages received from clients. This example lays the foundation for more complex server functionalities such as broadcasting to multiple clients or handling various client requests simultaneously.

import asyncio

async def handle_client(reader, writer):
    data = await reader.read(100)
    message = data.decode('utf-8')
    print(f"Received: {message}")
    writer.close()

async def main():
    server = await asyncio.start_server(
        handle_client, '127.0.0.1', 8888)
    addr = server.sockets[0].getsockname()
    print(f'Serving on {addr}')

    async with server:
        await server.serve_forever()

asyncio.run(main())

In this basic setup, handle_client is a coroutine that reads data from the client, decodes it, and prints it out. The main coroutine creates the server and binds it to '127.0.0.1' on port 8888. The server is then set to run indefinitely.

Expanding the Server Functionality

With the foundational knowledge of setting up a basic AsyncIO socket server, let’s extend it to handle more complex scenarios like broadcasting messages to multiple clients. Below is an example that keeps track of connected clients and broadcasts any received messages to all connected clients.

import asyncio

clients = {}

async def handle_client(reader, writer):
    info = writer.get_extra_info('peername')
    welcome = f'Connected to server: {info}\n'
    clients[writer] = info

    try:
        while True:
            data = await reader.read(100)
            message = data.decode('utf-8')
            broadcast = f"[Client {info}] {message}"
            for client_writer in clients.keys():
                client_writer.write(broadcast.encode())
                await client_writer.drain()
            if message.lower() == 'quit':
                break
    except asyncio.CancelledError:
        pass
    finally:
        del clients[writer]
        writer.close()

async def main():
    server = await asyncio.start_server(
        handle_client, '127.0.0.1', 8888)
    print(f"Server started successfully")

    async with server:
        await server.serve_forever()

asyncio.run(main())

This modified server now functions as a simple chat server. The handle_client coroutine has been extended to broadcast messages received from one client to all others. Notably, the process for adding and removing clients from the clients dictionary to keep track of current connections.

Handling Errors and Server Shutdown

When building server applications, it’s crucial to handle errors and provide a clean shutdown process. Here’s how you can add error handling and a graceful shutdown mechanism to your AsyncIO socket server.

import asyncio
import signal

shutdown_event = asyncio.Event()

async def shutdown(signal, loop):
    print(f"Received exit signal {signal.name}...")
    loop.stop()
    shutdown_event.set()

async def main():
    # Server setup goes here
    loop = asyncio.get_running_loop()
    for s in (signal.SIGTERM, signal.SIGINT):
        loop.add_signal_handler(s, lambda s=s: asyncio.create_task(shutdown(s, loop)))

    # Wait for the shutdown event
    await shutdown_event.wait()

    # Close the server
    server.close()
    await server.wait_closed()

    # Perform cleanup operations if necessary

asyncio.run(main())

This approach allows for a more robust server that can handle system signals for termination (like CTRL+C) gracefully, setting the stage for any necessary cleanup before exit.

Conclusion

In this tutorial, we have walked through the basics of creating an asynchronous socket server using asyncio.start_server(). Starting from a simple echo server, we expanded its functionality to a multi-client chat server, and finally, we introduced error handling and graceful shutdown. Through these exercises, you should now have a solid understanding of Python’s asynchronous programming capabilities and how to apply them to real-world networking tasks.

Remember, the examples provided here are foundational and can be expanded further to build more complex server architectures depending on your project’s needs. The asynchronous nature of these servers makes them highly scalable and efficient for handling numerous client connections, making asyncio an excellent choice for network programming in Python.

Next Article: Python asyncio: Adding schedule callbacks to a Future

Previous Article: Python Stream: asyncio.open_connection() function explained (with examples)

Series: Python Asynchronous Programming Tutorials

Python

You May Also Like

  • Python Warning: Secure coding is not enabled for restorable state
  • Python TypeError: write() argument must be str, not bytes
  • 4 ways to install Python modules on Windows without admin rights
  • Python TypeError: object of type ‘NoneType’ has no len()
  • Python: How to access command-line arguments (3 approaches)
  • Understanding ‘Never’ type in Python 3.11+ (5 examples)
  • Python: 3 Ways to Retrieve City/Country from IP Address
  • Using Type Aliases in Python: A Practical Guide (with Examples)
  • Python: Defining distinct types using NewType class
  • Using Optional Type in Python (explained with examples)
  • Python: How to Override Methods in Classes
  • Python: Define Generic Types for Lists of Nested Dictionaries
  • Python: Defining type for a list that can contain both numbers and strings
  • Using TypeGuard in Python (Python 3.10+)
  • Python: Using ‘NoReturn’ type with functions
  • Type Casting in Python: The Ultimate Guide (with Examples)
  • Python: Using type hints with class methods and properties
  • Python: Typing a function with default parameters
  • Python: Typing a function that can return multiple types