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 either asyncio or trio, 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).

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()

Use An Other Runner

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

See also

new_builtin_backend()

The token is passed to this function.

 1from __future__ import annotations
 2
 3from collections.abc import AsyncGenerator
 4from contextlib import AsyncExitStack
 5from typing import Any
 6
 7import trio
 8
 9from easynetwork.protocol import StreamProtocol
10from easynetwork.serializers import JSONSerializer
11from easynetwork.servers import StandaloneTCPNetworkServer
12from easynetwork.servers.handlers import AsyncStreamClient, AsyncStreamRequestHandler
13
14
15class JSONProtocol(StreamProtocol[dict[str, Any], dict[str, Any]]):
16    def __init__(self) -> None:
17        super().__init__(JSONSerializer())
18
19
20class MyRequestHandler(AsyncStreamRequestHandler[dict[str, Any], dict[str, Any]]):
21    async def service_init(self, exit_stack: AsyncExitStack, server: Any) -> None:
22        # StandaloneTCPNetworkServer wraps an AsyncTCPNetworkServer instance.
23        # Therefore, "server" is still asynchronous.
24
25        from easynetwork.servers import AsyncTCPNetworkServer
26
27        assert isinstance(server, AsyncTCPNetworkServer)
28
29    async def handle(
30        self,
31        client: AsyncStreamClient[dict[str, Any]],
32    ) -> AsyncGenerator[None, dict[str, Any]]:
33        request: dict[str, Any] = yield
34
35        current_task = trio.lowlevel.current_task()
36
37        response = {"task": current_task.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, backend="trio")
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'}}