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.