How-to — Standalone Servers

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.


Introduction

Standalone servers are classes that can create ready-to-run servers with no predefined asynchronous context (i.e., no async / await). They use (and block) a thread to accept requests, and their methods are meant to be used by other threads for control (e.g. shutdown).

By default, the runner is "asyncio", but it can be changed during object creation.

Server Object

 1from __future__ import annotations
 2
 3import asyncio
 4from collections.abc import AsyncGenerator
 5from contextlib import AsyncExitStack
 6from typing import Any
 7
 8from easynetwork.protocol import StreamProtocol
 9from easynetwork.serializers import JSONSerializer
10from easynetwork.servers import StandaloneTCPNetworkServer
11from easynetwork.servers.handlers import AsyncStreamClient, AsyncStreamRequestHandler
12
13
14class JSONProtocol(StreamProtocol[dict[str, Any], dict[str, Any]]):
15    def __init__(self) -> None:
16        super().__init__(JSONSerializer())
17
18
19class MyRequestHandler(AsyncStreamRequestHandler[dict[str, Any], dict[str, Any]]):
20    async def service_init(self, exit_stack: AsyncExitStack, server: Any) -> None:
21        # StandaloneTCPNetworkServer wraps an AsyncTCPNetworkServer instance.
22        # Therefore, "server" is still asynchronous.
23
24        from easynetwork.servers import AsyncTCPNetworkServer
25
26        assert isinstance(server, AsyncTCPNetworkServer)
27
28    async def handle(
29        self,
30        client: AsyncStreamClient[dict[str, Any]],
31    ) -> AsyncGenerator[None, dict[str, Any]]:
32        request: dict[str, Any] = yield
33
34        current_task = asyncio.current_task()
35        assert current_task is not None
36
37        response = {"task": current_task.get_name(), "request": request}
38        await client.send_packet(response)
39
40
41def main() -> None:
42    host, port = "localhost", 9000
43    protocol = JSONProtocol()
44    handler = MyRequestHandler()
45
46    # All the parameters are the same as AsyncTCPNetworkServer.
47    server = StandaloneTCPNetworkServer(host, port, protocol, handler)
48
49    with server:
50        server.serve_forever()
51
52
53if __name__ == "__main__":
54    main()

Run Server In Background

Unlike asynchronous tasks, it is mandatory to spawn a separate execution thread. NetworkServerThread is useful for this case:

 1from __future__ import annotations
 2
 3import asyncio
 4import threading
 5from collections.abc import AsyncGenerator
 6from typing import Any
 7
 8from easynetwork.clients import TCPNetworkClient
 9from easynetwork.protocol import StreamProtocol
10from easynetwork.serializers import JSONSerializer
11from easynetwork.servers import StandaloneTCPNetworkServer
12from easynetwork.servers.handlers import AsyncStreamClient, AsyncStreamRequestHandler
13from easynetwork.servers.threads_helper import NetworkServerThread
14
15
16class JSONProtocol(StreamProtocol[dict[str, Any], dict[str, Any]]):
17    def __init__(self) -> None:
18        super().__init__(JSONSerializer())
19
20
21class MyRequestHandler(AsyncStreamRequestHandler[dict[str, Any], dict[str, Any]]):
22    async def handle(
23        self,
24        client: AsyncStreamClient[dict[str, Any]],
25    ) -> AsyncGenerator[None, dict[str, Any]]:
26        request: dict[str, Any] = yield
27
28        current_task = asyncio.current_task()
29        assert current_task is not None
30
31        response = {
32            "thread": threading.current_thread().name,
33            "task": current_task.get_name(),
34            "request": request,
35        }
36        await client.send_packet(response)
37
38
39def client(host: str, port: int, message: str) -> None:
40    with TCPNetworkClient((host, port), JSONProtocol()) as client:
41        client.send_packet({"message": message})
42        response = client.recv_packet()
43        print(f"From server: {response}")
44
45
46def main() -> None:
47    host, port = "localhost", 9000
48    protocol = JSONProtocol()
49    handler = MyRequestHandler()
50
51    server = StandaloneTCPNetworkServer(host, port, protocol, handler)
52
53    with server:
54        server_thread = NetworkServerThread(server)
55        server_thread.start()
56
57        print(f"Server loop running in thread: {server_thread.name}")
58
59        client(host, port, "Hello world 1")
60        client(host, port, "Hello world 2")
61        client(host, port, "Hello world 3")
62
63        server_thread.join()
64
65
66if __name__ == "__main__":
67    main()

The output of the example should look something like this:

$ python background_server.py
Server loop running in thread: Thread-1
From server: {'thread': 'Thread-1', 'task': 'Task-6', 'request': {'message': 'Hello world 1'}}
From server: {'thread': 'Thread-1', 'task': 'Task-8', 'request': {'message': 'Hello world 2'}}
From server: {'thread': 'Thread-1', 'task': 'Task-10', 'request': {'message': 'Hello world 3'}}