diff --git a/docs/source/_include/examples/howto/udp_clients/basics/__init__.py b/docs/source/_include/examples/howto/udp_clients/basics/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/docs/source/_include/examples/howto/udp_clients/basics/api_async/__init__.py b/docs/source/_include/examples/howto/udp_clients/basics/api_async/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/docs/source/_include/examples/howto/udp_clients/basics/api_async/connection_example1.py b/docs/source/_include/examples/howto/udp_clients/basics/api_async/connection_example1.py new file mode 100644 index 00000000..aac34a50 --- /dev/null +++ b/docs/source/_include/examples/howto/udp_clients/basics/api_async/connection_example1.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +import asyncio + +from easynetwork.api_async.client import AsyncUDPNetworkClient +from easynetwork.protocol import DatagramProtocol +from easynetwork.serializers import JSONSerializer + + +async def main() -> None: + protocol = DatagramProtocol(JSONSerializer()) + address = ("localhost", 9000) + + async with AsyncUDPNetworkClient(address, protocol) as client: + print(f"Remote address: {client.get_remote_address()}") + + ... + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs/source/_include/examples/howto/udp_clients/basics/api_async/socket_example1.py b/docs/source/_include/examples/howto/udp_clients/basics/api_async/socket_example1.py new file mode 100644 index 00000000..5bc8bedf --- /dev/null +++ b/docs/source/_include/examples/howto/udp_clients/basics/api_async/socket_example1.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +import asyncio +import socket + +from easynetwork.api_async.client import AsyncUDPNetworkClient +from easynetwork.protocol import DatagramProtocol +from easynetwork.serializers import JSONSerializer + + +async def obtain_a_connected_socket() -> socket.socket: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + + ... + + return sock + + +async def main() -> None: + protocol = DatagramProtocol(JSONSerializer()) + sock = await obtain_a_connected_socket() + + async with AsyncUDPNetworkClient(sock, protocol) as client: + print(f"Remote address: {client.get_remote_address()}") + + ... + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs/source/_include/examples/howto/udp_clients/basics/api_sync/__init__.py b/docs/source/_include/examples/howto/udp_clients/basics/api_sync/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/docs/source/_include/examples/howto/udp_clients/basics/api_sync/connection_example1.py b/docs/source/_include/examples/howto/udp_clients/basics/api_sync/connection_example1.py new file mode 100644 index 00000000..a42fb745 --- /dev/null +++ b/docs/source/_include/examples/howto/udp_clients/basics/api_sync/connection_example1.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from easynetwork.api_sync.client import UDPNetworkClient +from easynetwork.protocol import DatagramProtocol +from easynetwork.serializers import JSONSerializer + + +def main() -> None: + protocol = DatagramProtocol(JSONSerializer()) + address = ("127.0.0.1", 9000) + + with UDPNetworkClient(address, protocol) as client: + print(f"Remote address: {client.get_remote_address()}") + + ... + + +if __name__ == "__main__": + main() diff --git a/docs/source/_include/examples/howto/udp_clients/basics/api_sync/socket_example1.py b/docs/source/_include/examples/howto/udp_clients/basics/api_sync/socket_example1.py new file mode 100644 index 00000000..474ad99b --- /dev/null +++ b/docs/source/_include/examples/howto/udp_clients/basics/api_sync/socket_example1.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +import socket + +from easynetwork.api_sync.client import UDPNetworkClient +from easynetwork.protocol import DatagramProtocol +from easynetwork.serializers import JSONSerializer + + +def obtain_a_connected_socket() -> socket.socket: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + + ... + + return sock + + +def main() -> None: + protocol = DatagramProtocol(JSONSerializer()) + sock = obtain_a_connected_socket() + + with UDPNetworkClient(sock, protocol) as client: + print(f"Remote address: {client.get_remote_address()}") + + ... + + +if __name__ == "__main__": + main() diff --git a/docs/source/_include/examples/howto/udp_clients/usage/__init__.py b/docs/source/_include/examples/howto/udp_clients/usage/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/docs/source/_include/examples/howto/udp_clients/usage/api_async.py b/docs/source/_include/examples/howto/udp_clients/usage/api_async.py new file mode 100644 index 00000000..50251587 --- /dev/null +++ b/docs/source/_include/examples/howto/udp_clients/usage/api_async.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import asyncio +import socket +from typing import Any + +from easynetwork.api_async.client import AsyncUDPNetworkClient +from easynetwork.exceptions import DatagramProtocolParseError + +############### +# Basic usage # +############### + + +async def send_packet_example1(client: AsyncUDPNetworkClient[Any, Any]) -> None: + # [start] + await client.send_packet({"data": 42}) + + +async def recv_packet_example1(client: AsyncUDPNetworkClient[Any, Any]) -> None: + # [start] + packet = await client.recv_packet() + print(f"Received packet: {packet!r}") + + +async def recv_packet_example2(client: AsyncUDPNetworkClient[Any, Any]) -> None: + # [start] + try: + async with asyncio.timeout(30): + packet = await client.recv_packet() + except TimeoutError: + print("Timed out") + else: + print(f"Received packet: {packet!r}") + + +async def recv_packet_example3(client: AsyncUDPNetworkClient[Any, Any]) -> None: + # [start] + try: + async with asyncio.timeout(30): + packet = await client.recv_packet() + except DatagramProtocolParseError: + print("Received something, but was not valid") + except TimeoutError: + print("Timed out") + else: + print(f"Received packet: {packet!r}") + + +async def recv_packet_example4(client: AsyncUDPNetworkClient[Any, Any]) -> None: + # [start] + all_packets = [p async for p in client.iter_received_packets()] + + +async def recv_packet_example5(client: AsyncUDPNetworkClient[Any, Any]) -> None: + # [start] + all_packets = [p async for p in client.iter_received_packets(timeout=1)] + + +################## +# Advanced usage # +################## + + +async def socket_proxy_example(client: AsyncUDPNetworkClient[Any, Any]) -> None: + # [start] + client.socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, True) diff --git a/docs/source/_include/examples/howto/udp_clients/usage/api_sync.py b/docs/source/_include/examples/howto/udp_clients/usage/api_sync.py new file mode 100644 index 00000000..be9c1c17 --- /dev/null +++ b/docs/source/_include/examples/howto/udp_clients/usage/api_sync.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +import socket +from typing import Any + +from easynetwork.api_sync.client import UDPNetworkClient +from easynetwork.exceptions import DatagramProtocolParseError + +############### +# Basic usage # +############### + + +def send_packet_example1(client: UDPNetworkClient[Any, Any]) -> None: + # [start] + client.send_packet({"data": 42}) + + +def recv_packet_example1(client: UDPNetworkClient[Any, Any]) -> None: + # [start] + packet = client.recv_packet() + print(f"Received packet: {packet!r}") + + +def recv_packet_example2(client: UDPNetworkClient[Any, Any]) -> None: + # [start] + try: + packet = client.recv_packet(timeout=30) + except TimeoutError: + print("Timed out") + else: + print(f"Received packet: {packet!r}") + + +def recv_packet_example3(client: UDPNetworkClient[Any, Any]) -> None: + # [start] + try: + packet = client.recv_packet(timeout=30) + except DatagramProtocolParseError: + print("Received something, but was not valid") + except TimeoutError: + print("Timed out") + else: + print(f"Received packet: {packet!r}") + + +def recv_packet_example4(client: UDPNetworkClient[Any, Any]) -> None: + # [start] + all_packets = [p for p in client.iter_received_packets()] + + +def recv_packet_example5(client: UDPNetworkClient[Any, Any]) -> None: + # [start] + all_packets = [p for p in client.iter_received_packets(timeout=1)] + + +################## +# Advanced usage # +################## + + +def socket_proxy_example(client: UDPNetworkClient[Any, Any]) -> None: + # [start] + client.socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, True) diff --git a/docs/source/howto/index.rst b/docs/source/howto/index.rst index 517a421a..75aadb55 100644 --- a/docs/source/howto/index.rst +++ b/docs/source/howto/index.rst @@ -9,3 +9,4 @@ How-to Guide serializers tcp_clients tcp_servers + udp_clients diff --git a/docs/source/howto/udp_clients.rst b/docs/source/howto/udp_clients.rst new file mode 100644 index 00000000..9dbe0354 --- /dev/null +++ b/docs/source/howto/udp_clients.rst @@ -0,0 +1,299 @@ +***************************** +How-to — UDP Client Endpoints +***************************** + +.. include:: ../_include/sync-async-variants.rst + +.. contents:: Table of Contents + :local: + +------ + +The Basics +========== + +The Protocol Object +------------------- + +The UDP clients expect a :class:`.DatagramProtocol` instance to communicate with the remote endpoint. + +.. seealso:: + + :doc:`protocols` + Explains what a :class:`.DatagramProtocol` is and how to use it. + + +Connecting To The Remote Host +----------------------------- + +.. important:: + + If you are familiar with UDP sockets, you know that there are no real connections (communication pipes) like there are with TCP sockets. + + If not, I advise you to read the Unix manual pages :manpage:`udp(7)` and :manpage:`connect(2)`. + +You need the host address (domain name or IP) and the port of connection in order to connect to the remote host: + +.. tabs:: + + .. group-tab:: Synchronous + + .. literalinclude:: ../_include/examples/howto/udp_clients/basics/api_sync/connection_example1.py + :linenos: + + .. note:: + + The client does nothing when it enters the :keyword:`with` context. Everything is done on object creation. + + .. group-tab:: Asynchronous + + .. literalinclude:: ../_include/examples/howto/udp_clients/basics/api_async/connection_example1.py + :linenos: + + .. note:: + + The call to ``wait_connected()`` is required to actually initialize the client, since we cannot perform asynchronous operations + at object creation. This is what the client does when it enters the the :keyword:`async with` context. + + Once completed, ``wait_connected()`` is a no-op. + + +Using An Already Connected Socket +--------------------------------- + +If you have your own way to obtain a connected :class:`socket.socket` instance, you can pass it to the client. + +If the socket is not connected, an :exc:`OSError` is raised. + +.. important:: + + It *must* be a :data:`~socket.SOCK_DGRAM` socket with :data:`~socket.AF_INET` or :data:`~socket.AF_INET6` family. + +.. warning:: + + The resource ownership is given to the client. You must close the client to close the socket. + +.. tabs:: + + .. group-tab:: Synchronous + + .. literalinclude:: ../_include/examples/howto/udp_clients/basics/api_sync/socket_example1.py + :linenos: + + .. group-tab:: Asynchronous + + .. literalinclude:: ../_include/examples/howto/udp_clients/basics/api_async/socket_example1.py + :linenos: + + .. note:: + + Even with a ready-to-use socket, the call to ``wait_connected()`` is still required. + + +Basic Usage +=========== + +Sending Packets +--------------- + +There's not much to say, except that objects passed as arguments are automatically converted to bytes to send to the remote host +thanks to the :term:`protocol object`. + +.. tabs:: + + .. group-tab:: Synchronous + + .. literalinclude:: ../_include/examples/howto/udp_clients/usage/api_sync.py + :pyobject: send_packet_example1 + :start-after: [start] + :dedent: + :linenos: + + .. group-tab:: Asynchronous + + .. literalinclude:: ../_include/examples/howto/udp_clients/usage/api_async.py + :pyobject: send_packet_example1 + :start-after: [start] + :dedent: + :linenos: + + +Receiving Packets +----------------- + +You get the next available packet, already parsed. + +.. tabs:: + + .. group-tab:: Synchronous + + .. literalinclude:: ../_include/examples/howto/udp_clients/usage/api_sync.py + :pyobject: recv_packet_example1 + :start-after: [start] + :dedent: + :linenos: + + You can control the receive timeout with the ``timeout`` parameter: + + .. literalinclude:: ../_include/examples/howto/udp_clients/usage/api_sync.py + :pyobject: recv_packet_example2 + :start-after: [start] + :dedent: + :linenos: + + .. group-tab:: Asynchronous + + .. literalinclude:: ../_include/examples/howto/udp_clients/usage/api_async.py + :pyobject: recv_packet_example1 + :start-after: [start] + :dedent: + :linenos: + + You can control the receive timeout by adding a timeout scope using the asynchronous framework: + + .. literalinclude:: ../_include/examples/howto/udp_clients/usage/api_async.py + :pyobject: recv_packet_example2 + :start-after: [start] + :dedent: + :linenos: + + +.. tip:: + + Remember to catch invalid data parsing errors. + + .. tabs:: + + .. group-tab:: Synchronous + + .. literalinclude:: ../_include/examples/howto/udp_clients/usage/api_sync.py + :pyobject: recv_packet_example3 + :start-after: [start] + :dedent: + :linenos: + :emphasize-lines: 3-4 + + .. group-tab:: Asynchronous + + .. literalinclude:: ../_include/examples/howto/udp_clients/usage/api_async.py + :pyobject: recv_packet_example3 + :start-after: [start] + :dedent: + :linenos: + :emphasize-lines: 4-5 + + +Receiving Multiple Packets At Once +---------------------------------- + +You can use ``iter_received_packets()`` to get all the received packets in a sequence or a set. + +.. tabs:: + + .. group-tab:: Synchronous + + .. literalinclude:: ../_include/examples/howto/udp_clients/usage/api_sync.py + :pyobject: recv_packet_example4 + :start-after: [start] + :dedent: + :linenos: + + .. group-tab:: Asynchronous + + .. literalinclude:: ../_include/examples/howto/udp_clients/usage/api_async.py + :pyobject: recv_packet_example4 + :start-after: [start] + :dedent: + :linenos: + +The ``timeout`` parameter defaults to zero to get only the data already in the buffer, but you can change it. + +.. tabs:: + + .. group-tab:: Synchronous + + .. literalinclude:: ../_include/examples/howto/udp_clients/usage/api_sync.py + :pyobject: recv_packet_example5 + :start-after: [start] + :dedent: + :linenos: + + .. seealso:: + + :meth:`UDPNetworkClient.iter_received_packets() <.AbstractNetworkClient.iter_received_packets>` + The method description and usage (especially for the ``timeout`` parameter). + + .. group-tab:: Asynchronous + + .. literalinclude:: ../_include/examples/howto/udp_clients/usage/api_async.py + :pyobject: recv_packet_example5 + :start-after: [start] + :dedent: + :linenos: + + .. seealso:: + + :meth:`AsyncUDPNetworkClient.iter_received_packets() <.AbstractAsyncNetworkClient.iter_received_packets>` + The method description and usage (especially for the ``timeout`` parameter). + + +Advanced Usage +============== + +.. note:: + + This section is for people who know what they're doing and are looking for something specific. + + +Low-Level Socket Operations +--------------------------- + +For low-level operations such as :meth:`~socket.socket.setsockopt`, the client object exposes the socket through a :class:`.SocketProxy`: + +.. tabs:: + + .. group-tab:: Synchronous + + .. literalinclude:: ../_include/examples/howto/udp_clients/usage/api_sync.py + :pyobject: socket_proxy_example + :start-after: [start] + :dedent: + :linenos: + + .. group-tab:: Asynchronous + + .. literalinclude:: ../_include/examples/howto/udp_clients/usage/api_async.py + :pyobject: socket_proxy_example + :start-after: [start] + :dedent: + :linenos: + + .. warning:: + + Make sure that ``wait_connected()`` has been called before. + + +Concurrency And Multithreading +------------------------------ + +.. tabs:: + + .. group-tab:: Synchronous + + All client methods are thread-safe. Synchronization follows these rules: + + * :meth:`~.UDPNetworkClient.send_packet` and :meth:`~.UDPNetworkClient.recv_packet` do not share the same + :class:`threading.Lock` instance. + + * :meth:`~.UDPNetworkClient.close` will not wait for :meth:`~.UDPNetworkClient.recv_packet`. + + * The :attr:`client.socket <.UDPNetworkClient.socket>` methods are also thread-safe. This means that you cannot access + the underlying socket methods (e.g. :meth:`~socket.socket.getsockopt`) during a write operation. + + .. group-tab:: Asynchronous + + All client methods do not require external task synchronization. Synchronization follows these rules: + + * :meth:`~.AsyncUDPNetworkClient.send_packet` and :meth:`~.AsyncUDPNetworkClient.recv_packet` do not share the same lock instance. + + * :meth:`~.AsyncUDPNetworkClient.close` will not wait for :meth:`~.AsyncUDPNetworkClient.recv_packet`. diff --git a/tox.ini b/tox.ini index 1b27b95c..3dc64f6d 100644 --- a/tox.ini +++ b/tox.ini @@ -165,6 +165,7 @@ commands = docs: mypy {env:MYPY_OPTS} {[docs]examples_dir}{/}howto{/}protocols docs: mypy {env:MYPY_OPTS} {[docs]examples_dir}{/}howto{/}serializers docs: mypy {env:MYPY_OPTS} {[docs]examples_dir}{/}howto{/}tcp_clients + docs: mypy {env:MYPY_OPTS} {[docs]examples_dir}{/}howto{/}udp_clients docs: mypy {env:MYPY_OPTS} {[docs]examples_dir}{/}howto{/}tcp_servers [testenv:pre-commit]