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
deffunctions, usable in any context.Asynchronous API with
async deffunctions, using an asynchronous framework to perform I/O operations.
All asynchronous API examples assume that you are using either asyncio or trio,
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:
JSONSerializer: an incremental serializer using thejsonmodule.PickleSerializer: a one-shot serializer using thepicklemodule.StringLineSerializer: an incremental serializer for communication based on ASCII character strings (e.g. FTP).etc.
For the tutorial, JSONSerializer will be used.
Build Your Protocol Object
For communication via TCP, a StreamProtocol protocol object must be created.
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.
1from __future__ import annotations
2
3from collections.abc import AsyncGenerator
4from typing import Any, TypeAlias
5
6from easynetwork.exceptions import StreamProtocolParseError
7from easynetwork.servers.handlers import AsyncStreamClient, AsyncStreamRequestHandler, INETClientAttribute
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.
1from __future__ import annotations
2
3from easynetwork.servers 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 server.serve_forever()
17
18
19if __name__ == "__main__":
20 try:
21 main()
22 except* KeyboardInterrupt:
23 pass
1from __future__ import annotations
2
3import asyncio
4
5from easynetwork.servers import AsyncTCPNetworkServer
6
7from echo_request_handler import EchoRequestHandler
8from json_protocol import JSONProtocol
9
10
11async def main() -> None:
12 host = None
13 port = 9000
14 protocol = JSONProtocol()
15 handler = EchoRequestHandler()
16
17 async with AsyncTCPNetworkServer(host, port, protocol, handler) as server:
18 await server.serve_forever()
19
20
21if __name__ == "__main__":
22 try:
23 asyncio.run(main())
24 except* KeyboardInterrupt:
25 pass
1from __future__ import annotations
2
3import trio
4
5from easynetwork.servers import AsyncTCPNetworkServer
6
7from echo_request_handler import EchoRequestHandler
8from json_protocol import JSONProtocol
9
10
11async def main() -> None:
12 host = None
13 port = 9000
14 protocol = JSONProtocol()
15 handler = EchoRequestHandler()
16
17 async with AsyncTCPNetworkServer(host, port, protocol, handler) as server:
18 await server.serve_forever()
19
20
21if __name__ == "__main__":
22 try:
23 trio.run(main)
24 except* KeyboardInterrupt:
25 pass
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:
1from __future__ import annotations
2
3import sys
4
5from easynetwork.clients 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()
1from __future__ import annotations
2
3import asyncio
4import sys
5
6from easynetwork.clients import AsyncTCPNetworkClient
7
8from json_protocol import JSONProtocol
9
10
11async def main() -> None:
12 host = "localhost"
13 port = 9000
14 protocol = JSONProtocol()
15
16 # Connect to server
17 async with AsyncTCPNetworkClient((host, port), protocol) as client:
18 # Send data
19 request = {"command-line arguments": sys.argv[1:]}
20 await client.send_packet(request)
21
22 # Receive data from the server and shut down
23 response = await client.recv_packet()
24
25 print(f"Sent: {request}")
26 print(f"Received: {response}")
27
28
29if __name__ == "__main__":
30 asyncio.run(main())
1from __future__ import annotations
2
3import sys
4
5import trio
6
7from easynetwork.clients import AsyncTCPNetworkClient
8
9from json_protocol import JSONProtocol
10
11
12async def main() -> None:
13 host = "localhost"
14 port = 9000
15 protocol = JSONProtocol()
16
17 # Connect to server
18 async with AsyncTCPNetworkClient((host, port), protocol) as client:
19 # Send data
20 request = {"command-line arguments": sys.argv[1:]}
21 await client.send_packet(request)
22
23 # Receive data from the server and shut down
24 response = await client.recv_packet()
25
26 print(f"Sent: {request}")
27 print(f"Received: {response}")
28
29
30if __name__ == "__main__":
31 trio.run(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']}
(.venv) $ python server.py
::1 sent {'command-line arguments': ['Hello', 'world!']}
::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']}