Asyncio Server: Creating a TCP Socket

Alright ! Let’s talk about creating an asyncio server using TCP sockets in Python. This technology allows you to write concurrent code without having to sacrifice goats to the programming gods (or pull out your hair). First, let’s create a new file called “my_asyncio_server.py”. We need to import some necessary libraries:

# Import necessary libraries
import asyncio # Importing the asyncio library for creating asynchronous code
import socket # Importing the socket library for creating TCP sockets
from datetime import datetime # Importing the datetime library for working with dates and times

# Define a function to handle incoming connections
async def handle_connection(reader, writer): # Defining a function to handle incoming connections, using the "async" keyword to make it asynchronous
    data = await reader.read(100) # Using the "await" keyword to wait for data to be received from the client
    message = data.decode() # Decoding the received data into a string
    addr = writer.get_extra_info('peername') # Getting the address of the client
    print(f"Received {message!r} from {addr!r}") # Printing the received message and the client's address
    writer.write(data) # Sending the received data back to the client
    await writer.drain() # Using the "await" keyword to wait for the data to be sent
    print("Closing connection") # Printing a message to indicate that the connection is being closed
    writer.close() # Closing the connection

# Define a function to start the server
async def start_server(): # Defining a function to start the server, using the "async" keyword to make it asynchronous
    server = await asyncio.start_server(handle_connection, '127.0.0.1', 8888) # Using the "await" keyword to wait for the server to be started, specifying the IP address and port to listen on
    addr = server.sockets[0].getsockname() # Getting the address that the server is listening on
    print(f'Serving on {addr}') # Printing a message to indicate that the server is running
    async with server: # Using the "async with" statement to automatically close the server when it is no longer needed
        await server.serve_forever() # Using the "await" keyword to wait for the server to continue running

# Run the server
asyncio.run(start_server()) # Using the "asyncio.run()" function to run the server, using the "start_server()" function as the entry point

Next, we define our server function that will handle incoming connections. This is where the magic happens! Let’s call it `handle_client`.

# Define the server function that will handle incoming connections
async def handle_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
    # Get client IP address and timestamp
    ip = reader.get_extra_info('peername')[0] # Get the client's IP address
    time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S") # Get the current timestamp
    
    # Send welcome message to the client
    writer.write(f"Welcome, {ip}! It's currently {time_str}\n".encode()) # Send a welcome message to the client, including their IP and the current timestamp
    await writer.drain() # Wait for the message to be sent before continuing
    
    # Loop until the client disconnects or sends a quit command
    while True:
        data = await reader.readline() # Read the incoming data from the client
        
        if not data: # If there is no data, break out of the loop
            break
        
        # Handle incoming message from the client and send response back
        msg = data.decode().strip() # Decode the data and remove any extra whitespace
        if msg == "quit": # If the client sends a quit command, send a goodbye message and return from the function
            writer.write("Goodbye!\n".encode())
            await writer.drain()
            return
        else: # If the client sends any other message, send a response back with the message they sent
            writer.write(f"You said: {msg}\n".encode())
            await writer.drain()

This function takes two arguments `reader` and `writer`. The `StreamReader` is used to read data from the client, while the `StreamWriter` writes data back to the client. We’re also using a couple of helper libraries like datetime for getting the current time and string formatting.

Now that we have our server function defined, let’s create an event loop and start listening for incoming connections:



# Import necessary libraries
import asyncio # for creating asynchronous functions
import datetime # for getting current time
import string # for string formatting

# Define server function to handle client connections
async def handle_client(reader, writer): # takes in a reader and writer object
    data = await reader.read(100) # read data from client (max 100 bytes)
    message = data.decode() # decode data into string
    addr = writer.get_extra_info('peername') # get client's IP address
    print(f"Received {message!r} from {addr!r}") # print received message and client's IP address
    current_time = datetime.datetime.now().strftime("%H:%M:%S") # get current time in HH:MM:SS format
    response = f"Current time is {current_time}" # create response message with current time
    writer.write(response.encode()) # encode response message and send back to client
    await writer.drain() # wait for all data to be written to client
    print(f"Sent {response!r} to {addr!r}") # print sent message and client's IP address
    writer.close() # close connection with client

# Create event loop and start listening for incoming connections
async def main():
    server = await asyncio.start_server(handle_client, '127.0.0.1', 8000) # create server object with handle_client function, listening on localhost at port 8000
    async with server: # use async context manager to handle server
        print("Server running at http://127.0.0.1:8000/") # print message to indicate server is running
        try:
            await asyncio.Future() # run forever (blocking operation)
        except KeyboardInterrupt:
            pass # allow user to interrupt and stop server

# Run main function
asyncio.run(main()) # run main function in event loop

We’re using the `start_server` function to create a new server object, passing in our `handle_client` function as well as the IP address and port number we want to listen on. We then wrap it inside an async context manager (using `async with`) so that we can cleanly close the server when we’re done.

Finally, we print out a message indicating where the server is running at, and use a blocking operation (`await asyncio.Future()`) to keep our event loop running indefinitely until we press Ctrl+C or kill the process.

However, there’s an easier way to create an asyncio server using TCP sockets without having to worry about setting flags on the socket and buffering issues. Let’s take a look at it:

import asyncio
from datetime import datetime

# Create a class for our EchoServer that inherits from asyncio.DatagramProtocol
class EchoServer(asyncio.DatagramProtocol):
    # Initialize the class with an address pair
    def __init__(self, address_pair):
        self.address_pair = address_pair
        
    # Define a method for when a connection is made
    def connection_made(self, transport, addr):
        # Print a message indicating a connection has been made
        print("Connection from", addr)
        # Send a welcome message to the client using the transport and address pair
        self.transport.sendto(b"Welcome to the echo server! ", self.address_pair[0])
        
    # Define a method for when a datagram is received
    def datagram_received(self, data, addr):
        # Create a message with the received data and its length
        message = f"Received {len(data)} bytes: {data.decode()}"
        # Print the message
        print(message)
        # Send the received data back to the client using the transport and address
        self.transport.sendto(data, addr)
        
    # Define a method for when an error is received
    def error_received(self, exc):
        # Print a message indicating an error has occurred
        print("Error occurred in datagram protocol:", exc)
        # Close the transport
        self.transport.close()
        
    # Define a method for when a connection is lost
    def connection_lost(self, close_reason):
        # Print a message indicating the connection has been lost
        print("Connection lost: ", close_reason)
        
# Define an async function to run the server
async def run_server():
    # Create a server using the start_server method and passing in our EchoServer class, IP address, and port number
    server = await asyncio.start_server(EchoServer, '127.0.0.1', 8000)
    try:
        # Use a context manager to handle the server
        async with server:
            # Use a future to keep the event loop running indefinitely
            await asyncio.Future() # run forever (blocking operation)
    # Handle a keyboard interrupt
    except KeyboardInterrupt:
        pass

In this example, we’re using the `DatagramProtocol` class to handle incoming and outgoing data packets. We define a custom protocol called `EchoServer`, which has four methods `connection_made()`, `datagram_received()`, `error_received()`, and `connection_lost()`.

The `connection_made()` method is called when the server receives an incoming connection. We print out a message indicating that we’ve received a new connection, and send back a welcome message to the client using the `sendto()` function.

The `datagram_received()` method is called whenever data is received from the client. We decode the data into a string, print it out, and then send it back to the client using the `sendto()` function.

The `error_received()` method is called when an error occurs in the datagram protocol. In this case, we simply close the connection using the `close()` function.

Finally, the `connection_lost()` method is called whenever a client disconnects from the server. We print out a message indicating that the connection has been lost and why (if available).

To run our new echo server, we simply call the `run_server()` function:

# This script is used to create an echo server using asyncio library in python.

# Importing necessary libraries
import asyncio

# Defining a function to run the server
async def run_server():
    # Creating a server using asyncio's `start_server()` function
    server = await asyncio.start_server(handle_client, '127.0.0.1', 8888)
    # Printing a message to indicate that the server is running
    print('Server running on port 8888...')
    # Running the server indefinitely
    await server.serve_forever()

# Defining a function to handle client connections
async def handle_client(reader, writer):
    # Reading data from the client using `read()` function
    data = await reader.read(100)
    # Decoding the data received from the client
    message = data.decode()
    # Printing the message received from the client
    print(f'Received message: {message}')
    # Writing the same message back to the client using `write()` function
    writer.write(data)
    # Flushing the data to ensure it is sent to the client
    await writer.drain()
    # Closing the connection using `close()` function
    writer.close()

# Defining a function to run the server
async def main():
    # Calling the `run_server()` function
    await run_server()

# Checking if the script is being run directly
if __name__ == '__main__':
    # Running the `main()` function using asyncio's `run()` function
    asyncio.run(main())

# Explanation:
# - The `asyncio` library is imported to use its functions for asynchronous programming.
# - The `run_server()` function is defined to create and run the server.
# - The `start_server()` function is used to create a server, which takes in the `handle_client()` function, IP address and port number as parameters.
# - The `handle_client()` function is defined to handle client connections, which takes in the `reader` and `writer` objects as parameters.
# - The `read()` function is used to read data from the client, which takes in the number of bytes to be read as a parameter.
# - The `decode()` function is used to decode the data received from the client.
# - The `write()` function is used to write data back to the client, which takes in the data to be sent as a parameter.
# - The `drain()` function is used to ensure that the data is sent to the client.
# - The `close()` function is used to close the connection with the client.
# - The `main()` function is defined to call the `run_server()` function.
# - The `asyncio.run()` function is used to run the `main()` function.
# - The `if __name__ == '__main__'` statement is used to check if the script is being run directly.
# - The `asyncio.run()` function is used to run the `main()` function.

This way, you can easily create an echo server using TCP sockets with minimal effort and without having to worry about buffering issues or setting flags on the socket.

SICORPS