An Echo Client/Server Over TCP

To see how to create a server and a client with the minimum requirements, let’s create a server that will return everything sent by a connected client.

Note

This page uses two different API variants:

  • Synchronous API with classic def functions, usable in any context.

  • Asynchronous API with async def functions, using an asynchronous framework to perform I/O operations.

All asynchronous API examples assume that you are using asyncio, but you can use a different library thanks to the asynchronous backend engine API.


The Communication Protocol

Before doing all this networking stuff, you need to know what you want to transmit and in what format. It is your communication protocol.

Choose The Serializer

There is a bunch of serializers available in easynetwork.serializers for everyone to enjoy:

For the tutorial, JSONSerializer will be used.

Build Your Protocol Object

For communication via TCP, a StreamProtocol protocol object must be created.

json_protocol.py
 1from __future__ import annotations
 2
 3from typing import Any, TypeAlias
 4
 5from easynetwork.protocol import StreamProtocol
 6from easynetwork.serializers import JSONSerializer
 7
 8# Use of type aliases in order not to see two Any types without real meaning
 9# In our case, any serializable object will be sent/received
10SentDataType: TypeAlias = Any
11ReceivedDataType: TypeAlias = Any
12
13
14class JSONProtocol(StreamProtocol[SentDataType, ReceivedDataType]):
15    def __init__(self) -> None:
16        super().__init__(JSONSerializer())

Note

Of course, you are under no obligation to write a subclass. But see this note for details.

The Server

Now that we have established the communication protocol, we can create our server.

Create Your Request Handler

First, you must create a request handler class by subclassing the AsyncStreamRequestHandler class and overriding its handle() method; this method will process incoming requests.

echo_request_handler.py
 1from __future__ import annotations
 2
 3from collections.abc import AsyncGenerator
 4from typing import Any, TypeAlias
 5
 6from easynetwork.api_async.server import AsyncStreamClient, AsyncStreamRequestHandler, INETClientAttribute
 7from easynetwork.exceptions import StreamProtocolParseError
 8
 9# These TypeAliases are there to help you understand
10# where requests and responses are used
11RequestType: TypeAlias = Any
12ResponseType: TypeAlias = Any
13
14
15class EchoRequestHandler(AsyncStreamRequestHandler[RequestType, ResponseType]):
16    async def handle(
17        self,
18        client: AsyncStreamClient[ResponseType],
19    ) -> AsyncGenerator[None, RequestType]:
20        try:
21            request: RequestType = yield  # A JSON request has been sent by this client
22        except StreamProtocolParseError:
23            # Invalid JSON data sent
24            # This is an example of how you can answer to an invalid request
25            await client.send_packet({"error": "Invalid JSON", "code": "parse_error"})
26            return
27
28        client_address = client.extra(INETClientAttribute.remote_address)
29        print(f"{client_address.host} sent {request}")
30
31        # As a good echo handler, the request is sent back to the client
32        response: ResponseType = request
33        await client.send_packet(response)

Note

Pay attention to handle(), it is an asynchronous generator function. All requests sent by the client are literally injected into the generator via the yield statement.

16async def handle(
17    self,
18    client: AsyncStreamClient[ResponseType],
19) -> AsyncGenerator[None, RequestType]:
20    try:
21        request: RequestType = yield  # A JSON request has been sent by this client
22    except StreamProtocolParseError:
23        # Invalid JSON data sent
24        # This is an example of how you can answer to an invalid request
25        await client.send_packet({"error": "Invalid JSON", "code": "parse_error"})
26        return

You can yield several times if you want to wait for a new packet from the client in the same context.

Warning

Leaving the generator will not close the connection, a new generator will be created afterwards. You may, however, explicitly close the connection if you want to:

await client.aclose()

Start The Server

Second, you must instantiate the TCP server class, passing it the server’s address, the protocol object instance, and the request handler instance.

server.py
 1from __future__ import annotations
 2
 3from easynetwork.api_sync.server import StandaloneTCPNetworkServer
 4
 5from echo_request_handler import EchoRequestHandler
 6from json_protocol import JSONProtocol
 7
 8
 9def main() -> None:
10    host = None
11    port = 9000
12    protocol = JSONProtocol()
13    handler = EchoRequestHandler()
14
15    with StandaloneTCPNetworkServer(host, port, protocol, handler) as server:
16        try:
17            server.serve_forever()
18        except KeyboardInterrupt:
19            pass
20
21
22if __name__ == "__main__":
23    main()

Note

Setting host to None will bind the server to all interfaces. This means the server is ready to accept connections with IPv4 and IPv6 addresses (if available).

The Client

This is the client side:

client.py
 1from __future__ import annotations
 2
 3import sys
 4
 5from easynetwork.api_sync.client import TCPNetworkClient
 6
 7from json_protocol import JSONProtocol
 8
 9
10def main() -> None:
11    host = "localhost"
12    port = 9000
13
14    # Connect to server
15    with TCPNetworkClient((host, port), JSONProtocol()) as client:
16        # Send data
17        request = {"command-line arguments": sys.argv[1:]}
18        client.send_packet(request)
19
20        # Receive data from the server and shut down
21        response = client.recv_packet()
22
23    print(f"Sent:     {request}")
24    print(f"Received: {response}")
25
26
27if __name__ == "__main__":
28    main()

Outputs

The output of the example should look something like this:

Server:

(.venv) $ python server.py
127.0.0.1 sent {'command-line arguments': ['Hello', 'world!']}
127.0.0.1 sent {'command-line arguments': ['Python', 'is', 'nice']}

Client:

(.venv) $ python client.py Hello world!
Sent:     {'command-line arguments': ['Hello', 'world!']}
Received: {'command-line arguments': ['Hello', 'world!']}
(.venv) $ python client.py Python is nice
Sent:     {'command-line arguments': ['Python', 'is', 'nice']}
Received: {'command-line arguments': ['Python', 'is', 'nice']}