Asyncio Server with Timer in python

Question:

I’m using asyncio to run a straightforward server and a client. The server is a simple echo server with two "special" commands sent by the client, "quit" and "timer". The quit command closes the connection, and the timer command starts a timer that will print a message in the console (server and client) every second. The timer will stop when the client sends the "quit" command.

Unfortunately, I’m having some problems with the timer. It blocks the server and the client.

How can I solve this problem?

Server

import asyncio
import time

HOST = '127.0.0.1'
PORT = 9999

async def timer():
    while True:
        print('tick')
        await asyncio.sleep(1)

async def handle_echo(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
    '''Handle the echo protocol.'''
    data = None

    while True:
        if(data == b'quit'):
            writer.close()  # Close the connection
            await writer.wait_closed()  # Wait for the connection to close

        if(data == b'timer'):
            timertask = asyncio.create_task(timer())
            await timertask #<-- This line freezes the server and the client


        else:
            data = await reader.read(1024) # Read 256 bytes from the reader. Size of the message
            msg = data.decode() # Decode the message
            addr, port = writer.get_extra_info('peername') # Get the address of the client
            print(f"Received {msg!r} from {addr}:{port!r}")

            send_message = 'Message received: ' + msg
            writer.write(send_message.encode()) # Echo the data back to the client
            await writer.drain() # This will wait until everything is clear to move to the next thing.



async def run_server() -> None:
    
    # Our awaitable callable.
    # This callable is ran when the server recieves some data
    # Question: Does it run when a client connects?
    server = await asyncio.start_server(handle_echo, HOST, PORT)

    async with server:
        await server.serve_forever()


if __name__ == '__main__':
    loop = asyncio.new_event_loop() # new_event_loop() is for python 3.10. For older versions, use get_event_loop()
    loop.run_until_complete(run_server())

Client

import asyncio
import time

HOST = '127.0.0.1'
PORT = 9999

async def run_client() -> None:
    # It's a coroutine. It will wait until the connection is established
    reader, writer = await asyncio.open_connection(HOST, PORT) 

    while True:

        message = input('Enter a message: ')
        writer.write(message.encode())
        await writer.drain()

        data = await reader.read(1024)
        if not data:
            raise Exception('Socket not communicating with the client')
        print(f"Received {data.decode()!r}")
        
        if(message == 'quit'):
            writer.write(b"quit")
            writer.close()
            await writer.wait_closed()
            exit(2)
            #break # Don't know if this is necessary


if __name__ == '__main__':
    loop = asyncio.new_event_loop()
    loop.run_until_complete(run_client())
Asked By: nunodsousa

||

Answers:

I slightly updated your server code: removed the await timertask line (this task never ends, can be only canceled):

Server.py

import asyncio
import time

HOST = "127.0.0.1"
PORT = 9999


async def timer():
    while True:
        print("tick")
        await asyncio.sleep(1)


async def handle_echo(
    reader: asyncio.StreamReader, writer: asyncio.StreamWriter
) -> None:

    # timer task that the client can start:
    timer_task = None

    while True:
        data = await reader.read(1024)
        msg = data.decode()

        addr, port = writer.get_extra_info("peername")
        print(f"Received {msg!r} from {addr}:{port!r}")

        send_message = "Message received: " + msg
        writer.write(send_message.encode())
        await writer.drain()

        if data == b"quit":
            # cancel the timer_task (if any)
            if timer_task:
                timer_task.cancel()
                await timer_task

            writer.close()
            await writer.wait_closed()
        elif data == b"timer" and timer_task is None:
            timer_task = asyncio.create_task(timer())


async def run_server() -> None:
    server = await asyncio.start_server(handle_echo, HOST, PORT)

    async with server:
        await server.serve_forever()


if __name__ == "__main__":
    loop = asyncio.new_event_loop()
    loop.run_until_complete(run_server())

Client.py

import asyncio
import time

HOST = "127.0.0.1"
PORT = 9999


async def run_client() -> None:
    # It's a coroutine. It will wait until the connection is established
    reader, writer = await asyncio.open_connection(HOST, PORT)

    while True:

        message = input("Enter a message: ")
        writer.write(message.encode())
        await writer.drain()

        data = await reader.read(1024)
        if not data:
            raise Exception("Socket not communicating with the client")
        print(f"Received {data.decode()!r}")

        if message == "quit":
            writer.write(b"quit")
            await writer.drain()

            writer.close()
            await writer.wait_closed()
            exit(2)


if __name__ == "__main__":
    loop = asyncio.new_event_loop()
    loop.run_until_complete(run_client())

The server waits for a client’s messages. If message timer arrives, starts a new timer task and continues to read other messages.

When quit arrives, server cancels the timer_task (if exists) and exits.

Answered By: Andrej Kesely
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.