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:
JSONSerializer
: an incremental serializer using thejson
module.PickleSerializer
: a one-shot serializer using thepickle
module.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.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.
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()
1from __future__ import annotations
2
3import asyncio
4
5from easynetwork.api_async.server 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 try:
19 await server.serve_forever()
20 except asyncio.CancelledError:
21 pass
22
23
24if __name__ == "__main__":
25 asyncio.run(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:
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()
1from __future__ import annotations
2
3import asyncio
4import sys
5
6from easynetwork.api_async.client import AsyncTCPNetworkClient
7
8from json_protocol import JSONProtocol
9
10
11async def main() -> None:
12 host = "localhost"
13 port = 9000
14
15 # Connect to server
16 async with AsyncTCPNetworkClient((host, port), JSONProtocol()) as client:
17 # Send data
18 request = {"command-line arguments": sys.argv[1:]}
19 await client.send_packet(request)
20
21 # Receive data from the server and shut down
22 response = await client.recv_packet()
23
24 print(f"Sent: {request}")
25 print(f"Received: {response}")
26
27
28if __name__ == "__main__":
29 asyncio.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']}