Skip to content

Commit

Permalink
[DOCS] Added How-to guide for UDP servers
Browse files Browse the repository at this point in the history
  • Loading branch information
francis-clairicia committed Oct 31, 2023
1 parent fc7fea1 commit fa29119
Show file tree
Hide file tree
Showing 7 changed files with 398 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ async def handle(
#################

### On a 'return'
# When handle() returns, this means that the handling of ONE request
# has finished. There is no connection close or whatever.
# The server will immediately create a new generator.
# When handle() returns, it means that this request handler is finished.
# It does not close the connection or anything.
# The server immediately creates a new generator.
#################
return

Expand Down
49 changes: 49 additions & 0 deletions docs/source/_include/examples/howto/udp_servers/async_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from __future__ import annotations

import asyncio
from collections.abc import AsyncGenerator

from easynetwork.api_async.server import AsyncDatagramClient, AsyncDatagramRequestHandler, AsyncUDPNetworkServer
from easynetwork.protocol import DatagramProtocol


class Request:
...


class Response:
...


class MyRequestHandler(AsyncDatagramRequestHandler[Request, Response]):
async def handle(
self,
client: AsyncDatagramClient[Response],
) -> AsyncGenerator[None, Request]:
request: Request = yield

...

await client.send_packet(Response())


# NOTE: The sent packet is "Response" and the received packet is "Request"
class ServerProtocol(DatagramProtocol[Response, Request]):
def __init__(self) -> None:
...


async def main() -> None:
host, port = "localhost", 9000
protocol = ServerProtocol()
handler = MyRequestHandler()

# Create the server, binding to localhost on port 9000
async with AsyncUDPNetworkServer(host, port, protocol, handler) as server:
# Activate the server; this will keep running until you
# interrupt the program with Ctrl-C
await server.serve_forever()


if __name__ == "__main__":
asyncio.run(main())
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
from __future__ import annotations

import asyncio
import contextlib
from collections.abc import AsyncGenerator

from easynetwork.api_async.server import AsyncDatagramClient, AsyncDatagramRequestHandler, AsyncUDPNetworkServer
from easynetwork.exceptions import DatagramProtocolParseError


class Request:
...


class Response:
...


class BadRequest(Response):
...


class InternalError(Response):
...


class TimedOut(Response):
...


class MinimumRequestHandler(AsyncDatagramRequestHandler[Request, Response]):
async def handle(
self,
client: AsyncDatagramClient[Response],
) -> AsyncGenerator[None, Request]:
### Before 'yield'
# Initializes the generator.
# This is the setup part before receiving a request.
# Unlike the stream request handler, the generator is started
# when the datagram is received (but is not parsed yet).
##################

request: Request = yield

### After 'yield'
# The received datagram is parsed.
# you can do whatever you want with it and send responses back
# to the client if necessary.
await client.send_packet(Response())
#################

### On a 'return'
# When handle() returns, it means that this request handler is finished.
# The server creates a new generator when a new datagram is received.
#################
return


class SkipDatagramRequestHandler(AsyncDatagramRequestHandler[Request, Response]):
async def handle(
self,
client: AsyncDatagramClient[Response],
) -> AsyncGenerator[None, Request]:
if not self.should_handle(client):
# By returning before the "yield" statement, you ask the server to discard
# the received datagram.
return

request: Request = yield

def should_handle(self, client: AsyncDatagramClient[Response]) -> bool:
return True


class ErrorHandlingInRequestHandler(AsyncDatagramRequestHandler[Request, Response]):
async def handle(
self,
client: AsyncDatagramClient[Response],
) -> AsyncGenerator[None, Request]:
try:
# *All* exceptions are thrown through the "yield" statement
# (including BaseException). But you should only catch Exception subclasses.
request: Request = yield
except DatagramProtocolParseError:
await client.send_packet(BadRequest())
except Exception:
await client.send_packet(InternalError())
else:
await client.send_packet(Response())


class MultipleYieldInRequestHandler(AsyncDatagramRequestHandler[Request, Response]):
async def handle(
self,
client: AsyncDatagramClient[Response],
) -> AsyncGenerator[None, Request]:
request: Request = yield

...

await client.send_packet(Response())

if self.need_something_else(request, client):
additional_data: Request = yield

...

await client.send_packet(Response())

def need_something_else(self, request: Request, client: AsyncDatagramClient[Response]) -> bool:
return True


class TimeoutRequestHandler(AsyncDatagramRequestHandler[Request, Response]):
async def handle(
self,
client: AsyncDatagramClient[Response],
) -> AsyncGenerator[None, Request]:
# It is *never* useful to have a timeout for the 1st datagram because the datagram
# is already in the queue.
request: Request = yield

...

await client.send_packet(Response())

try:
async with asyncio.timeout(30):
# The client has 30 seconds to send the 2nd request to the server.
another_request: Request = yield
except TimeoutError:
await client.send_packet(TimedOut())
else:
await client.send_packet(Response())


class ServiceInitializationHookRequestHandler(AsyncDatagramRequestHandler[Request, Response]):
async def service_init(
self,
exit_stack: contextlib.AsyncExitStack,
server: AsyncUDPNetworkServer[Request, Response],
) -> None:
exit_stack.callback(self._service_quit)

self.background_tasks = await exit_stack.enter_async_context(asyncio.TaskGroup())

_ = self.background_tasks.create_task(self._service_actions())

async def _service_actions(self) -> None:
while True:
await asyncio.sleep(1)

# Do some stuff each second in background
...

def _service_quit(self) -> None:
print("Service stopped")
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from __future__ import annotations

from collections.abc import AsyncGenerator

from easynetwork.api_async.server import AsyncDatagramClient, AsyncDatagramRequestHandler


class Request:
"""Object representing the client request."""

...


class Response:
"""Object representing the response to send to the client."""

...


class MyRequestHandler(AsyncDatagramRequestHandler[Request, Response]):
"""
The request handler class for our server.
It is instantiated once to the server, and must
override the handle() method to implement communication to the
client.
"""

async def handle(
self,
client: AsyncDatagramClient[Response],
) -> AsyncGenerator[None, Request]:
# "client" a placeholder to have a stream-like API.
# All the datagrams sent by this client are sent
# through the "yield" statement.
request: Request = yield

# Do some stuff
...

response = Response()

# The corresponding call is server_socket.sendto(data, remote_address)
await client.send_packet(response)
3 changes: 2 additions & 1 deletion docs/source/howto/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ How-to Guide
protocols
serializers
tcp_clients
tcp_servers
udp_clients
tcp_servers
udp_servers
Loading

0 comments on commit fa29119

Please sign in to comment.