diff options
Diffstat (limited to 'testing/web-platform/tests/tools/third_party/websockets/docs')
70 files changed, 9587 insertions, 0 deletions
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/Makefile b/testing/web-platform/tests/tools/third_party/websockets/docs/Makefile new file mode 100644 index 0000000000..0458706458 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/Makefile @@ -0,0 +1,23 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +livehtml: + sphinx-autobuild --watch "$(SOURCEDIR)/../src" "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/_static/tidelift.png b/testing/web-platform/tests/tools/third_party/websockets/docs/_static/tidelift.png Binary files differdeleted file mode 100644 index 317dc4d985..0000000000 --- a/testing/web-platform/tests/tools/third_party/websockets/docs/_static/tidelift.png +++ /dev/null diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/conf.py b/testing/web-platform/tests/tools/third_party/websockets/docs/conf.py new file mode 100644 index 0000000000..9d61dc7173 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/conf.py @@ -0,0 +1,171 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +import datetime +import importlib +import inspect +import os +import subprocess +import sys + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path.insert(0, os.path.join(os.path.abspath(".."), "src")) + + +# -- Project information ----------------------------------------------------- + +project = "websockets" +copyright = f"2013-{datetime.date.today().year}, Aymeric Augustin and contributors" +author = "Aymeric Augustin" + +from websockets.version import tag as version, version as release + + +# -- General configuration --------------------------------------------------- + +nitpicky = True + +nitpick_ignore = [ + # topics/design.rst discusses undocumented APIs + ("py:meth", "client.WebSocketClientProtocol.handshake"), + ("py:meth", "server.WebSocketServerProtocol.handshake"), + ("py:attr", "legacy.protocol.WebSocketCommonProtocol.is_client"), + ("py:attr", "legacy.protocol.WebSocketCommonProtocol.messages"), + ("py:meth", "legacy.protocol.WebSocketCommonProtocol.close_connection"), + ("py:attr", "legacy.protocol.WebSocketCommonProtocol.close_connection_task"), + ("py:meth", "legacy.protocol.WebSocketCommonProtocol.keepalive_ping"), + ("py:attr", "legacy.protocol.WebSocketCommonProtocol.keepalive_ping_task"), + ("py:meth", "legacy.protocol.WebSocketCommonProtocol.transfer_data"), + ("py:attr", "legacy.protocol.WebSocketCommonProtocol.transfer_data_task"), + ("py:meth", "legacy.protocol.WebSocketCommonProtocol.connection_open"), + ("py:meth", "legacy.protocol.WebSocketCommonProtocol.ensure_open"), + ("py:meth", "legacy.protocol.WebSocketCommonProtocol.fail_connection"), + ("py:meth", "legacy.protocol.WebSocketCommonProtocol.connection_lost"), + ("py:meth", "legacy.protocol.WebSocketCommonProtocol.read_message"), + ("py:meth", "legacy.protocol.WebSocketCommonProtocol.write_frame"), +] + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.linkcode", + "sphinx.ext.napoleon", + "sphinx_copybutton", + "sphinx_inline_tabs", + "sphinxcontrib.spelling", + "sphinxcontrib_trio", + "sphinxext.opengraph", +] +# It is currently inconvenient to install PyEnchant on Apple Silicon. +try: + import sphinxcontrib.spelling +except ImportError: + extensions.remove("sphinxcontrib.spelling") + +autodoc_typehints = "description" + +autodoc_typehints_description_target = "documented" + +# Workaround for https://github.com/sphinx-doc/sphinx/issues/9560 +from sphinx.domains.python import PythonDomain + +assert PythonDomain.object_types["data"].roles == ("data", "obj") +PythonDomain.object_types["data"].roles = ("data", "class", "obj") + +intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} + +spelling_show_suggestions = True + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +# Configure viewcode extension. +from websockets.version import commit + +code_url = f"https://github.com/python-websockets/websockets/blob/{commit}" + +def linkcode_resolve(domain, info): + # Non-linkable objects from the starter kit in the tutorial. + if domain == "js" or info["module"] == "connect4": + return + + assert domain == "py", "expected only Python objects" + + mod = importlib.import_module(info["module"]) + if "." in info["fullname"]: + objname, attrname = info["fullname"].split(".") + obj = getattr(mod, objname) + try: + # object is a method of a class + obj = getattr(obj, attrname) + except AttributeError: + # object is an attribute of a class + return None + else: + obj = getattr(mod, info["fullname"]) + + try: + file = inspect.getsourcefile(obj) + lines = inspect.getsourcelines(obj) + except TypeError: + # e.g. object is a typing.Union + return None + file = os.path.relpath(file, os.path.abspath("..")) + if not file.startswith("src/websockets"): + # e.g. object is a typing.NewType + return None + start, end = lines[1], lines[1] + len(lines[0]) - 1 + + return f"{code_url}/{file}#L{start}-L{end}" + +# Configure opengraph extension + +# Social cards don't support the SVG logo. Also, the text preview looks bad. +ogp_social_cards = {"enable": False} + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = "furo" + +html_theme_options = { + "light_css_variables": { + "color-brand-primary": "#306998", # blue from logo + "color-brand-content": "#0b487a", # blue more saturated and less dark + }, + "dark_css_variables": { + "color-brand-primary": "#ffd43bcc", # yellow from logo, more muted than content + "color-brand-content": "#ffd43bd9", # yellow from logo, transparent like text + }, + "sidebar_hide_name": True, +} + +html_logo = "_static/websockets.svg" + +html_favicon = "_static/favicon.ico" + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] + +html_copy_source = False + +html_show_sphinx = False diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/faq/asyncio.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/asyncio.rst new file mode 100644 index 0000000000..e77f50addd --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/asyncio.rst @@ -0,0 +1,69 @@ +Using asyncio +============= + +.. currentmodule:: websockets + +How do I run two coroutines in parallel? +---------------------------------------- + +You must start two tasks, which the event loop will run concurrently. You can +achieve this with :func:`asyncio.gather` or :func:`asyncio.create_task`. + +Keep track of the tasks and make sure they terminate or you cancel them when +the connection terminates. + +Why does my program never receive any messages? +----------------------------------------------- + +Your program runs a coroutine that never yields control to the event loop. The +coroutine that receives messages never gets a chance to run. + +Putting an ``await`` statement in a ``for`` or a ``while`` loop isn't enough +to yield control. Awaiting a coroutine may yield control, but there's no +guarantee that it will. + +For example, :meth:`~legacy.protocol.WebSocketCommonProtocol.send` only yields +control when send buffers are full, which never happens in most practical +cases. + +If you run a loop that contains only synchronous operations and +a :meth:`~legacy.protocol.WebSocketCommonProtocol.send` call, you must yield +control explicitly with :func:`asyncio.sleep`:: + + async def producer(websocket): + message = generate_next_message() + await websocket.send(message) + await asyncio.sleep(0) # yield control to the event loop + +:func:`asyncio.sleep` always suspends the current task, allowing other tasks +to run. This behavior is documented precisely because it isn't expected from +every coroutine. + +See `issue 867`_. + +.. _issue 867: https://github.com/python-websockets/websockets/issues/867 + +Why am I having problems with threads? +-------------------------------------- + +If you choose websockets' default implementation based on :mod:`asyncio`, then +you shouldn't use threads. Indeed, choosing :mod:`asyncio` to handle concurrency +is mutually exclusive with :mod:`threading`. + +If you believe that you need to run websockets in a thread and some logic in +another thread, you should run that logic in a :class:`~asyncio.Task` instead. +If it blocks the event loop, :meth:`~asyncio.loop.run_in_executor` will help. + +This question is really about :mod:`asyncio`. Please review the advice about +:ref:`asyncio-multithreading` in the Python documentation. + +Why does my simple program misbehave mysteriously? +-------------------------------------------------- + +You are using :func:`time.sleep` instead of :func:`asyncio.sleep`, which +blocks the event loop and prevents asyncio from operating normally. + +This may lead to messages getting send but not received, to connection +timeouts, and to unexpected results of shotgun debugging e.g. adding an +unnecessary call to :meth:`~legacy.protocol.WebSocketCommonProtocol.send` +makes the program functional. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/faq/client.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/client.rst new file mode 100644 index 0000000000..c590ac107d --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/client.rst @@ -0,0 +1,101 @@ +Client +====== + +.. currentmodule:: websockets + +Why does the client close the connection prematurely? +----------------------------------------------------- + +You're exiting the context manager prematurely. Wait for the work to be +finished before exiting. + +For example, if your code has a structure similar to:: + + async with connect(...) as websocket: + asyncio.create_task(do_some_work()) + +change it to:: + + async with connect(...) as websocket: + await do_some_work() + +How do I access HTTP headers? +----------------------------- + +Once the connection is established, HTTP headers are available in +:attr:`~client.WebSocketClientProtocol.request_headers` and +:attr:`~client.WebSocketClientProtocol.response_headers`. + +How do I set HTTP headers? +-------------------------- + +To set the ``Origin``, ``Sec-WebSocket-Extensions``, or +``Sec-WebSocket-Protocol`` headers in the WebSocket handshake request, use the +``origin``, ``extensions``, or ``subprotocols`` arguments of +:func:`~client.connect`. + +To override the ``User-Agent`` header, use the ``user_agent_header`` argument. +Set it to :obj:`None` to remove the header. + +To set other HTTP headers, for example the ``Authorization`` header, use the +``extra_headers`` argument:: + + async with connect(..., extra_headers={"Authorization": ...}) as websocket: + ... + +In the :mod:`threading` API, this argument is named ``additional_headers``:: + + with connect(..., additional_headers={"Authorization": ...}) as websocket: + ... + +How do I force the IP address that the client connects to? +---------------------------------------------------------- + +Use the ``host`` argument of :meth:`~asyncio.loop.create_connection`:: + + await websockets.connect("ws://example.com", host="192.168.0.1") + +:func:`~client.connect` accepts the same arguments as +:meth:`~asyncio.loop.create_connection`. + +How do I close a connection? +---------------------------- + +The easiest is to use :func:`~client.connect` as a context manager:: + + async with connect(...) as websocket: + ... + +The connection is closed when exiting the context manager. + +How do I reconnect when the connection drops? +--------------------------------------------- + +Use :func:`~client.connect` as an asynchronous iterator:: + + async for websocket in websockets.connect(...): + try: + ... + except websockets.ConnectionClosed: + continue + +Make sure you handle exceptions in the ``async for`` loop. Uncaught exceptions +will break out of the loop. + +How do I stop a client that is processing messages in a loop? +------------------------------------------------------------- + +You can close the connection. + +Here's an example that terminates cleanly when it receives SIGTERM on Unix: + +.. literalinclude:: ../../example/faq/shutdown_client.py + :emphasize-lines: 10-13 + +How do I disable TLS/SSL certificate verification? +-------------------------------------------------- + +Look at the ``ssl`` argument of :meth:`~asyncio.loop.create_connection`. + +:func:`~client.connect` accepts the same arguments as +:meth:`~asyncio.loop.create_connection`. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/faq/common.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/common.rst new file mode 100644 index 0000000000..2c63c4f36f --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/common.rst @@ -0,0 +1,161 @@ +Both sides +========== + +.. currentmodule:: websockets + +What does ``ConnectionClosedError: no close frame received or sent`` mean? +-------------------------------------------------------------------------- + +If you're seeing this traceback in the logs of a server: + +.. code-block:: pytb + + connection handler failed + Traceback (most recent call last): + ... + asyncio.exceptions.IncompleteReadError: 0 bytes read on a total of 2 expected bytes + + The above exception was the direct cause of the following exception: + + Traceback (most recent call last): + ... + websockets.exceptions.ConnectionClosedError: no close frame received or sent + +or if a client crashes with this traceback: + +.. code-block:: pytb + + Traceback (most recent call last): + ... + ConnectionResetError: [Errno 54] Connection reset by peer + + The above exception was the direct cause of the following exception: + + Traceback (most recent call last): + ... + websockets.exceptions.ConnectionClosedError: no close frame received or sent + +it means that the TCP connection was lost. As a consequence, the WebSocket +connection was closed without receiving and sending a close frame, which is +abnormal. + +You can catch and handle :exc:`~exceptions.ConnectionClosed` to prevent it +from being logged. + +There are several reasons why long-lived connections may be lost: + +* End-user devices tend to lose network connectivity often and unpredictably + because they can move out of wireless network coverage, get unplugged from + a wired network, enter airplane mode, be put to sleep, etc. +* HTTP load balancers or proxies that aren't configured for long-lived + connections may terminate connections after a short amount of time, usually + 30 seconds, despite websockets' keepalive mechanism. + +If you're facing a reproducible issue, :ref:`enable debug logs <debugging>` to +see when and how connections are closed. + +What does ``ConnectionClosedError: sent 1011 (internal error) keepalive ping timeout; no close frame received`` mean? +--------------------------------------------------------------------------------------------------------------------- + +If you're seeing this traceback in the logs of a server: + +.. code-block:: pytb + + connection handler failed + Traceback (most recent call last): + ... + asyncio.exceptions.CancelledError + + The above exception was the direct cause of the following exception: + + Traceback (most recent call last): + ... + websockets.exceptions.ConnectionClosedError: sent 1011 (internal error) keepalive ping timeout; no close frame received + +or if a client crashes with this traceback: + +.. code-block:: pytb + + Traceback (most recent call last): + ... + asyncio.exceptions.CancelledError + + The above exception was the direct cause of the following exception: + + Traceback (most recent call last): + ... + websockets.exceptions.ConnectionClosedError: sent 1011 (internal error) keepalive ping timeout; no close frame received + +it means that the WebSocket connection suffered from excessive latency and was +closed after reaching the timeout of websockets' keepalive mechanism. + +You can catch and handle :exc:`~exceptions.ConnectionClosed` to prevent it +from being logged. + +There are two main reasons why latency may increase: + +* Poor network connectivity. +* More traffic than the recipient can handle. + +See the discussion of :doc:`timeouts <../topics/timeouts>` for details. + +If websockets' default timeout of 20 seconds is too short for your use case, +you can adjust it with the ``ping_timeout`` argument. + +How do I set a timeout on :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`? +-------------------------------------------------------------------------------- + +On Python ≥ 3.11, use :func:`asyncio.timeout`:: + + async with asyncio.timeout(timeout=10): + message = await websocket.recv() + +On older versions of Python, use :func:`asyncio.wait_for`:: + + message = await asyncio.wait_for(websocket.recv(), timeout=10) + +This technique works for most APIs. When it doesn't, for example with +asynchronous context managers, websockets provides an ``open_timeout`` argument. + +How can I pass arguments to a custom protocol subclass? +------------------------------------------------------- + +You can bind additional arguments to the protocol factory with +:func:`functools.partial`:: + + import asyncio + import functools + import websockets + + class MyServerProtocol(websockets.WebSocketServerProtocol): + def __init__(self, *args, extra_argument=None, **kwargs): + super().__init__(*args, **kwargs) + # do something with extra_argument + + create_protocol = functools.partial(MyServerProtocol, extra_argument=42) + start_server = websockets.serve(..., create_protocol=create_protocol) + +This example was for a server. The same pattern applies on a client. + +How do I keep idle connections open? +------------------------------------ + +websockets sends pings at 20 seconds intervals to keep the connection open. + +It closes the connection if it doesn't get a pong within 20 seconds. + +You can adjust this behavior with ``ping_interval`` and ``ping_timeout``. + +See :doc:`../topics/timeouts` for details. + +How do I respond to pings? +-------------------------- + +If you are referring to Ping_ and Pong_ frames defined in the WebSocket +protocol, don't bother, because websockets handles them for you. + +.. _Ping: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.2 +.. _Pong: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.3 + +If you are connecting to a server that defines its own heartbeat at the +application level, then you need to build that logic into your application. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/faq/index.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/index.rst new file mode 100644 index 0000000000..9d5b0d538a --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/index.rst @@ -0,0 +1,21 @@ +Frequently asked questions +========================== + +.. currentmodule:: websockets + +.. admonition:: Many questions asked in websockets' issue tracker are really + about :mod:`asyncio`. + :class: seealso + + Python's documentation about `developing with asyncio`_ is a good + complement. + + .. _developing with asyncio: https://docs.python.org/3/library/asyncio-dev.html + +.. toctree:: + + server + client + common + asyncio + misc diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/faq/misc.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/misc.rst new file mode 100644 index 0000000000..ee5ad23728 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/misc.rst @@ -0,0 +1,49 @@ +Miscellaneous +============= + +.. currentmodule:: websockets + +Why do I get the error: ``module 'websockets' has no attribute '...'``? +....................................................................... + +Often, this is because you created a script called ``websockets.py`` in your +current working directory. Then ``import websockets`` imports this module +instead of the websockets library. + +.. _real-import-paths: + +Why is the default implementation located in ``websockets.legacy``? +................................................................... + +This is an artifact of websockets' history. For its first eight years, only the +:mod:`asyncio` implementation existed. Then, the Sans-I/O implementation was +added. Moving the code in a ``legacy`` submodule eased this refactoring and +optimized maintainability. + +All public APIs were kept at their original locations. ``websockets.legacy`` +isn't a public API. It's only visible in the source code and in stack traces. +There is no intent to deprecate this implementation — at least until a superior +alternative exists. + +Why is websockets slower than another library in my benchmark? +.............................................................. + +Not all libraries are as feature-complete as websockets. For a fair benchmark, +you should disable features that the other library doesn't provide. Typically, +you may need to disable: + +* Compression: set ``compression=None`` +* Keepalive: set ``ping_interval=None`` +* UTF-8 decoding: send ``bytes`` rather than ``str`` + +If websockets is still slower than another Python library, please file a bug. + +Are there ``onopen``, ``onmessage``, ``onerror``, and ``onclose`` callbacks? +............................................................................ + +No, there aren't. + +websockets provides high-level, coroutine-based APIs. Compared to callbacks, +coroutines make it easier to manage control flow in concurrent code. + +If you prefer callback-based APIs, you should use another library. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/faq/server.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/server.rst new file mode 100644 index 0000000000..08b412d306 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/server.rst @@ -0,0 +1,336 @@ +Server +====== + +.. currentmodule:: websockets + +Why does the server close the connection prematurely? +----------------------------------------------------- + +Your connection handler exits prematurely. Wait for the work to be finished +before returning. + +For example, if your handler has a structure similar to:: + + async def handler(websocket): + asyncio.create_task(do_some_work()) + +change it to:: + + async def handler(websocket): + await do_some_work() + +Why does the server close the connection after one message? +----------------------------------------------------------- + +Your connection handler exits after processing one message. Write a loop to +process multiple messages. + +For example, if your handler looks like this:: + + async def handler(websocket): + print(websocket.recv()) + +change it like this:: + + async def handler(websocket): + async for message in websocket: + print(message) + +*Don't feel bad if this happens to you — it's the most common question in +websockets' issue tracker :-)* + +Why can only one client connect at a time? +------------------------------------------ + +Your connection handler blocks the event loop. Look for blocking calls. + +Any call that may take some time must be asynchronous. + +For example, this connection handler prevents the event loop from running during +one second:: + + async def handler(websocket): + time.sleep(1) + ... + +Change it to:: + + async def handler(websocket): + await asyncio.sleep(1) + ... + +In addition, calling a coroutine doesn't guarantee that it will yield control to +the event loop. + +For example, this connection handler blocks the event loop by sending messages +continuously:: + + async def handler(websocket): + while True: + await websocket.send("firehose!") + +:meth:`~legacy.protocol.WebSocketCommonProtocol.send` completes synchronously as +long as there's space in send buffers. The event loop never runs. (This pattern +is uncommon in real-world applications. It occurs mostly in toy programs.) + +You can avoid the issue by yielding control to the event loop explicitly:: + + async def handler(websocket): + while True: + await websocket.send("firehose!") + await asyncio.sleep(0) + +All this is part of learning asyncio. It isn't specific to websockets. + +See also Python's documentation about `running blocking code`_. + +.. _running blocking code: https://docs.python.org/3/library/asyncio-dev.html#running-blocking-code + +.. _send-message-to-all-users: + +How do I send a message to all users? +------------------------------------- + +Record all connections in a global variable:: + + CONNECTIONS = set() + + async def handler(websocket): + CONNECTIONS.add(websocket) + try: + await websocket.wait_closed() + finally: + CONNECTIONS.remove(websocket) + +Then, call :func:`~websockets.broadcast`:: + + import websockets + + def message_all(message): + websockets.broadcast(CONNECTIONS, message) + +If you're running multiple server processes, make sure you call ``message_all`` +in each process. + +.. _send-message-to-single-user: + +How do I send a message to a single user? +----------------------------------------- + +Record connections in a global variable, keyed by user identifier:: + + CONNECTIONS = {} + + async def handler(websocket): + user_id = ... # identify user in your app's context + CONNECTIONS[user_id] = websocket + try: + await websocket.wait_closed() + finally: + del CONNECTIONS[user_id] + +Then, call :meth:`~legacy.protocol.WebSocketCommonProtocol.send`:: + + async def message_user(user_id, message): + websocket = CONNECTIONS[user_id] # raises KeyError if user disconnected + await websocket.send(message) # may raise websockets.ConnectionClosed + +Add error handling according to the behavior you want if the user disconnected +before the message could be sent. + +This example supports only one connection per user. To support concurrent +connections by the same user, you can change ``CONNECTIONS`` to store a set of +connections for each user. + +If you're running multiple server processes, call ``message_user`` in each +process. The process managing the user's connection sends the message; other +processes do nothing. + +When you reach a scale where server processes cannot keep up with the stream of +all messages, you need a better architecture. For example, you could deploy an +external publish / subscribe system such as Redis_. Server processes would +subscribe their clients. Then, they would receive messages only for the +connections that they're managing. + +.. _Redis: https://redis.io/ + +How do I send a message to a channel, a topic, or some users? +------------------------------------------------------------- + +websockets doesn't provide built-in publish / subscribe functionality. + +Record connections in a global variable, keyed by user identifier, as shown in +:ref:`How do I send a message to a single user?<send-message-to-single-user>` + +Then, build the set of recipients and broadcast the message to them, as shown in +:ref:`How do I send a message to all users?<send-message-to-all-users>` + +:doc:`../howto/django` contains a complete implementation of this pattern. + +Again, as you scale, you may reach the performance limits of a basic in-process +implementation. You may need an external publish / subscribe system like Redis_. + +.. _Redis: https://redis.io/ + +How do I pass arguments to the connection handler? +-------------------------------------------------- + +You can bind additional arguments to the connection handler with +:func:`functools.partial`:: + + import asyncio + import functools + import websockets + + async def handler(websocket, extra_argument): + ... + + bound_handler = functools.partial(handler, extra_argument=42) + start_server = websockets.serve(bound_handler, ...) + +Another way to achieve this result is to define the ``handler`` coroutine in +a scope where the ``extra_argument`` variable exists instead of injecting it +through an argument. + +How do I access the request path? +--------------------------------- + +It is available in the :attr:`~server.WebSocketServerProtocol.path` attribute. + +You may route a connection to different handlers depending on the request path:: + + async def handler(websocket): + if websocket.path == "/blue": + await blue_handler(websocket) + elif websocket.path == "/green": + await green_handler(websocket) + else: + # No handler for this path; close the connection. + return + +You may also route the connection based on the first message received from the +client, as shown in the :doc:`tutorial <../intro/tutorial2>`. When you want to +authenticate the connection before routing it, this is usually more convenient. + +Generally speaking, there is far less emphasis on the request path in WebSocket +servers than in HTTP servers. When a WebSocket server provides a single endpoint, +it may ignore the request path entirely. + +How do I access HTTP headers? +----------------------------- + +To access HTTP headers during the WebSocket handshake, you can override +:attr:`~server.WebSocketServerProtocol.process_request`:: + + async def process_request(self, path, request_headers): + authorization = request_headers["Authorization"] + +Once the connection is established, HTTP headers are available in +:attr:`~server.WebSocketServerProtocol.request_headers` and +:attr:`~server.WebSocketServerProtocol.response_headers`:: + + async def handler(websocket): + authorization = websocket.request_headers["Authorization"] + +How do I set HTTP headers? +-------------------------- + +To set the ``Sec-WebSocket-Extensions`` or ``Sec-WebSocket-Protocol`` headers in +the WebSocket handshake response, use the ``extensions`` or ``subprotocols`` +arguments of :func:`~server.serve`. + +To override the ``Server`` header, use the ``server_header`` argument. Set it to +:obj:`None` to remove the header. + +To set other HTTP headers, use the ``extra_headers`` argument. + +How do I get the IP address of the client? +------------------------------------------ + +It's available in :attr:`~legacy.protocol.WebSocketCommonProtocol.remote_address`:: + + async def handler(websocket): + remote_ip = websocket.remote_address[0] + +How do I set the IP addresses that my server listens on? +-------------------------------------------------------- + +Use the ``host`` argument of :meth:`~asyncio.loop.create_server`:: + + await websockets.serve(handler, host="192.168.0.1", port=8080) + +:func:`~server.serve` accepts the same arguments as +:meth:`~asyncio.loop.create_server`. + +What does ``OSError: [Errno 99] error while attempting to bind on address ('::1', 80, 0, 0): address not available`` mean? +-------------------------------------------------------------------------------------------------------------------------- + +You are calling :func:`~server.serve` without a ``host`` argument in a context +where IPv6 isn't available. + +To listen only on IPv4, specify ``host="0.0.0.0"`` or ``family=socket.AF_INET``. + +Refer to the documentation of :meth:`~asyncio.loop.create_server` for details. + +How do I close a connection? +---------------------------- + +websockets takes care of closing the connection when the handler exits. + +How do I stop a server? +----------------------- + +Exit the :func:`~server.serve` context manager. + +Here's an example that terminates cleanly when it receives SIGTERM on Unix: + +.. literalinclude:: ../../example/faq/shutdown_server.py + :emphasize-lines: 12-15,18 + +How do I stop a server while keeping existing connections open? +--------------------------------------------------------------- + +Call the server's :meth:`~server.WebSocketServer.close` method with +``close_connections=False``. + +Here's how to adapt the example just above:: + + async def server(): + ... + + server = await websockets.serve(echo, "localhost", 8765) + await stop + await server.close(close_connections=False) + +How do I implement a health check? +---------------------------------- + +Intercept WebSocket handshake requests with the +:meth:`~server.WebSocketServerProtocol.process_request` hook. + +When a request is sent to the health check endpoint, treat is as an HTTP request +and return a ``(status, headers, body)`` tuple, as in this example: + +.. literalinclude:: ../../example/faq/health_check_server.py + :emphasize-lines: 7-9,18 + +How do I run HTTP and WebSocket servers on the same port? +--------------------------------------------------------- + +You don't. + +HTTP and WebSocket have widely different operational characteristics. Running +them with the same server becomes inconvenient when you scale. + +Providing an HTTP server is out of scope for websockets. It only aims at +providing a WebSocket server. + +There's limited support for returning HTTP responses with the +:attr:`~server.WebSocketServerProtocol.process_request` hook. + +If you need more, pick an HTTP server and run it separately. + +Alternatively, pick an HTTP framework that builds on top of ``websockets`` to +support WebSocket connections, like Sanic_. + +.. _Sanic: https://sanicframework.org/en/ diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/autoreload.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/autoreload.rst new file mode 100644 index 0000000000..fc736a5918 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/autoreload.rst @@ -0,0 +1,31 @@ +Reload on code changes +====================== + +When developing a websockets server, you may run it locally to test changes. +Unfortunately, whenever you want to try a new version of the code, you must +stop the server and restart it, which slows down your development process. + +Web frameworks such as Django or Flask provide a development server that +reloads the application automatically when you make code changes. There is no +such functionality in websockets because it's designed for production rather +than development. + +However, you can achieve the same result easily. + +Install watchdog_ with the ``watchmedo`` shell utility: + +.. code-block:: console + + $ pip install 'watchdog[watchmedo]' + +.. _watchdog: https://pypi.org/project/watchdog/ + +Run your server with ``watchmedo auto-restart``: + +.. code-block:: console + + $ watchmedo auto-restart --pattern "*.py" --recursive --signal SIGTERM \ + python app.py + +This example assumes that the server is defined in a script called ``app.py``. +Adapt it as necessary. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/cheatsheet.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/cheatsheet.rst new file mode 100644 index 0000000000..95b551f673 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/cheatsheet.rst @@ -0,0 +1,87 @@ +Cheat sheet +=========== + +.. currentmodule:: websockets + +Server +------ + +* Write a coroutine that handles a single connection. It receives a WebSocket + protocol instance and the URI path in argument. + + * Call :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` and + :meth:`~legacy.protocol.WebSocketCommonProtocol.send` to receive and send + messages at any time. + + * When :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` or + :meth:`~legacy.protocol.WebSocketCommonProtocol.send` raises + :exc:`~exceptions.ConnectionClosed`, clean up and exit. If you started + other :class:`asyncio.Task`, terminate them before exiting. + + * If you aren't awaiting :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`, + consider awaiting :meth:`~legacy.protocol.WebSocketCommonProtocol.wait_closed` + to detect quickly when the connection is closed. + + * You may :meth:`~legacy.protocol.WebSocketCommonProtocol.ping` or + :meth:`~legacy.protocol.WebSocketCommonProtocol.pong` if you wish but it isn't + needed in general. + +* Create a server with :func:`~server.serve` which is similar to asyncio's + :meth:`~asyncio.loop.create_server`. You can also use it as an asynchronous + context manager. + + * The server takes care of establishing connections, then lets the handler + execute the application logic, and finally closes the connection after the + handler exits normally or with an exception. + + * For advanced customization, you may subclass + :class:`~server.WebSocketServerProtocol` and pass either this subclass or + a factory function as the ``create_protocol`` argument. + +Client +------ + +* Create a client with :func:`~client.connect` which is similar to asyncio's + :meth:`~asyncio.loop.create_connection`. You can also use it as an + asynchronous context manager. + + * For advanced customization, you may subclass + :class:`~client.WebSocketClientProtocol` and pass either this subclass or + a factory function as the ``create_protocol`` argument. + +* Call :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` and + :meth:`~legacy.protocol.WebSocketCommonProtocol.send` to receive and send messages + at any time. + +* You may :meth:`~legacy.protocol.WebSocketCommonProtocol.ping` or + :meth:`~legacy.protocol.WebSocketCommonProtocol.pong` if you wish but it isn't + needed in general. + +* If you aren't using :func:`~client.connect` as a context manager, call + :meth:`~legacy.protocol.WebSocketCommonProtocol.close` to terminate the connection. + +.. _debugging: + +Debugging +--------- + +If you don't understand what websockets is doing, enable logging:: + + import logging + logger = logging.getLogger('websockets') + logger.setLevel(logging.DEBUG) + logger.addHandler(logging.StreamHandler()) + +The logs contain: + +* Exceptions in the connection handler at the ``ERROR`` level +* Exceptions in the opening or closing handshake at the ``INFO`` level +* All frames at the ``DEBUG`` level — this can be very verbose + +If you're new to ``asyncio``, you will certainly encounter issues that are +related to asynchronous programming in general rather than to websockets in +particular. Fortunately Python's official documentation provides advice to +`develop with asyncio`_. Check it out: it's invaluable! + +.. _develop with asyncio: https://docs.python.org/3/library/asyncio-dev.html + diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/django.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/django.rst new file mode 100644 index 0000000000..e3da0a878b --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/django.rst @@ -0,0 +1,294 @@ +Integrate with Django +===================== + +If you're looking at adding real-time capabilities to a Django project with +WebSocket, you have two main options. + +1. Using Django Channels_, a project adding WebSocket to Django, among other + features. This approach is fully supported by Django. However, it requires + switching to a new deployment architecture. + +2. Deploying a separate WebSocket server next to your Django project. This + technique is well suited when you need to add a small set of real-time + features — maybe a notification service — to an HTTP application. + +.. _Channels: https://channels.readthedocs.io/ + +This guide shows how to implement the second technique with websockets. It +assumes familiarity with Django. + +Authenticate connections +------------------------ + +Since the websockets server runs outside of Django, we need to integrate it +with ``django.contrib.auth``. + +We will generate authentication tokens in the Django project. Then we will +send them to the websockets server, where they will authenticate the user. + +Generating a token for the current user and making it available in the browser +is up to you. You could render the token in a template or fetch it with an API +call. + +Refer to the topic guide on :doc:`authentication <../topics/authentication>` +for details on this design. + +Generate tokens +............... + +We want secure, short-lived tokens containing the user ID. We'll rely on +`django-sesame`_, a small library designed exactly for this purpose. + +.. _django-sesame: https://github.com/aaugustin/django-sesame + +Add django-sesame to the dependencies of your Django project, install it, and +configure it in the settings of the project: + +.. code-block:: python + + AUTHENTICATION_BACKENDS = [ + "django.contrib.auth.backends.ModelBackend", + "sesame.backends.ModelBackend", + ] + +(If your project already uses another authentication backend than the default +``"django.contrib.auth.backends.ModelBackend"``, adjust accordingly.) + +You don't need ``"sesame.middleware.AuthenticationMiddleware"``. It is for +authenticating users in the Django server, while we're authenticating them in +the websockets server. + +We'd like our tokens to be valid for 30 seconds. We expect web pages to load +and to establish the WebSocket connection within this delay. Configure +django-sesame accordingly in the settings of your Django project: + +.. code-block:: python + + SESAME_MAX_AGE = 30 + +If you expect your web site to load faster for all clients, a shorter lifespan +is possible. However, in the context of this document, it would make manual +testing more difficult. + +You could also enable single-use tokens. However, this would update the last +login date of the user every time a WebSocket connection is established. This +doesn't seem like a good idea, both in terms of behavior and in terms of +performance. + +Now you can generate tokens in a ``django-admin shell`` as follows: + +.. code-block:: pycon + + >>> from django.contrib.auth import get_user_model + >>> User = get_user_model() + >>> user = User.objects.get(username="<your username>") + >>> from sesame.utils import get_token + >>> get_token(user) + '<your token>' + +Keep this console open: since tokens expire after 30 seconds, you'll have to +generate a new token every time you want to test connecting to the server. + +Validate tokens +............... + +Let's move on to the websockets server. + +Add websockets to the dependencies of your Django project and install it. +Indeed, we're going to reuse the environment of the Django project, so we can +call its APIs in the websockets server. + +Now here's how to implement authentication. + +.. literalinclude:: ../../example/django/authentication.py + +Let's unpack this code. + +We're calling ``django.setup()`` before doing anything with Django because +we're using Django in a `standalone script`_. This assumes that the +``DJANGO_SETTINGS_MODULE`` environment variable is set to the Python path to +your settings module. + +.. _standalone script: https://docs.djangoproject.com/en/stable/topics/settings/#calling-django-setup-is-required-for-standalone-django-usage + +The connection handler reads the first message received from the client, which +is expected to contain a django-sesame token. Then it authenticates the user +with ``get_user()``, the API for `authentication outside a view`_. If +authentication fails, it closes the connection and exits. + +.. _authentication outside a view: https://django-sesame.readthedocs.io/en/stable/howto.html#outside-a-view + +When we call an API that makes a database query such as ``get_user()``, we +wrap the call in :func:`~asyncio.to_thread`. Indeed, the Django ORM doesn't +support asynchronous I/O. It would block the event loop if it didn't run in a +separate thread. :func:`~asyncio.to_thread` is available since Python 3.9. In +earlier versions, use :meth:`~asyncio.loop.run_in_executor` instead. + +Finally, we start a server with :func:`~websockets.server.serve`. + +We're ready to test! + +Save this code to a file called ``authentication.py``, make sure the +``DJANGO_SETTINGS_MODULE`` environment variable is set properly, and start the +websockets server: + +.. code-block:: console + + $ python authentication.py + +Generate a new token — remember, they're only valid for 30 seconds — and use +it to connect to your server. Paste your token and press Enter when you get a +prompt: + +.. code-block:: console + + $ python -m websockets ws://localhost:8888/ + Connected to ws://localhost:8888/ + > <your token> + < Hello <your username>! + Connection closed: 1000 (OK). + +It works! + +If you enter an expired or invalid token, authentication fails and the server +closes the connection: + +.. code-block:: console + + $ python -m websockets ws://localhost:8888/ + Connected to ws://localhost:8888. + > not a token + Connection closed: 1011 (internal error) authentication failed. + +You can also test from a browser by generating a new token and running the +following code in the JavaScript console of the browser: + +.. code-block:: javascript + + websocket = new WebSocket("ws://localhost:8888/"); + websocket.onopen = (event) => websocket.send("<your token>"); + websocket.onmessage = (event) => console.log(event.data); + +If you don't want to import your entire Django project into the websockets +server, you can build a separate Django project with ``django.contrib.auth``, +``django-sesame``, a suitable ``User`` model, and a subset of the settings of +the main project. + +Stream events +------------- + +We can connect and authenticate but our server doesn't do anything useful yet! + +Let's send a message every time a user makes an action in the admin. This +message will be broadcast to all users who can access the model on which the +action was made. This may be used for showing notifications to other users. + +Many use cases for WebSocket with Django follow a similar pattern. + +Set up event bus +................ + +We need a event bus to enable communications between Django and websockets. +Both sides connect permanently to the bus. Then Django writes events and +websockets reads them. For the sake of simplicity, we'll rely on `Redis +Pub/Sub`_. + +.. _Redis Pub/Sub: https://redis.io/topics/pubsub + +The easiest way to add Redis to a Django project is by configuring a cache +backend with `django-redis`_. This library manages connections to Redis +efficiently, persisting them between requests, and provides an API to access +the Redis connection directly. + +.. _django-redis: https://github.com/jazzband/django-redis + +Install Redis, add django-redis to the dependencies of your Django project, +install it, and configure it in the settings of the project: + +.. code-block:: python + + CACHES = { + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": "redis://127.0.0.1:6379/1", + }, + } + +If you already have a default cache, add a new one with a different name and +change ``get_redis_connection("default")`` in the code below to the same name. + +Publish events +.............. + +Now let's write events to the bus. + +Add the following code to a module that is imported when your Django project +starts. Typically, you would put it in a ``signals.py`` module, which you +would import in the ``AppConfig.ready()`` method of one of your apps: + +.. literalinclude:: ../../example/django/signals.py + +This code runs every time the admin saves a ``LogEntry`` object to keep track +of a change. It extracts interesting data, serializes it to JSON, and writes +an event to Redis. + +Let's check that it works: + +.. code-block:: console + + $ redis-cli + 127.0.0.1:6379> SELECT 1 + OK + 127.0.0.1:6379[1]> SUBSCRIBE events + Reading messages... (press Ctrl-C to quit) + 1) "subscribe" + 2) "events" + 3) (integer) 1 + +Leave this command running, start the Django development server and make +changes in the admin: add, modify, or delete objects. You should see +corresponding events published to the ``"events"`` stream. + +Broadcast events +................ + +Now let's turn to reading events and broadcasting them to connected clients. +We need to add several features: + +* Keep track of connected clients so we can broadcast messages. +* Tell which content types the user has permission to view or to change. +* Connect to the message bus and read events. +* Broadcast these events to users who have corresponding permissions. + +Here's a complete implementation. + +.. literalinclude:: ../../example/django/notifications.py + +Since the ``get_content_types()`` function makes a database query, it is +wrapped inside :func:`asyncio.to_thread()`. It runs once when each WebSocket +connection is open; then its result is cached for the lifetime of the +connection. Indeed, running it for each message would trigger database queries +for all connected users at the same time, which would hurt the database. + +The connection handler merely registers the connection in a global variable, +associated to the list of content types for which events should be sent to +that connection, and waits until the client disconnects. + +The ``process_events()`` function reads events from Redis and broadcasts them +to all connections that should receive them. We don't care much if a sending a +notification fails — this happens when a connection drops between the moment +we iterate on connections and the moment the corresponding message is sent — +so we start a task with for each message and forget about it. Also, this means +we're immediately ready to process the next event, even if it takes time to +send a message to a slow client. + +Since Redis can publish a message to multiple subscribers, multiple instances +of this server can safely run in parallel. + +Does it scale? +-------------- + +In theory, given enough servers, this design can scale to a hundred million +clients, since Redis can handle ten thousand servers and each server can +handle ten thousand clients. In practice, you would need a more scalable +message bus before reaching that scale, due to the volume of messages. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/extensions.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/extensions.rst new file mode 100644 index 0000000000..3c8a7d72a6 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/extensions.rst @@ -0,0 +1,30 @@ +Write an extension +================== + +.. currentmodule:: websockets.extensions + +During the opening handshake, WebSocket clients and servers negotiate which +extensions_ will be used with which parameters. Then each frame is processed +by extensions before being sent or after being received. + +.. _extensions: https://www.rfc-editor.org/rfc/rfc6455.html#section-9 + +As a consequence, writing an extension requires implementing several classes: + +* Extension Factory: it negotiates parameters and instantiates the extension. + + Clients and servers require separate extension factories with distinct APIs. + + Extension factories are the public API of an extension. + +* Extension: it decodes incoming frames and encodes outgoing frames. + + If the extension is symmetrical, clients and servers can use the same + class. + + Extensions are initialized by extension factories, so they don't need to be + part of the public API of an extension. + +websockets provides base classes for extension factories and extensions. +See :class:`ClientExtensionFactory`, :class:`ServerExtensionFactory`, +and :class:`Extension` for details. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/fly.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/fly.rst new file mode 100644 index 0000000000..ed001a2aee --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/fly.rst @@ -0,0 +1,177 @@ +Deploy to Fly +================ + +This guide describes how to deploy a websockets server to Fly_. + +.. _Fly: https://fly.io/ + +.. admonition:: The free tier of Fly is sufficient for trying this guide. + :class: tip + + The `free tier`__ include up to three small VMs. This guide uses only one. + + __ https://fly.io/docs/about/pricing/ + +We're going to deploy a very simple app. The process would be identical for a +more realistic app. + +Create application +------------------ + +Here's the implementation of the app, an echo server. Save it in a file called +``app.py``: + +.. literalinclude:: ../../example/deployment/fly/app.py + :language: python + +This app implements typical requirements for running on a Platform as a Service: + +* it provides a health check at ``/healthz``; +* it closes connections and exits cleanly when it receives a ``SIGTERM`` signal. + +Create a ``requirements.txt`` file containing this line to declare a dependency +on websockets: + +.. literalinclude:: ../../example/deployment/fly/requirements.txt + :language: text + +The app is ready. Let's deploy it! + +Deploy application +------------------ + +Follow the instructions__ to install the Fly CLI, if you haven't done that yet. + +__ https://fly.io/docs/hands-on/install-flyctl/ + +Sign up or log in to Fly. + +Launch the app — you'll have to pick a different name because I'm already using +``websockets-echo``: + +.. code-block:: console + + $ fly launch + Creating app in ... + Scanning source code + Detected a Python app + Using the following build configuration: + Builder: paketobuildpacks/builder:base + ? App Name (leave blank to use an auto-generated name): websockets-echo + ? Select organization: ... + ? Select region: ... + Created app websockets-echo in organization ... + Wrote config file fly.toml + ? Would you like to set up a Postgresql database now? No + We have generated a simple Procfile for you. Modify it to fit your needs and run "fly deploy" to deploy your application. + +.. admonition:: This will build the image with a generic buildpack. + :class: tip + + Fly can `build images`__ with a Dockerfile or a buildpack. Here, ``fly + launch`` configures a generic Paketo buildpack. + + If you'd rather package the app with a Dockerfile, check out the guide to + :ref:`containerize an application <containerize-application>`. + + __ https://fly.io/docs/reference/builders/ + +Replace the auto-generated ``fly.toml`` with: + +.. literalinclude:: ../../example/deployment/fly/fly.toml + :language: toml + +This configuration: + +* listens on port 443, terminates TLS, and forwards to the app on port 8080; +* declares a health check at ``/healthz``; +* requests a ``SIGTERM`` for terminating the app. + +Replace the auto-generated ``Procfile`` with: + +.. literalinclude:: ../../example/deployment/fly/Procfile + :language: text + +This tells Fly how to run the app. + +Now you can deploy it: + +.. code-block:: console + + $ fly deploy + + ... lots of output... + + ==> Monitoring deployment + + 1 desired, 1 placed, 1 healthy, 0 unhealthy [health checks: 1 total, 1 passing] + --> v0 deployed successfully + +Validate deployment +------------------- + +Let's confirm that your application is running as expected. + +Since it's a WebSocket server, you need a WebSocket client, such as the +interactive client that comes with websockets. + +If you're currently building a websockets server, perhaps you're already in a +virtualenv where websockets is installed. If not, you can install it in a new +virtualenv as follows: + +.. code-block:: console + + $ python -m venv websockets-client + $ . websockets-client/bin/activate + $ pip install websockets + +Connect the interactive client — you must replace ``websockets-echo`` with the +name of your Fly app in this command: + +.. code-block:: console + + $ python -m websockets wss://websockets-echo.fly.dev/ + Connected to wss://websockets-echo.fly.dev/. + > + +Great! Your app is running! + +Once you're connected, you can send any message and the server will echo it, +or press Ctrl-D to terminate the connection: + +.. code-block:: console + + > Hello! + < Hello! + Connection closed: 1000 (OK). + +You can also confirm that your application shuts down gracefully. + +Connect an interactive client again — remember to replace ``websockets-echo`` +with your app: + +.. code-block:: console + + $ python -m websockets wss://websockets-echo.fly.dev/ + Connected to wss://websockets-echo.fly.dev/. + > + +In another shell, restart the app — again, replace ``websockets-echo`` with your +app: + +.. code-block:: console + + $ fly restart websockets-echo + websockets-echo is being restarted + +Go back to the first shell. The connection is closed with code 1001 (going +away). + +.. code-block:: console + + $ python -m websockets wss://websockets-echo.fly.dev/ + Connected to wss://websockets-echo.fly.dev/. + Connection closed: 1001 (going away). + +If graceful shutdown wasn't working, the server wouldn't perform a closing +handshake and the connection would be closed with code 1006 (abnormal closure). diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/haproxy.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/haproxy.rst new file mode 100644 index 0000000000..fdaab04011 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/haproxy.rst @@ -0,0 +1,61 @@ +Deploy behind HAProxy +===================== + +This guide demonstrates a way to load balance connections across multiple +websockets server processes running on the same machine with HAProxy_. + +We'll run server processes with Supervisor as described in :doc:`this guide +<supervisor>`. + +.. _HAProxy: https://www.haproxy.org/ + +Run server processes +-------------------- + +Save this app to ``app.py``: + +.. literalinclude:: ../../example/deployment/haproxy/app.py + :emphasize-lines: 24 + +Each server process listens on a different port by extracting an incremental +index from an environment variable set by Supervisor. + +Save this configuration to ``supervisord.conf``: + +.. literalinclude:: ../../example/deployment/haproxy/supervisord.conf + +This configuration runs four instances of the app. + +Install Supervisor and run it: + +.. code-block:: console + + $ supervisord -c supervisord.conf -n + +Configure and run HAProxy +------------------------- + +Here's a simple HAProxy configuration to load balance connections across four +processes: + +.. literalinclude:: ../../example/deployment/haproxy/haproxy.cfg + +In the backend configuration, we set the load balancing method to +``leastconn`` in order to balance the number of active connections across +servers. This is best for long running connections. + +Save the configuration to ``haproxy.cfg``, install HAProxy, and run it: + +.. code-block:: console + + $ haproxy -f haproxy.cfg + +You can confirm that HAProxy proxies connections properly: + +.. code-block:: console + + $ PYTHONPATH=src python -m websockets ws://localhost:8080/ + Connected to ws://localhost:8080/. + > Hello! + < Hello! + Connection closed: 1000 (OK). diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/heroku.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/heroku.rst new file mode 100644 index 0000000000..a97d2e7ce0 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/heroku.rst @@ -0,0 +1,183 @@ +Deploy to Heroku +================ + +This guide describes how to deploy a websockets server to Heroku_. The same +principles should apply to other Platform as a Service providers. + +.. _Heroku: https://www.heroku.com/ + +.. admonition:: Heroku no longer offers a free tier. + :class: attention + + When this tutorial was written, in September 2021, Heroku offered a free + tier where a websockets app could run at no cost. In November 2022, Heroku + removed the free tier, making it impossible to maintain this document. As a + consequence, it isn't updated anymore and may be removed in the future. + +We're going to deploy a very simple app. The process would be identical for a +more realistic app. + +Create repository +----------------- + +Deploying to Heroku requires a git repository. Let's initialize one: + +.. code-block:: console + + $ mkdir websockets-echo + $ cd websockets-echo + $ git init -b main + Initialized empty Git repository in websockets-echo/.git/ + $ git commit --allow-empty -m "Initial commit." + [main (root-commit) 1e7947d] Initial commit. + +Create application +------------------ + +Here's the implementation of the app, an echo server. Save it in a file called +``app.py``: + +.. literalinclude:: ../../example/deployment/heroku/app.py + :language: python + +Heroku expects the server to `listen on a specific port`_, which is provided +in the ``$PORT`` environment variable. The app reads it and passes it to +:func:`~websockets.server.serve`. + +.. _listen on a specific port: https://devcenter.heroku.com/articles/preparing-a-codebase-for-heroku-deployment#4-listen-on-the-correct-port + +Heroku sends a ``SIGTERM`` signal to all processes when `shutting down a +dyno`_. When the app receives this signal, it closes connections and exits +cleanly. + +.. _shutting down a dyno: https://devcenter.heroku.com/articles/dynos#shutdown + +Create a ``requirements.txt`` file containing this line to declare a dependency +on websockets: + +.. literalinclude:: ../../example/deployment/heroku/requirements.txt + :language: text + +Create a ``Procfile``. + +.. literalinclude:: ../../example/deployment/heroku/Procfile + +This tells Heroku how to run the app. + +Confirm that you created the correct files and commit them to git: + +.. code-block:: console + + $ ls + Procfile app.py requirements.txt + $ git add . + $ git commit -m "Initial implementation." + [main 8418c62] Initial implementation. + 3 files changed, 32 insertions(+) + create mode 100644 Procfile + create mode 100644 app.py + create mode 100644 requirements.txt + +The app is ready. Let's deploy it! + +Deploy application +------------------ + +Follow the instructions_ to install the Heroku CLI, if you haven't done that +yet. + +.. _instructions: https://devcenter.heroku.com/articles/getting-started-with-python#set-up + +Sign up or log in to Heroku. + +Create a Heroku app — you'll have to pick a different name because I'm already +using ``websockets-echo``: + +.. code-block:: console + + $ heroku create websockets-echo + Creating ⬢ websockets-echo... done + https://websockets-echo.herokuapp.com/ | https://git.heroku.com/websockets-echo.git + +.. code-block:: console + + $ git push heroku + + ... lots of output... + + remote: -----> Launching... + remote: Released v1 + remote: https://websockets-echo.herokuapp.com/ deployed to Heroku + remote: + remote: Verifying deploy... done. + To https://git.heroku.com/websockets-echo.git + * [new branch] main -> main + +Validate deployment +------------------- + +Let's confirm that your application is running as expected. + +Since it's a WebSocket server, you need a WebSocket client, such as the +interactive client that comes with websockets. + +If you're currently building a websockets server, perhaps you're already in a +virtualenv where websockets is installed. If not, you can install it in a new +virtualenv as follows: + +.. code-block:: console + + $ python -m venv websockets-client + $ . websockets-client/bin/activate + $ pip install websockets + +Connect the interactive client — you must replace ``websockets-echo`` with the +name of your Heroku app in this command: + +.. code-block:: console + + $ python -m websockets wss://websockets-echo.herokuapp.com/ + Connected to wss://websockets-echo.herokuapp.com/. + > + +Great! Your app is running! + +Once you're connected, you can send any message and the server will echo it, +or press Ctrl-D to terminate the connection: + +.. code-block:: console + + > Hello! + < Hello! + Connection closed: 1000 (OK). + +You can also confirm that your application shuts down gracefully. + +Connect an interactive client again — remember to replace ``websockets-echo`` +with your app: + +.. code-block:: console + + $ python -m websockets wss://websockets-echo.herokuapp.com/ + Connected to wss://websockets-echo.herokuapp.com/. + > + +In another shell, restart the app — again, replace ``websockets-echo`` with your +app: + +.. code-block:: console + + $ heroku dyno:restart -a websockets-echo + Restarting dynos on ⬢ websockets-echo... done + +Go back to the first shell. The connection is closed with code 1001 (going +away). + +.. code-block:: console + + $ python -m websockets wss://websockets-echo.herokuapp.com/ + Connected to wss://websockets-echo.herokuapp.com/. + Connection closed: 1001 (going away). + +If graceful shutdown wasn't working, the server wouldn't perform a closing +handshake and the connection would be closed with code 1006 (abnormal closure). diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/index.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/index.rst new file mode 100644 index 0000000000..ddbe67d3ae --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/index.rst @@ -0,0 +1,56 @@ +How-to guides +============= + +In a hurry? Check out these examples. + +.. toctree:: + :titlesonly: + + quickstart + +If you're stuck, perhaps you'll find the answer here. + +.. toctree:: + :titlesonly: + + cheatsheet + patterns + autoreload + +This guide will help you integrate websockets into a broader system. + +.. toctree:: + :titlesonly: + + django + +The WebSocket protocol makes provisions for extending or specializing its +features, which websockets supports fully. + +.. toctree:: + :titlesonly: + + extensions + +.. _deployment-howto: + +Once your application is ready, learn how to deploy it on various platforms. + +.. toctree:: + :titlesonly: + + render + fly + heroku + kubernetes + supervisor + nginx + haproxy + +If you're integrating the Sans-I/O layer of websockets into a library, rather +than building an application with websockets, follow this guide. + +.. toctree:: + :maxdepth: 2 + + sansio diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/kubernetes.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/kubernetes.rst new file mode 100644 index 0000000000..064a6ac4d5 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/kubernetes.rst @@ -0,0 +1,215 @@ +Deploy to Kubernetes +==================== + +This guide describes how to deploy a websockets server to Kubernetes_. It +assumes familiarity with Docker and Kubernetes. + +We're going to deploy a simple app to a local Kubernetes cluster and to ensure +that it scales as expected. + +In a more realistic context, you would follow your organization's practices +for deploying to Kubernetes, but you would apply the same principles as far as +websockets is concerned. + +.. _Kubernetes: https://kubernetes.io/ + +.. _containerize-application: + +Containerize application +------------------------ + +Here's the app we're going to deploy. Save it in a file called +``app.py``: + +.. literalinclude:: ../../example/deployment/kubernetes/app.py + +This is an echo server with one twist: every message blocks the server for +100ms, which creates artificial starvation of CPU time. This makes it easier +to saturate the server for load testing. + +The app exposes a health check on ``/healthz``. It also provides two other +endpoints for testing purposes: ``/inemuri`` will make the app unresponsive +for 10 seconds and ``/seppuku`` will terminate it. + +The quest for the perfect Python container image is out of scope of this +guide, so we'll go for the simplest possible configuration instead: + +.. literalinclude:: ../../example/deployment/kubernetes/Dockerfile + +After saving this ``Dockerfile``, build the image: + +.. code-block:: console + + $ docker build -t websockets-test:1.0 . + +Test your image by running: + +.. code-block:: console + + $ docker run --name run-websockets-test --publish 32080:80 --rm \ + websockets-test:1.0 + +Then, in another shell, in a virtualenv where websockets is installed, connect +to the app and check that it echoes anything you send: + +.. code-block:: console + + $ python -m websockets ws://localhost:32080/ + Connected to ws://localhost:32080/. + > Hey there! + < Hey there! + > + +Now, in yet another shell, stop the app with: + +.. code-block:: console + + $ docker kill -s TERM run-websockets-test + +Going to the shell where you connected to the app, you can confirm that it +shut down gracefully: + +.. code-block:: console + + $ python -m websockets ws://localhost:32080/ + Connected to ws://localhost:32080/. + > Hey there! + < Hey there! + Connection closed: 1001 (going away). + +If it didn't, you'd get code 1006 (abnormal closure). + +Deploy application +------------------ + +Configuring Kubernetes is even further beyond the scope of this guide, so +we'll use a basic configuration for testing, with just one Service_ and one +Deployment_: + +.. literalinclude:: ../../example/deployment/kubernetes/deployment.yaml + +For local testing, a service of type NodePort_ is good enough. For deploying +to production, you would configure an Ingress_. + +.. _Service: https://kubernetes.io/docs/concepts/services-networking/service/ +.. _Deployment: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/ +.. _NodePort: https://kubernetes.io/docs/concepts/services-networking/service/#nodeport +.. _Ingress: https://kubernetes.io/docs/concepts/services-networking/ingress/ + +After saving this to a file called ``deployment.yaml``, you can deploy: + +.. code-block:: console + + $ kubectl apply -f deployment.yaml + service/websockets-test created + deployment.apps/websockets-test created + +Now you have a deployment with one pod running: + +.. code-block:: console + + $ kubectl get deployment websockets-test + NAME READY UP-TO-DATE AVAILABLE AGE + websockets-test 1/1 1 1 10s + $ kubectl get pods -l app=websockets-test + NAME READY STATUS RESTARTS AGE + websockets-test-86b48f4bb7-nltfh 1/1 Running 0 10s + +You can connect to the service — press Ctrl-D to exit: + +.. code-block:: console + + $ python -m websockets ws://localhost:32080/ + Connected to ws://localhost:32080/. + Connection closed: 1000 (OK). + +Validate deployment +------------------- + +First, let's ensure the liveness probe works by making the app unresponsive: + +.. code-block:: console + + $ curl http://localhost:32080/inemuri + Sleeping for 10s + +Since we have only one pod, we know that this pod will go to sleep. + +The liveness probe is configured to run every second. By default, liveness +probes time out after one second and have a threshold of three failures. +Therefore Kubernetes should restart the pod after at most 5 seconds. + +Indeed, after a few seconds, the pod reports a restart: + +.. code-block:: console + + $ kubectl get pods -l app=websockets-test + NAME READY STATUS RESTARTS AGE + websockets-test-86b48f4bb7-nltfh 1/1 Running 1 42s + +Next, let's take it one step further and crash the app: + +.. code-block:: console + + $ curl http://localhost:32080/seppuku + Terminating + +The pod reports a second restart: + +.. code-block:: console + + $ kubectl get pods -l app=websockets-test + NAME READY STATUS RESTARTS AGE + websockets-test-86b48f4bb7-nltfh 1/1 Running 2 72s + +All good — Kubernetes delivers on its promise to keep our app alive! + +Scale deployment +---------------- + +Of course, Kubernetes is for scaling. Let's scale — modestly — to 10 pods: + +.. code-block:: console + + $ kubectl scale deployment.apps/websockets-test --replicas=10 + deployment.apps/websockets-test scaled + +After a few seconds, we have 10 pods running: + +.. code-block:: console + + $ kubectl get deployment websockets-test + NAME READY UP-TO-DATE AVAILABLE AGE + websockets-test 10/10 10 10 10m + +Now let's generate load. We'll use this script: + +.. literalinclude:: ../../example/deployment/kubernetes/benchmark.py + +We'll connect 500 clients in parallel, meaning 50 clients per pod, and have +each client send 6 messages. Since the app blocks for 100ms before responding, +if connections are perfectly distributed, we expect a total run time slightly +over 50 * 6 * 0.1 = 30 seconds. + +Let's try it: + +.. code-block:: console + + $ ulimit -n 512 + $ time python benchmark.py 500 6 + python benchmark.py 500 6 2.40s user 0.51s system 7% cpu 36.471 total + +A total runtime of 36 seconds is in the right ballpark. Repeating this +experiment with other parameters shows roughly consistent results, with the +high variability you'd expect from a quick benchmark without any effort to +stabilize the test setup. + +Finally, we can scale back to one pod. + +.. code-block:: console + + $ kubectl scale deployment.apps/websockets-test --replicas=1 + deployment.apps/websockets-test scaled + $ kubectl get deployment websockets-test + NAME READY UP-TO-DATE AVAILABLE AGE + websockets-test 1/1 1 1 15m diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/nginx.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/nginx.rst new file mode 100644 index 0000000000..30545fbc7d --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/nginx.rst @@ -0,0 +1,84 @@ +Deploy behind nginx +=================== + +This guide demonstrates a way to load balance connections across multiple +websockets server processes running on the same machine with nginx_. + +We'll run server processes with Supervisor as described in :doc:`this guide +<supervisor>`. + +.. _nginx: https://nginx.org/ + +Run server processes +-------------------- + +Save this app to ``app.py``: + +.. literalinclude:: ../../example/deployment/nginx/app.py + :emphasize-lines: 21,23 + +We'd like to nginx to connect to websockets servers via Unix sockets in order +to avoid the overhead of TCP for communicating between processes running in +the same OS. + +We start the app with :func:`~websockets.server.unix_serve`. Each server +process listens on a different socket thanks to an environment variable set +by Supervisor to a different value. + +Save this configuration to ``supervisord.conf``: + +.. literalinclude:: ../../example/deployment/nginx/supervisord.conf + +This configuration runs four instances of the app. + +Install Supervisor and run it: + +.. code-block:: console + + $ supervisord -c supervisord.conf -n + +Configure and run nginx +----------------------- + +Here's a simple nginx configuration to load balance connections across four +processes: + +.. literalinclude:: ../../example/deployment/nginx/nginx.conf + +We set ``daemon off`` so we can run nginx in the foreground for testing. + +Then we combine the `WebSocket proxying`_ and `load balancing`_ guides: + +* The WebSocket protocol requires HTTP/1.1. We must set the HTTP protocol + version to 1.1, else nginx defaults to HTTP/1.0 for proxying. + +* The WebSocket handshake involves the ``Connection`` and ``Upgrade`` HTTP + headers. We must pass them to the upstream explicitly, else nginx drops + them because they're hop-by-hop headers. + + We deviate from the `WebSocket proxying`_ guide because its example adds a + ``Connection: Upgrade`` header to every upstream request, even if the + original request didn't contain that header. + +* In the upstream configuration, we set the load balancing method to + ``least_conn`` in order to balance the number of active connections across + servers. This is best for long running connections. + +.. _WebSocket proxying: http://nginx.org/en/docs/http/websocket.html +.. _load balancing: http://nginx.org/en/docs/http/load_balancing.html + +Save the configuration to ``nginx.conf``, install nginx, and run it: + +.. code-block:: console + + $ nginx -c nginx.conf -p . + +You can confirm that nginx proxies connections properly: + +.. code-block:: console + + $ PYTHONPATH=src python -m websockets ws://localhost:8080/ + Connected to ws://localhost:8080/. + > Hello! + < Hello! + Connection closed: 1000 (OK). diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/patterns.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/patterns.rst new file mode 100644 index 0000000000..c6f325d213 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/patterns.rst @@ -0,0 +1,110 @@ +Patterns +======== + +.. currentmodule:: websockets + +Here are typical patterns for processing messages in a WebSocket server or +client. You will certainly implement some of them in your application. + +This page gives examples of connection handlers for a server. However, they're +also applicable to a client, simply by assuming that ``websocket`` is a +connection created with :func:`~client.connect`. + +WebSocket connections are long-lived. You will usually write a loop to process +several messages during the lifetime of a connection. + +Consumer +-------- + +To receive messages from the WebSocket connection:: + + async def consumer_handler(websocket): + async for message in websocket: + await consumer(message) + +In this example, ``consumer()`` is a coroutine implementing your business +logic for processing a message received on the WebSocket connection. Each +message may be :class:`str` or :class:`bytes`. + +Iteration terminates when the client disconnects. + +Producer +-------- + +To send messages to the WebSocket connection:: + + async def producer_handler(websocket): + while True: + message = await producer() + await websocket.send(message) + +In this example, ``producer()`` is a coroutine implementing your business +logic for generating the next message to send on the WebSocket connection. +Each message must be :class:`str` or :class:`bytes`. + +Iteration terminates when the client disconnects +because :meth:`~server.WebSocketServerProtocol.send` raises a +:exc:`~exceptions.ConnectionClosed` exception, +which breaks out of the ``while True`` loop. + +Consumer and producer +--------------------- + +You can receive and send messages on the same WebSocket connection by +combining the consumer and producer patterns. This requires running two tasks +in parallel:: + + async def handler(websocket): + await asyncio.gather( + consumer_handler(websocket), + producer_handler(websocket), + ) + +If a task terminates, :func:`~asyncio.gather` doesn't cancel the other task. +This can result in a situation where the producer keeps running after the +consumer finished, which may leak resources. + +Here's a way to exit and close the WebSocket connection as soon as a task +terminates, after canceling the other task:: + + async def handler(websocket): + consumer_task = asyncio.create_task(consumer_handler(websocket)) + producer_task = asyncio.create_task(producer_handler(websocket)) + done, pending = await asyncio.wait( + [consumer_task, producer_task], + return_when=asyncio.FIRST_COMPLETED, + ) + for task in pending: + task.cancel() + +Registration +------------ + +To keep track of currently connected clients, you can register them when they +connect and unregister them when they disconnect:: + + connected = set() + + async def handler(websocket): + # Register. + connected.add(websocket) + try: + # Broadcast a message to all connected clients. + websockets.broadcast(connected, "Hello!") + await asyncio.sleep(10) + finally: + # Unregister. + connected.remove(websocket) + +This example maintains the set of connected clients in memory. This works as +long as you run a single process. It doesn't scale to multiple processes. + +Publish–subscribe +----------------- + +If you plan to run multiple processes and you want to communicate updates +between processes, then you must deploy a messaging system. You may find +publish-subscribe functionality useful. + +A complete implementation of this idea with Redis is described in +the :doc:`Django integration guide <../howto/django>`. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/quickstart.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/quickstart.rst new file mode 100644 index 0000000000..ab870952c1 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/quickstart.rst @@ -0,0 +1,170 @@ +Quick start +=========== + +.. currentmodule:: websockets + +Here are a few examples to get you started quickly with websockets. + +Say "Hello world!" +------------------ + +Here's a WebSocket server. + +It receives a name from the client, sends a greeting, and closes the connection. + +.. literalinclude:: ../../example/quickstart/server.py + :caption: server.py + :language: python + :linenos: + +:func:`~server.serve` executes the connection handler coroutine ``hello()`` +once for each WebSocket connection. It closes the WebSocket connection when +the handler returns. + +Here's a corresponding WebSocket client. + +It sends a name to the server, receives a greeting, and closes the connection. + +.. literalinclude:: ../../example/quickstart/client.py + :caption: client.py + :language: python + :linenos: + +Using :func:`~client.connect` as an asynchronous context manager ensures the +WebSocket connection is closed. + +.. _secure-server-example: + +Encrypt connections +------------------- + +Secure WebSocket connections improve confidentiality and also reliability +because they reduce the risk of interference by bad proxies. + +The ``wss`` protocol is to ``ws`` what ``https`` is to ``http``. The +connection is encrypted with TLS_ (Transport Layer Security). ``wss`` +requires certificates like ``https``. + +.. _TLS: https://developer.mozilla.org/en-US/docs/Web/Security/Transport_Layer_Security + +.. admonition:: TLS vs. SSL + :class: tip + + TLS is sometimes referred to as SSL (Secure Sockets Layer). SSL was an + earlier encryption protocol; the name stuck. + +Here's how to adapt the server to encrypt connections. You must download +:download:`localhost.pem <../../example/quickstart/localhost.pem>` and save it +in the same directory as ``server_secure.py``. + +.. literalinclude:: ../../example/quickstart/server_secure.py + :caption: server_secure.py + :language: python + :linenos: + +Here's how to adapt the client similarly. + +.. literalinclude:: ../../example/quickstart/client_secure.py + :caption: client_secure.py + :language: python + :linenos: + +In this example, the client needs a TLS context because the server uses a +self-signed certificate. + +When connecting to a secure WebSocket server with a valid certificate — any +certificate signed by a CA that your Python installation trusts — you can +simply pass ``ssl=True`` to :func:`~client.connect`. + +.. admonition:: Configure the TLS context securely + :class: attention + + This example demonstrates the ``ssl`` argument with a TLS certificate shared + between the client and the server. This is a simplistic setup. + + Please review the advice and security considerations in the documentation of + the :mod:`ssl` module to configure the TLS context securely. + +Connect from a browser +---------------------- + +The WebSocket protocol was invented for the web — as the name says! + +Here's how to connect to a WebSocket server from a browser. + +Run this script in a console: + +.. literalinclude:: ../../example/quickstart/show_time.py + :caption: show_time.py + :language: python + :linenos: + +Save this file as ``show_time.html``: + +.. literalinclude:: ../../example/quickstart/show_time.html + :caption: show_time.html + :language: html + :linenos: + +Save this file as ``show_time.js``: + +.. literalinclude:: ../../example/quickstart/show_time.js + :caption: show_time.js + :language: js + :linenos: + +Then, open ``show_time.html`` in several browsers. Clocks tick irregularly. + +Broadcast messages +------------------ + +Let's change the previous example to send the same timestamps to all browsers, +instead of generating independent sequences for each client. + +Stop the previous script if it's still running and run this script in a console: + +.. literalinclude:: ../../example/quickstart/show_time_2.py + :caption: show_time_2.py + :language: python + :linenos: + +Refresh ``show_time.html`` in all browsers. Clocks tick in sync. + +Manage application state +------------------------ + +A WebSocket server can receive events from clients, process them to update the +application state, and broadcast the updated state to all connected clients. + +Here's an example where any client can increment or decrement a counter. The +concurrency model of :mod:`asyncio` guarantees that updates are serialized. + +Run this script in a console: + +.. literalinclude:: ../../example/quickstart/counter.py + :caption: counter.py + :language: python + :linenos: + +Save this file as ``counter.html``: + +.. literalinclude:: ../../example/quickstart/counter.html + :caption: counter.html + :language: html + :linenos: + +Save this file as ``counter.css``: + +.. literalinclude:: ../../example/quickstart/counter.css + :caption: counter.css + :language: css + :linenos: + +Save this file as ``counter.js``: + +.. literalinclude:: ../../example/quickstart/counter.js + :caption: counter.js + :language: js + :linenos: + +Then open ``counter.html`` file in several browsers and play with [+] and [-]. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/render.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/render.rst new file mode 100644 index 0000000000..70bf8c376c --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/render.rst @@ -0,0 +1,172 @@ +Deploy to Render +================ + +This guide describes how to deploy a websockets server to Render_. + +.. _Render: https://render.com/ + +.. admonition:: The free plan of Render is sufficient for trying this guide. + :class: tip + + However, on a `free plan`__, connections are dropped after five minutes, + which is quite short for WebSocket application. + + __ https://render.com/docs/free + +We're going to deploy a very simple app. The process would be identical for a +more realistic app. + +Create repository +----------------- + +Deploying to Render requires a git repository. Let's initialize one: + +.. code-block:: console + + $ mkdir websockets-echo + $ cd websockets-echo + $ git init -b main + Initialized empty Git repository in websockets-echo/.git/ + $ git commit --allow-empty -m "Initial commit." + [main (root-commit) 816c3b1] Initial commit. + +Render requires the git repository to be hosted at GitHub or GitLab. + +Sign up or log in to GitHub. Create a new repository named ``websockets-echo``. +Don't enable any of the initialization options offered by GitHub. Then, follow +instructions for pushing an existing repository from the command line. + +After pushing, refresh your repository's homepage on GitHub. You should see an +empty repository with an empty initial commit. + +Create application +------------------ + +Here's the implementation of the app, an echo server. Save it in a file called +``app.py``: + +.. literalinclude:: ../../example/deployment/render/app.py + :language: python + +This app implements requirements for `zero downtime deploys`_: + +* it provides a health check at ``/healthz``; +* it closes connections and exits cleanly when it receives a ``SIGTERM`` signal. + +.. _zero downtime deploys: https://render.com/docs/deploys#zero-downtime-deploys + +Create a ``requirements.txt`` file containing this line to declare a dependency +on websockets: + +.. literalinclude:: ../../example/deployment/render/requirements.txt + :language: text + +Confirm that you created the correct files and commit them to git: + +.. code-block:: console + + $ ls + app.py requirements.txt + $ git add . + $ git commit -m "Initial implementation." + [main f26bf7f] Initial implementation. + 2 files changed, 37 insertions(+) + create mode 100644 app.py + create mode 100644 requirements.txt + +Push the changes to GitHub: + +.. code-block:: console + + $ git push + ... + To github.com:<username>/websockets-echo.git + 816c3b1..f26bf7f main -> main + +The app is ready. Let's deploy it! + +Deploy application +------------------ + +Sign up or log in to Render. + +Create a new web service. Connect the git repository that you just created. + +Then, finalize the configuration of your app as follows: + +* **Name**: websockets-echo +* **Start Command**: ``python app.py`` + +If you're just experimenting, select the free plan. Create the web service. + +To configure the health check, go to Settings, scroll down to Health & Alerts, +and set: + +* **Health Check Path**: /healthz + +This triggers a new deployment. + +Validate deployment +------------------- + +Let's confirm that your application is running as expected. + +Since it's a WebSocket server, you need a WebSocket client, such as the +interactive client that comes with websockets. + +If you're currently building a websockets server, perhaps you're already in a +virtualenv where websockets is installed. If not, you can install it in a new +virtualenv as follows: + +.. code-block:: console + + $ python -m venv websockets-client + $ . websockets-client/bin/activate + $ pip install websockets + +Connect the interactive client — you must replace ``websockets-echo`` with the +name of your Render app in this command: + +.. code-block:: console + + $ python -m websockets wss://websockets-echo.onrender.com/ + Connected to wss://websockets-echo.onrender.com/. + > + +Great! Your app is running! + +Once you're connected, you can send any message and the server will echo it, +or press Ctrl-D to terminate the connection: + +.. code-block:: console + + > Hello! + < Hello! + Connection closed: 1000 (OK). + +You can also confirm that your application shuts down gracefully when you deploy +a new version. Due to limitations of Render's free plan, you must upgrade to a +paid plan before you perform this test. + +Connect an interactive client again — remember to replace ``websockets-echo`` +with your app: + +.. code-block:: console + + $ python -m websockets wss://websockets-echo.onrender.com/ + Connected to wss://websockets-echo.onrender.com/. + > + +Trigger a new deployment with Manual Deploy > Deploy latest commit. When the +deployment completes, the connection is closed with code 1001 (going away). + +.. code-block:: console + + $ python -m websockets wss://websockets-echo.onrender.com/ + Connected to wss://websockets-echo.onrender.com/. + Connection closed: 1001 (going away). + +If graceful shutdown wasn't working, the server wouldn't perform a closing +handshake and the connection would be closed with code 1006 (abnormal closure). + +Remember to downgrade to a free plan if you upgraded just for testing this feature. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/sansio.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/sansio.rst new file mode 100644 index 0000000000..d41519ff09 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/sansio.rst @@ -0,0 +1,322 @@ +Integrate the Sans-I/O layer +============================ + +.. currentmodule:: websockets + +This guide explains how to integrate the `Sans-I/O`_ layer of websockets to +add support for WebSocket in another library. + +.. _Sans-I/O: https://sans-io.readthedocs.io/ + +As a prerequisite, you should decide how you will handle network I/O and +asynchronous control flow. + +Your integration layer will provide an API for the application on one side, +will talk to the network on the other side, and will rely on websockets to +implement the protocol in the middle. + +.. image:: ../topics/data-flow.svg + :align: center + +Opening a connection +-------------------- + +Client-side +........... + +If you're building a client, parse the URI you'd like to connect to:: + + from websockets.uri import parse_uri + + wsuri = parse_uri("ws://example.com/") + +Open a TCP connection to ``(wsuri.host, wsuri.port)`` and perform a TLS +handshake if ``wsuri.secure`` is :obj:`True`. + +Initialize a :class:`~client.ClientProtocol`:: + + from websockets.client import ClientProtocol + + protocol = ClientProtocol(wsuri) + +Create a WebSocket handshake request +with :meth:`~client.ClientProtocol.connect` and send it +with :meth:`~client.ClientProtocol.send_request`:: + + request = protocol.connect() + protocol.send_request(request) + +Then, call :meth:`~protocol.Protocol.data_to_send` and send its output to +the network, as described in `Send data`_ below. + +Once you receive enough data, as explained in `Receive data`_ below, the first +event returned by :meth:`~protocol.Protocol.events_received` is the WebSocket +handshake response. + +When the handshake fails, the reason is available in +:attr:`~client.ClientProtocol.handshake_exc`:: + + if protocol.handshake_exc is not None: + raise protocol.handshake_exc + +Else, the WebSocket connection is open. + +A WebSocket client API usually performs the handshake then returns a wrapper +around the network socket and the :class:`~client.ClientProtocol`. + +Server-side +........... + +If you're building a server, accept network connections from clients and +perform a TLS handshake if desired. + +For each connection, initialize a :class:`~server.ServerProtocol`:: + + from websockets.server import ServerProtocol + + protocol = ServerProtocol() + +Once you receive enough data, as explained in `Receive data`_ below, the first +event returned by :meth:`~protocol.Protocol.events_received` is the WebSocket +handshake request. + +Create a WebSocket handshake response +with :meth:`~server.ServerProtocol.accept` and send it +with :meth:`~server.ServerProtocol.send_response`:: + + response = protocol.accept(request) + protocol.send_response(response) + +Alternatively, you may reject the WebSocket handshake and return an HTTP +response with :meth:`~server.ServerProtocol.reject`:: + + response = protocol.reject(status, explanation) + protocol.send_response(response) + +Then, call :meth:`~protocol.Protocol.data_to_send` and send its output to +the network, as described in `Send data`_ below. + +Even when you call :meth:`~server.ServerProtocol.accept`, the WebSocket +handshake may fail if the request is incorrect or unsupported. + +When the handshake fails, the reason is available in +:attr:`~server.ServerProtocol.handshake_exc`:: + + if protocol.handshake_exc is not None: + raise protocol.handshake_exc + +Else, the WebSocket connection is open. + +A WebSocket server API usually builds a wrapper around the network socket and +the :class:`~server.ServerProtocol`. Then it invokes a connection handler that +accepts the wrapper in argument. + +It may also provide a way to close all connections and to shut down the server +gracefully. + +Going forwards, this guide focuses on handling an individual connection. + +From the network to the application +----------------------------------- + +Go through the five steps below until you reach the end of the data stream. + +Receive data +............ + +When receiving data from the network, feed it to the protocol's +:meth:`~protocol.Protocol.receive_data` method. + +When reaching the end of the data stream, call the protocol's +:meth:`~protocol.Protocol.receive_eof` method. + +For example, if ``sock`` is a :obj:`~socket.socket`:: + + try: + data = sock.recv(65536) + except OSError: # socket closed + data = b"" + if data: + protocol.receive_data(data) + else: + protocol.receive_eof() + +These methods aren't expected to raise exceptions — unless you call them again +after calling :meth:`~protocol.Protocol.receive_eof`, which is an error. +(If you get an exception, please file a bug!) + +Send data +......... + +Then, call :meth:`~protocol.Protocol.data_to_send` and send its output to +the network:: + + for data in protocol.data_to_send(): + if data: + sock.sendall(data) + else: + sock.shutdown(socket.SHUT_WR) + +The empty bytestring signals the end of the data stream. When you see it, you +must half-close the TCP connection. + +Sending data right after receiving data is necessary because websockets +responds to ping frames, close frames, and incorrect inputs automatically. + +Expect TCP connection to close +.............................. + +Closing a WebSocket connection normally involves a two-way WebSocket closing +handshake. Then, regardless of whether the closure is normal or abnormal, the +server starts the four-way TCP closing handshake. If the network fails at the +wrong point, you can end up waiting until the TCP timeout, which is very long. + +To prevent dangling TCP connections when you expect the end of the data stream +but you never reach it, call :meth:`~protocol.Protocol.close_expected` +and, if it returns :obj:`True`, schedule closing the TCP connection after a +short timeout:: + + # start a new execution thread to run this code + sleep(10) + sock.close() # does nothing if the socket is already closed + +If the connection is still open when the timeout elapses, closing the socket +makes the execution thread that reads from the socket reach the end of the +data stream, possibly with an exception. + +Close TCP connection +.................... + +If you called :meth:`~protocol.Protocol.receive_eof`, close the TCP +connection now. This is a clean closure because the receive buffer is empty. + +After :meth:`~protocol.Protocol.receive_eof` signals the end of the read +stream, :meth:`~protocol.Protocol.data_to_send` always signals the end of +the write stream, unless it already ended. So, at this point, the TCP +connection is already half-closed. The only reason for closing it now is to +release resources related to the socket. + +Now you can exit the loop relaying data from the network to the application. + +Receive events +.............. + +Finally, call :meth:`~protocol.Protocol.events_received` to obtain events +parsed from the data provided to :meth:`~protocol.Protocol.receive_data`:: + + events = connection.events_received() + +The first event will be the WebSocket opening handshake request or response. +See `Opening a connection`_ above for details. + +All later events are WebSocket frames. There are two types of frames: + +* Data frames contain messages transferred over the WebSocket connections. You + should provide them to the application. See `Fragmentation`_ below for + how to reassemble messages from frames. +* Control frames provide information about the connection's state. The main + use case is to expose an abstraction over ping and pong to the application. + Keep in mind that websockets responds to ping frames and close frames + automatically. Don't duplicate this functionality! + +From the application to the network +----------------------------------- + +The connection object provides one method for each type of WebSocket frame. + +For sending a data frame: + +* :meth:`~protocol.Protocol.send_continuation` +* :meth:`~protocol.Protocol.send_text` +* :meth:`~protocol.Protocol.send_binary` + +These methods raise :exc:`~exceptions.ProtocolError` if you don't set +the :attr:`FIN <websockets.frames.Frame.fin>` bit correctly in fragmented +messages. + +For sending a control frame: + +* :meth:`~protocol.Protocol.send_close` +* :meth:`~protocol.Protocol.send_ping` +* :meth:`~protocol.Protocol.send_pong` + +:meth:`~protocol.Protocol.send_close` initiates the closing handshake. +See `Closing a connection`_ below for details. + +If you encounter an unrecoverable error and you must fail the WebSocket +connection, call :meth:`~protocol.Protocol.fail`. + +After any of the above, call :meth:`~protocol.Protocol.data_to_send` and +send its output to the network, as shown in `Send data`_ above. + +If you called :meth:`~protocol.Protocol.send_close` +or :meth:`~protocol.Protocol.fail`, you expect the end of the data +stream. You should follow the process described in `Close TCP connection`_ +above in order to prevent dangling TCP connections. + +Closing a connection +-------------------- + +Under normal circumstances, when a server wants to close the TCP connection: + +* it closes the write side; +* it reads until the end of the stream, because it expects the client to close + the read side; +* it closes the socket. + +When a client wants to close the TCP connection: + +* it reads until the end of the stream, because it expects the server to close + the read side; +* it closes the write side; +* it closes the socket. + +Applying the rules described earlier in this document gives the intended +result. As a reminder, the rules are: + +* When :meth:`~protocol.Protocol.data_to_send` returns the empty + bytestring, close the write side of the TCP connection. +* When you reach the end of the read stream, close the TCP connection. +* When :meth:`~protocol.Protocol.close_expected` returns :obj:`True`, if + you don't reach the end of the read stream quickly, close the TCP connection. + +Fragmentation +------------- + +WebSocket messages may be fragmented. Since this is a protocol-level concern, +you may choose to reassemble fragmented messages before handing them over to +the application. + +To reassemble a message, read data frames until you get a frame where +the :attr:`FIN <websockets.frames.Frame.fin>` bit is set, then concatenate +the payloads of all frames. + +You will never receive an inconsistent sequence of frames because websockets +raises a :exc:`~exceptions.ProtocolError` and fails the connection when this +happens. However, you may receive an incomplete sequence if the connection +drops in the middle of a fragmented message. + +Tips +---- + +Serialize operations +.................... + +The Sans-I/O layer expects to run sequentially. If your interact with it from +multiple threads or coroutines, you must ensure correct serialization. This +should happen automatically in a cooperative multitasking environment. + +However, you still have to make sure you don't break this property by +accident. For example, serialize writes to the network +when :meth:`~protocol.Protocol.data_to_send` returns multiple values to +prevent concurrent writes from interleaving incorrectly. + +Avoid buffers +............. + +The Sans-I/O layer doesn't do any buffering. It makes events available in +:meth:`~protocol.Protocol.events_received` as soon as they're received. + +You should make incoming messages available to the application immediately and +stop further processing until the application fetches them. This will usually +result in the best performance. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/supervisor.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/supervisor.rst new file mode 100644 index 0000000000..5eefc7711b --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/supervisor.rst @@ -0,0 +1,131 @@ +Deploy with Supervisor +====================== + +This guide proposes a simple way to deploy a websockets server directly on a +Linux or BSD operating system. + +We'll configure Supervisor_ to run several server processes and to restart +them if needed. + +.. _Supervisor: http://supervisord.org/ + +We'll bind all servers to the same port. The OS will take care of balancing +connections. + +Create and activate a virtualenv: + +.. code-block:: console + + $ python -m venv supervisor-websockets + $ . supervisor-websockets/bin/activate + +Install websockets and Supervisor: + +.. code-block:: console + + $ pip install websockets + $ pip install supervisor + +Save this app to a file called ``app.py``: + +.. literalinclude:: ../../example/deployment/supervisor/app.py + +This is an echo server with two features added for the purpose of this guide: + +* It shuts down gracefully when receiving a ``SIGTERM`` signal; +* It enables the ``reuse_port`` option of :meth:`~asyncio.loop.create_server`, + which in turns sets ``SO_REUSEPORT`` on the accept socket. + +Save this Supervisor configuration to ``supervisord.conf``: + +.. literalinclude:: ../../example/deployment/supervisor/supervisord.conf + +This is the minimal configuration required to keep four instances of the app +running, restarting them if they exit. + +Now start Supervisor in the foreground: + +.. code-block:: console + + $ supervisord -c supervisord.conf -n + INFO Increased RLIMIT_NOFILE limit to 1024 + INFO supervisord started with pid 43596 + INFO spawned: 'websockets-test_00' with pid 43597 + INFO spawned: 'websockets-test_01' with pid 43598 + INFO spawned: 'websockets-test_02' with pid 43599 + INFO spawned: 'websockets-test_03' with pid 43600 + INFO success: websockets-test_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs) + INFO success: websockets-test_01 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs) + INFO success: websockets-test_02 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs) + INFO success: websockets-test_03 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs) + +In another shell, after activating the virtualenv, we can connect to the app — +press Ctrl-D to exit: + +.. code-block:: console + + $ python -m websockets ws://localhost:8080/ + Connected to ws://localhost:8080/. + > Hello! + < Hello! + Connection closed: 1000 (OK). + +Look at the pid of an instance of the app in the logs and terminate it: + +.. code-block:: console + + $ kill -TERM 43597 + +The logs show that Supervisor restarted this instance: + +.. code-block:: console + + INFO exited: websockets-test_00 (exit status 0; expected) + INFO spawned: 'websockets-test_00' with pid 43629 + INFO success: websockets-test_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs) + +Now let's check what happens when we shut down Supervisor, but first let's +establish a connection and leave it open: + +.. code-block:: console + + $ python -m websockets ws://localhost:8080/ + Connected to ws://localhost:8080/. + > + +Look at the pid of supervisord itself in the logs and terminate it: + +.. code-block:: console + + $ kill -TERM 43596 + +The logs show that Supervisor terminated all instances of the app before +exiting: + +.. code-block:: console + + WARN received SIGTERM indicating exit request + INFO waiting for websockets-test_00, websockets-test_01, websockets-test_02, websockets-test_03 to die + INFO stopped: websockets-test_02 (exit status 0) + INFO stopped: websockets-test_03 (exit status 0) + INFO stopped: websockets-test_01 (exit status 0) + INFO stopped: websockets-test_00 (exit status 0) + +And you can see that the connection to the app was closed gracefully: + +.. code-block:: console + + $ python -m websockets ws://localhost:8080/ + Connected to ws://localhost:8080/. + Connection closed: 1001 (going away). + +In this example, we've been sharing the same virtualenv for supervisor and +websockets. + +In a real deployment, you would likely: + +* Install Supervisor with the package manager of the OS. +* Create a virtualenv dedicated to your application. +* Add ``environment=PATH="path/to/your/virtualenv/bin"`` in the Supervisor + configuration. Then ``python app.py`` runs in that virtualenv. + diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/index.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/index.rst new file mode 100644 index 0000000000..d9737db12a --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/index.rst @@ -0,0 +1,75 @@ +websockets +========== + +|licence| |version| |pyversions| |tests| |docs| |openssf| + +.. |licence| image:: https://img.shields.io/pypi/l/websockets.svg + :target: https://pypi.python.org/pypi/websockets + +.. |version| image:: https://img.shields.io/pypi/v/websockets.svg + :target: https://pypi.python.org/pypi/websockets + +.. |pyversions| image:: https://img.shields.io/pypi/pyversions/websockets.svg + :target: https://pypi.python.org/pypi/websockets + +.. |tests| image:: https://img.shields.io/github/checks-status/python-websockets/websockets/main?label=tests + :target: https://github.com/python-websockets/websockets/actions/workflows/tests.yml + +.. |docs| image:: https://img.shields.io/readthedocs/websockets.svg + :target: https://websockets.readthedocs.io/ + +.. |openssf| image:: https://bestpractices.coreinfrastructure.org/projects/6475/badge + :target: https://bestpractices.coreinfrastructure.org/projects/6475 + +websockets is a library for building WebSocket_ servers and clients in Python +with a focus on correctness, simplicity, robustness, and performance. + +.. _WebSocket: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API + +It supports several network I/O and control flow paradigms: + +1. The default implementation builds upon :mod:`asyncio`, Python's standard + asynchronous I/O framework. It provides an elegant coroutine-based API. It's + ideal for servers that handle many clients concurrently. +2. The :mod:`threading` implementation is a good alternative for clients, + especially if you aren't familiar with :mod:`asyncio`. It may also be used + for servers that don't need to serve many clients. +3. The `Sans-I/O`_ implementation is designed for integrating in third-party + libraries, typically application servers, in addition being used internally + by websockets. + +.. _Sans-I/O: https://sans-io.readthedocs.io/ + +Here's an echo server with the :mod:`asyncio` API: + +.. literalinclude:: ../example/echo.py + +Here's how a client sends and receives messages with the :mod:`threading` API: + +.. literalinclude:: ../example/hello.py + +Don't worry about the opening and closing handshakes, pings and pongs, or any +other behavior described in the WebSocket specification. websockets takes care +of this under the hood so you can focus on your application! + +Also, websockets provides an interactive client: + +.. code-block:: console + + $ python -m websockets ws://localhost:8765/ + Connected to ws://localhost:8765/. + > Hello world! + < Hello world! + Connection closed: 1000 (OK). + +Do you like it? :doc:`Let's dive in! <intro/index>` + +.. toctree:: + :hidden: + + intro/index + howto/index + faq/index + reference/index + topics/index + project/index diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/intro/index.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/intro/index.rst new file mode 100644 index 0000000000..095262a207 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/intro/index.rst @@ -0,0 +1,46 @@ +Getting started +=============== + +.. currentmodule:: websockets + +Requirements +------------ + +websockets requires Python ≥ 3.8. + +.. admonition:: Use the most recent Python release + :class: tip + + For each minor version (3.x), only the latest bugfix or security release + (3.x.y) is officially supported. + +It doesn't have any dependencies. + +.. _install: + +Installation +------------ + +Install websockets with: + +.. code-block:: console + + $ pip install websockets + +Wheels are available for all platforms. + +Tutorial +-------- + +Learn how to build an real-time web application with websockets. + +.. toctree:: + + tutorial1 + tutorial2 + tutorial3 + +In a hurry? +----------- + +Look at the :doc:`quick start guide <../howto/quickstart>`. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial1.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial1.rst new file mode 100644 index 0000000000..ff85003b58 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial1.rst @@ -0,0 +1,591 @@ +Part 1 - Send & receive +======================= + +.. currentmodule:: websockets + +In this tutorial, you're going to build a web-based `Connect Four`_ game. + +.. _Connect Four: https://en.wikipedia.org/wiki/Connect_Four + +The web removes the constraint of being in the same room for playing a game. +Two players can connect over of the Internet, regardless of where they are, +and play in their browsers. + +When a player makes a move, it should be reflected immediately on both sides. +This is difficult to implement over HTTP due to the request-response style of +the protocol. + +Indeed, there is no good way to be notified when the other player makes a +move. Workarounds such as polling or long-polling introduce significant +overhead. + +Enter `WebSocket <websocket>`_. + +The WebSocket protocol provides two-way communication between a browser and a +server over a persistent connection. That's exactly what you need to exchange +moves between players, via a server. + +.. admonition:: This is the first part of the tutorial. + + * In this :doc:`first part <tutorial1>`, you will create a server and + connect one browser; you can play if you share the same browser. + * In the :doc:`second part <tutorial2>`, you will connect a second + browser; you can play from different browsers on a local network. + * In the :doc:`third part <tutorial3>`, you will deploy the game to the + web; you can play from any browser connected to the Internet. + +Prerequisites +------------- + +This tutorial assumes basic knowledge of Python and JavaScript. + +If you're comfortable with :doc:`virtual environments <python:tutorial/venv>`, +you can use one for this tutorial. Else, don't worry: websockets doesn't have +any dependencies; it shouldn't create trouble in the default environment. + +If you haven't installed websockets yet, do it now: + +.. code-block:: console + + $ pip install websockets + +Confirm that websockets is installed: + +.. code-block:: console + + $ python -m websockets --version + +.. admonition:: This tutorial is written for websockets |release|. + :class: tip + + If you installed another version, you should switch to the corresponding + version of the documentation. + +Download the starter kit +------------------------ + +Create a directory and download these three files: +:download:`connect4.js <../../example/tutorial/start/connect4.js>`, +:download:`connect4.css <../../example/tutorial/start/connect4.css>`, +and :download:`connect4.py <../../example/tutorial/start/connect4.py>`. + +The JavaScript module, along with the CSS file, provides a web-based user +interface. Here's its API. + +.. js:module:: connect4 + +.. js:data:: PLAYER1 + + Color of the first player. + +.. js:data:: PLAYER2 + + Color of the second player. + +.. js:function:: createBoard(board) + + Draw a board. + + :param board: DOM element containing the board; must be initially empty. + +.. js:function:: playMove(board, player, column, row) + + Play a move. + + :param board: DOM element containing the board. + :param player: :js:data:`PLAYER1` or :js:data:`PLAYER2`. + :param column: between ``0`` and ``6``. + :param row: between ``0`` and ``5``. + +The Python module provides a class to record moves and tell when a player +wins. Here's its API. + +.. module:: connect4 + +.. data:: PLAYER1 + :value: "red" + + Color of the first player. + +.. data:: PLAYER2 + :value: "yellow" + + Color of the second player. + +.. class:: Connect4 + + A Connect Four game. + + .. method:: play(player, column) + + Play a move. + + :param player: :data:`~connect4.PLAYER1` or :data:`~connect4.PLAYER2`. + :param column: between ``0`` and ``6``. + :returns: Row where the checker lands, between ``0`` and ``5``. + :raises RuntimeError: if the move is illegal. + + .. attribute:: moves + + List of moves played during this game, as ``(player, column, row)`` + tuples. + + .. attribute:: winner + + :data:`~connect4.PLAYER1` or :data:`~connect4.PLAYER2` if they + won; :obj:`None` if the game is still ongoing. + +.. currentmodule:: websockets + +Bootstrap the web UI +-------------------- + +Create an ``index.html`` file next to ``connect4.js`` and ``connect4.css`` +with this content: + +.. literalinclude:: ../../example/tutorial/step1/index.html + :language: html + +This HTML page contains an empty ``<div>`` element where you will draw the +Connect Four board. It loads a ``main.js`` script where you will write all +your JavaScript code. + +Create a ``main.js`` file next to ``index.html``. In this script, when the +page loads, draw the board: + +.. code-block:: javascript + + import { createBoard, playMove } from "./connect4.js"; + + window.addEventListener("DOMContentLoaded", () => { + // Initialize the UI. + const board = document.querySelector(".board"); + createBoard(board); + }); + +Open a shell, navigate to the directory containing these files, and start an +HTTP server: + +.. code-block:: console + + $ python -m http.server + +Open http://localhost:8000/ in a web browser. The page displays an empty board +with seven columns and six rows. You will play moves in this board later. + +Bootstrap the server +-------------------- + +Create an ``app.py`` file next to ``connect4.py`` with this content: + +.. code-block:: python + + #!/usr/bin/env python + + import asyncio + + import websockets + + + async def handler(websocket): + while True: + message = await websocket.recv() + print(message) + + + async def main(): + async with websockets.serve(handler, "", 8001): + await asyncio.Future() # run forever + + + if __name__ == "__main__": + asyncio.run(main()) + +The entry point of this program is ``asyncio.run(main())``. It creates an +asyncio event loop, runs the ``main()`` coroutine, and shuts down the loop. + +The ``main()`` coroutine calls :func:`~server.serve` to start a websockets +server. :func:`~server.serve` takes three positional arguments: + +* ``handler`` is a coroutine that manages a connection. When a client + connects, websockets calls ``handler`` with the connection in argument. + When ``handler`` terminates, websockets closes the connection. +* The second argument defines the network interfaces where the server can be + reached. Here, the server listens on all interfaces, so that other devices + on the same local network can connect. +* The third argument is the port on which the server listens. + +Invoking :func:`~server.serve` as an asynchronous context manager, in an +``async with`` block, ensures that the server shuts down properly when +terminating the program. + +For each connection, the ``handler()`` coroutine runs an infinite loop that +receives messages from the browser and prints them. + +Open a shell, navigate to the directory containing ``app.py``, and start the +server: + +.. code-block:: console + + $ python app.py + +This doesn't display anything. Hopefully the WebSocket server is running. +Let's make sure that it works. You cannot test the WebSocket server with a +web browser like you tested the HTTP server. However, you can test it with +websockets' interactive client. + +Open another shell and run this command: + +.. code-block:: console + + $ python -m websockets ws://localhost:8001/ + +You get a prompt. Type a message and press "Enter". Switch to the shell where +the server is running and check that the server received the message. Good! + +Exit the interactive client with Ctrl-C or Ctrl-D. + +Now, if you look at the console where you started the server, you can see the +stack trace of an exception: + +.. code-block:: pytb + + connection handler failed + Traceback (most recent call last): + ... + File "app.py", line 22, in handler + message = await websocket.recv() + ... + websockets.exceptions.ConnectionClosedOK: received 1000 (OK); then sent 1000 (OK) + +Indeed, the server was waiting for the next message +with :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` when the client +disconnected. When this happens, websockets raises +a :exc:`~exceptions.ConnectionClosedOK` exception to let you know that you +won't receive another message on this connection. + +This exception creates noise in the server logs, making it more difficult to +spot real errors when you add functionality to the server. Catch it in the +``handler()`` coroutine: + +.. code-block:: python + + async def handler(websocket): + while True: + try: + message = await websocket.recv() + except websockets.ConnectionClosedOK: + break + print(message) + +Stop the server with Ctrl-C and start it again: + +.. code-block:: console + + $ python app.py + +.. admonition:: You must restart the WebSocket server when you make changes. + :class: tip + + The WebSocket server loads the Python code in ``app.py`` then serves every + WebSocket request with this version of the code. As a consequence, + changes to ``app.py`` aren't visible until you restart the server. + + This is unlike the HTTP server that you started earlier with ``python -m + http.server``. For every request, this HTTP server reads the target file + and sends it. That's why changes are immediately visible. + + It is possible to :doc:`restart the WebSocket server automatically + <../howto/autoreload>` but this isn't necessary for this tutorial. + +Try connecting and disconnecting the interactive client again. +The :exc:`~exceptions.ConnectionClosedOK` exception doesn't appear anymore. + +This pattern is so common that websockets provides a shortcut for iterating +over messages received on the connection until the client disconnects: + +.. code-block:: python + + async def handler(websocket): + async for message in websocket: + print(message) + +Restart the server and check with the interactive client that its behavior +didn't change. + +At this point, you bootstrapped a web application and a WebSocket server. +Let's connect them. + +Transmit from browser to server +------------------------------- + +In JavaScript, you open a WebSocket connection as follows: + +.. code-block:: javascript + + const websocket = new WebSocket("ws://localhost:8001/"); + +Before you exchange messages with the server, you need to decide their format. +There is no universal convention for this. + +Let's use JSON objects with a ``type`` key identifying the type of the event +and the rest of the object containing properties of the event. + +Here's an event describing a move in the middle slot of the board: + +.. code-block:: javascript + + const event = {type: "play", column: 3}; + +Here's how to serialize this event to JSON and send it to the server: + +.. code-block:: javascript + + websocket.send(JSON.stringify(event)); + +Now you have all the building blocks to send moves to the server. + +Add this function to ``main.js``: + +.. literalinclude:: ../../example/tutorial/step1/main.js + :language: js + :start-at: function sendMoves + :end-before: window.addEventListener + +``sendMoves()`` registers a listener for ``click`` events on the board. The +listener figures out which column was clicked, builds a event of type +``"play"``, serializes it, and sends it to the server. + +Modify the initialization to open the WebSocket connection and call the +``sendMoves()`` function: + +.. code-block:: javascript + + window.addEventListener("DOMContentLoaded", () => { + // Initialize the UI. + const board = document.querySelector(".board"); + createBoard(board); + // Open the WebSocket connection and register event handlers. + const websocket = new WebSocket("ws://localhost:8001/"); + sendMoves(board, websocket); + }); + +Check that the HTTP server and the WebSocket server are still running. If you +stopped them, here are the commands to start them again: + +.. code-block:: console + + $ python -m http.server + +.. code-block:: console + + $ python app.py + +Refresh http://localhost:8000/ in your web browser. Click various columns in +the board. The server receives messages with the expected column number. + +There isn't any feedback in the board because you haven't implemented that +yet. Let's do it. + +Transmit from server to browser +------------------------------- + +In JavaScript, you receive WebSocket messages by listening to ``message`` +events. Here's how to receive a message from the server and deserialize it +from JSON: + +.. code-block:: javascript + + websocket.addEventListener("message", ({ data }) => { + const event = JSON.parse(data); + // do something with event + }); + +You're going to need three types of messages from the server to the browser: + +.. code-block:: javascript + + {type: "play", player: "red", column: 3, row: 0} + {type: "win", player: "red"} + {type: "error", message: "This slot is full."} + +The JavaScript code receiving these messages will dispatch events depending on +their type and take appropriate action. For example, it will react to an +event of type ``"play"`` by displaying the move on the board with +the :js:func:`~connect4.playMove` function. + +Add this function to ``main.js``: + +.. literalinclude:: ../../example/tutorial/step1/main.js + :language: js + :start-at: function showMessage + :end-before: function sendMoves + +.. admonition:: Why does ``showMessage`` use ``window.setTimeout``? + :class: hint + + When :js:func:`playMove` modifies the state of the board, the browser + renders changes asynchronously. Conversely, ``window.alert()`` runs + synchronously and blocks rendering while the alert is visible. + + If you called ``window.alert()`` immediately after :js:func:`playMove`, + the browser could display the alert before rendering the move. You could + get a "Player red wins!" alert without seeing red's last move. + + We're using ``window.alert()`` for simplicity in this tutorial. A real + application would display these messages in the user interface instead. + It wouldn't be vulnerable to this problem. + +Modify the initialization to call the ``receiveMoves()`` function: + +.. literalinclude:: ../../example/tutorial/step1/main.js + :language: js + :start-at: window.addEventListener + +At this point, the user interface should receive events properly. Let's test +it by modifying the server to send some events. + +Sending an event from Python is quite similar to JavaScript: + +.. code-block:: python + + event = {"type": "play", "player": "red", "column": 3, "row": 0} + await websocket.send(json.dumps(event)) + +.. admonition:: Don't forget to serialize the event with :func:`json.dumps`. + :class: tip + + Else, websockets raises ``TypeError: data is a dict-like object``. + +Modify the ``handler()`` coroutine in ``app.py`` as follows: + +.. code-block:: python + + import json + + from connect4 import PLAYER1, PLAYER2 + + async def handler(websocket): + for player, column, row in [ + (PLAYER1, 3, 0), + (PLAYER2, 3, 1), + (PLAYER1, 4, 0), + (PLAYER2, 4, 1), + (PLAYER1, 2, 0), + (PLAYER2, 1, 0), + (PLAYER1, 5, 0), + ]: + event = { + "type": "play", + "player": player, + "column": column, + "row": row, + } + await websocket.send(json.dumps(event)) + await asyncio.sleep(0.5) + event = { + "type": "win", + "player": PLAYER1, + } + await websocket.send(json.dumps(event)) + +Restart the WebSocket server and refresh http://localhost:8000/ in your web +browser. Seven moves appear at 0.5 second intervals. Then an alert announces +the winner. + +Good! Now you know how to communicate both ways. + +Once you plug the game engine to process moves, you will have a fully +functional game. + +Add the game logic +------------------ + +In the ``handler()`` coroutine, you're going to initialize a game: + +.. code-block:: python + + from connect4 import Connect4 + + async def handler(websocket): + # Initialize a Connect Four game. + game = Connect4() + + ... + +Then, you're going to iterate over incoming messages and take these steps: + +* parse an event of type ``"play"``, the only type of event that the user + interface sends; +* play the move in the board with the :meth:`~connect4.Connect4.play` method, + alternating between the two players; +* if :meth:`~connect4.Connect4.play` raises :exc:`RuntimeError` because the + move is illegal, send an event of type ``"error"``; +* else, send an event of type ``"play"`` to tell the user interface where the + checker lands; +* if the move won the game, send an event of type ``"win"``. + +Try to implement this by yourself! + +Keep in mind that you must restart the WebSocket server and reload the page in +the browser when you make changes. + +When it works, you can play the game from a single browser, with players +taking alternate turns. + +.. admonition:: Enable debug logs to see all messages sent and received. + :class: tip + + Here's how to enable debug logs: + + .. code-block:: python + + import logging + + logging.basicConfig(format="%(message)s", level=logging.DEBUG) + +If you're stuck, a solution is available at the bottom of this document. + +Summary +------- + +In this first part of the tutorial, you learned how to: + +* build and run a WebSocket server in Python with :func:`~server.serve`; +* receive a message in a connection handler + with :meth:`~server.WebSocketServerProtocol.recv`; +* send a message in a connection handler + with :meth:`~server.WebSocketServerProtocol.send`; +* iterate over incoming messages with ``async for + message in websocket: ...``; +* open a WebSocket connection in JavaScript with the ``WebSocket`` API; +* send messages in a browser with ``WebSocket.send()``; +* receive messages in a browser by listening to ``message`` events; +* design a set of events to be exchanged between the browser and the server. + +You can now play a Connect Four game in a browser, communicating over a +WebSocket connection with a server where the game logic resides! + +However, the two players share a browser, so the constraint of being in the +same room still applies. + +Move on to the :doc:`second part <tutorial2>` of the tutorial to break this +constraint and play from separate browsers. + +Solution +-------- + +.. literalinclude:: ../../example/tutorial/step1/app.py + :caption: app.py + :language: python + :linenos: + +.. literalinclude:: ../../example/tutorial/step1/index.html + :caption: index.html + :language: html + :linenos: + +.. literalinclude:: ../../example/tutorial/step1/main.js + :caption: main.js + :language: js + :linenos: diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial2.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial2.rst new file mode 100644 index 0000000000..5ac4ae9dd5 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial2.rst @@ -0,0 +1,565 @@ +Part 2 - Route & broadcast +========================== + +.. currentmodule:: websockets + +.. admonition:: This is the second part of the tutorial. + + * In the :doc:`first part <tutorial1>`, you created a server and + connected one browser; you could play if you shared the same browser. + * In this :doc:`second part <tutorial2>`, you will connect a second + browser; you can play from different browsers on a local network. + * In the :doc:`third part <tutorial3>`, you will deploy the game to the + web; you can play from any browser connected to the Internet. + +In the first part of the tutorial, you opened a WebSocket connection from a +browser to a server and exchanged events to play moves. The state of the game +was stored in an instance of the :class:`~connect4.Connect4` class, +referenced as a local variable in the connection handler coroutine. + +Now you want to open two WebSocket connections from two separate browsers, one +for each player, to the same server in order to play the same game. This +requires moving the state of the game to a place where both connections can +access it. + +Share game state +---------------- + +As long as you're running a single server process, you can share state by +storing it in a global variable. + +.. admonition:: What if you need to scale to multiple server processes? + :class: hint + + In that case, you must design a way for the process that handles a given + connection to be aware of relevant events for that client. This is often + achieved with a publish / subscribe mechanism. + +How can you make two connection handlers agree on which game they're playing? +When the first player starts a game, you give it an identifier. Then, you +communicate the identifier to the second player. When the second player joins +the game, you look it up with the identifier. + +In addition to the game itself, you need to keep track of the WebSocket +connections of the two players. Since both players receive the same events, +you don't need to treat the two connections differently; you can store both +in the same set. + +Let's sketch this in code. + +A module-level :class:`dict` enables lookups by identifier: + +.. code-block:: python + + JOIN = {} + +When the first player starts the game, initialize and store it: + +.. code-block:: python + + import secrets + + async def handler(websocket): + ... + + # Initialize a Connect Four game, the set of WebSocket connections + # receiving moves from this game, and secret access token. + game = Connect4() + connected = {websocket} + + join_key = secrets.token_urlsafe(12) + JOIN[join_key] = game, connected + + try: + + ... + + finally: + del JOIN[join_key] + +When the second player joins the game, look it up: + +.. code-block:: python + + async def handler(websocket): + ... + + join_key = ... # TODO + + # Find the Connect Four game. + game, connected = JOIN[join_key] + + # Register to receive moves from this game. + connected.add(websocket) + try: + + ... + + finally: + connected.remove(websocket) + +Notice how we're carefully cleaning up global state with ``try: ... +finally: ...`` blocks. Else, we could leave references to games or +connections in global state, which would cause a memory leak. + +In both connection handlers, you have a ``game`` pointing to the same +:class:`~connect4.Connect4` instance, so you can interact with the game, +and a ``connected`` set of connections, so you can send game events to +both players as follows: + +.. code-block:: python + + async def handler(websocket): + + ... + + for connection in connected: + await connection.send(json.dumps(event)) + + ... + +Perhaps you spotted a major piece missing from the puzzle. How does the second +player obtain ``join_key``? Let's design new events to carry this information. + +To start a game, the first player sends an ``"init"`` event: + +.. code-block:: javascript + + {type: "init"} + +The connection handler for the first player creates a game as shown above and +responds with: + +.. code-block:: javascript + + {type: "init", join: "<join_key>"} + +With this information, the user interface of the first player can create a +link to ``http://localhost:8000/?join=<join_key>``. For the sake of simplicity, +we will assume that the first player shares this link with the second player +outside of the application, for example via an instant messaging service. + +To join the game, the second player sends a different ``"init"`` event: + +.. code-block:: javascript + + {type: "init", join: "<join_key>"} + +The connection handler for the second player can look up the game with the +join key as shown above. There is no need to respond. + +Let's dive into the details of implementing this design. + +Start a game +------------ + +We'll start with the initialization sequence for the first player. + +In ``main.js``, define a function to send an initialization event when the +WebSocket connection is established, which triggers an ``open`` event: + +.. code-block:: javascript + + function initGame(websocket) { + websocket.addEventListener("open", () => { + // Send an "init" event for the first player. + const event = { type: "init" }; + websocket.send(JSON.stringify(event)); + }); + } + +Update the initialization sequence to call ``initGame()``: + +.. literalinclude:: ../../example/tutorial/step2/main.js + :language: js + :start-at: window.addEventListener + +In ``app.py``, define a new ``handler`` coroutine — keep a copy of the +previous one to reuse it later: + +.. code-block:: python + + import secrets + + + JOIN = {} + + + async def start(websocket): + # Initialize a Connect Four game, the set of WebSocket connections + # receiving moves from this game, and secret access token. + game = Connect4() + connected = {websocket} + + join_key = secrets.token_urlsafe(12) + JOIN[join_key] = game, connected + + try: + # Send the secret access token to the browser of the first player, + # where it'll be used for building a "join" link. + event = { + "type": "init", + "join": join_key, + } + await websocket.send(json.dumps(event)) + + # Temporary - for testing. + print("first player started game", id(game)) + async for message in websocket: + print("first player sent", message) + + finally: + del JOIN[join_key] + + + async def handler(websocket): + # Receive and parse the "init" event from the UI. + message = await websocket.recv() + event = json.loads(message) + assert event["type"] == "init" + + # First player starts a new game. + await start(websocket) + +In ``index.html``, add an ``<a>`` element to display the link to share with +the other player. + +.. code-block:: html + + <body> + <div class="actions"> + <a class="action join" href="">Join</a> + </div> + <!-- ... --> + </body> + +In ``main.js``, modify ``receiveMoves()`` to handle the ``"init"`` message and +set the target of that link: + +.. code-block:: javascript + + switch (event.type) { + case "init": + // Create link for inviting the second player. + document.querySelector(".join").href = "?join=" + event.join; + break; + // ... + } + +Restart the WebSocket server and reload http://localhost:8000/ in the browser. +There's a link labeled JOIN below the board with a target that looks like +http://localhost:8000/?join=95ftAaU5DJVP1zvb. + +The server logs say ``first player started game ...``. If you click the board, +you see ``"play"`` events. There is no feedback in the UI, though, because +you haven't restored the game logic yet. + +Before we get there, let's handle links with a ``join`` query parameter. + +Join a game +----------- + +We'll now update the initialization sequence to account for the second +player. + +In ``main.js``, update ``initGame()`` to send the join key in the ``"init"`` +message when it's in the URL: + +.. code-block:: javascript + + function initGame(websocket) { + websocket.addEventListener("open", () => { + // Send an "init" event according to who is connecting. + const params = new URLSearchParams(window.location.search); + let event = { type: "init" }; + if (params.has("join")) { + // Second player joins an existing game. + event.join = params.get("join"); + } else { + // First player starts a new game. + } + websocket.send(JSON.stringify(event)); + }); + } + +In ``app.py``, update the ``handler`` coroutine to look for the join key in +the ``"init"`` message, then load that game: + +.. code-block:: python + + async def error(websocket, message): + event = { + "type": "error", + "message": message, + } + await websocket.send(json.dumps(event)) + + + async def join(websocket, join_key): + # Find the Connect Four game. + try: + game, connected = JOIN[join_key] + except KeyError: + await error(websocket, "Game not found.") + return + + # Register to receive moves from this game. + connected.add(websocket) + try: + + # Temporary - for testing. + print("second player joined game", id(game)) + async for message in websocket: + print("second player sent", message) + + finally: + connected.remove(websocket) + + + async def handler(websocket): + # Receive and parse the "init" event from the UI. + message = await websocket.recv() + event = json.loads(message) + assert event["type"] == "init" + + if "join" in event: + # Second player joins an existing game. + await join(websocket, event["join"]) + else: + # First player starts a new game. + await start(websocket) + +Restart the WebSocket server and reload http://localhost:8000/ in the browser. + +Copy the link labeled JOIN and open it in another browser. You may also open +it in another tab or another window of the same browser; however, that makes +it a bit tricky to remember which one is the first or second player. + +.. admonition:: You must start a new game when you restart the server. + :class: tip + + Since games are stored in the memory of the Python process, they're lost + when you stop the server. + + Whenever you make changes to ``app.py``, you must restart the server, + create a new game in a browser, and join it in another browser. + +The server logs say ``first player started game ...`` and ``second player +joined game ...``. The numbers match, proving that the ``game`` local +variable in both connection handlers points to same object in the memory of +the Python process. + +Click the board in either browser. The server receives ``"play"`` events from +the corresponding player. + +In the initialization sequence, you're routing connections to ``start()`` or +``join()`` depending on the first message received by the server. This is a +common pattern in servers that handle different clients. + +.. admonition:: Why not use different URIs for ``start()`` and ``join()``? + :class: hint + + Instead of sending an initialization event, you could encode the join key + in the WebSocket URI e.g. ``ws://localhost:8001/join/<join_key>``. The + WebSocket server would parse ``websocket.path`` and route the connection, + similar to how HTTP servers route requests. + + When you need to send sensitive data like authentication credentials to + the server, sending it an event is considered more secure than encoding + it in the URI because URIs end up in logs. + + For the purposes of this tutorial, both approaches are equivalent because + the join key comes from an HTTP URL. There isn't much at risk anyway! + +Now you can restore the logic for playing moves and you'll have a fully +functional two-player game. + +Add the game logic +------------------ + +Once the initialization is done, the game is symmetrical, so you can write a +single coroutine to process the moves of both players: + +.. code-block:: python + + async def play(websocket, game, player, connected): + ... + +With such a coroutine, you can replace the temporary code for testing in +``start()`` by: + +.. code-block:: python + + await play(websocket, game, PLAYER1, connected) + +and in ``join()`` by: + +.. code-block:: python + + await play(websocket, game, PLAYER2, connected) + +The ``play()`` coroutine will reuse much of the code you wrote in the first +part of the tutorial. + +Try to implement this by yourself! + +Keep in mind that you must restart the WebSocket server, reload the page to +start a new game with the first player, copy the JOIN link, and join the game +with the second player when you make changes. + +When ``play()`` works, you can play the game from two separate browsers, +possibly running on separate computers on the same local network. + +A complete solution is available at the bottom of this document. + +Watch a game +------------ + +Let's add one more feature: allow spectators to watch the game. + +The process for inviting a spectator can be the same as for inviting the +second player. You will have to duplicate all the initialization logic: + +- declare a ``WATCH`` global variable similar to ``JOIN``; +- generate a watch key when creating a game; it must be different from the + join key, or else a spectator could hijack a game by tweaking the URL; +- include the watch key in the ``"init"`` event sent to the first player; +- generate a WATCH link in the UI with a ``watch`` query parameter; +- update the ``initGame()`` function to handle such links; +- update the ``handler()`` coroutine to invoke a ``watch()`` coroutine for + spectators; +- prevent ``sendMoves()`` from sending ``"play"`` events for spectators. + +Once the initialization sequence is done, watching a game is as simple as +registering the WebSocket connection in the ``connected`` set in order to +receive game events and doing nothing until the spectator disconnects. You +can wait for a connection to terminate with +:meth:`~legacy.protocol.WebSocketCommonProtocol.wait_closed`: + +.. code-block:: python + + async def watch(websocket, watch_key): + + ... + + connected.add(websocket) + try: + await websocket.wait_closed() + finally: + connected.remove(websocket) + +The connection can terminate because the ``receiveMoves()`` function closed it +explicitly after receiving a ``"win"`` event, because the spectator closed +their browser, or because the network failed. + +Again, try to implement this by yourself. + +When ``watch()`` works, you can invite spectators to watch the game from other +browsers, as long as they're on the same local network. + +As a further improvement, you may support adding spectators while a game is +already in progress. This requires replaying moves that were played before +the spectator was added to the ``connected`` set. Past moves are available in +the :attr:`~connect4.Connect4.moves` attribute of the game. + +This feature is included in the solution proposed below. + +Broadcast +--------- + +When you need to send a message to the two players and to all spectators, +you're using this pattern: + +.. code-block:: python + + async def handler(websocket): + + ... + + for connection in connected: + await connection.send(json.dumps(event)) + + ... + +Since this is a very common pattern in WebSocket servers, websockets provides +the :func:`broadcast` helper for this purpose: + +.. code-block:: python + + async def handler(websocket): + + ... + + websockets.broadcast(connected, json.dumps(event)) + + ... + +Calling :func:`broadcast` once is more efficient than +calling :meth:`~legacy.protocol.WebSocketCommonProtocol.send` in a loop. + +However, there's a subtle difference in behavior. Did you notice that there's +no ``await`` in the second version? Indeed, :func:`broadcast` is a function, +not a coroutine like :meth:`~legacy.protocol.WebSocketCommonProtocol.send` +or :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`. + +It's quite obvious why :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` +is a coroutine. When you want to receive the next message, you have to wait +until the client sends it and the network transmits it. + +It's less obvious why :meth:`~legacy.protocol.WebSocketCommonProtocol.send` is +a coroutine. If you send many messages or large messages, you could write +data faster than the network can transmit it or the client can read it. Then, +outgoing data will pile up in buffers, which will consume memory and may +crash your application. + +To avoid this problem, :meth:`~legacy.protocol.WebSocketCommonProtocol.send` +waits until the write buffer drains. By slowing down the application as +necessary, this ensures that the server doesn't send data too quickly. This +is called backpressure and it's useful for building robust systems. + +That said, when you're sending the same messages to many clients in a loop, +applying backpressure in this way can become counterproductive. When you're +broadcasting, you don't want to slow down everyone to the pace of the slowest +clients; you want to drop clients that cannot keep up with the data stream. +That's why :func:`broadcast` doesn't wait until write buffers drain. + +For our Connect Four game, there's no difference in practice: the total amount +of data sent on a connection for a game of Connect Four is less than 64 KB, +so the write buffer never fills up and backpressure never kicks in anyway. + +Summary +------- + +In this second part of the tutorial, you learned how to: + +* configure a connection by exchanging initialization messages; +* keep track of connections within a single server process; +* wait until a client disconnects in a connection handler; +* broadcast a message to many connections efficiently. + +You can now play a Connect Four game from separate browser, communicating over +WebSocket connections with a server that synchronizes the game logic! + +However, the two players have to be on the same local network as the server, +so the constraint of being in the same place still mostly applies. + +Head over to the :doc:`third part <tutorial3>` of the tutorial to deploy the +game to the web and remove this constraint. + +Solution +-------- + +.. literalinclude:: ../../example/tutorial/step2/app.py + :caption: app.py + :language: python + :linenos: + +.. literalinclude:: ../../example/tutorial/step2/index.html + :caption: index.html + :language: html + :linenos: + +.. literalinclude:: ../../example/tutorial/step2/main.js + :caption: main.js + :language: js + :linenos: diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial3.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial3.rst new file mode 100644 index 0000000000..6fdec113b2 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial3.rst @@ -0,0 +1,290 @@ +Part 3 - Deploy to the web +========================== + +.. currentmodule:: websockets + +.. admonition:: This is the third part of the tutorial. + + * In the :doc:`first part <tutorial1>`, you created a server and + connected one browser; you could play if you shared the same browser. + * In this :doc:`second part <tutorial2>`, you connected a second browser; + you could play from different browsers on a local network. + * In this :doc:`third part <tutorial3>`, you will deploy the game to the + web; you can play from any browser connected to the Internet. + +In the first and second parts of the tutorial, for local development, you ran +an HTTP server on ``http://localhost:8000/`` with: + +.. code-block:: console + + $ python -m http.server + +and a WebSocket server on ``ws://localhost:8001/`` with: + +.. code-block:: console + + $ python app.py + +Now you want to deploy these servers on the Internet. There's a vast range of +hosting providers to choose from. For the sake of simplicity, we'll rely on: + +* GitHub Pages for the HTTP server; +* Heroku for the WebSocket server. + +Commit project to git +--------------------- + +Perhaps you committed your work to git while you were progressing through the +tutorial. If you didn't, now is a good time, because GitHub and Heroku offer +git-based deployment workflows. + +Initialize a git repository: + +.. code-block:: console + + $ git init -b main + Initialized empty Git repository in websockets-tutorial/.git/ + $ git commit --allow-empty -m "Initial commit." + [main (root-commit) ...] Initial commit. + +Add all files and commit: + +.. code-block:: console + + $ git add . + $ git commit -m "Initial implementation of Connect Four game." + [main ...] Initial implementation of Connect Four game. + 6 files changed, 500 insertions(+) + create mode 100644 app.py + create mode 100644 connect4.css + create mode 100644 connect4.js + create mode 100644 connect4.py + create mode 100644 index.html + create mode 100644 main.js + +Prepare the WebSocket server +---------------------------- + +Before you deploy the server, you must adapt it to meet requirements of +Heroku's runtime. This involves two small changes: + +1. Heroku expects the server to `listen on a specific port`_, provided in the + ``$PORT`` environment variable. + +2. Heroku sends a ``SIGTERM`` signal when `shutting down a dyno`_, which + should trigger a clean exit. + +.. _listen on a specific port: https://devcenter.heroku.com/articles/preparing-a-codebase-for-heroku-deployment#4-listen-on-the-correct-port + +.. _shutting down a dyno: https://devcenter.heroku.com/articles/dynos#shutdown + +Adapt the ``main()`` coroutine accordingly: + +.. code-block:: python + + import os + import signal + +.. literalinclude:: ../../example/tutorial/step3/app.py + :pyobject: main + +To catch the ``SIGTERM`` signal, ``main()`` creates a :class:`~asyncio.Future` +called ``stop`` and registers a signal handler that sets the result of this +future. The value of the future doesn't matter; it's only for waiting for +``SIGTERM``. + +Then, by using :func:`~server.serve` as a context manager and exiting the +context when ``stop`` has a result, ``main()`` ensures that the server closes +connections cleanly and exits on ``SIGTERM``. + +The app is now fully compatible with Heroku. + +Deploy the WebSocket server +--------------------------- + +Create a ``requirements.txt`` file with this content to install ``websockets`` +when building the image: + +.. literalinclude:: ../../example/tutorial/step3/requirements.txt + :language: text + +.. admonition:: Heroku treats ``requirements.txt`` as a signal to `detect a Python app`_. + :class: tip + + That's why you don't need to declare that you need a Python runtime. + +.. _detect a Python app: https://devcenter.heroku.com/articles/python-support#recognizing-a-python-app + +Create a ``Procfile`` file with this content to configure the command for +running the server: + +.. literalinclude:: ../../example/tutorial/step3/Procfile + :language: text + +Commit your changes: + +.. code-block:: console + + $ git add . + $ git commit -m "Deploy to Heroku." + [main ...] Deploy to Heroku. + 3 files changed, 12 insertions(+), 2 deletions(-) + create mode 100644 Procfile + create mode 100644 requirements.txt + +Follow the `set-up instructions`_ to install the Heroku CLI and to log in, if +you haven't done that yet. + +.. _set-up instructions: https://devcenter.heroku.com/articles/getting-started-with-python#set-up + +Create a Heroku app. You must choose a unique name and replace +``websockets-tutorial`` by this name in the following command: + +.. code-block:: console + + $ heroku create websockets-tutorial + Creating ⬢ websockets-tutorial... done + https://websockets-tutorial.herokuapp.com/ | https://git.heroku.com/websockets-tutorial.git + +If you reuse a name that someone else already uses, you will receive this +error; if this happens, try another name: + +.. code-block:: console + + $ heroku create websockets-tutorial + Creating ⬢ websockets-tutorial... ! + ▸ Name websockets-tutorial is already taken + +Deploy by pushing the code to Heroku: + +.. code-block:: console + + $ git push heroku + + ... lots of output... + + remote: Released v1 + remote: https://websockets-tutorial.herokuapp.com/ deployed to Heroku + remote: + remote: Verifying deploy... done. + To https://git.heroku.com/websockets-tutorial.git + * [new branch] main -> main + +You can test the WebSocket server with the interactive client exactly like you +did in the first part of the tutorial. Replace ``websockets-tutorial`` by the +name of your app in the following command: + +.. code-block:: console + + $ python -m websockets wss://websockets-tutorial.herokuapp.com/ + Connected to wss://websockets-tutorial.herokuapp.com/. + > {"type": "init"} + < {"type": "init", "join": "54ICxFae_Ip7TJE2", "watch": "634w44TblL5Dbd9a"} + Connection closed: 1000 (OK). + +It works! + +Prepare the web application +--------------------------- + +Before you deploy the web application, perhaps you're wondering how it will +locate the WebSocket server? Indeed, at this point, its address is hard-coded +in ``main.js``: + +.. code-block:: javascript + + const websocket = new WebSocket("ws://localhost:8001/"); + +You can take this strategy one step further by checking the address of the +HTTP server and determining the address of the WebSocket server accordingly. + +Add this function to ``main.js``; replace ``python-websockets`` by your GitHub +username and ``websockets-tutorial`` by the name of your app on Heroku: + +.. literalinclude:: ../../example/tutorial/step3/main.js + :language: js + :start-at: function getWebSocketServer + :end-before: function initGame + +Then, update the initialization to connect to this address instead: + +.. code-block:: javascript + + const websocket = new WebSocket(getWebSocketServer()); + +Commit your changes: + +.. code-block:: console + + $ git add . + $ git commit -m "Configure WebSocket server address." + [main ...] Configure WebSocket server address. + 1 file changed, 11 insertions(+), 1 deletion(-) + +Deploy the web application +-------------------------- + +Go to GitHub and create a new repository called ``websockets-tutorial``. + +Push your code to this repository. You must replace ``python-websockets`` by +your GitHub username in the following command: + +.. code-block:: console + + $ git remote add origin git@github.com:python-websockets/websockets-tutorial.git + $ git push -u origin main + Enumerating objects: 11, done. + Counting objects: 100% (11/11), done. + Delta compression using up to 8 threads + Compressing objects: 100% (10/10), done. + Writing objects: 100% (11/11), 5.90 KiB | 2.95 MiB/s, done. + Total 11 (delta 0), reused 0 (delta 0), pack-reused 0 + To github.com:<username>/websockets-tutorial.git + * [new branch] main -> main + Branch 'main' set up to track remote branch 'main' from 'origin'. + +Go back to GitHub, open the Settings tab of the repository and select Pages in +the menu. Select the main branch as source and click Save. GitHub tells you +that your site is published. + +Follow the link and start a game! + +Summary +------- + +In this third part of the tutorial, you learned how to deploy a WebSocket +application with Heroku. + +You can start a Connect Four game, send the JOIN link to a friend, and play +over the Internet! + +Congratulations for completing the tutorial. Enjoy building real-time web +applications with websockets! + +Solution +-------- + +.. literalinclude:: ../../example/tutorial/step3/app.py + :caption: app.py + :language: python + :linenos: + +.. literalinclude:: ../../example/tutorial/step3/index.html + :caption: index.html + :language: html + :linenos: + +.. literalinclude:: ../../example/tutorial/step3/main.js + :caption: main.js + :language: js + :linenos: + +.. literalinclude:: ../../example/tutorial/step3/Procfile + :caption: Procfile + :language: text + :linenos: + +.. literalinclude:: ../../example/tutorial/step3/requirements.txt + :caption: requirements.txt + :language: text + :linenos: diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/make.bat b/testing/web-platform/tests/tools/third_party/websockets/docs/make.bat new file mode 100644 index 0000000000..922152e96a --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/project/changelog.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/project/changelog.rst new file mode 100644 index 0000000000..264e6e42d1 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/project/changelog.rst @@ -0,0 +1,1230 @@ +Changelog +========= + +.. currentmodule:: websockets + +.. _backwards-compatibility policy: + +Backwards-compatibility policy +------------------------------ + +websockets is intended for production use. Therefore, stability is a goal. + +websockets also aims at providing the best API for WebSocket in Python. + +While we value stability, we value progress more. When an improvement requires +changing a public API, we make the change and document it in this changelog. + +When possible with reasonable effort, we preserve backwards-compatibility for +five years after the release that introduced the change. + +When a release contains backwards-incompatible API changes, the major version +is increased, else the minor version is increased. Patch versions are only for +fixing regressions shortly after a release. + +Only documented APIs are public. Undocumented, private APIs may change without +notice. + +12.0 +---- + +*October 21, 2023* + +Backwards-incompatible changes +.............................. + +.. admonition:: websockets 12.0 requires Python ≥ 3.8. + :class: tip + + websockets 11.0 is the last version supporting Python 3.7. + +Improvements +............ + +* Made convenience imports from ``websockets`` compatible with static code + analysis tools such as auto-completion in an IDE or type checking with mypy_. + + .. _mypy: https://github.com/python/mypy + +* Accepted a plain :class:`int` where an :class:`~http.HTTPStatus` is expected. + +* Added :class:`~frames.CloseCode`. + +11.0.3 +------ + +*May 7, 2023* + +Bug fixes +......... + +* Fixed the :mod:`threading` implementation of servers on Windows. + +11.0.2 +------ + +*April 18, 2023* + +Bug fixes +......... + +* Fixed a deadlock in the :mod:`threading` implementation when closing a + connection without reading all messages. + +11.0.1 +------ + +*April 6, 2023* + +Bug fixes +......... + +* Restored the C extension in the source distribution. + +11.0 +---- + +*April 2, 2023* + +Backwards-incompatible changes +.............................. + +.. admonition:: The Sans-I/O implementation was moved. + :class: caution + + Aliases provide compatibility for all previously public APIs according to + the `backwards-compatibility policy`_. + + * The ``connection`` module was renamed to ``protocol``. + + * The ``connection.Connection``, ``server.ServerConnection``, and + ``client.ClientConnection`` classes were renamed to ``protocol.Protocol``, + ``server.ServerProtocol``, and ``client.ClientProtocol``. + +.. admonition:: Sans-I/O protocol constructors now use keyword-only arguments. + :class: caution + + If you instantiate :class:`~server.ServerProtocol` or + :class:`~client.ClientProtocol` directly, make sure you are using keyword + arguments. + +.. admonition:: Closing a connection without an empty close frame is OK. + :class: note + + Receiving an empty close frame now results in + :exc:`~exceptions.ConnectionClosedOK` instead of + :exc:`~exceptions.ConnectionClosedError`. + + As a consequence, calling ``WebSocket.close()`` without arguments in a + browser isn't reported as an error anymore. + +.. admonition:: :func:`~server.serve` times out on the opening handshake after 10 seconds by default. + :class: note + + You can adjust the timeout with the ``open_timeout`` parameter. Set it to + :obj:`None` to disable the timeout entirely. + +New features +............ + +.. admonition:: websockets 11.0 introduces a implementation on top of :mod:`threading`. + :class: important + + It may be more convenient if you don't need to manage many connections and + you're more comfortable with :mod:`threading` than :mod:`asyncio`. + + It is particularly suited to client applications that establish only one + connection. It may be used for servers handling few connections. + + See :func:`~sync.client.connect` and :func:`~sync.server.serve` for details. + +* Added ``open_timeout`` to :func:`~server.serve`. + +* Made it possible to close a server without closing existing connections. + +* Added :attr:`~server.ServerProtocol.select_subprotocol` to customize + negotiation of subprotocols in the Sans-I/O layer. + +Improvements +............ + +* Added platform-independent wheels. + +* Improved error handling in :func:`~websockets.broadcast`. + +* Set ``server_hostname`` automatically on TLS connections when providing a + ``sock`` argument to :func:`~sync.client.connect`. + +10.4 +---- + +*October 25, 2022* + +New features +............ + +* Validated compatibility with Python 3.11. + +* Added the :attr:`~legacy.protocol.WebSocketCommonProtocol.latency` property to + protocols. + +* Changed :attr:`~legacy.protocol.WebSocketCommonProtocol.ping` to return the + latency of the connection. + +* Supported overriding or removing the ``User-Agent`` header in clients and the + ``Server`` header in servers. + +* Added deployment guides for more Platform as a Service providers. + +Improvements +............ + +* Improved FAQ. + +10.3 +---- + +*April 17, 2022* + +Backwards-incompatible changes +.............................. + +.. admonition:: The ``exception`` attribute of :class:`~http11.Request` and :class:`~http11.Response` is deprecated. + :class: note + + Use the ``handshake_exc`` attribute of :class:`~server.ServerProtocol` and + :class:`~client.ClientProtocol` instead. + + See :doc:`../howto/sansio` for details. + +Improvements +............ + +* Reduced noise in logs when :mod:`ssl` or :mod:`zlib` raise exceptions. + +10.2 +---- + +*February 21, 2022* + +Improvements +............ + +* Made compression negotiation more lax for compatibility with Firefox. + +* Improved FAQ and quick start guide. + +Bug fixes +......... + +* Fixed backwards-incompatibility in 10.1 for connection handlers created with + :func:`functools.partial`. + +* Avoided leaking open sockets when :func:`~client.connect` is canceled. + +10.1 +---- + +*November 14, 2021* + +New features +............ + +* Added a tutorial. + +* Made the second parameter of connection handlers optional. It will be + deprecated in the next major release. The request path is available in + the :attr:`~legacy.protocol.WebSocketCommonProtocol.path` attribute of + the first argument. + + If you implemented the connection handler of a server as:: + + async def handler(request, path): + ... + + You should replace it by:: + + async def handler(request): + path = request.path # if handler() uses the path argument + ... + +* Added ``python -m websockets --version``. + +Improvements +............ + +* Added wheels for Python 3.10, PyPy 3.7, and for more platforms. + +* Reverted optimization of default compression settings for clients, mainly to + avoid triggering bugs in poorly implemented servers like `AWS API Gateway`_. + + .. _AWS API Gateway: https://github.com/python-websockets/websockets/issues/1065 + +* Mirrored the entire :class:`~asyncio.Server` API + in :class:`~server.WebSocketServer`. + +* Improved performance for large messages on ARM processors. + +* Documented how to auto-reload on code changes in development. + +Bug fixes +......... + +* Avoided half-closing TCP connections that are already closed. + +10.0 +---- + +*September 9, 2021* + +Backwards-incompatible changes +.............................. + +.. admonition:: websockets 10.0 requires Python ≥ 3.7. + :class: tip + + websockets 9.1 is the last version supporting Python 3.6. + +.. admonition:: The ``loop`` parameter is deprecated from all APIs. + :class: caution + + This reflects a decision made in Python 3.8. See the release notes of + Python 3.10 for details. + + The ``loop`` parameter is also removed + from :class:`~server.WebSocketServer`. This should be transparent. + +.. admonition:: :func:`~client.connect` times out after 10 seconds by default. + :class: note + + You can adjust the timeout with the ``open_timeout`` parameter. Set it to + :obj:`None` to disable the timeout entirely. + +.. admonition:: The ``legacy_recv`` option is deprecated. + :class: note + + See the release notes of websockets 3.0 for details. + +.. admonition:: The signature of :exc:`~exceptions.ConnectionClosed` changed. + :class: note + + If you raise :exc:`~exceptions.ConnectionClosed` or a subclass, rather + than catch them when websockets raises them, you must change your code. + +.. admonition:: A ``msg`` parameter was added to :exc:`~exceptions.InvalidURI`. + :class: note + + If you raise :exc:`~exceptions.InvalidURI`, rather than catch it when + websockets raises it, you must change your code. + +New features +............ + +.. admonition:: websockets 10.0 introduces a `Sans-I/O API + <https://sans-io.readthedocs.io/>`_ for easier integration + in third-party libraries. + :class: important + + If you're integrating websockets in a library, rather than just using it, + look at the :doc:`Sans-I/O integration guide <../howto/sansio>`. + +* Added compatibility with Python 3.10. + +* Added :func:`~websockets.broadcast` to send a message to many clients. + +* Added support for reconnecting automatically by using + :func:`~client.connect` as an asynchronous iterator. + +* Added ``open_timeout`` to :func:`~client.connect`. + +* Documented how to integrate with `Django <https://www.djangoproject.com/>`_. + +* Documented how to deploy websockets in production, with several options. + +* Documented how to authenticate connections. + +* Documented how to broadcast messages to many connections. + +Improvements +............ + +* Improved logging. See the :doc:`logging guide <../topics/logging>`. + +* Optimized default compression settings to reduce memory usage. + +* Optimized processing of client-to-server messages when the C extension isn't + available. + +* Supported relative redirects in :func:`~client.connect`. + +* Handled TCP connection drops during the opening handshake. + +* Made it easier to customize authentication with + :meth:`~auth.BasicAuthWebSocketServerProtocol.check_credentials`. + +* Provided additional information in :exc:`~exceptions.ConnectionClosed` + exceptions. + +* Clarified several exceptions or log messages. + +* Restructured documentation. + +* Improved API documentation. + +* Extended FAQ. + +Bug fixes +......... + +* Avoided a crash when receiving a ping while the connection is closing. + +9.1 +--- + +*May 27, 2021* + +Security fix +............ + +.. admonition:: websockets 9.1 fixes a security issue introduced in 8.0. + :class: important + + Version 8.0 was vulnerable to timing attacks on HTTP Basic Auth passwords + (`CVE-2021-33880`_). + + .. _CVE-2021-33880: https://nvd.nist.gov/vuln/detail/CVE-2021-33880 + +9.0.2 +----- + +*May 15, 2021* + +Bug fixes +......... + +* Restored compatibility of ``python -m websockets`` with Python < 3.9. + +* Restored compatibility with mypy. + +9.0.1 +----- + +*May 2, 2021* + +Bug fixes +......... + +* Fixed issues with the packaging of the 9.0 release. + +9.0 +--- + +*May 1, 2021* + +Backwards-incompatible changes +.............................. + +.. admonition:: Several modules are moved or deprecated. + :class: caution + + Aliases provide compatibility for all previously public APIs according to + the `backwards-compatibility policy`_ + + * :class:`~datastructures.Headers` and + :exc:`~datastructures.MultipleValuesError` are moved from + ``websockets.http`` to :mod:`websockets.datastructures`. If you're using + them, you should adjust the import path. + + * The ``client``, ``server``, ``protocol``, and ``auth`` modules were + moved from the ``websockets`` package to a ``websockets.legacy`` + sub-package. Despite the name, they're still fully supported. + + * The ``framing``, ``handshake``, ``headers``, ``http``, and ``uri`` + modules in the ``websockets`` package are deprecated. These modules + provided low-level APIs for reuse by other projects, but they didn't + reach that goal. Keeping these APIs public makes it more difficult to + improve websockets. + + These changes pave the path for a refactoring that should be a transparent + upgrade for most uses and facilitate integration by other projects. + +.. admonition:: Convenience imports from ``websockets`` are performed lazily. + :class: note + + While Python supports this, tools relying on static code analysis don't. + This breaks auto-completion in an IDE or type checking with mypy_. + + .. _mypy: https://github.com/python/mypy + + If you depend on such tools, use the real import paths, which can be found + in the API documentation, for example:: + + from websockets.client import connect + from websockets.server import serve + +New features +............ + +* Added compatibility with Python 3.9. + +Improvements +............ + +* Added support for IRIs in addition to URIs. + +* Added close codes 1012, 1013, and 1014. + +* Raised an error when passing a :class:`dict` to + :meth:`~legacy.protocol.WebSocketCommonProtocol.send`. + +* Improved error reporting. + +Bug fixes +......... + +* Fixed sending fragmented, compressed messages. + +* Fixed ``Host`` header sent when connecting to an IPv6 address. + +* Fixed creating a client or a server with an existing Unix socket. + +* Aligned maximum cookie size with popular web browsers. + +* Ensured cancellation always propagates, even on Python versions where + :exc:`~asyncio.CancelledError` inherits :exc:`Exception`. + +8.1 +--- + +*November 1, 2019* + +New features +............ + +* Added compatibility with Python 3.8. + +8.0.2 +----- + +*July 31, 2019* + +Bug fixes +......... + +* Restored the ability to pass a socket with the ``sock`` parameter of + :func:`~server.serve`. + +* Removed an incorrect assertion when a connection drops. + +8.0.1 +----- + +*July 21, 2019* + +Bug fixes +......... + +* Restored the ability to import ``WebSocketProtocolError`` from + ``websockets``. + +8.0 +--- + +*July 7, 2019* + +Backwards-incompatible changes +.............................. + +.. admonition:: websockets 8.0 requires Python ≥ 3.6. + :class: tip + + websockets 7.0 is the last version supporting Python 3.4 and 3.5. + +.. admonition:: ``process_request`` is now expected to be a coroutine. + :class: note + + If you're passing a ``process_request`` argument to + :func:`~server.serve` or :class:`~server.WebSocketServerProtocol`, or if + you're overriding + :meth:`~server.WebSocketServerProtocol.process_request` in a subclass, + define it with ``async def`` instead of ``def``. Previously, both were supported. + + For backwards compatibility, functions are still accepted, but mixing + functions and coroutines won't work in some inheritance scenarios. + +.. admonition:: ``max_queue`` must be :obj:`None` to disable the limit. + :class: note + + If you were setting ``max_queue=0`` to make the queue of incoming messages + unbounded, change it to ``max_queue=None``. + +.. admonition:: The ``host``, ``port``, and ``secure`` attributes + of :class:`~legacy.protocol.WebSocketCommonProtocol` are deprecated. + :class: note + + Use :attr:`~legacy.protocol.WebSocketCommonProtocol.local_address` in + servers and + :attr:`~legacy.protocol.WebSocketCommonProtocol.remote_address` in clients + instead of ``host`` and ``port``. + +.. admonition:: ``WebSocketProtocolError`` is renamed + to :exc:`~exceptions.ProtocolError`. + :class: note + + An alias provides backwards compatibility. + +.. admonition:: ``read_response()`` now returns the reason phrase. + :class: note + + If you're using this low-level API, you must change your code. + +New features +............ + +* Added :func:`~auth.basic_auth_protocol_factory` to enforce HTTP + Basic Auth on the server side. + +* :func:`~client.connect` handles redirects from the server during the + handshake. + +* :func:`~client.connect` supports overriding ``host`` and ``port``. + +* Added :func:`~client.unix_connect` for connecting to Unix sockets. + +* Added support for asynchronous generators + in :meth:`~legacy.protocol.WebSocketCommonProtocol.send` + to generate fragmented messages incrementally. + +* Enabled readline in the interactive client. + +* Added type hints (:pep:`484`). + +* Added a FAQ to the documentation. + +* Added documentation for extensions. + +* Documented how to optimize memory usage. + +Improvements +............ + +* :meth:`~legacy.protocol.WebSocketCommonProtocol.send`, + :meth:`~legacy.protocol.WebSocketCommonProtocol.ping`, and + :meth:`~legacy.protocol.WebSocketCommonProtocol.pong` support bytes-like + types :class:`bytearray` and :class:`memoryview` in addition to + :class:`bytes`. + +* Added :exc:`~exceptions.ConnectionClosedOK` and + :exc:`~exceptions.ConnectionClosedError` subclasses of + :exc:`~exceptions.ConnectionClosed` to tell apart normal connection + termination from errors. + +* Changed :meth:`WebSocketServer.close() + <server.WebSocketServer.close>` to perform a proper closing handshake + instead of failing the connection. + +* Improved error messages when HTTP parsing fails. + +* Improved API documentation. + +Bug fixes +......... + +* Prevented spurious log messages about :exc:`~exceptions.ConnectionClosed` + exceptions in keepalive ping task. If you were using ``ping_timeout=None`` + as a workaround, you can remove it. + +* Avoided a crash when a ``extra_headers`` callable returns :obj:`None`. + +7.0 +--- + +*November 1, 2018* + +Backwards-incompatible changes +.............................. + +.. admonition:: Keepalive is enabled by default. + :class: important + + websockets now sends Ping frames at regular intervals and closes the + connection if it doesn't receive a matching Pong frame. + See :class:`~legacy.protocol.WebSocketCommonProtocol` for details. + +.. admonition:: Termination of connections by :meth:`WebSocketServer.close() + <server.WebSocketServer.close>` changes. + :class: caution + + Previously, connections handlers were canceled. Now, connections are + closed with close code 1001 (going away). + + From the perspective of the connection handler, this is the same as if the + remote endpoint was disconnecting. This removes the need to prepare for + :exc:`~asyncio.CancelledError` in connection handlers. + + You can restore the previous behavior by adding the following line at the + beginning of connection handlers:: + + def handler(websocket, path): + closed = asyncio.ensure_future(websocket.wait_closed()) + closed.add_done_callback(lambda task: task.cancel()) + +.. admonition:: Calling :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` + concurrently raises a :exc:`RuntimeError`. + :class: note + + Concurrent calls lead to non-deterministic behavior because there are no + guarantees about which coroutine will receive which message. + +.. admonition:: The ``timeout`` argument of :func:`~server.serve` + and :func:`~client.connect` is renamed to ``close_timeout`` . + :class: note + + This prevents confusion with ``ping_timeout``. + + For backwards compatibility, ``timeout`` is still supported. + +.. admonition:: The ``origins`` argument of :func:`~server.serve` changes. + :class: note + + Include :obj:`None` in the list rather than ``''`` to allow requests that + don't contain an Origin header. + +.. admonition:: Pending pings aren't canceled when the connection is closed. + :class: note + + A ping — as in ``ping = await websocket.ping()`` — for which no pong was + received yet used to be canceled when the connection is closed, so that + ``await ping`` raised :exc:`~asyncio.CancelledError`. + + Now ``await ping`` raises :exc:`~exceptions.ConnectionClosed` like other + public APIs. + +New features +............ + +* Added ``process_request`` and ``select_subprotocol`` arguments to + :func:`~server.serve` and + :class:`~server.WebSocketServerProtocol` to facilitate customization of + :meth:`~server.WebSocketServerProtocol.process_request` and + :meth:`~server.WebSocketServerProtocol.select_subprotocol`. + +* Added support for sending fragmented messages. + +* Added the :meth:`~legacy.protocol.WebSocketCommonProtocol.wait_closed` + method to protocols. + +* Added an interactive client: ``python -m websockets <uri>``. + +Improvements +............ + +* Improved handling of multiple HTTP headers with the same name. + +* Improved error messages when a required HTTP header is missing. + +Bug fixes +......... + +* Fixed a data loss bug in + :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`: + canceling it at the wrong time could result in messages being dropped. + +6.0 +--- + +*July 16, 2018* + +Backwards-incompatible changes +.............................. + +.. admonition:: The :class:`~datastructures.Headers` class is introduced and + several APIs are updated to use it. + :class: caution + + * The ``request_headers`` argument + of :meth:`~server.WebSocketServerProtocol.process_request` is now + a :class:`~datastructures.Headers` instead of + an ``http.client.HTTPMessage``. + + * The ``request_headers`` and ``response_headers`` attributes of + :class:`~legacy.protocol.WebSocketCommonProtocol` are now + :class:`~datastructures.Headers` instead of ``http.client.HTTPMessage``. + + * The ``raw_request_headers`` and ``raw_response_headers`` attributes of + :class:`~legacy.protocol.WebSocketCommonProtocol` are removed. Use + :meth:`~datastructures.Headers.raw_items` instead. + + * Functions defined in the ``handshake`` module now receive + :class:`~datastructures.Headers` in argument instead of ``get_header`` + or ``set_header`` functions. This affects libraries that rely on + low-level APIs. + + * Functions defined in the ``http`` module now return HTTP headers as + :class:`~datastructures.Headers` instead of lists of ``(name, value)`` + pairs. + + Since :class:`~datastructures.Headers` and ``http.client.HTTPMessage`` + provide similar APIs, much of the code dealing with HTTP headers won't + require changes. + +New features +............ + +* Added compatibility with Python 3.7. + +5.0.1 +----- + +*May 24, 2018* + +Bug fixes +......... + +* Fixed a regression in 5.0 that broke some invocations of + :func:`~server.serve` and :func:`~client.connect`. + +5.0 +--- + +*May 22, 2018* + +Security fix +............ + +.. admonition:: websockets 5.0 fixes a security issue introduced in 4.0. + :class: important + + Version 4.0 was vulnerable to denial of service by memory exhaustion + because it didn't enforce ``max_size`` when decompressing compressed + messages (`CVE-2018-1000518`_). + + .. _CVE-2018-1000518: https://nvd.nist.gov/vuln/detail/CVE-2018-1000518 + +Backwards-incompatible changes +.............................. + +.. admonition:: A ``user_info`` field is added to the return value of + ``parse_uri`` and ``WebSocketURI``. + :class: note + + If you're unpacking ``WebSocketURI`` into four variables, adjust your code + to account for that fifth field. + +New features +............ + +* :func:`~client.connect` performs HTTP Basic Auth when the URI contains + credentials. + +* :func:`~server.unix_serve` can be used as an asynchronous context + manager on Python ≥ 3.5.1. + +* Added the :attr:`~legacy.protocol.WebSocketCommonProtocol.closed` property + to protocols. + +* Added new examples in the documentation. + +Improvements +............ + +* Iterating on incoming messages no longer raises an exception when the + connection terminates with close code 1001 (going away). + +* A plain HTTP request now receives a 426 Upgrade Required response and + doesn't log a stack trace. + +* If a :meth:`~legacy.protocol.WebSocketCommonProtocol.ping` doesn't receive a + pong, it's canceled when the connection is closed. + +* Reported the cause of :exc:`~exceptions.ConnectionClosed` exceptions. + +* Stopped logging stack traces when the TCP connection dies prematurely. + +* Prevented writing to a closing TCP connection during unclean shutdowns. + +* Made connection termination more robust to network congestion. + +* Prevented processing of incoming frames after failing the connection. + +* Updated documentation with new features from Python 3.6. + +* Improved several sections of the documentation. + +Bug fixes +......... + +* Prevented :exc:`TypeError` due to missing close code on connection close. + +* Fixed a race condition in the closing handshake that raised + :exc:`~exceptions.InvalidState`. + +4.0.1 +----- + +*November 2, 2017* + +Bug fixes +......... + +* Fixed issues with the packaging of the 4.0 release. + +4.0 +--- + +*November 2, 2017* + +Backwards-incompatible changes +.............................. + +.. admonition:: websockets 4.0 requires Python ≥ 3.4. + :class: tip + + websockets 3.4 is the last version supporting Python 3.3. + +.. admonition:: Compression is enabled by default. + :class: important + + In August 2017, Firefox and Chrome support the permessage-deflate + extension, but not Safari and IE. + + Compression should improve performance but it increases RAM and CPU use. + + If you want to disable compression, add ``compression=None`` when calling + :func:`~server.serve` or :func:`~client.connect`. + +.. admonition:: The ``state_name`` attribute of protocols is deprecated. + :class: note + + Use ``protocol.state.name`` instead of ``protocol.state_name``. + +New features +............ + +* :class:`~legacy.protocol.WebSocketCommonProtocol` instances can be used as + asynchronous iterators on Python ≥ 3.6. They yield incoming messages. + +* Added :func:`~server.unix_serve` for listening on Unix sockets. + +* Added the :attr:`~server.WebSocketServer.sockets` attribute to the + return value of :func:`~server.serve`. + +* Allowed ``extra_headers`` to override ``Server`` and ``User-Agent`` headers. + +Improvements +............ + +* Reorganized and extended documentation. + +* Rewrote connection termination to increase robustness in edge cases. + +* Reduced verbosity of "Failing the WebSocket connection" logs. + +Bug fixes +......... + +* Aborted connections if they don't close within the configured ``timeout``. + +* Stopped leaking pending tasks when :meth:`~asyncio.Task.cancel` is called on + a connection while it's being closed. + +3.4 +--- + +*August 20, 2017* + +Backwards-incompatible changes +.............................. + +.. admonition:: ``InvalidStatus`` is replaced + by :class:`~exceptions.InvalidStatusCode`. + :class: note + + This exception is raised when :func:`~client.connect` receives an invalid + response status code from the server. + +New features +............ + +* :func:`~server.serve` can be used as an asynchronous context manager + on Python ≥ 3.5.1. + +* Added support for customizing handling of incoming connections with + :meth:`~server.WebSocketServerProtocol.process_request`. + +* Made read and write buffer sizes configurable. + +Improvements +............ + +* Renamed :func:`~server.serve` and :func:`~client.connect`'s + ``klass`` argument to ``create_protocol`` to reflect that it can also be a + callable. For backwards compatibility, ``klass`` is still supported. + +* Rewrote HTTP handling for simplicity and performance. + +* Added an optional C extension to speed up low-level operations. + +Bug fixes +......... + +* Providing a ``sock`` argument to :func:`~client.connect` no longer + crashes. + +3.3 +--- + +*March 29, 2017* + +New features +............ + +* Ensured compatibility with Python 3.6. + +Improvements +............ + +* Reduced noise in logs caused by connection resets. + +Bug fixes +......... + +* Avoided crashing on concurrent writes on slow connections. + +3.2 +--- + +*August 17, 2016* + +New features +............ + +* Added ``timeout``, ``max_size``, and ``max_queue`` arguments to + :func:`~client.connect` and :func:`~server.serve`. + +Improvements +............ + +* Made server shutdown more robust. + +3.1 +--- + +*April 21, 2016* + +New features +............ + +* Added flow control for incoming data. + +Bug fixes +......... + +* Avoided a warning when closing a connection before the opening handshake. + +3.0 +--- + +*December 25, 2015* + +Backwards-incompatible changes +.............................. + +.. admonition:: :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` now + raises an exception when the connection is closed. + :class: caution + + :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` used to return + :obj:`None` when the connection was closed. This required checking the + return value of every call:: + + message = await websocket.recv() + if message is None: + return + + Now it raises a :exc:`~exceptions.ConnectionClosed` exception instead. + This is more Pythonic. The previous code can be simplified to:: + + message = await websocket.recv() + + When implementing a server, there's no strong reason to handle such + exceptions. Let them bubble up, terminate the handler coroutine, and the + server will simply ignore them. + + In order to avoid stranding projects built upon an earlier version, the + previous behavior can be restored by passing ``legacy_recv=True`` to + :func:`~server.serve`, :func:`~client.connect`, + :class:`~server.WebSocketServerProtocol`, or + :class:`~client.WebSocketClientProtocol`. + +New features +............ + +* :func:`~client.connect` can be used as an asynchronous context + manager on Python ≥ 3.5.1. + +* :meth:`~legacy.protocol.WebSocketCommonProtocol.ping` and + :meth:`~legacy.protocol.WebSocketCommonProtocol.pong` support data passed as + :class:`str` in addition to :class:`bytes`. + +* Made ``state_name`` attribute on protocols a public API. + +Improvements +............ + +* Updated documentation with ``await`` and ``async`` syntax from Python 3.5. + +* Worked around an :mod:`asyncio` bug affecting connection termination under + load. + +* Improved documentation. + +2.7 +--- + +*November 18, 2015* + +New features +............ + +* Added compatibility with Python 3.5. + +Improvements +............ + +* Refreshed documentation. + +2.6 +--- + +*August 18, 2015* + +New features +............ + +* Added ``local_address`` and ``remote_address`` attributes on protocols. + +* Closed open connections with code 1001 when a server shuts down. + +Bug fixes +......... + +* Avoided TCP fragmentation of small frames. + +2.5 +--- + +*July 28, 2015* + +New features +............ + +* Provided access to handshake request and response HTTP headers. + +* Allowed customizing handshake request and response HTTP headers. + +* Added support for running on a non-default event loop. + +Improvements +............ + +* Improved documentation. + +* Sent a 403 status code instead of 400 when request Origin isn't allowed. + +* Clarified that the closing handshake can be initiated by the client. + +* Set the close code and reason more consistently. + +* Strengthened connection termination. + +Bug fixes +......... + +* Canceling :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` no longer + drops the next message. + +2.4 +--- + +*January 31, 2015* + +New features +............ + +* Added support for subprotocols. + +* Added ``loop`` argument to :func:`~client.connect` and + :func:`~server.serve`. + +2.3 +--- + +*November 3, 2014* + +Improvements +............ + +* Improved compliance of close codes. + +2.2 +--- + +*July 28, 2014* + +New features +............ + +* Added support for limiting message size. + +2.1 +--- + +*April 26, 2014* + +New features +............ + +* Added ``host``, ``port`` and ``secure`` attributes on protocols. + +* Added support for providing and checking Origin_. + +.. _Origin: https://www.rfc-editor.org/rfc/rfc6455.html#section-10.2 + +2.0 +--- + +*February 16, 2014* + +Backwards-incompatible changes +.............................. + +.. admonition:: :meth:`~legacy.protocol.WebSocketCommonProtocol.send`, + :meth:`~legacy.protocol.WebSocketCommonProtocol.ping`, and + :meth:`~legacy.protocol.WebSocketCommonProtocol.pong` are now coroutines. + :class: caution + + They used to be functions. + + Instead of:: + + websocket.send(message) + + you must write:: + + await websocket.send(message) + +New features +............ + +* Added flow control for outgoing data. + +1.0 +--- + +*November 14, 2013* + +New features +............ + +* Initial public release. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/project/contributing.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/project/contributing.rst new file mode 100644 index 0000000000..020ed7ad85 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/project/contributing.rst @@ -0,0 +1,66 @@ +Contributing +============ + +Thanks for taking the time to contribute to websockets! + +Code of Conduct +--------------- + +This project and everyone participating in it is governed by the `Code of +Conduct`_. By participating, you are expected to uphold this code. Please +report inappropriate behavior to aymeric DOT augustin AT fractalideas DOT com. + +.. _Code of Conduct: https://github.com/python-websockets/websockets/blob/main/CODE_OF_CONDUCT.md + +*(If I'm the person with the inappropriate behavior, please accept my +apologies. I know I can mess up. I can't expect you to tell me, but if you +choose to do so, I'll do my best to handle criticism constructively. +-- Aymeric)* + +Contributions +------------- + +Bug reports, patches and suggestions are welcome! + +Please open an issue_ or send a `pull request`_. + +Feedback about the documentation is especially valuable, as the primary author +feels more confident about writing code than writing docs :-) + +If you're wondering why things are done in a certain way, the :doc:`design +document <../topics/design>` provides lots of details about the internals of +websockets. + +.. _issue: https://github.com/python-websockets/websockets/issues/new +.. _pull request: https://github.com/python-websockets/websockets/compare/ + +Questions +--------- + +GitHub issues aren't a good medium for handling questions. There are better +places to ask questions, for example Stack Overflow. + +If you want to ask a question anyway, please make sure that: + +- it's a question about websockets and not about :mod:`asyncio`; +- it isn't answered in the documentation; +- it wasn't asked already. + +A good question can be written as a suggestion to improve the documentation. + +Cryptocurrency users +-------------------- + +websockets appears to be quite popular for interfacing with Bitcoin or other +cryptocurrency trackers. I'm strongly opposed to Bitcoin's carbon footprint. + +I'm aware of efforts to build proof-of-stake models. I'll care once the total +energy consumption of all cryptocurrencies drops to a non-bullshit level. + +You already negated all of humanity's efforts to develop renewable energy. +Please stop heating the planet where my children will have to live. + +Since websockets is released under an open-source license, you can use it for +any purpose you like. However, I won't spend any of my time to help you. + +I will summarily close issues related to Bitcoin or cryptocurrency in any way. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/project/index.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/project/index.rst new file mode 100644 index 0000000000..459146345b --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/project/index.rst @@ -0,0 +1,12 @@ +About websockets +================ + +This is about websockets-the-project rather than websockets-the-software. + +.. toctree:: + :titlesonly: + + changelog + contributing + license + For enterprise <tidelift> diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/project/license.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/project/license.rst new file mode 100644 index 0000000000..0a3b8703d5 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/project/license.rst @@ -0,0 +1,4 @@ +License +======= + +.. include:: ../../LICENSE diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/project/tidelift.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/project/tidelift.rst new file mode 100644 index 0000000000..42100fade9 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/project/tidelift.rst @@ -0,0 +1,112 @@ +websockets for enterprise +========================= + +Available as part of the Tidelift Subscription +---------------------------------------------- + +.. image:: ../_static/tidelift.png + :height: 150px + :width: 150px + :align: left + +Tidelift is working with the maintainers of websockets and thousands of other +open source projects to deliver commercial support and maintenance for the +open source dependencies you use to build your applications. Save time, reduce +risk, and improve code health, while paying the maintainers of the exact +dependencies you use. + +.. raw:: html + + <style type="text/css"> + .tidelift-links { + display: flex; + justify-content: center; + } + @media only screen and (max-width: 600px) { + .tidelift-links { + flex-direction: column; + } + } + .tidelift-links a { + border: thin solid #f6914d; + border-radius: 0.25em; + font-family: Verdana, sans-serif; + font-size: 15px; + margin: 0.5em 2em; + padding: 0.5em 2em; + text-align: center; + text-decoration: none; + text-transform: uppercase; + } + .tidelift-links a.tidelift-links__learn-more { + background-color: white; + color: #f6914d; + } + .tidelift-links a.tidelift-links__request-a-demo { + background-color: #f6914d; + color: white; + } + </style> + + <div class="tidelift-links"> + <a class="tidelift-links__learn-more" href="https://tidelift.com/subscription/pkg/pypi-websockets?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=enterprise">Learn more</a> + <a class="tidelift-links__request-a-demo" href="https://tidelift.com/subscription/request-a-demo?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=enterprise">Request a demo</a> + </div> + +Enterprise-ready open source software—managed for you +----------------------------------------------------- + +The Tidelift Subscription is a managed open source subscription for +application dependencies covering millions of open source projects across +JavaScript, Python, Java, PHP, Ruby, .NET, and more. + +Your subscription includes: + +* **Security updates** + + * Tidelift’s security response team coordinates patches for new breaking + security vulnerabilities and alerts immediately through a private channel, + so your software supply chain is always secure. + +* **Licensing verification and indemnification** + + * Tidelift verifies license information to enable easy policy enforcement + and adds intellectual property indemnification to cover creators and users + in case something goes wrong. You always have a 100% up-to-date bill of + materials for your dependencies to share with your legal team, customers, + or partners. + +* **Maintenance and code improvement** + + * Tidelift ensures the software you rely on keeps working as long as you + need it to work. Your managed dependencies are actively maintained and we + recruit additional maintainers where required. + +* **Package selection and version guidance** + + * We help you choose the best open source packages from the start—and then + guide you through updates to stay on the best releases as new issues + arise. + +* **Roadmap input** + + * Take a seat at the table with the creators behind the software you use. + Tidelift’s participating maintainers earn more income as their software is + used by more subscribers, so they’re interested in knowing what you need. + +* **Tooling and cloud integration** + + * Tidelift works with GitHub, GitLab, BitBucket, and more. We support every + cloud platform (and other deployment targets, too). + +The end result? All of the capabilities you expect from commercial-grade +software, for the full breadth of open source you use. That means less time +grappling with esoteric open source trivia, and more time building your own +applications—and your business. + +.. raw:: html + + <div class="tidelift-links"> + <a class="tidelift-links__learn-more" href="https://tidelift.com/subscription/pkg/pypi-websockets?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=enterprise">Learn more</a> + <a class="tidelift-links__request-a-demo" href="https://tidelift.com/subscription/request-a-demo?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=enterprise">Request a demo</a> + </div> diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/client.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/client.rst new file mode 100644 index 0000000000..5086015b7b --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/client.rst @@ -0,0 +1,64 @@ +Client (:mod:`asyncio`) +======================= + +.. automodule:: websockets.client + +Opening a connection +-------------------- + +.. autofunction:: connect(uri, *, create_protocol=None, logger=None, compression="deflate", origin=None, extensions=None, subprotocols=None, extra_headers=None, user_agent_header="Python/x.y.z websockets/X.Y", open_timeout=10, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16, **kwds) + :async: + +.. autofunction:: unix_connect(path, uri="ws://localhost/", *, create_protocol=None, logger=None, compression="deflate", origin=None, extensions=None, subprotocols=None, extra_headers=None, user_agent_header="Python/x.y.z websockets/X.Y", open_timeout=10, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16, **kwds) + :async: + +Using a connection +------------------ + +.. autoclass:: WebSocketClientProtocol(*, logger=None, origin=None, extensions=None, subprotocols=None, extra_headers=None, user_agent_header="Python/x.y.z websockets/X.Y", ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16) + + .. automethod:: recv + + .. automethod:: send + + .. automethod:: close + + .. automethod:: wait_closed + + .. automethod:: ping + + .. automethod:: pong + + WebSocket connection objects also provide these attributes: + + .. autoattribute:: id + + .. autoattribute:: logger + + .. autoproperty:: local_address + + .. autoproperty:: remote_address + + .. autoproperty:: open + + .. autoproperty:: closed + + .. autoattribute:: latency + + The following attributes are available after the opening handshake, + once the WebSocket connection is open: + + .. autoattribute:: path + + .. autoattribute:: request_headers + + .. autoattribute:: response_headers + + .. autoattribute:: subprotocol + + The following attributes are available after the closing handshake, + once the WebSocket connection is closed: + + .. autoproperty:: close_code + + .. autoproperty:: close_reason diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/common.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/common.rst new file mode 100644 index 0000000000..dc7a54ee1a --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/common.rst @@ -0,0 +1,54 @@ +:orphan: + +Both sides (:mod:`asyncio`) +=========================== + +.. automodule:: websockets.legacy.protocol + +.. autoclass:: WebSocketCommonProtocol(*, logger=None, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16) + + .. automethod:: recv + + .. automethod:: send + + .. automethod:: close + + .. automethod:: wait_closed + + .. automethod:: ping + + .. automethod:: pong + + WebSocket connection objects also provide these attributes: + + .. autoattribute:: id + + .. autoattribute:: logger + + .. autoproperty:: local_address + + .. autoproperty:: remote_address + + .. autoproperty:: open + + .. autoproperty:: closed + + .. autoattribute:: latency + + The following attributes are available after the opening handshake, + once the WebSocket connection is open: + + .. autoattribute:: path + + .. autoattribute:: request_headers + + .. autoattribute:: response_headers + + .. autoattribute:: subprotocol + + The following attributes are available after the closing handshake, + once the WebSocket connection is closed: + + .. autoproperty:: close_code + + .. autoproperty:: close_reason diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/server.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/server.rst new file mode 100644 index 0000000000..1063179162 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/server.rst @@ -0,0 +1,113 @@ +Server (:mod:`asyncio`) +======================= + +.. automodule:: websockets.server + +Starting a server +----------------- + +.. autofunction:: serve(ws_handler, host=None, port=None, *, create_protocol=None, logger=None, compression="deflate", origins=None, extensions=None, subprotocols=None, extra_headers=None, server_header="Python/x.y.z websockets/X.Y", process_request=None, select_subprotocol=None, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16, **kwds) + :async: + +.. autofunction:: unix_serve(ws_handler, path=None, *, create_protocol=None, logger=None, compression="deflate", origins=None, extensions=None, subprotocols=None, extra_headers=None, server_header="Python/x.y.z websockets/X.Y", process_request=None, select_subprotocol=None, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16, **kwds) + :async: + +Stopping a server +----------------- + +.. autoclass:: WebSocketServer + + .. automethod:: close + + .. automethod:: wait_closed + + .. automethod:: get_loop + + .. automethod:: is_serving + + .. automethod:: start_serving + + .. automethod:: serve_forever + + .. autoattribute:: sockets + +Using a connection +------------------ + +.. autoclass:: WebSocketServerProtocol(ws_handler, ws_server, *, logger=None, origins=None, extensions=None, subprotocols=None, extra_headers=None, server_header="Python/x.y.z websockets/X.Y", process_request=None, select_subprotocol=None, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16) + + .. automethod:: recv + + .. automethod:: send + + .. automethod:: close + + .. automethod:: wait_closed + + .. automethod:: ping + + .. automethod:: pong + + You can customize the opening handshake in a subclass by overriding these methods: + + .. automethod:: process_request + + .. automethod:: select_subprotocol + + WebSocket connection objects also provide these attributes: + + .. autoattribute:: id + + .. autoattribute:: logger + + .. autoproperty:: local_address + + .. autoproperty:: remote_address + + .. autoproperty:: open + + .. autoproperty:: closed + + .. autoattribute:: latency + + The following attributes are available after the opening handshake, + once the WebSocket connection is open: + + .. autoattribute:: path + + .. autoattribute:: request_headers + + .. autoattribute:: response_headers + + .. autoattribute:: subprotocol + + The following attributes are available after the closing handshake, + once the WebSocket connection is closed: + + .. autoproperty:: close_code + + .. autoproperty:: close_reason + + +Basic authentication +-------------------- + +.. automodule:: websockets.auth + +websockets supports HTTP Basic Authentication according to +:rfc:`7235` and :rfc:`7617`. + +.. autofunction:: basic_auth_protocol_factory + +.. autoclass:: BasicAuthWebSocketServerProtocol + + .. autoattribute:: realm + + .. autoattribute:: username + + .. automethod:: check_credentials + +Broadcast +--------- + +.. autofunction:: websockets.broadcast diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/datastructures.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/datastructures.rst new file mode 100644 index 0000000000..ec02d42101 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/datastructures.rst @@ -0,0 +1,66 @@ +Data structures +=============== + +WebSocket events +---------------- + +.. automodule:: websockets.frames + + .. autoclass:: Frame + + .. autoclass:: Opcode + + .. autoattribute:: CONT + .. autoattribute:: TEXT + .. autoattribute:: BINARY + .. autoattribute:: CLOSE + .. autoattribute:: PING + .. autoattribute:: PONG + + .. autoclass:: Close + + .. autoclass:: CloseCode + + .. autoattribute:: NORMAL_CLOSURE + .. autoattribute:: GOING_AWAY + .. autoattribute:: PROTOCOL_ERROR + .. autoattribute:: UNSUPPORTED_DATA + .. autoattribute:: NO_STATUS_RCVD + .. autoattribute:: ABNORMAL_CLOSURE + .. autoattribute:: INVALID_DATA + .. autoattribute:: POLICY_VIOLATION + .. autoattribute:: MESSAGE_TOO_BIG + .. autoattribute:: MANDATORY_EXTENSION + .. autoattribute:: INTERNAL_ERROR + .. autoattribute:: SERVICE_RESTART + .. autoattribute:: TRY_AGAIN_LATER + .. autoattribute:: BAD_GATEWAY + .. autoattribute:: TLS_HANDSHAKE + +HTTP events +----------- + +.. automodule:: websockets.http11 + + .. autoclass:: Request + + .. autoclass:: Response + +.. automodule:: websockets.datastructures + + .. autoclass:: Headers + + .. automethod:: get_all + + .. automethod:: raw_items + + .. autoexception:: MultipleValuesError + +URIs +---- + +.. automodule:: websockets.uri + + .. autofunction:: parse_uri + + .. autoclass:: WebSocketURI diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/exceptions.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/exceptions.rst new file mode 100644 index 0000000000..907a650d20 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/exceptions.rst @@ -0,0 +1,6 @@ +Exceptions +========== + +.. automodule:: websockets.exceptions + :members: + diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/extensions.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/extensions.rst new file mode 100644 index 0000000000..a70f1b1e58 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/extensions.rst @@ -0,0 +1,60 @@ +Extensions +========== + +.. currentmodule:: websockets.extensions + +The WebSocket protocol supports extensions_. + +At the time of writing, there's only one `registered extension`_ with a public +specification, WebSocket Per-Message Deflate. + +.. _extensions: https://www.rfc-editor.org/rfc/rfc6455.html#section-9 +.. _registered extension: https://www.iana.org/assignments/websocket/websocket.xhtml#extension-name + +Per-Message Deflate +------------------- + +.. automodule:: websockets.extensions.permessage_deflate + + :mod:`websockets.extensions.permessage_deflate` implements WebSocket + Per-Message Deflate. + + This extension is specified in :rfc:`7692`. + + Refer to the :doc:`topic guide on compression <../topics/compression>` to + learn more about tuning compression settings. + + .. autoclass:: ClientPerMessageDeflateFactory + + .. autoclass:: ServerPerMessageDeflateFactory + +Base classes +------------ + +.. automodule:: websockets.extensions + + :mod:`websockets.extensions` defines base classes for implementing + extensions. + + Refer to the :doc:`how-to guide on extensions <../howto/extensions>` to + learn more about writing an extension. + + .. autoclass:: Extension + + .. autoattribute:: name + + .. automethod:: decode + + .. automethod:: encode + + .. autoclass:: ClientExtensionFactory + + .. autoattribute:: name + + .. automethod:: get_request_params + + .. automethod:: process_response_params + + .. autoclass:: ServerExtensionFactory + + .. automethod:: process_request_params diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/features.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/features.rst new file mode 100644 index 0000000000..98b3c0ddaf --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/features.rst @@ -0,0 +1,187 @@ +Features +======== + +.. currentmodule:: websockets + +Feature support matrices summarize which implementations support which features. + +.. raw:: html + + <style> + .support-matrix-table { width: 100%; } + .support-matrix-table th:first-child { text-align: left; } + .support-matrix-table th:not(:first-child) { text-align: center; width: 15%; } + .support-matrix-table td:not(:first-child) { text-align: center; } + </style> + +.. |aio| replace:: :mod:`asyncio` +.. |sync| replace:: :mod:`threading` +.. |sans| replace:: `Sans-I/O`_ +.. _Sans-I/O: https://sans-io.readthedocs.io/ + +Both sides +---------- + +.. table:: + :class: support-matrix-table + + +------------------------------------+--------+--------+--------+ + | | |aio| | |sync| | |sans| | + +====================================+========+========+========+ + | Perform the opening handshake | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Send a message | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Receive a message | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Iterate over received messages | ✅ | ✅ | ❌ | + +------------------------------------+--------+--------+--------+ + | Send a fragmented message | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Receive a fragmented message after | ✅ | ✅ | ❌ | + | reassembly | | | | + +------------------------------------+--------+--------+--------+ + | Receive a fragmented message frame | ❌ | ✅ | ✅ | + | by frame (`#479`_) | | | | + +------------------------------------+--------+--------+--------+ + | Send a ping | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Respond to pings automatically | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Send a pong | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Perform the closing handshake | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Report close codes and reasons | ❌ | ✅ | ✅ | + | from both sides | | | | + +------------------------------------+--------+--------+--------+ + | Compress messages (:rfc:`7692`) | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Tune memory usage for compression | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Negotiate extensions | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Implement custom extensions | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Negotiate a subprotocol | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Enforce security limits | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Log events | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Enforce opening timeout | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Enforce closing timeout | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Keepalive | ✅ | ❌ | — | + +------------------------------------+--------+--------+--------+ + | Heartbeat | ✅ | ❌ | — | + +------------------------------------+--------+--------+--------+ + +.. _#479: https://github.com/python-websockets/websockets/issues/479 + +Server +------ + +.. table:: + :class: support-matrix-table + + +------------------------------------+--------+--------+--------+ + | | |aio| | |sync| | |sans| | + +====================================+========+========+========+ + | Listen on a TCP socket | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Listen on a Unix socket | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Listen using a preexisting socket | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Encrypt connection with TLS | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Close server on context exit | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Close connection on handler exit | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Shut down server gracefully | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Check ``Origin`` header | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Customize subprotocol selection | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Configure ``Server`` header | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Alter opening handshake request | ❌ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Alter opening handshake response | ❌ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Perform HTTP Basic Authentication | ✅ | ❌ | ❌ | + +------------------------------------+--------+--------+--------+ + | Perform HTTP Digest Authentication | ❌ | ❌ | ❌ | + +------------------------------------+--------+--------+--------+ + | Force HTTP response | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + +Client +------ + +.. table:: + :class: support-matrix-table + + +------------------------------------+--------+--------+--------+ + | | |aio| | |sync| | |sans| | + +====================================+========+========+========+ + | Connect to a TCP socket | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Connect to a Unix socket | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Connect using a preexisting socket | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Encrypt connection with TLS | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Close connection on context exit | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Reconnect automatically | ✅ | ❌ | — | + +------------------------------------+--------+--------+--------+ + | Configure ``Origin`` header | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Configure ``User-Agent`` header | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Alter opening handshake request | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Connect to non-ASCII IRIs | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Perform HTTP Basic Authentication | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Perform HTTP Digest Authentication | ❌ | ❌ | ❌ | + | (`#784`_) | | | | + +------------------------------------+--------+--------+--------+ + | Follow HTTP redirects | ✅ | ❌ | — | + +------------------------------------+--------+--------+--------+ + | Connect via a HTTP proxy (`#364`_) | ❌ | ❌ | — | + +------------------------------------+--------+--------+--------+ + | Connect via a SOCKS5 proxy | ❌ | ❌ | — | + | (`#475`_) | | | | + +------------------------------------+--------+--------+--------+ + +.. _#364: https://github.com/python-websockets/websockets/issues/364 +.. _#475: https://github.com/python-websockets/websockets/issues/475 +.. _#784: https://github.com/python-websockets/websockets/issues/784 + +Known limitations +----------------- + +There is no way to control compression of outgoing frames on a per-frame basis +(`#538`_). If compression is enabled, all frames are compressed. + +.. _#538: https://github.com/python-websockets/websockets/issues/538 + +The server doesn't check the Host header and respond with a HTTP 400 Bad Request +if it is missing or invalid (`#1246`). + +.. _#1246: https://github.com/python-websockets/websockets/issues/1246 + +The client API doesn't attempt to guarantee that there is no more than one +connection to a given IP address in a CONNECTING state. This behavior is +`mandated by RFC 6455`_. However, :func:`~client.connect()` isn't the right +layer for enforcing this constraint. It's the caller's responsibility. + +.. _mandated by RFC 6455: https://www.rfc-editor.org/rfc/rfc6455.html#section-4.1 diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/index.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/index.rst new file mode 100644 index 0000000000..0b80f087a1 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/index.rst @@ -0,0 +1,90 @@ +API reference +============= + +.. currentmodule:: websockets + +Features +-------- + +Check which implementations support which features and known limitations. + +.. toctree:: + :titlesonly: + + features + + +:mod:`asyncio` +-------------- + +This is the default implementation. It's ideal for servers that handle many +clients concurrently. + +.. toctree:: + :titlesonly: + + asyncio/server + asyncio/client + +:mod:`threading` +---------------- + +This alternative implementation can be a good choice for clients. + +.. toctree:: + :titlesonly: + + sync/server + sync/client + +`Sans-I/O`_ +----------- + +This layer is designed for integrating in third-party libraries, typically +application servers. + +.. _Sans-I/O: https://sans-io.readthedocs.io/ + +.. toctree:: + :titlesonly: + + sansio/server + sansio/client + +Extensions +---------- + +The Per-Message Deflate extension is built in. You may also define custom +extensions. + +.. toctree:: + :titlesonly: + + extensions + +Shared +------ + +These low-level APIs are shared by all implementations. + +.. toctree:: + :titlesonly: + + datastructures + exceptions + types + +API stability +------------- + +Public APIs documented in this API reference are subject to the +:ref:`backwards-compatibility policy <backwards-compatibility policy>`. + +Anything that isn't listed in the API reference is a private API. There's no +guarantees of behavior or backwards-compatibility for private APIs. + +Convenience imports +------------------- + +For convenience, many public APIs can be imported directly from the +``websockets`` package. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/client.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/client.rst new file mode 100644 index 0000000000..09bafc7455 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/client.rst @@ -0,0 +1,58 @@ +Client (`Sans-I/O`_) +==================== + +.. _Sans-I/O: https://sans-io.readthedocs.io/ + +.. currentmodule:: websockets.client + +.. autoclass:: ClientProtocol(wsuri, origin=None, extensions=None, subprotocols=None, state=State.CONNECTING, max_size=2 ** 20, logger=None) + + .. automethod:: receive_data + + .. automethod:: receive_eof + + .. automethod:: connect + + .. automethod:: send_request + + .. automethod:: send_continuation + + .. automethod:: send_text + + .. automethod:: send_binary + + .. automethod:: send_close + + .. automethod:: send_ping + + .. automethod:: send_pong + + .. automethod:: fail + + .. automethod:: events_received + + .. automethod:: data_to_send + + .. automethod:: close_expected + + WebSocket protocol objects also provide these attributes: + + .. autoattribute:: id + + .. autoattribute:: logger + + .. autoproperty:: state + + The following attributes are available after the opening handshake, + once the WebSocket connection is open: + + .. autoattribute:: handshake_exc + + The following attributes are available after the closing handshake, + once the WebSocket connection is closed: + + .. autoproperty:: close_code + + .. autoproperty:: close_reason + + .. autoproperty:: close_exc diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/common.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/common.rst new file mode 100644 index 0000000000..cd1ef3c63a --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/common.rst @@ -0,0 +1,64 @@ +:orphan: + +Both sides (`Sans-I/O`_) +========================= + +.. _Sans-I/O: https://sans-io.readthedocs.io/ + +.. automodule:: websockets.protocol + +.. autoclass:: Protocol(side, state=State.OPEN, max_size=2 ** 20, logger=None) + + .. automethod:: receive_data + + .. automethod:: receive_eof + + .. automethod:: send_continuation + + .. automethod:: send_text + + .. automethod:: send_binary + + .. automethod:: send_close + + .. automethod:: send_ping + + .. automethod:: send_pong + + .. automethod:: fail + + .. automethod:: events_received + + .. automethod:: data_to_send + + .. automethod:: close_expected + + .. autoattribute:: id + + .. autoattribute:: logger + + .. autoproperty:: state + + .. autoproperty:: close_code + + .. autoproperty:: close_reason + + .. autoproperty:: close_exc + +.. autoclass:: Side + + .. autoattribute:: SERVER + + .. autoattribute:: CLIENT + +.. autoclass:: State + + .. autoattribute:: CONNECTING + + .. autoattribute:: OPEN + + .. autoattribute:: CLOSING + + .. autoattribute:: CLOSED + +.. autodata:: SEND_EOF diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/server.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/server.rst new file mode 100644 index 0000000000..d70df6277a --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/server.rst @@ -0,0 +1,62 @@ +Server (`Sans-I/O`_) +==================== + +.. _Sans-I/O: https://sans-io.readthedocs.io/ + +.. currentmodule:: websockets.server + +.. autoclass:: ServerProtocol(origins=None, extensions=None, subprotocols=None, state=State.CONNECTING, max_size=2 ** 20, logger=None) + + .. automethod:: receive_data + + .. automethod:: receive_eof + + .. automethod:: accept + + .. automethod:: select_subprotocol + + .. automethod:: reject + + .. automethod:: send_response + + .. automethod:: send_continuation + + .. automethod:: send_text + + .. automethod:: send_binary + + .. automethod:: send_close + + .. automethod:: send_ping + + .. automethod:: send_pong + + .. automethod:: fail + + .. automethod:: events_received + + .. automethod:: data_to_send + + .. automethod:: close_expected + + WebSocket protocol objects also provide these attributes: + + .. autoattribute:: id + + .. autoattribute:: logger + + .. autoproperty:: state + + The following attributes are available after the opening handshake, + once the WebSocket connection is open: + + .. autoattribute:: handshake_exc + + The following attributes are available after the closing handshake, + once the WebSocket connection is closed: + + .. autoproperty:: close_code + + .. autoproperty:: close_reason + + .. autoproperty:: close_exc diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/client.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/client.rst new file mode 100644 index 0000000000..6cccd6ec48 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/client.rst @@ -0,0 +1,49 @@ +Client (:mod:`threading`) +========================= + +.. automodule:: websockets.sync.client + +Opening a connection +-------------------- + +.. autofunction:: connect(uri, *, sock=None, ssl_context=None, server_hostname=None, origin=None, extensions=None, subprotocols=None, additional_headers=None, user_agent_header="Python/x.y.z websockets/X.Y", compression="deflate", open_timeout=10, close_timeout=10, max_size=2 ** 20, logger=None, create_connection=None) + +.. autofunction:: unix_connect(path, uri=None, *, sock=None, ssl_context=None, server_hostname=None, origin=None, extensions=None, subprotocols=None, additional_headers=None, user_agent_header="Python/x.y.z websockets/X.Y", compression="deflate", open_timeout=10, close_timeout=10, max_size=2 ** 20, logger=None, create_connection=None) + +Using a connection +------------------ + +.. autoclass:: ClientConnection + + .. automethod:: __iter__ + + .. automethod:: recv + + .. automethod:: recv_streaming + + .. automethod:: send + + .. automethod:: close + + .. automethod:: ping + + .. automethod:: pong + + WebSocket connection objects also provide these attributes: + + .. autoattribute:: id + + .. autoattribute:: logger + + .. autoproperty:: local_address + + .. autoproperty:: remote_address + + The following attributes are available after the opening handshake, + once the WebSocket connection is open: + + .. autoattribute:: request + + .. autoattribute:: response + + .. autoproperty:: subprotocol diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/common.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/common.rst new file mode 100644 index 0000000000..3dc6d4a509 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/common.rst @@ -0,0 +1,41 @@ +:orphan: + +Both sides (:mod:`threading`) +============================= + +.. automodule:: websockets.sync.connection + +.. autoclass:: Connection + + .. automethod:: __iter__ + + .. automethod:: recv + + .. automethod:: recv_streaming + + .. automethod:: send + + .. automethod:: close + + .. automethod:: ping + + .. automethod:: pong + + WebSocket connection objects also provide these attributes: + + .. autoattribute:: id + + .. autoattribute:: logger + + .. autoproperty:: local_address + + .. autoproperty:: remote_address + + The following attributes are available after the opening handshake, + once the WebSocket connection is open: + + .. autoattribute:: request + + .. autoattribute:: response + + .. autoproperty:: subprotocol diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/server.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/server.rst new file mode 100644 index 0000000000..35c112046a --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/server.rst @@ -0,0 +1,60 @@ +Server (:mod:`threading`) +========================= + +.. automodule:: websockets.sync.server + +Creating a server +----------------- + +.. autofunction:: serve(handler, host=None, port=None, *, sock=None, ssl_context=None, origins=None, extensions=None, subprotocols=None, select_subprotocol=None, process_request=None, process_response=None, server_header="Python/x.y.z websockets/X.Y", compression="deflate", open_timeout=10, close_timeout=10, max_size=2 ** 20, logger=None, create_connection=None) + +.. autofunction:: unix_serve(handler, path=None, *, sock=None, ssl_context=None, origins=None, extensions=None, subprotocols=None, select_subprotocol=None, process_request=None, process_response=None, server_header="Python/x.y.z websockets/X.Y", compression="deflate", open_timeout=10, close_timeout=10, max_size=2 ** 20, logger=None, create_connection=None) + +Running a server +---------------- + +.. autoclass:: WebSocketServer + + .. automethod:: serve_forever + + .. automethod:: shutdown + + .. automethod:: fileno + +Using a connection +------------------ + +.. autoclass:: ServerConnection + + .. automethod:: __iter__ + + .. automethod:: recv + + .. automethod:: recv_streaming + + .. automethod:: send + + .. automethod:: close + + .. automethod:: ping + + .. automethod:: pong + + WebSocket connection objects also provide these attributes: + + .. autoattribute:: id + + .. autoattribute:: logger + + .. autoproperty:: local_address + + .. autoproperty:: remote_address + + The following attributes are available after the opening handshake, + once the WebSocket connection is open: + + .. autoattribute:: request + + .. autoattribute:: response + + .. autoproperty:: subprotocol diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/types.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/types.rst new file mode 100644 index 0000000000..9d3aa8310b --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/types.rst @@ -0,0 +1,24 @@ +Types +===== + +.. automodule:: websockets.typing + + .. autodata:: Data + + .. autodata:: LoggerLike + + .. autodata:: StatusLike + + .. autodata:: Origin + + .. autodata:: Subprotocol + + .. autodata:: ExtensionName + + .. autodata:: ExtensionParameter + +.. autodata:: websockets.protocol.Event + +.. autodata:: websockets.datastructures.HeadersLike + +.. autodata:: websockets.datastructures.SupportsKeysAndGetItem diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/requirements.txt b/testing/web-platform/tests/tools/third_party/websockets/docs/requirements.txt new file mode 100644 index 0000000000..bcd1d71143 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/requirements.txt @@ -0,0 +1,8 @@ +furo +sphinx +sphinx-autobuild +sphinx-copybutton +sphinx-inline-tabs +sphinxcontrib-spelling +sphinxcontrib-trio +sphinxext-opengraph diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/spelling_wordlist.txt b/testing/web-platform/tests/tools/third_party/websockets/docs/spelling_wordlist.txt new file mode 100644 index 0000000000..dfa7065e79 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/spelling_wordlist.txt @@ -0,0 +1,85 @@ +augustin +auth +autoscaler +aymeric +backend +backoff +backpressure +balancer +balancers +bottlenecked +bufferbloat +bugfix +buildpack +bytestring +bytestrings +changelog +coroutine +coroutines +cryptocurrencies +cryptocurrency +css +ctrl +deserialize +django +dev +Dockerfile +dyno +formatter +fractalideas +gunicorn +healthz +html +hypercorn +iframe +IPv +istio +iterable +js +keepalive +KiB +kubernetes +lifecycle +linkerd +liveness +lookups +MiB +mutex +mypy +nginx +Paketo +permessage +pid +procfile +proxying +py +pythonic +reconnection +redis +redistributions +retransmit +runtime +scalable +stateful +subclasses +subclassing +submodule +subpackages +subprotocol +subprotocols +supervisord +tidelift +tls +tox +txt +unregister +uple +uvicorn +uvloop +virtualenv +WebSocket +websocket +websockets +ws +wsgi +www diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/authentication.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/authentication.rst new file mode 100644 index 0000000000..31bc8e6da8 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/authentication.rst @@ -0,0 +1,348 @@ +Authentication +============== + +The WebSocket protocol was designed for creating web applications that need +bidirectional communication between clients running in browsers and servers. + +In most practical use cases, WebSocket servers need to authenticate clients in +order to route communications appropriately and securely. + +:rfc:`6455` stays elusive when it comes to authentication: + + This protocol doesn't prescribe any particular way that servers can + authenticate clients during the WebSocket handshake. The WebSocket + server can use any client authentication mechanism available to a + generic HTTP server, such as cookies, HTTP authentication, or TLS + authentication. + +None of these three mechanisms works well in practice. Using cookies is +cumbersome, HTTP authentication isn't supported by all mainstream browsers, +and TLS authentication in a browser is an esoteric user experience. + +Fortunately, there are better alternatives! Let's discuss them. + +System design +------------- + +Consider a setup where the WebSocket server is separate from the HTTP server. + +Most servers built with websockets to complement a web application adopt this +design because websockets doesn't aim at supporting HTTP. + +The following diagram illustrates the authentication flow. + +.. image:: authentication.svg + +Assuming the current user is authenticated with the HTTP server (1), the +application needs to obtain credentials from the HTTP server (2) in order to +send them to the WebSocket server (3), who can check them against the database +of user accounts (4). + +Usernames and passwords aren't a good choice of credentials here, if only +because passwords aren't available in clear text in the database. + +Tokens linked to user accounts are a better choice. These tokens must be +impossible to forge by an attacker. For additional security, they can be +short-lived or even single-use. + +Sending credentials +------------------- + +Assume the web application obtained authentication credentials, likely a +token, from the HTTP server. There's four options for passing them to the +WebSocket server. + +1. **Sending credentials as the first message in the WebSocket connection.** + + This is fully reliable and the most secure mechanism in this discussion. It + has two minor downsides: + + * Authentication is performed at the application layer. Ideally, it would + be managed at the protocol layer. + + * Authentication is performed after the WebSocket handshake, making it + impossible to monitor authentication failures with HTTP response codes. + +2. **Adding credentials to the WebSocket URI in a query parameter.** + + This is also fully reliable but less secure. Indeed, it has a major + downside: + + * URIs end up in logs, which leaks credentials. Even if that risk could be + lowered with single-use tokens, it is usually considered unacceptable. + + Authentication is still performed at the application layer but it can + happen before the WebSocket handshake, which improves separation of + concerns and enables responding to authentication failures with HTTP 401. + +3. **Setting a cookie on the domain of the WebSocket URI.** + + Cookies are undoubtedly the most common and hardened mechanism for sending + credentials from a web application to a server. In an HTTP application, + credentials would be a session identifier or a serialized, signed session. + + Unfortunately, when the WebSocket server runs on a different domain from + the web application, this idea bumps into the `Same-Origin Policy`_. For + security reasons, setting a cookie on a different origin is impossible. + + The proper workaround consists in: + + * creating a hidden iframe_ served from the domain of the WebSocket server + * sending the token to the iframe with postMessage_ + * setting the cookie in the iframe + + before opening the WebSocket connection. + + Sharing a parent domain (e.g. example.com) between the HTTP server (e.g. + www.example.com) and the WebSocket server (e.g. ws.example.com) and setting + the cookie on that parent domain would work too. + + However, the cookie would be shared with all subdomains of the parent + domain. For a cookie containing credentials, this is unacceptable. + +.. _Same-Origin Policy: https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy +.. _iframe: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe +.. _postMessage: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort/postMessage + +4. **Adding credentials to the WebSocket URI in user information.** + + Letting the browser perform HTTP Basic Auth is a nice idea in theory. + + In practice it doesn't work due to poor support in browsers. + + As of May 2021: + + * Chrome 90 behaves as expected. + + * Firefox 88 caches credentials too aggressively. + + When connecting again to the same server with new credentials, it reuses + the old credentials, which may be expired, resulting in an HTTP 401. Then + the next connection succeeds. Perhaps errors clear the cache. + + When tokens are short-lived or single-use, this bug produces an + interesting effect: every other WebSocket connection fails. + + * Safari 14 ignores credentials entirely. + +Two other options are off the table: + +1. **Setting a custom HTTP header** + + This would be the most elegant mechanism, solving all issues with the options + discussed above. + + Unfortunately, it doesn't work because the `WebSocket API`_ doesn't support + `setting custom headers`_. + +.. _WebSocket API: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API +.. _setting custom headers: https://github.com/whatwg/html/issues/3062 + +2. **Authenticating with a TLS certificate** + + While this is suggested by the RFC, installing a TLS certificate is too far + from the mainstream experience of browser users. This could make sense in + high security contexts. I hope developers working on such projects don't + take security advice from the documentation of random open source projects. + +Let's experiment! +----------------- + +The `experiments/authentication`_ directory demonstrates these techniques. + +Run the experiment in an environment where websockets is installed: + +.. _experiments/authentication: https://github.com/python-websockets/websockets/tree/main/experiments/authentication + +.. code-block:: console + + $ python experiments/authentication/app.py + Running on http://localhost:8000/ + +When you browse to the HTTP server at http://localhost:8000/ and you submit a +username, the server creates a token and returns a testing web page. + +This page opens WebSocket connections to four WebSocket servers running on +four different origins. It attempts to authenticate with the token in four +different ways. + +First message +............. + +As soon as the connection is open, the client sends a message containing the +token: + +.. code-block:: javascript + + const websocket = new WebSocket("ws://.../"); + websocket.onopen = () => websocket.send(token); + + // ... + +At the beginning of the connection handler, the server receives this message +and authenticates the user. If authentication fails, the server closes the +connection: + +.. code-block:: python + + async def first_message_handler(websocket): + token = await websocket.recv() + user = get_user(token) + if user is None: + await websocket.close(CloseCode.INTERNAL_ERROR, "authentication failed") + return + + ... + +Query parameter +............... + +The client adds the token to the WebSocket URI in a query parameter before +opening the connection: + +.. code-block:: javascript + + const uri = `ws://.../?token=${token}`; + const websocket = new WebSocket(uri); + + // ... + +The server intercepts the HTTP request, extracts the token and authenticates +the user. If authentication fails, it returns an HTTP 401: + +.. code-block:: python + + class QueryParamProtocol(websockets.WebSocketServerProtocol): + async def process_request(self, path, headers): + token = get_query_parameter(path, "token") + if token is None: + return http.HTTPStatus.UNAUTHORIZED, [], b"Missing token\n" + + user = get_user(token) + if user is None: + return http.HTTPStatus.UNAUTHORIZED, [], b"Invalid token\n" + + self.user = user + + async def query_param_handler(websocket): + user = websocket.user + + ... + +Cookie +...... + +The client sets a cookie containing the token before opening the connection. + +The cookie must be set by an iframe loaded from the same origin as the +WebSocket server. This requires passing the token to this iframe. + +.. code-block:: javascript + + // in main window + iframe.contentWindow.postMessage(token, "http://..."); + + // in iframe + document.cookie = `token=${data}; SameSite=Strict`; + + // in main window + const websocket = new WebSocket("ws://.../"); + + // ... + +This sequence must be synchronized between the main window and the iframe. +This involves several events. Look at the full implementation for details. + +The server intercepts the HTTP request, extracts the token and authenticates +the user. If authentication fails, it returns an HTTP 401: + +.. code-block:: python + + class CookieProtocol(websockets.WebSocketServerProtocol): + async def process_request(self, path, headers): + # Serve iframe on non-WebSocket requests + ... + + token = get_cookie(headers.get("Cookie", ""), "token") + if token is None: + return http.HTTPStatus.UNAUTHORIZED, [], b"Missing token\n" + + user = get_user(token) + if user is None: + return http.HTTPStatus.UNAUTHORIZED, [], b"Invalid token\n" + + self.user = user + + async def cookie_handler(websocket): + user = websocket.user + + ... + +User information +................ + +The client adds the token to the WebSocket URI in user information before +opening the connection: + +.. code-block:: javascript + + const uri = `ws://token:${token}@.../`; + const websocket = new WebSocket(uri); + + // ... + +Since HTTP Basic Auth is designed to accept a username and a password rather +than a token, we send ``token`` as username and the token as password. + +The server intercepts the HTTP request, extracts the token and authenticates +the user. If authentication fails, it returns an HTTP 401: + +.. code-block:: python + + class UserInfoProtocol(websockets.BasicAuthWebSocketServerProtocol): + async def check_credentials(self, username, password): + if username != "token": + return False + + user = get_user(password) + if user is None: + return False + + self.user = user + return True + + async def user_info_handler(websocket): + user = websocket.user + + ... + +Machine-to-machine authentication +--------------------------------- + +When the WebSocket client is a standalone program rather than a script running +in a browser, there are far fewer constraints. HTTP Authentication is the best +solution in this scenario. + +To authenticate a websockets client with HTTP Basic Authentication +(:rfc:`7617`), include the credentials in the URI: + +.. code-block:: python + + async with websockets.connect( + f"wss://{username}:{password}@example.com", + ) as websocket: + ... + +(You must :func:`~urllib.parse.quote` ``username`` and ``password`` if they +contain unsafe characters.) + +To authenticate a websockets client with HTTP Bearer Authentication +(:rfc:`6750`), add a suitable ``Authorization`` header: + +.. code-block:: python + + async with websockets.connect( + "wss://example.com", + extra_headers={"Authorization": f"Bearer {token}"} + ) as websocket: + ... diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/authentication.svg b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/authentication.svg new file mode 100644 index 0000000000..79b5fb8d56 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/authentication.svg @@ -0,0 +1,63 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="-514 74 644 380" width="644px" height="380px"><defs><style>@font-face{ + font-family:"DIN Next"; + font-weight: 400; + src:url("https://whimsical.com/fonts/139e8e25-0eea-4cb4-a5d4-79048803e73d.eot?#iefix"); + src:url("https://whimsical.com/fonts/139e8e25-0eea-4cb4-a5d4-79048803e73d.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/094b15e3-94bd-435b-a595-d40edfde661a.woff2") format("woff2"),url("https://whimsical.com/fonts/7e5fbe11-4858-4bd1-9ec6-a1d9f9d227aa.woff") format("woff"),url("https://whimsical.com/fonts/0f11eff9-9f05-46f5-9703-027c510065d7.ttf") format("truetype"),url("https://whimsical.com/fonts/48b61978-3f30-4274-823c-5cdcd1876918.svg#48b61978-3f30-4274-823c-5cdcd1876918") format("svg"); +} +@font-face{ + font-family:"DIN Next"; + font-weight: 400; + font-style: italic; + src:url("https://whimsical.com/fonts/df24d5e8-5087-42fd-99b1-b16042d666c8.eot?#iefix"); + src:url("https://whimsical.com/fonts/df24d5e8-5087-42fd-99b1-b16042d666c8.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/46251881-ffe9-4bfb-99c7-d6ce3bebaf3e.woff2") format("woff2"),url("https://whimsical.com/fonts/790ebbf2-62c5-4a32-946f-99d405f9243e.woff") format("woff"),url("https://whimsical.com/fonts/d28199e6-0f4a-42df-97f4-606701c6f75a.ttf") format("truetype"),url("https://whimsical.com/fonts/37a462c0-d86e-492c-b9ab-35e6bd417f6c.svg#37a462c0-d86e-492c-b9ab-35e6bd417f6c") format("svg"); +} +@font-face{ + font-weight: 500; + font-family:"DIN Next"; + src:url("https://whimsical.com/fonts/c5b058fc-55ce-4e06-a175-5c7d9a7e90f4.eot?#iefix"); + src:url("https://whimsical.com/fonts/c5b058fc-55ce-4e06-a175-5c7d9a7e90f4.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/7b29ae40-30ff-4f99-a2b9-cde88669fa2f.woff2") format("woff2"),url("https://whimsical.com/fonts/bf73077c-e354-4562-a085-f4703eb1d653.woff") format("woff"),url("https://whimsical.com/fonts/0ffa6947-5317-4d07-b525-14d08a028821.ttf") format("truetype"),url("https://whimsical.com/fonts/9e423e45-5450-4991-a157-dbe6cf61eb4e.svg#9e423e45-5450-4991-a157-dbe6cf61eb4e") format("svg"); +} +@font-face{ + font-family:"DIN Next"; + font-weight: 500; + font-style: italic; + src:url("https://whimsical.com/fonts/9897c008-fd65-48a4-afc7-36de2fea97b9.eot?#iefix"); + src:url("https://whimsical.com/fonts/9897c008-fd65-48a4-afc7-36de2fea97b9.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/c7717981-647d-4b76-8817-33062e42d11f.woff2") format("woff2"),url("https://whimsical.com/fonts/b852cd4c-1255-40b1-a2be-73a9105b0155.woff") format("woff"),url("https://whimsical.com/fonts/821b00ad-e741-4e2d-af1a-85594367c8a2.ttf") format("truetype"),url("https://whimsical.com/fonts/d3e3b689-a6b0-45f2-b279-f5e194f87409.svg#d3e3b689-a6b0-45f2-b279-f5e194f87409") format("svg"); +} +@font-face{ + font-weight: 700; + font-family:"DIN Next"; + src:url("https://whimsical.com/fonts/81cd3b08-fd39-4ae3-8d05-9d24709eba84.eot?#iefix"); + src:url("https://whimsical.com/fonts/81cd3b08-fd39-4ae3-8d05-9d24709eba84.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/31704504-4671-47a6-a61e-397f07410d91.woff2") format("woff2"),url("https://whimsical.com/fonts/b8a280da-481f-44a0-8d9c-1bc64bd7227c.woff") format("woff"),url("https://whimsical.com/fonts/276d122a-0fab-447b-9fc0-5d7fb0eafce2.ttf") format("truetype"),url("https://whimsical.com/fonts/8fb8273a-8213-4928-808b-b5bfaf3fd7e9.svg#8fb8273a-8213-4928-808b-b5bfaf3fd7e9") format("svg"); +} +@font-face{ + font-family:"DIN Next"; + font-weight: 700; + font-style: italic; + src:url("https://whimsical.com/fonts/00cc6df3-ed32-4004-8dd8-1c576600a408.eot?#iefix"); + src:url("https://whimsical.com/fonts/00cc6df3-ed32-4004-8dd8-1c576600a408.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/4132c4c8-680c-4d6d-9251-a2da38503bbd.woff2") format("woff2"),url("https://whimsical.com/fonts/366401fe-6df4-47be-8f55-8a411cff0dd2.woff") format("woff"),url("https://whimsical.com/fonts/dbe4f7ba-fc16-44a6-a577-571620e9edaf.ttf") format("truetype"),url("https://whimsical.com/fonts/f874edca-ee87-4ccf-8b1d-921fbc3c1c36.svg#f874edca-ee87-4ccf-8b1d-921fbc3c1c36") format("svg"); +} + +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-Regular.woff') format('woff'); + font-weight: 400; +} +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-Italic.woff') format('woff'); + font-style: italic; + font-weight: 400; +} +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-Bold.woff') format('woff'); + font-weight: 700; +} +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-BoldItalic.woff') format('woff'); + font-style: italic; + font-weight: 700; +} +* {-webkit-font-smoothing: auto; -moz-osx-font-smoothing: auto;}@media print { svg { width: 100%; height: 100%; } }</style><filter id="fill-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.16"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="fill-light-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.04"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="image-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="8"/><feOffset result="offsetblur" dx="0" dy="3"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.06"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="frame-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="8"/><feOffset result="offsetblur" dx="0" dy="3"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.06"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="badge-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.08"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><g><g><rect x="-504" y="228" width="192" height="72" rx="3" ry="3" fill="#cfeeeb"/><rect y="228" rx="3" stroke="#1AAE9F" fill="none" stroke-linejoin="round" width="192" stroke-linecap="round" stroke-width="2" x="-504" ry="3" height="72"/><text y="240" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="168" xml:space="preserve" x="-492" font-family="DIN Next, sans-serif" height="48"><tspan x="-408" y="258" text-anchor="middle" style="white-space:pre;">HTTP</tspan><tspan x="-408" y="282" text-anchor="middle" style="white-space:pre;">server</tspan></text></g></g><g><g><rect x="-72" y="228" width="192" height="72" rx="3" ry="3" fill="#d3e6f7"/><rect y="228" rx="3" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="192" stroke-linecap="round" stroke-width="2" x="-72" ry="3" height="72"/><text y="240" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="168" xml:space="preserve" x="-60" font-family="DIN Next, sans-serif" height="48"><tspan x="24" y="258" text-anchor="middle" style="white-space:pre;">WebSocket</tspan><tspan x="24" y="282" text-anchor="middle" style="white-space:pre;">server</tspan></text></g></g><g><g><rect x="-288" y="84" width="192" height="72" rx="3" ry="3" fill="#d9dde0"/><rect y="84" rx="3" stroke="#4B5C6B" fill="none" stroke-linejoin="round" width="192" stroke-linecap="round" stroke-width="2" x="-288" ry="3" height="72"/><text y="96" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="168" xml:space="preserve" x="-276" font-family="DIN Next, sans-serif" height="48"><tspan x="-192" y="114" text-anchor="middle" style="white-space:pre;">web app</tspan><tspan x="-192" y="138" text-anchor="middle" style="white-space:pre;">in browser</tspan></text></g></g><g><g><path d="M-96,372c0,6.62741 -42.98074,12 -96,12c-53.01926,0 -96,-5.37259 -96,-12c0,-6.62741 42.98074,-12 96,-12c53.01926,0 96,5.37259 96,12c0,6.62741 0,53.37259 0,60c0,6.62741 -42.98074,12 -96,12c-53.01926,0 -96,-5.37259 -96,-12c0,-6.62741 0,-60 0,-60" fill="#e2cdf2"/><path d="M-96,372c0,6.62741 -42.98074,12 -96,12c-53.01926,0 -96,-5.37259 -96,-12c0,-6.62741 42.98074,-12 96,-12c53.01926,0 96,5.37259 96,12c0,6.62741 0,53.37259 0,60c0,6.62741 -42.98074,12 -96,12c-53.01926,0 -96,-5.37259 -96,-12c0,-6.62741 0,-60 0,-60" fill="none" stroke="#730FC3" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/><text y="396" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="168" xml:space="preserve" x="-276" font-family="DIN Next, sans-serif" height="24"><tspan x="-192" y="414" text-anchor="middle" style="white-space:pre;">user accounts</tspan></text></g></g><g><g><path d="M-400.46606,302.69069l37.26606,13.30931M-284.8,344l33.4991,11.96396" fill="none" stroke="#1AAE9F" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-252.34924,349.70583 -247.53394,357.30931 -256.07648,360.14213" fill="#1AAE9F" stroke="#1AAE9F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="318" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="160" xml:space="preserve" x="-404" font-family="DIN Next, sans-serif" height="24"><tspan x="-324" y="336" text-anchor="middle" style="white-space:pre;">(1) authenticate user</tspan></text></g></g><g><g><path d="M-247.35316,159.15135l-43.98017,18.84865M-356.66667,206l-40.30359,17.27297" fill="none" stroke="#1AAE9F" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-391.94549,227.14787 -400.64684,224.84865 -396.31086,216.96199" fill="#1AAE9F" stroke="#1AAE9F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="180" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="164" xml:space="preserve" x="-406" font-family="DIN Next, sans-serif" height="24"><tspan x="-324" y="198" text-anchor="middle" style="white-space:pre;">(2) obtain credentials</tspan></text></g></g><g><g><path d="M-136.64684,159.15135l43.98017,18.84865M-27.33333,206l40.30359,17.27297" fill="none" stroke="#2C88D9" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="12.31086,216.96199 16.64684,224.84865 7.94549,227.14787" fill="#2C88D9" stroke="#2C88D9" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="180" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="153" xml:space="preserve" x="-136.5" font-family="DIN Next, sans-serif" height="24"><tspan x="-60" y="198" text-anchor="middle" style="white-space:pre;">(3) send credentials</tspan></text></g></g><g><g><path d="M16.46606,302.69069l-37.26606,13.30931M-99.2,344l-33.4991,11.96396" fill="none" stroke="#2C88D9" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-127.92352,360.14213 -136.46606,357.30931 -131.65076,349.70583" fill="#2C88D9" stroke="#2C88D9" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="318" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="160" xml:space="preserve" x="-140" font-family="DIN Next, sans-serif" height="24"><tspan x="-60" y="336" text-anchor="middle" style="white-space:pre;">(4) authenticate user</tspan></text></g></g></svg> diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/broadcast.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/broadcast.rst new file mode 100644 index 0000000000..1acb372d4f --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/broadcast.rst @@ -0,0 +1,348 @@ +Broadcasting messages +===================== + +.. currentmodule:: websockets + + +.. admonition:: If you just want to send a message to all connected clients, + use :func:`broadcast`. + :class: tip + + If you want to learn about its design in depth, continue reading this + document. + +WebSocket servers often send the same message to all connected clients or to a +subset of clients for which the message is relevant. + +Let's explore options for broadcasting a message, explain the design +of :func:`broadcast`, and discuss alternatives. + +For each option, we'll provide a connection handler called ``handler()`` and a +function or coroutine called ``broadcast()`` that sends a message to all +connected clients. + +Integrating them is left as an exercise for the reader. You could start with:: + + import asyncio + import websockets + + async def handler(websocket): + ... + + async def broadcast(message): + ... + + async def broadcast_messages(): + while True: + await asyncio.sleep(1) + message = ... # your application logic goes here + await broadcast(message) + + async def main(): + async with websockets.serve(handler, "localhost", 8765): + await broadcast_messages() # runs forever + + if __name__ == "__main__": + asyncio.run(main()) + +``broadcast_messages()`` must yield control to the event loop between each +message, or else it will never let the server run. That's why it includes +``await asyncio.sleep(1)``. + +A complete example is available in the `experiments/broadcast`_ directory. + +.. _experiments/broadcast: https://github.com/python-websockets/websockets/tree/main/experiments/broadcast + +The naive way +------------- + +The most obvious way to send a message to all connected clients consists in +keeping track of them and sending the message to each of them. + +Here's a connection handler that registers clients in a global variable:: + + CLIENTS = set() + + async def handler(websocket): + CLIENTS.add(websocket) + try: + await websocket.wait_closed() + finally: + CLIENTS.remove(websocket) + +This implementation assumes that the client will never send any messages. If +you'd rather not make this assumption, you can change:: + + await websocket.wait_closed() + +to:: + + async for _ in websocket: + pass + +Here's a coroutine that broadcasts a message to all clients:: + + async def broadcast(message): + for websocket in CLIENTS.copy(): + try: + await websocket.send(message) + except websockets.ConnectionClosed: + pass + +There are two tricks in this version of ``broadcast()``. + +First, it makes a copy of ``CLIENTS`` before iterating it. Else, if a client +connects or disconnects while ``broadcast()`` is running, the loop would fail +with:: + + RuntimeError: Set changed size during iteration + +Second, it ignores :exc:`~exceptions.ConnectionClosed` exceptions because a +client could disconnect between the moment ``broadcast()`` makes a copy of +``CLIENTS`` and the moment it sends a message to this client. This is fine: a +client that disconnected doesn't belongs to "all connected clients" anymore. + +The naive way can be very fast. Indeed, if all connections have enough free +space in their write buffers, ``await websocket.send(message)`` writes the +message and returns immediately, as it doesn't need to wait for the buffer to +drain. In this case, ``broadcast()`` doesn't yield control to the event loop, +which minimizes overhead. + +The naive way can also fail badly. If the write buffer of a connection reaches +``write_limit``, ``broadcast()`` waits for the buffer to drain before sending +the message to other clients. This can cause a massive drop in performance. + +As a consequence, this pattern works only when write buffers never fill up, +which is usually outside of the control of the server. + +If you know for sure that you will never write more than ``write_limit`` bytes +within ``ping_interval + ping_timeout``, then websockets will terminate slow +connections before the write buffer has time to fill up. + +Don't set extreme ``write_limit``, ``ping_interval``, and ``ping_timeout`` +values to ensure that this condition holds. Set reasonable values and use the +built-in :func:`broadcast` function instead. + +The concurrent way +------------------ + +The naive way didn't work well because it serialized writes, while the whole +point of asynchronous I/O is to perform I/O concurrently. + +Let's modify ``broadcast()`` to send messages concurrently:: + + async def send(websocket, message): + try: + await websocket.send(message) + except websockets.ConnectionClosed: + pass + + def broadcast(message): + for websocket in CLIENTS: + asyncio.create_task(send(websocket, message)) + +We move the error handling logic in a new coroutine and we schedule +a :class:`~asyncio.Task` to run it instead of executing it immediately. + +Since ``broadcast()`` no longer awaits coroutines, we can make it a function +rather than a coroutine and do away with the copy of ``CLIENTS``. + +This version of ``broadcast()`` makes clients independent from one another: a +slow client won't block others. As a side effect, it makes messages +independent from one another. + +If you broadcast several messages, there is no strong guarantee that they will +be sent in the expected order. Fortunately, the event loop runs tasks in the +order in which they are created, so the order is correct in practice. + +Technically, this is an implementation detail of the event loop. However, it +seems unlikely for an event loop to run tasks in an order other than FIFO. + +If you wanted to enforce the order without relying this implementation detail, +you could be tempted to wait until all clients have received the message:: + + async def broadcast(message): + if CLIENTS: # asyncio.wait doesn't accept an empty list + await asyncio.wait([ + asyncio.create_task(send(websocket, message)) + for websocket in CLIENTS + ]) + +However, this doesn't really work in practice. Quite often, it will block +until the slowest client times out. + +Backpressure meets broadcast +---------------------------- + +At this point, it becomes apparent that backpressure, usually a good practice, +doesn't work well when broadcasting a message to thousands of clients. + +When you're sending messages to a single client, you don't want to send them +faster than the network can transfer them and the client accept them. This is +why :meth:`~server.WebSocketServerProtocol.send` checks if the write buffer +is full and, if it is, waits until it drain, giving the network and the +client time to catch up. This provides backpressure. + +Without backpressure, you could pile up data in the write buffer until the +server process runs out of memory and the operating system kills it. + +The :meth:`~server.WebSocketServerProtocol.send` API is designed to enforce +backpressure by default. This helps users of websockets write robust programs +even if they never heard about backpressure. + +For comparison, :class:`asyncio.StreamWriter` requires users to understand +backpressure and to await :meth:`~asyncio.StreamWriter.drain` explicitly +after each :meth:`~asyncio.StreamWriter.write`. + +When broadcasting messages, backpressure consists in slowing down all clients +in an attempt to let the slowest client catch up. With thousands of clients, +the slowest one is probably timing out and isn't going to receive the message +anyway. So it doesn't make sense to synchronize with the slowest client. + +How do we avoid running out of memory when slow clients can't keep up with the +broadcast rate, then? The most straightforward option is to disconnect them. + +If a client gets too far behind, eventually it reaches the limit defined by +``ping_timeout`` and websockets terminates the connection. You can read the +discussion of :doc:`keepalive and timeouts <./timeouts>` for details. + +How :func:`broadcast` works +--------------------------- + +The built-in :func:`broadcast` function is similar to the naive way. The main +difference is that it doesn't apply backpressure. + +This provides the best performance by avoiding the overhead of scheduling and +running one task per client. + +Also, when sending text messages, encoding to UTF-8 happens only once rather +than once per client, providing a small performance gain. + +Per-client queues +----------------- + +At this point, we deal with slow clients rather brutally: we disconnect then. + +Can we do better? For example, we could decide to skip or to batch messages, +depending on how far behind a client is. + +To implement this logic, we can create a queue of messages for each client and +run a task that gets messages from the queue and sends them to the client:: + + import asyncio + + CLIENTS = set() + + async def relay(queue, websocket): + while True: + # Implement custom logic based on queue.qsize() and + # websocket.transport.get_write_buffer_size() here. + message = await queue.get() + await websocket.send(message) + + async def handler(websocket): + queue = asyncio.Queue() + relay_task = asyncio.create_task(relay(queue, websocket)) + CLIENTS.add(queue) + try: + await websocket.wait_closed() + finally: + CLIENTS.remove(queue) + relay_task.cancel() + +Then we can broadcast a message by pushing it to all queues:: + + def broadcast(message): + for queue in CLIENTS: + queue.put_nowait(message) + +The queues provide an additional buffer between the ``broadcast()`` function +and clients. This makes it easier to support slow clients without excessive +memory usage because queued messages aren't duplicated to write buffers +until ``relay()`` processes them. + +Publish–subscribe +----------------- + +Can we avoid centralizing the list of connected clients in a global variable? + +If each client subscribes to a stream a messages, then broadcasting becomes as +simple as publishing a message to the stream. + +Here's a message stream that supports multiple consumers:: + + class PubSub: + def __init__(self): + self.waiter = asyncio.Future() + + def publish(self, value): + waiter, self.waiter = self.waiter, asyncio.Future() + waiter.set_result((value, self.waiter)) + + async def subscribe(self): + waiter = self.waiter + while True: + value, waiter = await waiter + yield value + + __aiter__ = subscribe + + PUBSUB = PubSub() + +The stream is implemented as a linked list of futures. It isn't necessary to +synchronize consumers. They can read the stream at their own pace, +independently from one another. Once all consumers read a message, there are +no references left, therefore the garbage collector deletes it. + +The connection handler subscribes to the stream and sends messages:: + + async def handler(websocket): + async for message in PUBSUB: + await websocket.send(message) + +The broadcast function publishes to the stream:: + + def broadcast(message): + PUBSUB.publish(message) + +Like per-client queues, this version supports slow clients with limited memory +usage. Unlike per-client queues, it makes it difficult to tell how far behind +a client is. The ``PubSub`` class could be extended or refactored to provide +this information. + +The ``for`` loop is gone from this version of the ``broadcast()`` function. +However, there's still a ``for`` loop iterating on all clients hidden deep +inside :mod:`asyncio`. When ``publish()`` sets the result of the ``waiter`` +future, :mod:`asyncio` loops on callbacks registered with this future and +schedules them. This is how connection handlers receive the next value from +the asynchronous iterator returned by ``subscribe()``. + +Performance considerations +-------------------------- + +The built-in :func:`broadcast` function sends all messages without yielding +control to the event loop. So does the naive way when the network and clients +are fast and reliable. + +For each client, a WebSocket frame is prepared and sent to the network. This +is the minimum amount of work required to broadcast a message. + +It would be tempting to prepare a frame and reuse it for all connections. +However, this isn't possible in general for two reasons: + +* Clients can negotiate different extensions. You would have to enforce the + same extensions with the same parameters. For example, you would have to + select some compression settings and reject clients that cannot support + these settings. + +* Extensions can be stateful, producing different encodings of the same + message depending on previous messages. For example, you would have to + disable context takeover to make compression stateless, resulting in poor + compression rates. + +All other patterns discussed above yield control to the event loop once per +client because messages are sent by different tasks. This makes them slower +than the built-in :func:`broadcast` function. + +There is no major difference between the performance of per-client queues and +publish–subscribe. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/compression.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/compression.rst new file mode 100644 index 0000000000..eaf99070db --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/compression.rst @@ -0,0 +1,222 @@ +Compression +=========== + +.. currentmodule:: websockets.extensions.permessage_deflate + +Most WebSocket servers exchange JSON messages because they're convenient to +parse and serialize in a browser. These messages contain text data and tend to +be repetitive. + +This makes the stream of messages highly compressible. Enabling compression +can reduce network traffic by more than 80%. + +There's a standard for compressing messages. :rfc:`7692` defines WebSocket +Per-Message Deflate, a compression extension based on the Deflate_ algorithm. + +.. _Deflate: https://en.wikipedia.org/wiki/Deflate + +Configuring compression +----------------------- + +:func:`~websockets.client.connect` and :func:`~websockets.server.serve` enable +compression by default because the reduction in network bandwidth is usually +worth the additional memory and CPU cost. + +If you want to disable compression, set ``compression=None``:: + + import websockets + + websockets.connect(..., compression=None) + + websockets.serve(..., compression=None) + +If you want to customize compression settings, you can enable the Per-Message +Deflate extension explicitly with :class:`ClientPerMessageDeflateFactory` or +:class:`ServerPerMessageDeflateFactory`:: + + import websockets + from websockets.extensions import permessage_deflate + + websockets.connect( + ..., + extensions=[ + permessage_deflate.ClientPerMessageDeflateFactory( + server_max_window_bits=11, + client_max_window_bits=11, + compress_settings={"memLevel": 4}, + ), + ], + ) + + websockets.serve( + ..., + extensions=[ + permessage_deflate.ServerPerMessageDeflateFactory( + server_max_window_bits=11, + client_max_window_bits=11, + compress_settings={"memLevel": 4}, + ), + ], + ) + +The Window Bits and Memory Level values in these examples reduce memory usage +at the expense of compression rate. + +Compression settings +-------------------- + +When a client and a server enable the Per-Message Deflate extension, they +negotiate two parameters to guarantee compatibility between compression and +decompression. These parameters affect the trade-off between compression rate +and memory usage for both sides. + +* **Context Takeover** means that the compression context is retained between + messages. In other words, compression is applied to the stream of messages + rather than to each message individually. + + Context takeover should remain enabled to get good performance on + applications that send a stream of messages with similar structure, + that is, most applications. + + This requires retaining the compression context and state between messages, + which increases the memory footprint of a connection. + +* **Window Bits** controls the size of the compression context. It must be + an integer between 9 (lowest memory usage) and 15 (best compression). + Setting it to 8 is possible but rejected by some versions of zlib. + + On the server side, websockets defaults to 12. Specifically, the compression + window size (server to client) is always 12 while the decompression window + (client to server) size may be 12 or 15 depending on whether the client + supports configuring it. + + On the client side, websockets lets the server pick a suitable value, which + has the same effect as defaulting to 15. + +:mod:`zlib` offers additional parameters for tuning compression. They control +the trade-off between compression rate, memory usage, and CPU usage only for +compressing. They're transparent for decompressing. Unless mentioned +otherwise, websockets inherits defaults of :func:`~zlib.compressobj`. + +* **Memory Level** controls the size of the compression state. It must be an + integer between 1 (lowest memory usage) and 9 (best compression). + + websockets defaults to 5. This is lower than zlib's default of 8. Not only + does a lower memory level reduce memory usage, but it can also increase + speed thanks to memory locality. + +* **Compression Level** controls the effort to optimize compression. It must + be an integer between 1 (lowest CPU usage) and 9 (best compression). + +* **Strategy** selects the compression strategy. The best choice depends on + the type of data being compressed. + + +Tuning compression +------------------ + +For servers +........... + +By default, websockets enables compression with conservative settings that +optimize memory usage at the cost of a slightly worse compression rate: +Window Bits = 12 and Memory Level = 5. This strikes a good balance for small +messages that are typical of WebSocket servers. + +Here's how various compression settings affect memory usage of a single +connection on a 64-bit system, as well a benchmark of compressed size and +compression time for a corpus of small JSON documents. + +=========== ============ ============ ================ ================ +Window Bits Memory Level Memory usage Size vs. default Time vs. default +=========== ============ ============ ================ ================ +15 8 322 KiB -4.0% +15% +14 7 178 KiB -2.6% +10% +13 6 106 KiB -1.4% +5% +**12** **5** **70 KiB** **=** **=** +11 4 52 KiB +3.7% -5% +10 3 43 KiB +90% +50% +9 2 39 KiB +160% +100% +— — 19 KiB +452% — +=========== ============ ============ ================ ================ + +Window Bits and Memory Level don't have to move in lockstep. However, other +combinations don't yield significantly better results than those shown above. + +Compressed size and compression time depend heavily on the kind of messages +exchanged by the application so this example may not apply to your use case. + +You can adapt `compression/benchmark.py`_ by creating a list of typical +messages and passing it to the ``_run`` function. + +Window Bits = 11 and Memory Level = 4 looks like the sweet spot in this table. + +websockets defaults to Window Bits = 12 and Memory Level = 5 to stay away from +Window Bits = 10 or Memory Level = 3 where performance craters, raising doubts +on what could happen at Window Bits = 11 and Memory Level = 4 on a different +corpus. + +Defaults must be safe for all applications, hence a more conservative choice. + +.. _compression/benchmark.py: https://github.com/python-websockets/websockets/blob/main/experiments/compression/benchmark.py + +The benchmark focuses on compression because it's more expensive than +decompression. Indeed, leaving aside small allocations, theoretical memory +usage is: + +* ``(1 << (windowBits + 2)) + (1 << (memLevel + 9))`` for compression; +* ``1 << windowBits`` for decompression. + +CPU usage is also higher for compression than decompression. + +While it's always possible for a server to use a smaller window size for +compressing outgoing messages, using a smaller window size for decompressing +incoming messages requires collaboration from clients. + +When a client doesn't support configuring the size of its compression window, +websockets enables compression with the largest possible decompression window. +In most use cases, this is more efficient than disabling compression both ways. + +If you are very sensitive to memory usage, you can reverse this behavior by +setting the ``require_client_max_window_bits`` parameter of +:class:`ServerPerMessageDeflateFactory` to ``True``. + +For clients +........... + +By default, websockets enables compression with Memory Level = 5 but leaves +the Window Bits setting up to the server. + +There's two good reasons and one bad reason for not optimizing the client side +like the server side: + +1. If the maintainers of a server configured some optimized settings, we don't + want to override them with more restrictive settings. + +2. Optimizing memory usage doesn't matter very much for clients because it's + uncommon to open thousands of client connections in a program. + +3. On a more pragmatic note, some servers misbehave badly when a client + configures compression settings. `AWS API Gateway`_ is the worst offender. + + .. _AWS API Gateway: https://github.com/python-websockets/websockets/issues/1065 + + Unfortunately, even though websockets is right and AWS is wrong, many users + jump to the conclusion that websockets doesn't work. + + Until the ecosystem levels up, interoperability with buggy servers seems + more valuable than optimizing memory usage. + + +Further reading +--------------- + +This `blog post by Ilya Grigorik`_ provides more details about how compression +settings affect memory usage and how to optimize them. + +.. _blog post by Ilya Grigorik: https://www.igvita.com/2013/11/27/configuring-and-optimizing-websocket-compression/ + +This `experiment by Peter Thorson`_ recommends Window Bits = 11 and Memory +Level = 4 for optimizing memory usage. + +.. _experiment by Peter Thorson: https://mailarchive.ietf.org/arch/msg/hybi/F9t4uPufVEy8KBLuL36cZjCmM_Y/ diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/data-flow.svg b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/data-flow.svg new file mode 100644 index 0000000000..22a198ed83 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/data-flow.svg @@ -0,0 +1,63 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="-382 -214 500 464" width="500px" height="464px"><defs><style>@font-face{ + font-family:"DIN Next"; + font-weight: 400; + src:url("https://whimsical.com/fonts/139e8e25-0eea-4cb4-a5d4-79048803e73d.eot?#iefix"); + src:url("https://whimsical.com/fonts/139e8e25-0eea-4cb4-a5d4-79048803e73d.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/094b15e3-94bd-435b-a595-d40edfde661a.woff2") format("woff2"),url("https://whimsical.com/fonts/7e5fbe11-4858-4bd1-9ec6-a1d9f9d227aa.woff") format("woff"),url("https://whimsical.com/fonts/0f11eff9-9f05-46f5-9703-027c510065d7.ttf") format("truetype"),url("https://whimsical.com/fonts/48b61978-3f30-4274-823c-5cdcd1876918.svg#48b61978-3f30-4274-823c-5cdcd1876918") format("svg"); +} +@font-face{ + font-family:"DIN Next"; + font-weight: 400; + font-style: italic; + src:url("https://whimsical.com/fonts/df24d5e8-5087-42fd-99b1-b16042d666c8.eot?#iefix"); + src:url("https://whimsical.com/fonts/df24d5e8-5087-42fd-99b1-b16042d666c8.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/46251881-ffe9-4bfb-99c7-d6ce3bebaf3e.woff2") format("woff2"),url("https://whimsical.com/fonts/790ebbf2-62c5-4a32-946f-99d405f9243e.woff") format("woff"),url("https://whimsical.com/fonts/d28199e6-0f4a-42df-97f4-606701c6f75a.ttf") format("truetype"),url("https://whimsical.com/fonts/37a462c0-d86e-492c-b9ab-35e6bd417f6c.svg#37a462c0-d86e-492c-b9ab-35e6bd417f6c") format("svg"); +} +@font-face{ + font-weight: 500; + font-family:"DIN Next"; + src:url("https://whimsical.com/fonts/c5b058fc-55ce-4e06-a175-5c7d9a7e90f4.eot?#iefix"); + src:url("https://whimsical.com/fonts/c5b058fc-55ce-4e06-a175-5c7d9a7e90f4.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/7b29ae40-30ff-4f99-a2b9-cde88669fa2f.woff2") format("woff2"),url("https://whimsical.com/fonts/bf73077c-e354-4562-a085-f4703eb1d653.woff") format("woff"),url("https://whimsical.com/fonts/0ffa6947-5317-4d07-b525-14d08a028821.ttf") format("truetype"),url("https://whimsical.com/fonts/9e423e45-5450-4991-a157-dbe6cf61eb4e.svg#9e423e45-5450-4991-a157-dbe6cf61eb4e") format("svg"); +} +@font-face{ + font-family:"DIN Next"; + font-weight: 500; + font-style: italic; + src:url("https://whimsical.com/fonts/9897c008-fd65-48a4-afc7-36de2fea97b9.eot?#iefix"); + src:url("https://whimsical.com/fonts/9897c008-fd65-48a4-afc7-36de2fea97b9.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/c7717981-647d-4b76-8817-33062e42d11f.woff2") format("woff2"),url("https://whimsical.com/fonts/b852cd4c-1255-40b1-a2be-73a9105b0155.woff") format("woff"),url("https://whimsical.com/fonts/821b00ad-e741-4e2d-af1a-85594367c8a2.ttf") format("truetype"),url("https://whimsical.com/fonts/d3e3b689-a6b0-45f2-b279-f5e194f87409.svg#d3e3b689-a6b0-45f2-b279-f5e194f87409") format("svg"); +} +@font-face{ + font-weight: 700; + font-family:"DIN Next"; + src:url("https://whimsical.com/fonts/81cd3b08-fd39-4ae3-8d05-9d24709eba84.eot?#iefix"); + src:url("https://whimsical.com/fonts/81cd3b08-fd39-4ae3-8d05-9d24709eba84.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/31704504-4671-47a6-a61e-397f07410d91.woff2") format("woff2"),url("https://whimsical.com/fonts/b8a280da-481f-44a0-8d9c-1bc64bd7227c.woff") format("woff"),url("https://whimsical.com/fonts/276d122a-0fab-447b-9fc0-5d7fb0eafce2.ttf") format("truetype"),url("https://whimsical.com/fonts/8fb8273a-8213-4928-808b-b5bfaf3fd7e9.svg#8fb8273a-8213-4928-808b-b5bfaf3fd7e9") format("svg"); +} +@font-face{ + font-family:"DIN Next"; + font-weight: 700; + font-style: italic; + src:url("https://whimsical.com/fonts/00cc6df3-ed32-4004-8dd8-1c576600a408.eot?#iefix"); + src:url("https://whimsical.com/fonts/00cc6df3-ed32-4004-8dd8-1c576600a408.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/4132c4c8-680c-4d6d-9251-a2da38503bbd.woff2") format("woff2"),url("https://whimsical.com/fonts/366401fe-6df4-47be-8f55-8a411cff0dd2.woff") format("woff"),url("https://whimsical.com/fonts/dbe4f7ba-fc16-44a6-a577-571620e9edaf.ttf") format("truetype"),url("https://whimsical.com/fonts/f874edca-ee87-4ccf-8b1d-921fbc3c1c36.svg#f874edca-ee87-4ccf-8b1d-921fbc3c1c36") format("svg"); +} + +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-Regular.woff') format('woff'); + font-weight: 400; +} +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-Italic.woff') format('woff'); + font-style: italic; + font-weight: 400; +} +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-Bold.woff') format('woff'); + font-weight: 700; +} +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-BoldItalic.woff') format('woff'); + font-style: italic; + font-weight: 700; +} +* {-webkit-font-smoothing: auto; -moz-osx-font-smoothing: auto;}@media print { svg { width: 100%; height: 100%; } }</style><filter id="fill-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.16"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="fill-light-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.04"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="image-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="8"/><feOffset result="offsetblur" dx="0" dy="3"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.06"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="frame-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="8"/><feOffset result="offsetblur" dx="0" dy="3"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.06"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="badge-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.08"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><g><g><rect x="-372" y="-12" width="480" height="72" rx="3" ry="3" fill="#f6d8dd"/><rect y="-12" rx="3" stroke="#D3455B" fill="none" stroke-linejoin="round" width="480" stroke-linecap="round" stroke-width="2" x="-372" ry="3" height="72"/><text y="12" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="456" xml:space="preserve" x="-360" font-family="DIN Next, sans-serif" height="24"><tspan x="-132" y="30" text-anchor="middle" style="white-space:pre;">Integration layer</tspan></text></g></g><g><g><rect x="-372" y="168" width="480" height="72" rx="3" ry="3" fill="#d3e6f7"/><rect y="168" rx="3" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="480" stroke-linecap="round" stroke-width="2" x="-372" ry="3" height="72"/><text y="192" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="456" xml:space="preserve" x="-360" font-family="DIN Next, sans-serif" height="24"><tspan x="-132" y="210" text-anchor="middle" style="white-space:pre;">Sans-I/O layer</tspan></text></g></g><g><g><rect x="-372" y="-192" width="216" height="72" rx="3" ry="3" fill="#cfeeeb"/><rect y="-192" rx="3" stroke="#1AAE9F" fill="none" stroke-linejoin="round" width="216" stroke-linecap="round" stroke-width="2" x="-372" ry="3" height="72"/><text y="-168" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="192" xml:space="preserve" x="-360" font-family="DIN Next, sans-serif" height="24"><tspan x="-264" y="-150" text-anchor="middle" style="white-space:pre;">Application</tspan></text></g></g><g><g><path d="M-324,-20v-20M-324,-92v-16" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-329.54095,-104.9079 -324,-112 -318.45905,-104.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="-90" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="79" xml:space="preserve" x="-363.5" font-family="DIN Next, sans-serif" height="48"><tspan x="-324" y="-72" text-anchor="middle" style="white-space:pre;">receive</tspan><tspan x="-324" y="-48" text-anchor="middle" style="white-space:pre;">messages</tspan></text></g></g><g><g><path d="M-204,-112l0,20M-204,-40v16" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-198.45905,-27.0921 -204,-20 -209.54095,-27.0921" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="-90" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="79" xml:space="preserve" x="-243.5" font-family="DIN Next, sans-serif" height="48"><tspan x="-204" y="-72" text-anchor="middle" style="white-space:pre;">send</tspan><tspan x="-204" y="-48" text-anchor="middle" style="white-space:pre;">messages</tspan></text></g></g><g><g><path d="M96.94345,-173.14258c4.53106,1.57532 8.08271,4.56716 9.84726,8.29515c1.76456,3.72799 1.59205,7.87526 -0.4783,11.4987c1.926,5.62898 0.07743,11.66356 -4.87418,15.91158c-4.95161,4.24802 -12.28578,6.09138 -19.33826,4.86046c-2.37232,7.09867 -9.03221,12.73751 -17.68526,14.97387c-8.65305,2.23636 -18.11659,0.76458 -25.13032,-3.90829c-8.39761,8.37233 -20.93327,13.315 -34.25257,13.5054c-13.3193,0.1904 -26.06267,-4.39089 -34.82012,-12.51799c-15.68591,7.18472 -35.58915,2.88148 -44.76245,-9.678c-7.38426,1.34031 -15.11657,-0.22307 -20.85552,-4.21672c-5.73895,-3.99365 -8.80965,-9.94793 -8.28226,-16.05983c-3.457,-2.81303 -4.97104,-6.82861 -4.04383,-10.72516c0.92721,-3.89655 4.17556,-7.16931 8.67598,-8.74119c1.08624,-6.50166 5.74572,-12.25693 12.67613,-15.65724c6.93042,-3.40031 15.38838,-4.08092 23.00992,-1.85161c7.11534,-8.80068 18.56556,-14.69575 31.42744,-16.1802c12.86188,-1.48445 25.89062,1.58538 35.76002,8.42577c6.64655,-4.34187 15.16482,-6.34654 23.65054,-5.56587c8.48572,0.78068 16.23125,4.28161 21.50507,9.72014c19.34894,-5.64384 40.71077,2.33211 47.97072,17.91102z" fill="#e3e6e9"/><path d="M96.94345,-173.14258c4.53106,1.57532 8.08271,4.56716 9.84726,8.29515c1.76456,3.72799 1.59205,7.87526 -0.4783,11.4987c1.926,5.62898 0.07743,11.66356 -4.87418,15.91158c-4.95161,4.24802 -12.28578,6.09138 -19.33826,4.86046c-2.37232,7.09867 -9.03221,12.73751 -17.68526,14.97387c-8.65305,2.23636 -18.11659,0.76458 -25.13032,-3.90829c-8.39761,8.37233 -20.93327,13.315 -34.25257,13.5054c-13.3193,0.1904 -26.06267,-4.39089 -34.82012,-12.51799c-15.68591,7.18472 -35.58915,2.88148 -44.76245,-9.678c-7.38426,1.34031 -15.11657,-0.22307 -20.85552,-4.21672c-5.73895,-3.99365 -8.80965,-9.94793 -8.28226,-16.05983c-3.457,-2.81303 -4.97104,-6.82861 -4.04383,-10.72516c0.92721,-3.89655 4.17556,-7.16931 8.67598,-8.74119c1.08624,-6.50166 5.74572,-12.25693 12.67613,-15.65724c6.93042,-3.40031 15.38838,-4.08092 23.00992,-1.85161c7.11534,-8.80068 18.56556,-14.69575 31.42744,-16.1802c12.86188,-1.48445 25.89062,1.58538 35.76002,8.42577c6.64655,-4.34187 15.16482,-6.34654 23.65054,-5.56587c8.48572,0.78068 16.23125,4.28161 21.50507,9.72014c19.34894,-5.64384 40.71077,2.33211 47.97072,17.91102z" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/><text y="-168" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="192" xml:space="preserve" x="-96" font-family="DIN Next, sans-serif" height="24"><tspan x="0" y="-150" text-anchor="middle" style="white-space:pre;">Network</tspan></text></g></g><g><g><path d="M-60,-20v-20M-60,-92v-15.53727" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-65.54095,-104.44518 -60,-111.53727 -54.45905,-104.44518" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="-90" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="38" xml:space="preserve" x="-79" font-family="DIN Next, sans-serif" height="48"><tspan x="-60" y="-72" text-anchor="middle" style="white-space:pre;">send</tspan><tspan x="-60" y="-48" text-anchor="middle" style="white-space:pre;">data</tspan></text></g></g><g><g><path d="M60,-108.78722v18.78722M60,-38v14" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="65.54095,-27.0921 60,-20 54.45905,-27.0921" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="-88" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="56" xml:space="preserve" x="32" font-family="DIN Next, sans-serif" height="48"><tspan x="60" y="-70" text-anchor="middle" style="white-space:pre;">receive</tspan><tspan x="60" y="-46" text-anchor="middle" style="white-space:pre;">data</tspan></text></g></g><g><g><path d="M60,68v20M60,140v16" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="65.54095,152.9079 60,160 54.45905,152.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="90" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="56" xml:space="preserve" x="32" font-family="DIN Next, sans-serif" height="48"><tspan x="60" y="108" text-anchor="middle" style="white-space:pre;">receive</tspan><tspan x="60" y="132" text-anchor="middle" style="white-space:pre;">bytes</tspan></text></g></g><g><g><path d="M-60,160v-20M-60,88v-16" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-65.54095,75.0921 -60,68 -54.45905,75.0921" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="90" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="42" xml:space="preserve" x="-81" font-family="DIN Next, sans-serif" height="48"><tspan x="-60" y="108" text-anchor="middle" style="white-space:pre;">send</tspan><tspan x="-60" y="132" text-anchor="middle" style="white-space:pre;">bytes</tspan></text></g></g><g><g><path d="M-212,42" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-212,42 -212,42 -212,42" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g></g><g><g><path d="M-204,68v20M-204,140v16" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-198.45905,152.9079 -204,160 -209.54095,152.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="90" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="52" xml:space="preserve" x="-230" font-family="DIN Next, sans-serif" height="48"><tspan x="-204" y="108" text-anchor="middle" style="white-space:pre;">send</tspan><tspan x="-204" y="132" text-anchor="middle" style="white-space:pre;">events</tspan></text></g></g><g><g><path d="M-324,160l0,-18M-324,90v-14" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-329.54095,79.0921 -324,72 -318.45905,79.0921" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="92" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="56" xml:space="preserve" x="-352" font-family="DIN Next, sans-serif" height="48"><tspan x="-324" y="110" text-anchor="middle" style="white-space:pre;">receive</tspan><tspan x="-324" y="134" text-anchor="middle" style="white-space:pre;">events</tspan></text></g></g></svg> diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/deployment.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/deployment.rst new file mode 100644 index 0000000000..2a1fe9a785 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/deployment.rst @@ -0,0 +1,181 @@ +Deployment +========== + +.. currentmodule:: websockets + +When you deploy your websockets server to production, at a high level, your +architecture will almost certainly look like the following diagram: + +.. image:: deployment.svg + +The basic unit for scaling a websockets server is "one server process". Each +blue box in the diagram represents one server process. + +There's more variation in routing. While the routing layer is shown as one big +box, it is likely to involve several subsystems. + +When you design a deployment, your should consider two questions: + +1. How will I run the appropriate number of server processes? +2. How will I route incoming connections to these processes? + +These questions are strongly related. There's a wide range of acceptable +answers, depending on your goals and your constraints. + +You can find a few concrete examples in the :ref:`deployment how-to guides +<deployment-howto>`. + +Running server processes +------------------------ + +How many processes do I need? +............................. + +Typically, one server process will manage a few hundreds or thousands +connections, depending on the frequency of messages and the amount of work +they require. + +CPU and memory usage increase with the number of connections to the server. + +Often CPU is the limiting factor. If a server process goes to 100% CPU, then +you reached the limit. How much headroom you want to keep is up to you. + +Once you know how many connections a server process can manage and how many +connections you need to handle, you can calculate how many processes to run. + +You can also automate this calculation by configuring an autoscaler to keep +CPU usage or connection count within acceptable limits. + +Don't scale with threads. Threads doesn't make sense for a server built with +:mod:`asyncio`. + +How do I run processes? +....................... + +Most solutions for running multiple instances of a server process fall into +one of these three buckets: + +1. Running N processes on a platform: + + * a Kubernetes Deployment + + * its equivalent on a Platform as a Service provider + +2. Running N servers: + + * an AWS Auto Scaling group, a GCP Managed instance group, etc. + + * a fixed set of long-lived servers + +3. Running N processes on a server: + + * preferably via a process manager or supervisor + +Option 1 is easiest of you have access to such a platform. + +Option 2 almost always combines with option 3. + +How do I start a process? +......................... + +Run a Python program that invokes :func:`~server.serve`. That's it. + +Don't run an ASGI server such as Uvicorn, Hypercorn, or Daphne. They're +alternatives to websockets, not complements. + +Don't run a WSGI server such as Gunicorn, Waitress, or mod_wsgi. They aren't +designed to run WebSocket applications. + +Applications servers handle network connections and expose a Python API. You +don't need one because websockets handles network connections directly. + +How do I stop a process? +........................ + +Process managers send the SIGTERM signal to terminate processes. Catch this +signal and exit the server to ensure a graceful shutdown. + +Here's an example: + +.. literalinclude:: ../../example/faq/shutdown_server.py + :emphasize-lines: 12-15,18 + +When exiting the context manager, :func:`~server.serve` closes all connections +with code 1001 (going away). As a consequence: + +* If the connection handler is awaiting + :meth:`~server.WebSocketServerProtocol.recv`, it receives a + :exc:`~exceptions.ConnectionClosedOK` exception. It can catch the exception + and clean up before exiting. + +* Otherwise, it should be waiting on + :meth:`~server.WebSocketServerProtocol.wait_closed`, so it can receive the + :exc:`~exceptions.ConnectionClosedOK` exception and exit. + +This example is easily adapted to handle other signals. + +If you override the default signal handler for SIGINT, which raises +:exc:`KeyboardInterrupt`, be aware that you won't be able to interrupt a +program with Ctrl-C anymore when it's stuck in a loop. + +Routing connections +------------------- + +What does routing involve? +.......................... + +Since the routing layer is directly exposed to the Internet, it should provide +appropriate protection against threats ranging from Internet background noise +to targeted attacks. + +You should always secure WebSocket connections with TLS. Since the routing +layer carries the public domain name, it should terminate TLS connections. + +Finally, it must route connections to the server processes, balancing new +connections across them. + +How do I route connections? +........................... + +Here are typical solutions for load balancing, matched to ways of running +processes: + +1. If you're running on a platform, it comes with a routing layer: + + * a Kubernetes Ingress and Service + + * a service mesh: Istio, Consul, Linkerd, etc. + + * the routing mesh of a Platform as a Service + +2. If you're running N servers, you may load balance with: + + * a cloud load balancer: AWS Elastic Load Balancing, GCP Cloud Load + Balancing, etc. + + * A software load balancer: HAProxy, NGINX, etc. + +3. If you're running N processes on a server, you may load balance with: + + * A software load balancer: HAProxy, NGINX, etc. + + * The operating system — all processes listen on the same port + +You may trust the load balancer to handle encryption and to provide security. +You may add another layer in front of the load balancer for these purposes. + +There are many possibilities. Don't add layers that you don't need, though. + +How do I implement a health check? +.................................. + +Load balancers need a way to check whether server processes are up and running +to avoid routing connections to a non-functional backend. + +websockets provide minimal support for responding to HTTP requests with the +:meth:`~server.WebSocketServerProtocol.process_request` hook. + +Here's an example: + +.. literalinclude:: ../../example/faq/health_check_server.py + :emphasize-lines: 7-9,18 diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/deployment.svg b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/deployment.svg new file mode 100644 index 0000000000..cb948d8d6b --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/deployment.svg @@ -0,0 +1,63 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="-490 -34 644 356" width="644px" height="356px"><defs><style>@font-face{ + font-family:"DIN Next"; + font-weight: 400; + src:url("https://whimsical.com/fonts/139e8e25-0eea-4cb4-a5d4-79048803e73d.eot?#iefix"); + src:url("https://whimsical.com/fonts/139e8e25-0eea-4cb4-a5d4-79048803e73d.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/094b15e3-94bd-435b-a595-d40edfde661a.woff2") format("woff2"),url("https://whimsical.com/fonts/7e5fbe11-4858-4bd1-9ec6-a1d9f9d227aa.woff") format("woff"),url("https://whimsical.com/fonts/0f11eff9-9f05-46f5-9703-027c510065d7.ttf") format("truetype"),url("https://whimsical.com/fonts/48b61978-3f30-4274-823c-5cdcd1876918.svg#48b61978-3f30-4274-823c-5cdcd1876918") format("svg"); +} +@font-face{ + font-family:"DIN Next"; + font-weight: 400; + font-style: italic; + src:url("https://whimsical.com/fonts/df24d5e8-5087-42fd-99b1-b16042d666c8.eot?#iefix"); + src:url("https://whimsical.com/fonts/df24d5e8-5087-42fd-99b1-b16042d666c8.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/46251881-ffe9-4bfb-99c7-d6ce3bebaf3e.woff2") format("woff2"),url("https://whimsical.com/fonts/790ebbf2-62c5-4a32-946f-99d405f9243e.woff") format("woff"),url("https://whimsical.com/fonts/d28199e6-0f4a-42df-97f4-606701c6f75a.ttf") format("truetype"),url("https://whimsical.com/fonts/37a462c0-d86e-492c-b9ab-35e6bd417f6c.svg#37a462c0-d86e-492c-b9ab-35e6bd417f6c") format("svg"); +} +@font-face{ + font-weight: 500; + font-family:"DIN Next"; + src:url("https://whimsical.com/fonts/c5b058fc-55ce-4e06-a175-5c7d9a7e90f4.eot?#iefix"); + src:url("https://whimsical.com/fonts/c5b058fc-55ce-4e06-a175-5c7d9a7e90f4.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/7b29ae40-30ff-4f99-a2b9-cde88669fa2f.woff2") format("woff2"),url("https://whimsical.com/fonts/bf73077c-e354-4562-a085-f4703eb1d653.woff") format("woff"),url("https://whimsical.com/fonts/0ffa6947-5317-4d07-b525-14d08a028821.ttf") format("truetype"),url("https://whimsical.com/fonts/9e423e45-5450-4991-a157-dbe6cf61eb4e.svg#9e423e45-5450-4991-a157-dbe6cf61eb4e") format("svg"); +} +@font-face{ + font-family:"DIN Next"; + font-weight: 500; + font-style: italic; + src:url("https://whimsical.com/fonts/9897c008-fd65-48a4-afc7-36de2fea97b9.eot?#iefix"); + src:url("https://whimsical.com/fonts/9897c008-fd65-48a4-afc7-36de2fea97b9.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/c7717981-647d-4b76-8817-33062e42d11f.woff2") format("woff2"),url("https://whimsical.com/fonts/b852cd4c-1255-40b1-a2be-73a9105b0155.woff") format("woff"),url("https://whimsical.com/fonts/821b00ad-e741-4e2d-af1a-85594367c8a2.ttf") format("truetype"),url("https://whimsical.com/fonts/d3e3b689-a6b0-45f2-b279-f5e194f87409.svg#d3e3b689-a6b0-45f2-b279-f5e194f87409") format("svg"); +} +@font-face{ + font-weight: 700; + font-family:"DIN Next"; + src:url("https://whimsical.com/fonts/81cd3b08-fd39-4ae3-8d05-9d24709eba84.eot?#iefix"); + src:url("https://whimsical.com/fonts/81cd3b08-fd39-4ae3-8d05-9d24709eba84.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/31704504-4671-47a6-a61e-397f07410d91.woff2") format("woff2"),url("https://whimsical.com/fonts/b8a280da-481f-44a0-8d9c-1bc64bd7227c.woff") format("woff"),url("https://whimsical.com/fonts/276d122a-0fab-447b-9fc0-5d7fb0eafce2.ttf") format("truetype"),url("https://whimsical.com/fonts/8fb8273a-8213-4928-808b-b5bfaf3fd7e9.svg#8fb8273a-8213-4928-808b-b5bfaf3fd7e9") format("svg"); +} +@font-face{ + font-family:"DIN Next"; + font-weight: 700; + font-style: italic; + src:url("https://whimsical.com/fonts/00cc6df3-ed32-4004-8dd8-1c576600a408.eot?#iefix"); + src:url("https://whimsical.com/fonts/00cc6df3-ed32-4004-8dd8-1c576600a408.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/4132c4c8-680c-4d6d-9251-a2da38503bbd.woff2") format("woff2"),url("https://whimsical.com/fonts/366401fe-6df4-47be-8f55-8a411cff0dd2.woff") format("woff"),url("https://whimsical.com/fonts/dbe4f7ba-fc16-44a6-a577-571620e9edaf.ttf") format("truetype"),url("https://whimsical.com/fonts/f874edca-ee87-4ccf-8b1d-921fbc3c1c36.svg#f874edca-ee87-4ccf-8b1d-921fbc3c1c36") format("svg"); +} + +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-Regular.woff') format('woff'); + font-weight: 400; +} +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-Italic.woff') format('woff'); + font-style: italic; + font-weight: 400; +} +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-Bold.woff') format('woff'); + font-weight: 700; +} +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-BoldItalic.woff') format('woff'); + font-style: italic; + font-weight: 700; +} +* {-webkit-font-smoothing: auto; -moz-osx-font-smoothing: auto;}@media print { svg { width: 100%; height: 100%; } }</style><filter id="fill-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.16"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="fill-light-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.04"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="image-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="8"/><feOffset result="offsetblur" dx="0" dy="3"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.06"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="frame-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="8"/><feOffset result="offsetblur" dx="0" dy="3"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.06"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="badge-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.08"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><g><g><path d="M132.40052,16.91125c8.20504,1.34238 13.19097,6.77278 11.13766,12.13055c-0.76086,3.6926 -4.09828,6.94687 -9.03771,8.81254c-4.93944,1.86567 -10.9238,2.13232 -16.20563,0.72209c-3.50792,6.79759 -12.12825,11.93728 -22.81409,13.60245c-10.68585,1.66516 -21.93194,-0.37877 -29.7633,-5.40937c-7.5447,11.32261 -23.72737,19.17936 -42.50646,20.63705c-18.77909,1.45769 -37.33189,-3.70278 -48.732,-13.55484c-11.00862,8.39145 -26.81112,13.46368 -43.71109,14.03023c-16.89997,0.56655 -33.41499,-3.42228 -45.68197,-11.03344c-12.09168,9.57066 -30.08677,15.12494 -49.08843,15.1514c-19.00166,0.02646 -37.03287,-5.47764 -49.18697,-15.01453c-32.22671,18.71922 -81.52408,17.01732 -110.54782,-3.81645c-9.61896,5.63808 -22.48701,8.30277 -35.32441,7.3149c-12.8374,-0.98787 -24.40407,-5.53286 -31.75198,-12.47659c-8.38039,4.12685 -19.07889,5.69428 -29.35634,4.30095c-10.27744,-1.39333 -19.13592,-5.61211 -24.30736,-11.5762c-6.03705,3.01428 -14.12058,3.62226 -21.04917,1.58316c-6.92858,-2.0391 -11.58448,-6.39632 -12.12376,-11.34603c-4.11078,-3.88947 -2.69127,-9.21224 3.19106,-11.96556c2.8597,-6.26181 10.13013,-11.25433 19.56423,-13.4345c9.43409,-2.18018 19.89581,-1.28549 28.15172,2.40755c4.56946,-5.60994 12.84561,-9.53066 22.43123,-10.62652c9.58562,-1.09585 19.40964,0.75561 26.62651,5.0181c13.21632,-10.89584 32.7134,-17.76363 53.91728,-18.99222c21.20388,-1.22858 42.24851,3.29017 58.19687,12.49616c11.75754,-9.7851 29.59785,-15.62692 48.64043,-15.92732c19.04258,-0.30041 37.29416,4.97204 49.76173,14.37498c12.30654,-11.80391 32.97339,-18.70238 54.83436,-18.30339c21.86097,0.39899 41.90535,8.04051 53.18277,20.27486c10.58585,-9.80304 28.36041,-15.18816 46.66019,-14.13654c18.29978,1.05162 34.36046,8.38113 42.16106,19.24077c7.63845,-6.15855 19.75125,-9.16677 31.72723,-7.87948c11.97598,1.2873 21.97263,6.67206 26.18437,14.10438c7.68618,-3.10406 17.06471,-3.86266 25.67861,-2.07706c8.6139,1.78561 15.60478,5.93747 19.14117,11.3679z" fill="#e3e6e9"/><path d="M132.40052,16.91125c8.20504,1.34238 13.19097,6.77278 11.13766,12.13055c-0.76086,3.6926 -4.09828,6.94687 -9.03771,8.81254c-4.93944,1.86567 -10.9238,2.13232 -16.20563,0.72209c-3.50792,6.79759 -12.12825,11.93728 -22.81409,13.60245c-10.68585,1.66516 -21.93194,-0.37877 -29.7633,-5.40937c-7.5447,11.32261 -23.72737,19.17936 -42.50646,20.63705c-18.77909,1.45769 -37.33189,-3.70278 -48.732,-13.55484c-11.00862,8.39145 -26.81112,13.46368 -43.71109,14.03023c-16.89997,0.56655 -33.41499,-3.42228 -45.68197,-11.03344c-12.09168,9.57066 -30.08677,15.12494 -49.08843,15.1514c-19.00166,0.02646 -37.03287,-5.47764 -49.18697,-15.01453c-32.22671,18.71922 -81.52408,17.01732 -110.54782,-3.81645c-9.61896,5.63808 -22.48701,8.30277 -35.32441,7.3149c-12.8374,-0.98787 -24.40407,-5.53286 -31.75198,-12.47659c-8.38039,4.12685 -19.07889,5.69428 -29.35634,4.30095c-10.27744,-1.39333 -19.13592,-5.61211 -24.30736,-11.5762c-6.03705,3.01428 -14.12058,3.62226 -21.04917,1.58316c-6.92858,-2.0391 -11.58448,-6.39632 -12.12376,-11.34603c-4.11078,-3.88947 -2.69127,-9.21224 3.19106,-11.96556c2.8597,-6.26181 10.13013,-11.25433 19.56423,-13.4345c9.43409,-2.18018 19.89581,-1.28549 28.15172,2.40755c4.56946,-5.60994 12.84561,-9.53066 22.43123,-10.62652c9.58562,-1.09585 19.40964,0.75561 26.62651,5.0181c13.21632,-10.89584 32.7134,-17.76363 53.91728,-18.99222c21.20388,-1.22858 42.24851,3.29017 58.19687,12.49616c11.75754,-9.7851 29.59785,-15.62692 48.64043,-15.92732c19.04258,-0.30041 37.29416,4.97204 49.76173,14.37498c12.30654,-11.80391 32.97339,-18.70238 54.83436,-18.30339c21.86097,0.39899 41.90535,8.04051 53.18277,20.27486c10.58585,-9.80304 28.36041,-15.18816 46.66019,-14.13654c18.29978,1.05162 34.36046,8.38113 42.16106,19.24077c7.63845,-6.15855 19.75125,-9.16677 31.72723,-7.87948c11.97598,1.2873 21.97263,6.67206 26.18437,14.10438c7.68618,-3.10406 17.06471,-3.86266 25.67861,-2.07706c8.6139,1.78561 15.60478,5.93747 19.14117,11.3679z" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/><text y="12" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="600" xml:space="preserve" x="-468" font-family="DIN Next, sans-serif" height="24"><tspan x="-168" y="30" text-anchor="middle" style="white-space:pre;">Internet</tspan></text></g></g><g><g><rect x="-480" y="240" width="144" height="72" rx="3" ry="3" fill="#d3e6f7"/><rect y="240" rx="3" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="144" stroke-linecap="round" stroke-width="2" x="-480" ry="3" height="72"/><text y="264" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="120" xml:space="preserve" x="-468" font-family="DIN Next, sans-serif" height="24"><tspan x="-408" y="282" text-anchor="middle" style="white-space:pre;">websockets</tspan></text></g></g><g><g><rect x="-312" y="240" width="144" height="72" rx="3" ry="3" fill="#d3e6f7"/><rect y="240" rx="3" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="144" stroke-linecap="round" stroke-width="2" x="-312" ry="3" height="72"/><text y="264" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="120" xml:space="preserve" x="-300" font-family="DIN Next, sans-serif" height="24"><tspan x="-240" y="282" text-anchor="middle" style="white-space:pre;">websockets</tspan></text></g></g><g><g><rect x="0" y="240" width="144" height="72" rx="3" ry="3" fill="#d3e6f7"/><rect y="240" rx="3" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="144" stroke-linecap="round" stroke-width="2" x="0" ry="3" height="72"/><text y="264" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="120" xml:space="preserve" x="12" font-family="DIN Next, sans-serif" height="24"><tspan x="72" y="282" text-anchor="middle" style="white-space:pre;">websockets</tspan></text></g></g><g><g><rect x="-144" y="258" width="24" height="36" rx="1.5" ry="1.5" fill="#d3e6f7"/><rect y="258" rx="1.5" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="24" stroke-linecap="round" stroke-width="2" x="-144" ry="1.5" height="36"/></g></g><g><g><rect x="-48" y="258" width="24" height="36" rx="1.5" ry="1.5" fill="#d3e6f7"/><rect y="258" rx="1.5" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="24" stroke-linecap="round" stroke-width="2" x="-48" ry="1.5" height="36"/></g></g><g><g><rect x="-96" y="258" width="24" height="36" rx="1.5" ry="1.5" fill="#d3e6f7"/><rect y="258" rx="1.5" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="24" stroke-linecap="round" stroke-width="2" x="-96" ry="1.5" height="36"/></g></g><g><g><rect x="-480" y="120" width="624" height="72" rx="3" ry="3" fill="#cfeeeb"/><rect y="120" rx="3" stroke="#1AAE9F" fill="none" stroke-linejoin="round" width="624" stroke-linecap="round" stroke-width="2" x="-480" ry="3" height="72"/><text y="144" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="600" xml:space="preserve" x="-468" font-family="DIN Next, sans-serif" height="24"><tspan x="-168" y="162" text-anchor="middle" style="white-space:pre;">routing</tspan></text></g></g><g><g><line stroke="#788896" fill="none" stroke-linejoin="round" y1="199.99999999999997" stroke-linecap="round" stroke-width="4" x1="-240" y2="227.99999999999994" x2="-240"/><polygon points="-234.45905,224.9079 -240,232 -245.54095,224.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g></g><g><g><line stroke="#788896" fill="none" stroke-linejoin="round" y1="199.99999999999997" stroke-linecap="round" stroke-width="4" x1="-407.99999999999994" y2="227.99999999999994" x2="-407.99999999999994"/><polygon points="-402.45905,224.9079 -408,232 -413.54095,224.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g></g><g><g><line stroke="#788896" fill="none" stroke-linejoin="round" y1="200.00000000000003" stroke-linecap="round" stroke-width="4" x1="72" y2="228" x2="72"/><polygon points="77.54095,224.9079 72,232 66.45905,224.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g></g><g><g><line stroke="#788896" fill="none" stroke-linejoin="round" y1="79.92376615249583" stroke-linecap="round" stroke-width="4" x1="-168" y2="107.99999999999994" x2="-168"/><polygon points="-162.45905,104.9079 -168,112 -173.54095,104.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g></g></svg> diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/design.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/design.rst new file mode 100644 index 0000000000..f164d29905 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/design.rst @@ -0,0 +1,572 @@ +Design +====== + +.. currentmodule:: websockets + +This document describes the design of websockets. It assumes familiarity with +the specification of the WebSocket protocol in :rfc:`6455`. + +It's primarily intended at maintainers. It may also be useful for users who +wish to understand what happens under the hood. + +.. warning:: + + Internals described in this document may change at any time. + + Backwards compatibility is only guaranteed for :doc:`public APIs + <../reference/index>`. + +Lifecycle +--------- + +State +..... + +WebSocket connections go through a trivial state machine: + +- ``CONNECTING``: initial state, +- ``OPEN``: when the opening handshake is complete, +- ``CLOSING``: when the closing handshake is started, +- ``CLOSED``: when the TCP connection is closed. + +Transitions happen in the following places: + +- ``CONNECTING -> OPEN``: in + :meth:`~legacy.protocol.WebSocketCommonProtocol.connection_open` which runs when + the :ref:`opening handshake <opening-handshake>` completes and the WebSocket + connection is established — not to be confused with + :meth:`~asyncio.BaseProtocol.connection_made` which runs when the TCP connection + is established; +- ``OPEN -> CLOSING``: in + :meth:`~legacy.protocol.WebSocketCommonProtocol.write_frame` immediately before + sending a close frame; since receiving a close frame triggers sending a + close frame, this does the right thing regardless of which side started the + :ref:`closing handshake <closing-handshake>`; also in + :meth:`~legacy.protocol.WebSocketCommonProtocol.fail_connection` which duplicates + a few lines of code from ``write_close_frame()`` and ``write_frame()``; +- ``* -> CLOSED``: in + :meth:`~legacy.protocol.WebSocketCommonProtocol.connection_lost` which is always + called exactly once when the TCP connection is closed. + +Coroutines +.......... + +The following diagram shows which coroutines are running at each stage of the +connection lifecycle on the client side. + +.. image:: lifecycle.svg + :target: _images/lifecycle.svg + +The lifecycle is identical on the server side, except inversion of control +makes the equivalent of :meth:`~client.connect` implicit. + +Coroutines shown in green are called by the application. Multiple coroutines +may interact with the WebSocket connection concurrently. + +Coroutines shown in gray manage the connection. When the opening handshake +succeeds, :meth:`~legacy.protocol.WebSocketCommonProtocol.connection_open` starts +two tasks: + +- :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` runs + :meth:`~legacy.protocol.WebSocketCommonProtocol.transfer_data` which handles + incoming data and lets :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` + consume it. It may be canceled to terminate the connection. It never exits + with an exception other than :exc:`~asyncio.CancelledError`. See :ref:`data + transfer <data-transfer>` below. + +- :attr:`~legacy.protocol.WebSocketCommonProtocol.keepalive_ping_task` runs + :meth:`~legacy.protocol.WebSocketCommonProtocol.keepalive_ping` which sends Ping + frames at regular intervals and ensures that corresponding Pong frames are + received. It is canceled when the connection terminates. It never exits + with an exception other than :exc:`~asyncio.CancelledError`. + +- :attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` runs + :meth:`~legacy.protocol.WebSocketCommonProtocol.close_connection` which waits for + the data transfer to terminate, then takes care of closing the TCP + connection. It must not be canceled. It never exits with an exception. See + :ref:`connection termination <connection-termination>` below. + +Besides, :meth:`~legacy.protocol.WebSocketCommonProtocol.fail_connection` starts +the same :attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` when +the opening handshake fails, in order to close the TCP connection. + +Splitting the responsibilities between two tasks makes it easier to guarantee +that websockets can terminate connections: + +- within a fixed timeout, +- without leaking pending tasks, +- without leaking open TCP connections, + +regardless of whether the connection terminates normally or abnormally. + +:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` completes when no +more data will be received on the connection. Under normal circumstances, it +exits after exchanging close frames. + +:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` completes when +the TCP connection is closed. + + +.. _opening-handshake: + +Opening handshake +----------------- + +websockets performs the opening handshake when establishing a WebSocket +connection. On the client side, :meth:`~client.connect` executes it +before returning the protocol to the caller. On the server side, it's executed +before passing the protocol to the ``ws_handler`` coroutine handling the +connection. + +While the opening handshake is asymmetrical — the client sends an HTTP Upgrade +request and the server replies with an HTTP Switching Protocols response — +websockets aims at keeping the implementation of both sides consistent with +one another. + +On the client side, :meth:`~client.WebSocketClientProtocol.handshake`: + +- builds an HTTP request based on the ``uri`` and parameters passed to + :meth:`~client.connect`; +- writes the HTTP request to the network; +- reads an HTTP response from the network; +- checks the HTTP response, validates ``extensions`` and ``subprotocol``, and + configures the protocol accordingly; +- moves to the ``OPEN`` state. + +On the server side, :meth:`~server.WebSocketServerProtocol.handshake`: + +- reads an HTTP request from the network; +- calls :meth:`~server.WebSocketServerProtocol.process_request` which may + abort the WebSocket handshake and return an HTTP response instead; this + hook only makes sense on the server side; +- checks the HTTP request, negotiates ``extensions`` and ``subprotocol``, and + configures the protocol accordingly; +- builds an HTTP response based on the above and parameters passed to + :meth:`~server.serve`; +- writes the HTTP response to the network; +- moves to the ``OPEN`` state; +- returns the ``path`` part of the ``uri``. + +The most significant asymmetry between the two sides of the opening handshake +lies in the negotiation of extensions and, to a lesser extent, of the +subprotocol. The server knows everything about both sides and decides what the +parameters should be for the connection. The client merely applies them. + +If anything goes wrong during the opening handshake, websockets :ref:`fails +the connection <connection-failure>`. + + +.. _data-transfer: + +Data transfer +------------- + +Symmetry +........ + +Once the opening handshake has completed, the WebSocket protocol enters the +data transfer phase. This part is almost symmetrical. There are only two +differences between a server and a client: + +- `client-to-server masking`_: the client masks outgoing frames; the server + unmasks incoming frames; +- `closing the TCP connection`_: the server closes the connection immediately; + the client waits for the server to do it. + +.. _client-to-server masking: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.3 +.. _closing the TCP connection: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.1 + +These differences are so minor that all the logic for `data framing`_, for +`sending and receiving data`_ and for `closing the connection`_ is implemented +in the same class, :class:`~legacy.protocol.WebSocketCommonProtocol`. + +.. _data framing: https://www.rfc-editor.org/rfc/rfc6455.html#section-5 +.. _sending and receiving data: https://www.rfc-editor.org/rfc/rfc6455.html#section-6 +.. _closing the connection: https://www.rfc-editor.org/rfc/rfc6455.html#section-7 + +The :attr:`~legacy.protocol.WebSocketCommonProtocol.is_client` attribute tells which +side a protocol instance is managing. This attribute is defined on the +:attr:`~server.WebSocketServerProtocol` and +:attr:`~client.WebSocketClientProtocol` classes. + +Data flow +......... + +The following diagram shows how data flows between an application built on top +of websockets and a remote endpoint. It applies regardless of which side is +the server or the client. + +.. image:: protocol.svg + :target: _images/protocol.svg + +Public methods are shown in green, private methods in yellow, and buffers in +orange. Methods related to connection termination are omitted; connection +termination is discussed in another section below. + +Receiving data +.............. + +The left side of the diagram shows how websockets receives data. + +Incoming data is written to a :class:`~asyncio.StreamReader` in order to +implement flow control and provide backpressure on the TCP connection. + +:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task`, which is started +when the WebSocket connection is established, processes this data. + +When it receives data frames, it reassembles fragments and puts the resulting +messages in the :attr:`~legacy.protocol.WebSocketCommonProtocol.messages` queue. + +When it encounters a control frame: + +- if it's a close frame, it starts the closing handshake; +- if it's a ping frame, it answers with a pong frame; +- if it's a pong frame, it acknowledges the corresponding ping (unless it's an + unsolicited pong). + +Running this process in a task guarantees that control frames are processed +promptly. Without such a task, websockets would depend on the application to +drive the connection by having exactly one coroutine awaiting +:meth:`~legacy.protocol.WebSocketCommonProtocol.recv` at any time. While this +happens naturally in many use cases, it cannot be relied upon. + +Then :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` fetches the next message +from the :attr:`~legacy.protocol.WebSocketCommonProtocol.messages` queue, with some +complexity added for handling backpressure and termination correctly. + +Sending data +............ + +The right side of the diagram shows how websockets sends data. + +:meth:`~legacy.protocol.WebSocketCommonProtocol.send` writes one or several data +frames containing the message. While sending a fragmented message, concurrent +calls to :meth:`~legacy.protocol.WebSocketCommonProtocol.send` are put on hold until +all fragments are sent. This makes concurrent calls safe. + +:meth:`~legacy.protocol.WebSocketCommonProtocol.ping` writes a ping frame and +yields a :class:`~asyncio.Future` which will be completed when a matching pong +frame is received. + +:meth:`~legacy.protocol.WebSocketCommonProtocol.pong` writes a pong frame. + +:meth:`~legacy.protocol.WebSocketCommonProtocol.close` writes a close frame and +waits for the TCP connection to terminate. + +Outgoing data is written to a :class:`~asyncio.StreamWriter` in order to +implement flow control and provide backpressure from the TCP connection. + +.. _closing-handshake: + +Closing handshake +................. + +When the other side of the connection initiates the closing handshake, +:meth:`~legacy.protocol.WebSocketCommonProtocol.read_message` receives a close +frame while in the ``OPEN`` state. It moves to the ``CLOSING`` state, sends a +close frame, and returns :obj:`None`, causing +:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` to terminate. + +When this side of the connection initiates the closing handshake with +:meth:`~legacy.protocol.WebSocketCommonProtocol.close`, it moves to the ``CLOSING`` +state and sends a close frame. When the other side sends a close frame, +:meth:`~legacy.protocol.WebSocketCommonProtocol.read_message` receives it in the +``CLOSING`` state and returns :obj:`None`, also causing +:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` to terminate. + +If the other side doesn't send a close frame within the connection's close +timeout, websockets :ref:`fails the connection <connection-failure>`. + +The closing handshake can take up to ``2 * close_timeout``: one +``close_timeout`` to write a close frame and one ``close_timeout`` to receive +a close frame. + +Then websockets terminates the TCP connection. + + +.. _connection-termination: + +Connection termination +---------------------- + +:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task`, which is +started when the WebSocket connection is established, is responsible for +eventually closing the TCP connection. + +First :attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` waits +for :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` to terminate, +which may happen as a result of: + +- a successful closing handshake: as explained above, this exits the infinite + loop in :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task`; +- a timeout while waiting for the closing handshake to complete: this cancels + :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task`; +- a protocol error, including connection errors: depending on the exception, + :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` :ref:`fails the + connection <connection-failure>` with a suitable code and exits. + +:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` is separate +from :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` to make it +easier to implement the timeout on the closing handshake. Canceling +:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` creates no risk +of canceling :attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` +and failing to close the TCP connection, thus leaking resources. + +Then :attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` cancels +:meth:`~legacy.protocol.WebSocketCommonProtocol.keepalive_ping`. This task has no +protocol compliance responsibilities. Terminating it to avoid leaking it is +the only concern. + +Terminating the TCP connection can take up to ``2 * close_timeout`` on the +server side and ``3 * close_timeout`` on the client side. Clients start by +waiting for the server to close the connection, hence the extra +``close_timeout``. Then both sides go through the following steps until the +TCP connection is lost: half-closing the connection (only for non-TLS +connections), closing the connection, aborting the connection. At this point +the connection drops regardless of what happens on the network. + + +.. _connection-failure: + +Connection failure +------------------ + +If the opening handshake doesn't complete successfully, websockets fails the +connection by closing the TCP connection. + +Once the opening handshake has completed, websockets fails the connection by +canceling :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` +and sending a close frame if appropriate. + +:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` exits, unblocking +:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task`, which closes +the TCP connection. + + +.. _server-shutdown: + +Server shutdown +--------------- + +:class:`~websockets.server.WebSocketServer` closes asynchronously like +:class:`asyncio.Server`. The shutdown happen in two steps: + +1. Stop listening and accepting new connections; +2. Close established connections with close code 1001 (going away) or, if + the opening handshake is still in progress, with HTTP status code 503 + (Service Unavailable). + +The first call to :class:`~websockets.server.WebSocketServer.close` starts a +task that performs this sequence. Further calls are ignored. This is the +easiest way to make :class:`~websockets.server.WebSocketServer.close` and +:class:`~websockets.server.WebSocketServer.wait_closed` idempotent. + + +.. _cancellation: + +Cancellation +------------ + +User code +......... + +websockets provides a WebSocket application server. It manages connections and +passes them to user-provided connection handlers. This is an *inversion of +control* scenario: library code calls user code. + +If a connection drops, the corresponding handler should terminate. If the +server shuts down, all connection handlers must terminate. Canceling +connection handlers would terminate them. + +However, using cancellation for this purpose would require all connection +handlers to handle it properly. For example, if a connection handler starts +some tasks, it should catch :exc:`~asyncio.CancelledError`, terminate or +cancel these tasks, and then re-raise the exception. + +Cancellation is tricky in :mod:`asyncio` applications, especially when it +interacts with finalization logic. In the example above, what if a handler +gets interrupted with :exc:`~asyncio.CancelledError` while it's finalizing +the tasks it started, after detecting that the connection dropped? + +websockets considers that cancellation may only be triggered by the caller of +a coroutine when it doesn't care about the results of that coroutine anymore. +(Source: `Guido van Rossum <https://groups.google.com/forum/#!msg +/python-tulip/LZQe38CR3bg/7qZ1p_q5yycJ>`_). Since connection handlers run +arbitrary user code, websockets has no way of deciding whether that code is +still doing something worth caring about. + +For these reasons, websockets never cancels connection handlers. Instead it +expects them to detect when the connection is closed, execute finalization +logic if needed, and exit. + +Conversely, cancellation isn't a concern for WebSocket clients because they +don't involve inversion of control. + +Library +....... + +Most :doc:`public APIs <../reference/index>` of websockets are coroutines. +They may be canceled, for example if the user starts a task that calls these +coroutines and cancels the task later. websockets must handle this situation. + +Cancellation during the opening handshake is handled like any other exception: +the TCP connection is closed and the exception is re-raised. This can only +happen on the client side. On the server side, the opening handshake is +managed by websockets and nothing results in a cancellation. + +Once the WebSocket connection is established, internal tasks +:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` and +:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` mustn't get +accidentally canceled if a coroutine that awaits them is canceled. In other +words, they must be shielded from cancellation. + +:meth:`~legacy.protocol.WebSocketCommonProtocol.recv` waits for the next message in +the queue or for :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` +to terminate, whichever comes first. It relies on :func:`~asyncio.wait` for +waiting on two futures in parallel. As a consequence, even though it's waiting +on a :class:`~asyncio.Future` signaling the next message and on +:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task`, it doesn't +propagate cancellation to them. + +:meth:`~legacy.protocol.WebSocketCommonProtocol.ensure_open` is called by +:meth:`~legacy.protocol.WebSocketCommonProtocol.send`, +:meth:`~legacy.protocol.WebSocketCommonProtocol.ping`, and +:meth:`~legacy.protocol.WebSocketCommonProtocol.pong`. When the connection state is +``CLOSING``, it waits for +:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` but shields it to +prevent cancellation. + +:meth:`~legacy.protocol.WebSocketCommonProtocol.close` waits for the data transfer +task to terminate with :func:`~asyncio.timeout`. If it's canceled or if the +timeout elapses, :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` +is canceled, which is correct at this point. +:meth:`~legacy.protocol.WebSocketCommonProtocol.close` then waits for +:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` but shields it +to prevent cancellation. + +:meth:`~legacy.protocol.WebSocketCommonProtocol.close` and +:meth:`~legacy.protocol.WebSocketCommonProtocol.fail_connection` are the only +places where :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` may +be canceled. + +:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` starts by +waiting for :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task`. It +catches :exc:`~asyncio.CancelledError` to prevent a cancellation of +:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` from propagating +to :attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task`. + +.. _backpressure: + +Backpressure +------------ + +.. note:: + + This section discusses backpressure from the perspective of a server but + the concept applies to clients symmetrically. + +With a naive implementation, if a server receives inputs faster than it can +process them, or if it generates outputs faster than it can send them, data +accumulates in buffers, eventually causing the server to run out of memory and +crash. + +The solution to this problem is backpressure. Any part of the server that +receives inputs faster than it can process them and send the outputs +must propagate that information back to the previous part in the chain. + +websockets is designed to make it easy to get backpressure right. + +For incoming data, websockets builds upon :class:`~asyncio.StreamReader` which +propagates backpressure to its own buffer and to the TCP stream. Frames are +parsed from the input stream and added to a bounded queue. If the queue fills +up, parsing halts until the application reads a frame. + +For outgoing data, websockets builds upon :class:`~asyncio.StreamWriter` which +implements flow control. If the output buffers grow too large, it waits until +they're drained. That's why all APIs that write frames are asynchronous. + +Of course, it's still possible for an application to create its own unbounded +buffers and break the backpressure. Be careful with queues. + + +.. _buffers: + +Buffers +------- + +.. note:: + + This section discusses buffers from the perspective of a server but it + applies to clients as well. + +An asynchronous systems works best when its buffers are almost always empty. + +For example, if a client sends data too fast for a server, the queue of +incoming messages will be constantly full. The server will always be 32 +messages (by default) behind the client. This consumes memory and increases +latency for no good reason. The problem is called bufferbloat. + +If buffers are almost always full and that problem cannot be solved by adding +capacity — typically because the system is bottlenecked by the output and +constantly regulated by backpressure — reducing the size of buffers minimizes +negative consequences. + +By default websockets has rather high limits. You can decrease them according +to your application's characteristics. + +Bufferbloat can happen at every level in the stack where there is a buffer. +For each connection, the receiving side contains these buffers: + +- OS buffers: tuning them is an advanced optimization. +- :class:`~asyncio.StreamReader` bytes buffer: the default limit is 64 KiB. + You can set another limit by passing a ``read_limit`` keyword argument to + :func:`~client.connect()` or :func:`~server.serve`. +- Incoming messages :class:`~collections.deque`: its size depends both on + the size and the number of messages it contains. By default the maximum + UTF-8 encoded size is 1 MiB and the maximum number is 32. In the worst case, + after UTF-8 decoding, a single message could take up to 4 MiB of memory and + the overall memory consumption could reach 128 MiB. You should adjust these + limits by setting the ``max_size`` and ``max_queue`` keyword arguments of + :func:`~client.connect()` or :func:`~server.serve` according to your + application's requirements. + +For each connection, the sending side contains these buffers: + +- :class:`~asyncio.StreamWriter` bytes buffer: the default size is 64 KiB. + You can set another limit by passing a ``write_limit`` keyword argument to + :func:`~client.connect()` or :func:`~server.serve`. +- OS buffers: tuning them is an advanced optimization. + +Concurrency +----------- + +Awaiting any combination of :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`, +:meth:`~legacy.protocol.WebSocketCommonProtocol.send`, +:meth:`~legacy.protocol.WebSocketCommonProtocol.close` +:meth:`~legacy.protocol.WebSocketCommonProtocol.ping`, or +:meth:`~legacy.protocol.WebSocketCommonProtocol.pong` concurrently is safe, including +multiple calls to the same method, with one exception and one limitation. + +* **Only one coroutine can receive messages at a time.** This constraint + avoids non-deterministic behavior (and simplifies the implementation). If a + coroutine is awaiting :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`, + awaiting it again in another coroutine raises :exc:`RuntimeError`. + +* **Sending a fragmented message forces serialization.** Indeed, the WebSocket + protocol doesn't support multiplexing messages. If a coroutine is awaiting + :meth:`~legacy.protocol.WebSocketCommonProtocol.send` to send a fragmented message, + awaiting it again in another coroutine waits until the first call completes. + This will be transparent in many cases. It may be a concern if the + fragmented message is generated slowly by an asynchronous iterator. + +Receiving frames is independent from sending frames. This isolates +:meth:`~legacy.protocol.WebSocketCommonProtocol.recv`, which receives frames, from +the other methods, which send frames. + +While the connection is open, each frame is sent with a single write. Combined +with the concurrency model of :mod:`asyncio`, this enforces serialization. The +only other requirement is to prevent interleaving other data frames in the +middle of a fragmented message. + +After the connection is closed, sending a frame raises +:exc:`~websockets.exceptions.ConnectionClosed`, which is safe. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/index.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/index.rst new file mode 100644 index 0000000000..120a3dd327 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/index.rst @@ -0,0 +1,18 @@ +Topic guides +============ + +Get a deeper understanding of how websockets is built and why. + +.. toctree:: + :titlesonly: + + deployment + logging + authentication + broadcast + compression + timeouts + design + memory + security + performance diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/lifecycle.graffle b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/lifecycle.graffle new file mode 100644 index 0000000000..bd888dcf31 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/lifecycle.graffle @@ -0,0 +1,25 @@ +��][W�8~~���}�G�%��a�.�0KC��N�����J���i�i���ߒs�5n����cUI咾������}틌b/~Y�:Z�d���~Y}��u�Z�����?v����j#ߋ����������j�G#_�Z;���`��� �Vk�pU[=O�ћV���R���CE���p$���2[�M�U(f�{A��zN����ƅ��l;��E�W2�\�u���B�$r �M�њ]�X�lϱ��t��Ɏ"[]��'<�&H����D�x�������}�7ZS�5�B'�OL�9ɑl�fYOD'a��s'�h,[��-۹PE.7:��9��٧d�"������ +�i�zM�&_�L,���*�mߎ������2K,�;�L�$�� Y'��eF9�x�Y�Yj.y��ٗ�X�`�Gyq@�sE̮���c�1�K��m;�b�G�7�0Se��|Ef�/Y��k����a���AP� WeJ�;#ہ,�����M��"=�0K��V�0c�?�m��IE<�����:��o(~cp�����*�0*2��=@�U�*��x�ȷ�:��W��¡9C�m~]��WϗݫQ����i�f�:��2��%�N�w��M +�����<^ k`2}�0H��~���+��F�'�/2�T�s��\kS��*WO7��<&&{�v��s��(�yF[x�������J�%&Eq���Ց�jXX:%ƅ!���f`�[#�-D-$�y]����&���)Lr;wf�kM[E��톗yMe��m�}���pV�V�ݎ���a�fu1M͗3a=�۹*��m?NҜ�k{���Q]�<79/POZ��rͶ�l�f����`�=�LkM#��[Y&�%8��*ܤ��s�X)������a��0Ma0 +6�2�B�JKb@i���i"!8����u�;��H�����py2:��ǝwݥ��nz=(�P�������E����������֫�w�b�����QlC�)&$g��n�n1*A�Ж���{sF��,؊��٤���O����7�X�b� Ģ&C�04�ʽ�ۻ�0 +dtb��8�� �Bp�X��h��&Q~M7��K��%*�]�%Sd/J��g���8�&���� m��8����g�#���K����>����+���w��(� ���d}+��wݟ���Q`��{�t�+�����8���)տz���\����~odGn/��8�_�Iz�:Bӓ���0RC�$��$�$��N�$Pc(�;G�����78OdзU�C++�1�z��P����v!����/��Tx}[�� +���:���GQr�����H���O��g�=u�i�V�})3tF +Q +���%«��c/�u�gO���O�t�>^ǂ�����.�͏a�wLę���R�D��#F����I��@J���dfE>��s�X����t�(� �C0�"��V)8���aA�+h��kL˄��&LsMc\�b�b!L�xAn��D�z`��D|W��'�OH���"Ne�ځ��#�,np��֒�ӗQo��p��������v�Cӹ�w�'Øb�#bppO��2���� +�Y�c�YF:�����j`������ �:�A�V +� �nzD܂S�8�X�B��qk6�mp��q�d�0O�ʦ^L��s����*���7��h�=�0�i>'a�k(�қ�kZE�;��rKW>àG�$�ʑ�����2lQ]�hnRaLM�b�*_�|�E���e��(*d��2�����%4�c�sv���z����)���H:_�����ӊR���4���4��ҁ/�=b��b� +u�`����]�jʔ���] +n\�]] +rW�WӸ��b:������R�fg�� +E:f&5-�H�ݯ�7��� +Ͳ��[4��zՑ����-r��O�o��&�P�u�b���~-�9�H+���P�z�1�[��5�7��q=��iV��t5[��I-{���{�[^ۂ�C�'M��.sS��U�_��o�{O��I������O���{#ǰt�N̤�p<��rj��4�yA���ENi�eYwANw��9/9�z̽�GNw��97��Vq",A*M�~����N��"���}!������,��Ŀ�a��&��:�;�8�Zywy9�>��Z������/���o��1] �0��Fj�nh�ԗ�ԣ���DŽ)�q��9�T�� +N_�G=:<���.�TK�W��<7?ZSܓZ���-����V�YWy����0�e�w�Q�n���e��*{Y��۩la� �Q� +��/n'�v�2>���&.����{g�}�KŤ#!��@ƕ���[��#܍��E��R��aq7����dDEm�ߺ�z�,�ɭ�����S�h�J~վ�Z��i���#���8����.j� m8N7�v� +s�k/$%u�� �y�Nn���X�Uj�)mGv��K�5KRz?s7xAU7L*�jOo5{ �An�jV�l)xOD���R�)�L.Ñ�}l ��"�T�6]u���9��UT��?GŻ����O,�F�ZG�:fo{c��m�'�;�~&r�':�$�`C&�/��2M[PM�� +͋qs�� +�$����V����Y�}?��"����Y�I {a���^W��;��T��ޭ-h���������#��-���^ئ�o����=o��G�}��w���=z�?����mH��e�.��������a���4�~�/���,���'Q�2*7�1mT��5�ON����/����ZLa���4���o��7�aj�5S��[ +U��D����H���?QA�S�7=E�E��V�Q ��Xw�H�= +���m�Ix��G3����������w��j�&�(*�!ԩ�[��n��%���Yv��;���c��5ox���� +/�I����f5�^]�Ŧ|{E��T��8�&Lߝ��>MT���2�;�� +�)�*}���)�k�4k�j�z���l��y����<r���̾-�8��9[T����o��I��Rd��i�O�`��J�T���L�±���\Ө)��p���ɩ�Ŝ��Х�k�F��w +�Β���01�٭�����ס6W���Ǵj diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/lifecycle.svg b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/lifecycle.svg new file mode 100644 index 0000000000..0a9818d293 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/lifecycle.svg @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="-14.3464565 112.653543 624.6929 372.69291" width="624.6929pt" height="372.69291pt" xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata> Produced by OmniGraffle 6.6.2 <dc:date>2018-07-29 15:25:34 +0000</dc:date></metadata><defs><font-face font-family="Courier New" font-size="12" panose-1="2 7 6 9 2 2 5 2 4 4" units-per-em="1000" underline-position="-232.91016" underline-thickness="100.097656" slope="0" x-height="443.35938" cap-height="591.79688" ascent="832.51953" descent="-300.29297" font-weight="bold"><font-face-src><font-face-name name="CourierNewPS-BoldMT"/></font-face-src></font-face><font-face font-family="Courier New" font-size="12" panose-1="2 7 3 9 2 2 5 2 4 4" units-per-em="1000" underline-position="-232.91016" underline-thickness="41.015625" slope="0" x-height="422.85156" cap-height="571.28906" ascent="832.51953" descent="-300.29297" font-weight="500"><font-face-src><font-face-name name="CourierNewPSMT"/></font-face-src></font-face><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="StickArrow_Marker" viewBox="-1 -4 8 8" markerWidth="8" markerHeight="8" color="black"><g><path d="M 5.8666667 0 L 0 0 M 0 -2.2 L 5.8666667 0 L 0 2.2" fill="none" stroke="currentColor" stroke-width="1"/></g></marker><font-face font-family="Verdana" font-size="12" panose-1="2 11 6 4 3 5 4 4 2 4" units-per-em="1000" underline-position="-87.890625" underline-thickness="58.59375" slope="0" x-height="545.41016" cap-height="727.0508" ascent="1005.3711" descent="-209.96094" font-weight="500"><font-face-src><font-face-name name="Verdana"/></font-face-src></font-face></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Canvas 1</title><g><title>Layer 1</title><text transform="translate(19.173228 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="1.5138254" y="10" textLength="72.01172">CONNECTING</tspan></text><text transform="translate(160.90551 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="23.117341" y="10" textLength="28.804688">OPEN</tspan></text><text transform="translate(359.3307 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="12.315583" y="10" textLength="50.408203">CLOSING</tspan></text><text transform="translate(501.063 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="15.916169" y="10" textLength="43.20703">CLOSED</tspan></text><line x1="198.4252" y1="170.07874" x2="198.4252" y2="453.5433" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke-dasharray="1,3"/><line x1="396.8504" y1="170.07874" x2="396.8504" y2="453.5433" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke-dasharray="1,3"/><line x1="538.58267" y1="170.07874" x2="538.58267" y2="453.5433" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke-dasharray="1,3"/><line x1="56.692913" y1="170.07874" x2="56.692913" y2="453.5433" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke-dasharray="1,3"/><path d="M 240.94488 240.94488 L 411.02362 240.94488 C 418.85128 240.94488 425.19685 247.29045 425.19685 255.11811 L 425.19685 255.11811 C 425.19685 262.94577 418.85128 269.29134 411.02362 269.29134 L 240.94488 269.29134 C 233.11722 269.29134 226.77165 262.94577 226.77165 255.11811 L 226.77165 255.11811 C 226.77165 247.29045 233.11722 240.94488 240.94488 240.94488 Z" fill="#dadada"/><path d="M 240.94488 240.94488 L 411.02362 240.94488 C 418.85128 240.94488 425.19685 247.29045 425.19685 255.11811 L 425.19685 255.11811 C 425.19685 262.94577 418.85128 269.29134 411.02362 269.29134 L 240.94488 269.29134 C 233.11722 269.29134 226.77165 262.94577 226.77165 255.11811 L 226.77165 255.11811 C 226.77165 247.29045 233.11722 240.94488 240.94488 240.94488 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(226.77165 248.11811)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="52.40498" y="10" textLength="93.615234">transfer_data</tspan></text><path d="M 240.94488 354.3307 L 552.7559 354.3307 C 560.58356 354.3307 566.92913 360.67628 566.92913 368.50393 L 566.92913 368.50393 C 566.92913 376.3316 560.58356 382.67716 552.7559 382.67716 L 240.94488 382.67716 C 233.11722 382.67716 226.77165 376.3316 226.77165 368.50393 L 226.77165 368.50393 C 226.77165 360.67628 233.11722 354.3307 240.94488 354.3307 Z" fill="#dadada"/><path d="M 240.94488 354.3307 L 552.7559 354.3307 C 560.58356 354.3307 566.92913 360.67628 566.92913 368.50393 L 566.92913 368.50393 C 566.92913 376.3316 560.58356 382.67716 552.7559 382.67716 L 240.94488 382.67716 C 233.11722 382.67716 226.77165 376.3316 226.77165 368.50393 L 226.77165 368.50393 C 226.77165 360.67628 233.11722 354.3307 240.94488 354.3307 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(231.77165 361.50393)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="107.469364" y="10" textLength="115.21875">close_connection</tspan></text><path d="M 99.2126 184.25197 L 155.90551 184.25197 C 163.73317 184.25197 170.07874 190.59754 170.07874 198.4252 L 170.07874 198.4252 C 170.07874 206.25285 163.73317 212.59842 155.90551 212.59842 L 99.2126 212.59842 C 91.38494 212.59842 85.03937 206.25285 85.03937 198.4252 L 85.03937 198.4252 C 85.03937 190.59754 91.38494 184.25197 99.2126 184.25197 Z" fill="#6f6"/><path d="M 99.2126 184.25197 L 155.90551 184.25197 C 163.73317 184.25197 170.07874 190.59754 170.07874 198.4252 L 170.07874 198.4252 C 170.07874 206.25285 163.73317 212.59842 155.90551 212.59842 L 99.2126 212.59842 C 91.38494 212.59842 85.03937 206.25285 85.03937 198.4252 L 85.03937 198.4252 C 85.03937 190.59754 91.38494 184.25197 99.2126 184.25197 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(90.03937 191.4252)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="12.315583" y="10" textLength="50.408203">connect</tspan></text><path d="M 240.94488 184.25197 L 496.063 184.25197 C 503.89065 184.25197 510.23622 190.59754 510.23622 198.4252 L 510.23622 198.4252 C 510.23622 206.25285 503.89065 212.59842 496.063 212.59842 L 240.94488 212.59842 C 233.11722 212.59842 226.77165 206.25285 226.77165 198.4252 L 226.77165 198.4252 C 226.77165 190.59754 233.11722 184.25197 240.94488 184.25197 Z" fill="#6f6"/><path d="M 240.94488 184.25197 L 496.063 184.25197 C 503.89065 184.25197 510.23622 190.59754 510.23622 198.4252 L 510.23622 198.4252 C 510.23622 206.25285 503.89065 212.59842 496.063 212.59842 L 240.94488 212.59842 C 233.11722 212.59842 226.77165 206.25285 226.77165 198.4252 L 226.77165 198.4252 C 226.77165 190.59754 233.11722 184.25197 240.94488 184.25197 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(231.77165 191.4252)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="17.912947" y="10" textLength="100.816406">recv / send / </tspan><tspan font-family="Courier New" font-size="12" font-weight="500" x="118.72935" y="10" textLength="93.615234">ping / pong /</tspan><tspan font-family="Courier New" font-size="12" font-weight="bold" x="212.34459" y="10" textLength="50.408203"> close </tspan></text><path d="M 170.07874 198.4252 L 183.97874 198.4252 L 198.4252 198.4252 L 198.4252 283.46457 L 198.4252 368.50393 L 212.87165 368.50393 L 215.37165 368.50393" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(75.86614 410.19685)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="27.760296" y="12" textLength="52.083984">opening </tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="19.164593" y="27" textLength="58.02539">handshak</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="77.072796" y="27" textLength="7.1484375">e</tspan></text><text transform="translate(416.02362 410.19685)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="19.182171" y="12" textLength="65.021484">connection</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="16.861858" y="27" textLength="69.66211">termination</tspan></text><text transform="translate(217.59842 410.19685)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="41.03058" y="12" textLength="40.6875">data tr</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="81.507143" y="12" textLength="37.541016">ansfer</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="18.211245" y="27" textLength="116.625">& closing handshak</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="134.71906" y="27" textLength="7.1484375">e</tspan></text><path d="M 425.19685 255.11811 L 439.09685 255.11811 L 453.5433 255.11811 L 453.5433 342.9307" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 240.94488 297.6378 L 411.02362 297.6378 C 418.85128 297.6378 425.19685 303.98336 425.19685 311.81102 L 425.19685 311.81102 C 425.19685 319.63868 418.85128 325.98425 411.02362 325.98425 L 240.94488 325.98425 C 233.11722 325.98425 226.77165 319.63868 226.77165 311.81102 L 226.77165 311.81102 C 226.77165 303.98336 233.11722 297.6378 240.94488 297.6378 Z" fill="#dadada"/><path d="M 240.94488 297.6378 L 411.02362 297.6378 C 418.85128 297.6378 425.19685 303.98336 425.19685 311.81102 L 425.19685 311.81102 C 425.19685 319.63868 418.85128 325.98425 411.02362 325.98425 L 240.94488 325.98425 C 233.11722 325.98425 226.77165 319.63868 226.77165 311.81102 L 226.77165 311.81102 C 226.77165 303.98336 233.11722 297.6378 240.94488 297.6378 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(226.77165 304.81102)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="48.804395" y="10" textLength="100.816406">keepalive_ping</tspan></text><line x1="198.4252" y1="255.11811" x2="214.62165" y2="255.11811" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="198.4252" y1="311.81102" x2="215.37165" y2="311.81102" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/></g></g></svg> diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/logging.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/logging.rst new file mode 100644 index 0000000000..e7abd96ce5 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/logging.rst @@ -0,0 +1,245 @@ +Logging +======= + +.. currentmodule:: websockets + +Logs contents +------------- + +When you run a WebSocket client, your code calls coroutines provided by +websockets. + +If an error occurs, websockets tells you by raising an exception. For example, +it raises a :exc:`~exceptions.ConnectionClosed` exception if the other side +closes the connection. + +When you run a WebSocket server, websockets accepts connections, performs the +opening handshake, runs the connection handler coroutine that you provided, +and performs the closing handshake. + +Given this `inversion of control`_, if an error happens in the opening +handshake or if the connection handler crashes, there is no way to raise an +exception that you can handle. + +.. _inversion of control: https://en.wikipedia.org/wiki/Inversion_of_control + +Logs tell you about these errors. + +Besides errors, you may want to record the activity of the server. + +In a request/response protocol such as HTTP, there's an obvious way to record +activity: log one event per request/response. Unfortunately, this solution +doesn't work well for a bidirectional protocol such as WebSocket. + +Instead, when running as a server, websockets logs one event when a +`connection is established`_ and another event when a `connection is +closed`_. + +.. _connection is established: https://www.rfc-editor.org/rfc/rfc6455.html#section-4 +.. _connection is closed: https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.4 + +By default, websockets doesn't log an event for every message. That would be +excessive for many applications exchanging small messages at a fast rate. If +you need this level of detail, you could add logging in your own code. + +Finally, you can enable debug logs to get details about everything websockets +is doing. This can be useful when developing clients as well as servers. + +See :ref:`log levels <log-levels>` below for a list of events logged by +websockets logs at each log level. + +Configure logging +----------------- + +websockets relies on the :mod:`logging` module from the standard library in +order to maximize compatibility and integrate nicely with other libraries:: + + import logging + +websockets logs to the ``"websockets.client"`` and ``"websockets.server"`` +loggers. + +websockets doesn't provide a default logging configuration because +requirements vary a lot depending on the environment. + +Here's a basic configuration for a server in production:: + + logging.basicConfig( + format="%(asctime)s %(message)s", + level=logging.INFO, + ) + +Here's how to enable debug logs for development:: + + logging.basicConfig( + format="%(message)s", + level=logging.DEBUG, + ) + +Furthermore, websockets adds a ``websocket`` attribute to log records, so you +can include additional information about the current connection in logs. + +You could attempt to add information with a formatter:: + + # this doesn't work! + logging.basicConfig( + format="{asctime} {websocket.id} {websocket.remote_address[0]} {message}", + level=logging.INFO, + style="{", + ) + +However, this technique runs into two problems: + +* The formatter applies to all records. It will crash if it receives a record + without a ``websocket`` attribute. For example, this happens when logging + that the server starts because there is no current connection. + +* Even with :meth:`str.format` style, you're restricted to attribute and index + lookups, which isn't enough to implement some fairly simple requirements. + +There's a better way. :func:`~client.connect` and :func:`~server.serve` accept +a ``logger`` argument to override the default :class:`~logging.Logger`. You +can set ``logger`` to a :class:`~logging.LoggerAdapter` that enriches logs. + +For example, if the server is behind a reverse +proxy, :attr:`~legacy.protocol.WebSocketCommonProtocol.remote_address` gives +the IP address of the proxy, which isn't useful. IP addresses of clients are +provided in an HTTP header set by the proxy. + +Here's how to include them in logs, assuming they're in the +``X-Forwarded-For`` header:: + + logging.basicConfig( + format="%(asctime)s %(message)s", + level=logging.INFO, + ) + + class LoggerAdapter(logging.LoggerAdapter): + """Add connection ID and client IP address to websockets logs.""" + def process(self, msg, kwargs): + try: + websocket = kwargs["extra"]["websocket"] + except KeyError: + return msg, kwargs + xff = websocket.request_headers.get("X-Forwarded-For") + return f"{websocket.id} {xff} {msg}", kwargs + + async with websockets.serve( + ..., + # Python < 3.10 requires passing None as the second argument. + logger=LoggerAdapter(logging.getLogger("websockets.server"), None), + ): + ... + +Logging to JSON +--------------- + +Even though :mod:`logging` predates structured logging, it's still possible to +output logs as JSON with a bit of effort. + +First, we need a :class:`~logging.Formatter` that renders JSON: + +.. literalinclude:: ../../example/logging/json_log_formatter.py + +Then, we configure logging to apply this formatter:: + + handler = logging.StreamHandler() + handler.setFormatter(formatter) + + logger = logging.getLogger() + logger.addHandler(handler) + logger.setLevel(logging.INFO) + +Finally, we populate the ``event_data`` custom attribute in log records with +a :class:`~logging.LoggerAdapter`:: + + class LoggerAdapter(logging.LoggerAdapter): + """Add connection ID and client IP address to websockets logs.""" + def process(self, msg, kwargs): + try: + websocket = kwargs["extra"]["websocket"] + except KeyError: + return msg, kwargs + kwargs["extra"]["event_data"] = { + "connection_id": str(websocket.id), + "remote_addr": websocket.request_headers.get("X-Forwarded-For"), + } + return msg, kwargs + + async with websockets.serve( + ..., + # Python < 3.10 requires passing None as the second argument. + logger=LoggerAdapter(logging.getLogger("websockets.server"), None), + ): + ... + +Disable logging +--------------- + +If your application doesn't configure :mod:`logging`, Python outputs messages +of severity ``WARNING`` and higher to :data:`~sys.stderr`. As a consequence, +you will see a message and a stack trace if a connection handler coroutine +crashes or if you hit a bug in websockets. + +If you want to disable this behavior for websockets, you can add +a :class:`~logging.NullHandler`:: + + logging.getLogger("websockets").addHandler(logging.NullHandler()) + +Additionally, if your application configures :mod:`logging`, you must disable +propagation to the root logger, or else its handlers could output logs:: + + logging.getLogger("websockets").propagate = False + +Alternatively, you could set the log level to ``CRITICAL`` for the +``"websockets"`` logger, as the highest level currently used is ``ERROR``:: + + logging.getLogger("websockets").setLevel(logging.CRITICAL) + +Or you could configure a filter to drop all messages:: + + logging.getLogger("websockets").addFilter(lambda record: None) + +.. _log-levels: + +Log levels +---------- + +Here's what websockets logs at each level. + +``ERROR`` +......... + +* Exceptions raised by connection handler coroutines in servers +* Exceptions resulting from bugs in websockets + +``WARNING`` +........... + +* Failures in :func:`~websockets.broadcast` + +``INFO`` +........ + +* Server starting and stopping +* Server establishing and closing connections +* Client reconnecting automatically + +``DEBUG`` +......... + +* Changes to the state of connections +* Handshake requests and responses +* All frames sent and received +* Steps to close a connection +* Keepalive pings and pongs +* Errors handled transparently + +Debug messages have cute prefixes that make logs easier to scan: + +* ``>`` - send something +* ``<`` - receive something +* ``=`` - set connection state +* ``x`` - shut down connection +* ``%`` - manage pings and pongs +* ``!`` - handle errors and timeouts diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/memory.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/memory.rst new file mode 100644 index 0000000000..e44247a77c --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/memory.rst @@ -0,0 +1,48 @@ +Memory usage +============ + +.. currentmodule:: websockets + +In most cases, memory usage of a WebSocket server is proportional to the +number of open connections. When a server handles thousands of connections, +memory usage can become a bottleneck. + +Memory usage of a single connection is the sum of: + +1. the baseline amount of memory websockets requires for each connection, +2. the amount of data held in buffers before the application processes it, +3. any additional memory allocated by the application itself. + +Baseline +-------- + +Compression settings are the main factor affecting the baseline amount of +memory used by each connection. + +With websockets' defaults, on the server side, a single connections uses +70 KiB of memory. + +Refer to the :doc:`topic guide on compression <../topics/compression>` to +learn more about tuning compression settings. + +Buffers +------- + +Under normal circumstances, buffers are almost always empty. + +Under high load, if a server receives more messages than it can process, +bufferbloat can result in excessive memory usage. + +By default websockets has generous limits. It is strongly recommended to adapt +them to your application. When you call :func:`~server.serve`: + +- Set ``max_size`` (default: 1 MiB, UTF-8 encoded) to the maximum size of + messages your application generates. +- Set ``max_queue`` (default: 32) to the maximum number of messages your + application expects to receive faster than it can process them. The queue + provides burst tolerance without slowing down the TCP connection. + +Furthermore, you can lower ``read_limit`` and ``write_limit`` (default: +64 KiB) to reduce the size of buffers for incoming and outgoing data. + +The design document provides :ref:`more details about buffers <buffers>`. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/performance.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/performance.rst new file mode 100644 index 0000000000..45e23b2390 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/performance.rst @@ -0,0 +1,20 @@ +Performance +=========== + +Here are tips to optimize performance. + +uvloop +------ + +You can make a websockets application faster by running it with uvloop_. + +(This advice isn't specific to websockets. It applies to any :mod:`asyncio` +application.) + +.. _uvloop: https://github.com/MagicStack/uvloop + +broadcast +--------- + +:func:`~websockets.broadcast` is the most efficient way to send a message to +many clients. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/protocol.graffle b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/protocol.graffle new file mode 100644 index 0000000000..04a9e7acb5 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/protocol.graffle @@ -0,0 +1,37 @@ +��]ks�6���� +���[Ӹ��q�t�n�xc'�v<ӡ%X�"U���f���J�x-J���ۉl�A��\q��_FQ�N�0�|�=����~2�Ꮟ���#���'�����ѯ/z�(�����g�_������Ǒ��}~��w����Q���}��q��Y����=??�C����!�v�d����5t� +�A>x���^|;���G�=��/�����~\��U<�_��o�b�z�ӧ���寗M��a?ȡ�Ӟ獂4 +�/�=���)��KFq8L���{��+ +NO#-���HJ�X(�P>��D�d����&y�7��$���Ӊ��,�2��p��Y؟�͞}Ff(��k�a|����o���b���=����2`�b? +�f��$ +w�5y�|A9��<�S���"���1O/B�ɧ�������ip��b`oޖ�3:�����L�0ӿ%ɨÊ���A�6 +��b-\Nf1��W�hq����9o�J��(ދ�a�n���>}�bN�� zJ����'\*�d��p��0��y�7H�w��A����@E��~�5�$i����H�a��7��5Vm�<��Qpq��ƽ�����~���w�S飋q�qLS�-��?�8�s�|jIcng<3�c��ꊭ2�|<��X[e�I���O��un(,����>���猯J��|W�r�t�w_��.�C���R$�U�)�Qʐ���|I)�Xq�}�J� �ǂbd\}� +��5#��JH�+�S/o�N+�4f5;�yy�v|i��\ڑ���K�Ҵ2���[�4�Z�Ϣi�gJo��W<���A��B|DY!C�DGA��ن�1�g�! ϯ���Κ@\g���* *Ʃ���1��+*��1�=)�/��1�2#�$�UT(.a�()a��Q&��>RJ�oߚ��¾�g�X6����$ +u�F��rԙ���\,,\��;N�(�&U��������g�5>�+�%o�劀/�2T}c3� +�� +�h�q�A�i~���8���!&���~�K������=�z| ++7?��O���(�4>>�i�sԫ��|�~%)4��q����a�ul~9�&>���/������?��8���1&Ň������ U c �M[�����7L� ��Ad����Y��ӠZz��x����q��N�`��o~������"��+7t�J�K"|��`R��t��:t���"�v�{p�@��4?K�IDς�xlF`S�K�/]66����'9<��WT16b�OC>�o) �p����y�Խ|�\~��f�Ad�������Ǖd�*�K�|�f�ՍB_�#5�n� �M�P�kZm�N��ᵲ��������a +.���f�̸=���*v,�����٤?�a�!BV�c_����?gYk��y��"�2���P�J���&�4f�/<N�""���Ķ�9����V�Oa*�|)�pG��BQ�)�P��%yO� �6}����Xs�ږkY��$�� +�><�DǏn�U��� tS&�5fQS{��a�����H�F�X>�v=��f����ĩ�v6�-�p+��5L8·�*�j]��%l-�� )@G�m�ȅ�eqΰ5|ykǍ���8Y�;B�M���"&����S����֎;F\��M +��� +#.i��������Xu:)�ڛggnqS淍��``mb)}čJ�6�m��$W0H�"1_��P�,� +��W��!{� +�}���m��l�����j{ɖ�@1,i��1�@n;\|�!_�Ta!�tr�H;ϒh�=�TŤNhl��c��WV�����O�|m�yqUY$�7�� +��(�$'��sՄ�鏘}V�_��CKKl�K�w�ӎ1V�_h5�[��������D�f�wwCH��bE'&Z�fѢ(���e[�և\�4� +wr��X�'VA�1���@�v�mp�F3 +�I'�K:�b��A�p?%�B.��xx�b�h�XI��¤s)�:���rb��'�X�W,��{>%DA%gJ:�L�2��A���<`�L���ʀ��B�|ݐ?S�C�(�e�0 +r�OT�d�A-K�z����IM�@��w&u����6�4��+�u�,�c�����1]�����XpJ|�̨X��,��n3�$�c�{�+�Z�Y#������ԝ�-����4�hl��uO�ϖ.�+��yɱ�}���t'י�u���N�ui}���x�\殺 +��|A}_ N)�;�V.Q�"�3��8ج +��[����1Y���pmKҟW�߅7��\_��n]���E�\t�E�pt-����][K����!7�{�{F�T�3�8�fG�:��<�b�>�Ew`|��G�+0�[���[�GZ��:�sK�����;�|P +0����_V=�/���=�L�Fj��Jj�M�E�=�VqYر� +��x;͗������u�4�e�j����F�Z3�m�f���:��1t�S{���HgY0���MB�6,||R�h9�L��u,A�\��f���Q`N}�*��AA�|y �Z�H*�m�B�7��#�杫���*�I�R��0��u�8�i]�u�]���ǹ��sm���:L��Xv=a��B��u�,���&���7��]�C���YE��ݺ�D�#��SX��_�xh`[U�z��OX��Y\_$����֊{��\�H��O���RM 偙!1�����C�mtmR�pv5�� +:o�c=�����L��:�r�iy$�P����0��<��HR%WBZ�6�=�'8��J�"���d���,�R��И�������3.��/�X��0'sm���m[����N�� ,u�»�ݩ�ypC'�wǻ�;�aw��u���t�=�Y�9�s@瀮#Н\䷈s������}�܄m`G\P�&tn�/1�>K�ޠ�Њ�ͤV��m8�v�;�u8��p��47��Am=�i��HK�:�uH����&���g��N�a��ʠ,�ԫ"*PJ�h�� +�x������Aׇ��jٕ%��Ʒd��[��5��y�8_#�f �#�j�N+8vi���� s�.���0���W�1\�a`>G�pj�bk|�mk�8���xz��J�y�~��cE��\wJ��$#�4)@�Y�;�!��*��G�Ŵ��IX�n��曳��7߶ +��7L|SHh+�v�Hh�2,���ۥ���p +�Զ�n��"ugQ��,|eJ�տj�Rw�r����?�-aXT`�HܽJY���-����O�X��A8ɚ 3u胪L�l�c�<��A{/�w���&v�ᙧ0��:���� �4+lֻ��j�#U�`�j���G����s����}��[�X�µ���� ��˟��R)����q�e�e�) +�q)�,�����X��T�LF��M�n�,�ܒKzsn�N��݉�eq ��afw�3�aƲ�#��:��*��� ��WU�KeA��G�K0�i���1��qRA����D�l5��&����ܮ&���}MB�٘+ՏH�%y��R},F�*u�qJ�`0�(�}��"�8��D��N$߁�ֻ+���s"��H����̷��(��ڜ���\���b��Zw�$ҫ���}]��]r��q0o@�%ð���6�lV�i�JKm�-�J�VK7�C��8�i +%��>ɒ�'0�����G}rX|���FI|�&9�at�I5\���]T8����K�Ψp��=N�s*�}B�T��\�t<�l�� +���8�<ʠ31���������dՖ],����Ijm>��|�ܓ�̴Z�۳���q0Ύ��/�jt�ڈ�yC+��?�Yx�����X��9�C�P;BP�۴A����z|����(����L��B��;���5�h|�odJ�&Y���mz�I~�Z�uX����Y�a|���n�8�6���K|/GA���+2W�a�O~ ��a��ء|�ւ�P�V]VZA�|ø9�A�7� O�ވ�:�����6���9'��nX�_a��y2;�W� +����{��6�Ѭ@����R`g���\�K2OgF��қ�� �v�������Y�nf�q�����.�]�t�{{��$�ø�� +�s�'z����9��v���ӹsM��5}�:�nn�7�ϒ<OF��0,-�RTk��i�y��g�3\��bM��2I�?ͮ.w��]�F}���=�����9��#F��Ϣ�^�L������ዳ�����'���}�{��=�����h���GG��?~������b�����Kt������Z��ez�\���i�-�O?����N��1l�MŒ�ݦ�n��W�����%\H0�TuÜ�f��ӟA�iK܇����m������ut_���E������Ä�����rLy��W*�^��iMu�J��4g*#`�Q�7�o?�?YEQ���N]ø���^�ī:hwEc�q_���}�1������q�WM������0#���'i +3Q�dSO���/f&���).�q�;�X�xZ��2@��"�' �������A�)��X<��n-�r�^�I��rռ]\dyz`��(8y� +�� Jkj�L4��� +z-�|<n�"�G�ݝ�)�4��$��V��!���Y����ba��,��|/e0j��s�<}������� diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/protocol.svg b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/protocol.svg new file mode 100644 index 0000000000..51bfd982be --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/protocol.svg @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 624.34646 822.34646" width="624.34646pt" height="822.34646pt" xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata> Produced by OmniGraffle 6.6.2 <dc:date>2019-07-07 08:38:24 +0000</dc:date></metadata><defs><font-face font-family="Verdana" font-size="12" panose-1="2 11 6 4 3 5 4 4 2 4" units-per-em="1000" underline-position="-87.890625" underline-thickness="58.59375" slope="0" x-height="545.41016" cap-height="727.0508" ascent="1005.3711" descent="-209.96094" font-weight="500"><font-face-src><font-face-name name="Verdana"/></font-face-src></font-face><font-face font-family="Courier New" font-size="12" panose-1="2 7 3 9 2 2 5 2 4 4" units-per-em="1000" underline-position="-232.91016" underline-thickness="41.015625" slope="0" x-height="422.85156" cap-height="571.28906" ascent="832.51953" descent="-300.29297" font-weight="500"><font-face-src><font-face-name name="CourierNewPSMT"/></font-face-src></font-face><font-face font-family="Courier New" font-size="12" panose-1="2 7 6 9 2 2 5 2 4 4" units-per-em="1000" underline-position="-232.91016" underline-thickness="100.097656" slope="0" x-height="443.35938" cap-height="591.79688" ascent="832.51953" descent="-300.29297" font-weight="bold"><font-face-src><font-face-name name="CourierNewPS-BoldMT"/></font-face-src></font-face><font-face font-family="Courier New" font-size="10" panose-1="2 7 3 9 2 2 5 2 4 4" units-per-em="1000" underline-position="-232.91016" underline-thickness="41.015625" slope="0" x-height="422.85156" cap-height="571.28906" ascent="832.51953" descent="-300.29297" font-weight="500"><font-face-src><font-face-name name="CourierNewPSMT"/></font-face-src></font-face><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="StickArrow_Marker" viewBox="-1 -4 8 8" markerWidth="8" markerHeight="8" color="black"><g><path d="M 5.8666667 0 L 0 0 M 0 -2.2 L 5.8666667 0 L 0 2.2" fill="none" stroke="currentColor" stroke-width="1"/></g></marker><radialGradient cx="0" cy="0" r="1" id="Gradient" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="white"/><stop offset="1" stop-color="#a5a5a5"/></radialGradient><radialGradient id="Obj_Gradient" xl:href="#Gradient" gradientTransform="translate(311.81102 708.6614) scale(145.75703)"/><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="StickArrow_Marker_2" viewBox="-1 -6 14 12" markerWidth="14" markerHeight="12" color="black"><g><path d="M 12 0 L 0 0 M 0 -4.5 L 12 0 L 0 4.5" fill="none" stroke="currentColor" stroke-width="1"/></g></marker><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="StickArrow_Marker_3" viewBox="-1 -4 8 8" markerWidth="8" markerHeight="8" color="black"><g><path d="M 5.9253333 0 L 0 0 M 0 -2.222 L 5.9253333 0 L 0 2.222" fill="none" stroke="currentColor" stroke-width="1"/></g></marker></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Canvas 1</title><rect fill="white" width="1314" height="1698"/><g><title>Layer 1</title><rect x="28.346457" y="765.35433" width="566.92913" height="28.346457" fill="#6cf"/><rect x="28.346457" y="765.35433" width="566.92913" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(33.346457 772.02755)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="228.50753" y="12" textLength="99.91406">remote endpoint</tspan></text><rect x="28.346457" y="85.03937" width="566.92913" height="566.92913" fill="white"/><rect x="28.346457" y="85.03937" width="566.92913" height="566.92913" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(33.346457 90.03937)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="243.79171" y="12" textLength="51.333984">websock</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="295.00851" y="12" textLength="18.128906">ets</tspan><tspan font-family="Courier New" font-size="12" font-weight="500" x="195.65109" y="25" textLength="165.62695">WebSocketCommonProtocol</tspan></text><rect x="28.346457" y="28.346457" width="566.92913" height="28.346457" fill="#6f6"/><rect x="28.346457" y="28.346457" width="566.92913" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(33.346457 35.019685)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="230.0046" y="12" textLength="96.91992">application logic</tspan></text><path d="M 102.047243 586.77165 L 238.11023 586.77165 C 247.49858 586.77165 255.11811 596.93102 255.11811 609.4488 C 255.11811 621.9666 247.49858 632.12598 238.11023 632.12598 L 102.047243 632.12598 C 92.658897 632.12598 85.03937 621.9666 85.03937 609.4488 C 85.03937 596.93102 92.658897 586.77165 102.047243 586.77165" fill="#fc6"/><path d="M 102.047243 586.77165 L 238.11023 586.77165 C 247.49858 586.77165 255.11811 596.93102 255.11811 609.4488 C 255.11811 621.9666 247.49858 632.12598 238.11023 632.12598 L 102.047243 632.12598 C 92.658897 632.12598 85.03937 621.9666 85.03937 609.4488 C 85.03937 596.93102 92.658897 586.77165 102.047243 586.77165 M 238.11023 586.77165 C 228.72189 586.77165 221.10236 596.93102 221.10236 609.4488 C 221.10236 621.9666 228.72189 632.12598 238.11023 632.12598" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(125.33071 596.9488)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="14.896484" y="10" textLength="43.20703">reader</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x=".49414062" y="22" textLength="72.01172">StreamReader</tspan></text><path d="M 385.5118 586.77165 L 521.5748 586.77165 C 530.96315 586.77165 538.58267 596.93102 538.58267 609.4488 C 538.58267 621.9666 530.96315 632.12598 521.5748 632.12598 L 385.5118 632.12598 C 376.12346 632.12598 368.50393 621.9666 368.50393 609.4488 C 368.50393 596.93102 376.12346 586.77165 385.5118 586.77165" fill="#fc6"/><path d="M 385.5118 586.77165 L 521.5748 586.77165 C 530.96315 586.77165 538.58267 596.93102 538.58267 609.4488 C 538.58267 621.9666 530.96315 632.12598 521.5748 632.12598 L 385.5118 632.12598 C 376.12346 632.12598 368.50393 621.9666 368.50393 609.4488 C 368.50393 596.93102 376.12346 586.77165 385.5118 586.77165 M 521.5748 586.77165 C 512.18645 586.77165 504.56693 596.93102 504.56693 609.4488 C 504.56693 621.9666 512.18645 632.12598 521.5748 632.12598" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(408.79527 596.9488)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="14.896484" y="10" textLength="43.20703">writer</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x=".49414062" y="22" textLength="72.01172">StreamWriter</tspan></text><path d="M 481.88976 419.52756 L 481.88976 374.17323 C 481.88976 371.04378 469.19055 368.50393 453.5433 368.50393 C 437.89606 368.50393 425.19685 371.04378 425.19685 374.17323 L 425.19685 419.52756 C 425.19685 422.657 437.89606 425.19685 453.5433 425.19685 C 469.19055 425.19685 481.88976 422.657 481.88976 419.52756" fill="#fecc66"/><path d="M 481.88976 419.52756 L 481.88976 374.17323 C 481.88976 371.04378 469.19055 368.50393 453.5433 368.50393 C 437.89606 368.50393 425.19685 371.04378 425.19685 374.17323 L 425.19685 419.52756 C 425.19685 422.657 437.89606 425.19685 453.5433 425.19685 C 469.19055 425.19685 481.88976 422.657 481.88976 419.52756 M 481.88976 374.17323 C 481.88976 377.30267 469.19055 379.84252 453.5433 379.84252 C 437.89606 379.84252 425.19685 377.30267 425.19685 374.17323" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><text transform="translate(429.19685 387.18504)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="500" x="6.343527" y="10" textLength="36.00586">pings</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x="12.3445034" y="22" textLength="24.003906">dict</tspan></text><path d="M 85.039413 283.46457 L 255.11806 283.46457 C 270.7734 283.46457 283.46457 296.15573 283.46457 311.81107 L 283.46457 481.88972 C 283.46457 497.54506 270.7734 510.23622 255.11806 510.23622 L 85.039413 510.23622 C 69.384074 510.23622 56.692913 497.54506 56.692913 481.88972 L 56.692913 311.81107 C 56.692913 296.15573 69.384074 283.46457 85.039413 283.46457 Z" fill="#dadada"/><path d="M 85.039413 283.46457 L 255.11806 283.46457 C 270.7734 283.46457 283.46457 296.15573 283.46457 311.81107 L 283.46457 481.88972 C 283.46457 497.54506 270.7734 510.23622 255.11806 510.23622 L 85.039413 510.23622 C 69.384074 510.23622 56.692913 497.54506 56.692913 481.88972 L 56.692913 311.81107 C 56.692913 296.15573 69.384074 283.46457 85.039413 283.46457 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(61.692913 288.46457)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="43.57528" y="10" textLength="129.62109">transfer_data_task</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x="96.383873" y="22" textLength="24.003906">Task</tspan></text><path d="M 297.6378 765.35433 L 297.6378 609.4488 L 255.11811 609.4488 L 269.01811 609.4488 L 266.51811 609.4488" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 368.50393 609.4488 L 354.60393 609.4488 L 325.98425 609.4488 L 325.98425 753.95433" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 207.03401 712.3154 C 161.22047 708.6614 179.48976 677.90097 252.5726 683.1496 C 259.35307 672.91835 344.33858 674.579 343.783 683.1496 C 397.0715 672.1877 465.17102 694.04553 419.49354 705.00744 C 474.30425 710.32206 418.80189 738.9565 373.8189 734.17322 C 370.2189 742.14584 289.80283 744.9358 282.74457 734.17322 C 237.20882 745.66715 142.25953 727.9946 207.03401 712.3154 Z" fill="url(#Obj_Gradient)"/><path d="M 207.03401 712.3154 C 161.22047 708.6614 179.48976 677.90097 252.5726 683.1496 C 259.35307 672.91835 344.33858 674.579 343.783 683.1496 C 397.0715 672.1877 465.17102 694.04553 419.49354 705.00744 C 474.30425 710.32206 418.80189 738.9565 373.8189 734.17322 C 370.2189 742.14584 289.80283 744.9358 282.74457 734.17322 C 237.20882 745.66715 142.25953 727.9946 207.03401 712.3154 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(217.59842 701.1614)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="69.81416" y="12" textLength="48.796875">network</tspan></text><rect x="85.03937" y="453.5433" width="170.07874" height="28.346457" fill="#ff6"/><rect x="85.03937" y="453.5433" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(90.03937 460.71653)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="44.03351" y="10" textLength="72.01172">read_frame</tspan></text><rect x="85.03937" y="396.8504" width="170.07874" height="28.346457" fill="#ff6"/><rect x="85.03937" y="396.8504" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(90.03937 404.02362)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="26.03058" y="10" textLength="108.01758">read_data_frame</tspan></text><rect x="85.03937" y="340.15748" width="170.07874" height="28.346457" fill="#ff6"/><rect x="85.03937" y="340.15748" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(90.03937 347.3307)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="36.832338" y="10" textLength="86.41406">read_message</tspan></text><text transform="translate(178.07874 490.563)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="30.004883">bytes</tspan></text><text transform="translate(178.07874 433.87008)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="36.00586">frames</tspan></text><text transform="translate(178.07874 371.67716)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="24.003906">data</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="19" textLength="36.00586">frames</tspan></text><rect x="368.50393" y="510.23622" width="170.07874" height="28.346457" fill="#ff6"/><rect x="368.50393" y="510.23622" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(373.50393 517.40945)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="40.432924" y="10" textLength="79.21289">write_frame</tspan></text><path d="M 85.03937 609.4488 L 71.13937 609.4488 L 56.692913 609.4488 L 56.692913 595.2756 L 56.692913 566.92913 L 113.385826 566.92913 L 170.07874 566.92913 L 170.07874 495.78976 L 170.07874 494.03976" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 453.5433 539.33267 L 453.5433 552.48267 L 453.5433 566.92913 L 510.23622 566.92913 L 569.76378 566.92913 L 569.76378 595.2756 L 569.76378 609.4488 L 552.48267 609.4488 L 549.98267 609.4488" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="170.07874" y1="453.5433" x2="170.07874" y2="437.34685" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="170.07874" y1="396.8504" x2="170.07874" y2="380.65393" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 102.047243 204.09449 L 238.11023 204.09449 C 247.49858 204.09449 255.11811 214.25386 255.11811 226.77165 C 255.11811 239.28945 247.49858 249.44882 238.11023 249.44882 L 102.047243 249.44882 C 92.658897 249.44882 85.03937 239.28945 85.03937 226.77165 C 85.03937 214.25386 92.658897 204.09449 102.047243 204.09449" fill="#fc6"/><path d="M 102.047243 204.09449 L 238.11023 204.09449 C 247.49858 204.09449 255.11811 214.25386 255.11811 226.77165 C 255.11811 239.28945 247.49858 249.44882 238.11023 249.44882 L 102.047243 249.44882 C 92.658897 249.44882 85.03937 239.28945 85.03937 226.77165 C 85.03937 214.25386 92.658897 204.09449 102.047243 204.09449 M 238.11023 204.09449 C 228.72189 204.09449 221.10236 214.25386 221.10236 226.77165 C 221.10236 239.28945 228.72189 249.44882 238.11023 249.44882" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(132.33071 214.27165)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x=".1953125" y="10" textLength="57.609375">messages</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x="13.997559" y="22" textLength="30.004883">deque</tspan></text><path d="M 255.11811 354.3307 L 269.01811 354.3307 L 297.6378 354.3307 L 297.6378 328.8189 L 297.6378 226.77165 L 269.01811 226.77165 L 266.51811 226.77165" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><rect x="85.03937" y="141.73228" width="170.07874" height="28.346457" fill="#cf6"/><rect x="85.03937" y="141.73228" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(90.03937 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="65.637026" y="10" textLength="28.804688">recv</tspan></text><path d="M 85.03937 226.77165 L 71.13937 226.77165 L 42.519685 226.77165 L 42.519685 209.76378 L 42.519685 155.90551 L 71.13937 155.90551 L 73.63937 155.90551" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="170.07874" y1="141.73228" x2="170.07874" y2="68.092913" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="453.5433" y1="56.692913" x2="453.5433" y2="130.33228" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="467.71653" y1="56.692913" x2="467.71653" y2="187.8752" marker-end="url(#StickArrow_Marker_2)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><line x1="481.88976" y1="56.692913" x2="481.88976" y2="244.56811" marker-end="url(#StickArrow_Marker_2)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><line x1="496.063" y1="56.692913" x2="496.063" y2="300.32302" marker-end="url(#StickArrow_Marker_3)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><rect x="368.50393" y="141.73228" width="170.07874" height="28.346457" fill="#cf6"/><rect x="368.50393" y="141.73228" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(373.50393 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="65.637026" y="10" textLength="28.804688">send</tspan></text><rect x="368.50393" y="198.4252" width="170.07874" height="28.346457" fill="#cf6"/><rect x="368.50393" y="198.4252" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><text transform="translate(373.50393 205.59842)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="500" x="65.637026" y="10" textLength="28.804688">ping</tspan></text><rect x="368.50393" y="255.11811" width="170.07874" height="28.346457" fill="#cf6"/><rect x="368.50393" y="255.11811" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><text transform="translate(373.50393 262.29134)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="500" x="65.637026" y="10" textLength="28.804688">pong</tspan></text><rect x="368.50393" y="311.81102" width="170.07874" height="28.346457" fill="#cf6"/><rect x="368.50393" y="311.81102" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(373.50393 318.98425)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="62.03644" y="10" textLength="36.00586">close</tspan></text><path d="M 538.58267 155.90551 L 552.48267 155.90551 L 566.92913 155.90551 L 566.92913 481.88976 L 453.5433 481.88976 L 453.5433 496.33622 L 453.5433 498.08622" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="538.58267" y1="212.59842" x2="566.92913" y2="212.59842" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><line x1="538.58267" y1="269.29134" x2="566.92913" y2="269.29134" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><line x1="538.58267" y1="325.98425" x2="566.92913" y2="325.98425" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 255.86811 411.02362 L 262.61811 411.02362 L 340.15748 411.02362 L 340.15748 481.88976 L 453.5433 481.88976" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(291.94527 399.02362)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="42.006836">control</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="21" textLength="36.00586">frames</tspan></text><line x1="340.15748" y1="411.02362" x2="414.64685" y2="411.02362" marker-end="url(#StickArrow_Marker_2)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><path d="M 368.50393 212.59842 L 361.75393 212.59842 L 340.15748 212.59842 L 340.15748 340.15748 L 340.15748 382.67716" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><text transform="translate(461.5433 547.2559)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="30.004883">bytes</tspan></text><text transform="translate(461.5433 490.563)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="36.00586">frames</tspan></text><line x1="340.15748" y1="382.67716" x2="414.64685" y2="382.67716" marker-end="url(#StickArrow_Marker_2)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/></g></g></svg> diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/security.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/security.rst new file mode 100644 index 0000000000..d3dec21bd1 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/security.rst @@ -0,0 +1,41 @@ +Security +======== + +Encryption +---------- + +For production use, a server should require encrypted connections. + +See this example of :ref:`encrypting connections with TLS +<secure-server-example>`. + +Memory usage +------------ + +.. warning:: + + An attacker who can open an arbitrary number of connections will be able + to perform a denial of service by memory exhaustion. If you're concerned + by denial of service attacks, you must reject suspicious connections + before they reach websockets, typically in a reverse proxy. + +With the default settings, opening a connection uses 70 KiB of memory. + +Sending some highly compressed messages could use up to 128 MiB of memory with +an amplification factor of 1000 between network traffic and memory usage. + +Configuring a server to :doc:`optimize memory usage <memory>` will improve +security in addition to improving performance. + +Other limits +------------ + +websockets implements additional limits on the amount of data it accepts in +order to minimize exposure to security vulnerabilities. + +In the opening handshake, websockets limits the number of HTTP headers to 256 +and the size of an individual header to 4096 bytes. These limits are 10 to 20 +times larger than what's expected in standard use cases. They're hard-coded. + +If you need to change these limits, you can monkey-patch the constants in +``websockets.http11``. diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/timeouts.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/timeouts.rst new file mode 100644 index 0000000000..633fc1ab43 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/timeouts.rst @@ -0,0 +1,116 @@ +Timeouts +======== + +.. currentmodule:: websockets + +Long-lived connections +---------------------- + +Since the WebSocket protocol is intended for real-time communications over +long-lived connections, it is desirable to ensure that connections don't +break, and if they do, to report the problem quickly. + +Connections can drop as a consequence of temporary network connectivity issues, +which are very common, even within data centers. + +Furthermore, WebSocket builds on top of HTTP/1.1 where connections are +short-lived, even with ``Connection: keep-alive``. Typically, HTTP/1.1 +infrastructure closes idle connections after 30 to 120 seconds. + +As a consequence, proxies may terminate WebSocket connections prematurely when +no message was exchanged in 30 seconds. + +.. _keepalive: + +Keepalive in websockets +----------------------- + +To avoid these problems, websockets runs a keepalive and heartbeat mechanism +based on WebSocket Ping_ and Pong_ frames, which are designed for this purpose. + +.. _Ping: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.2 +.. _Pong: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.3 + +It loops through these steps: + +1. Wait 20 seconds. +2. Send a Ping frame. +3. Receive a corresponding Pong frame within 20 seconds. + +If the Pong frame isn't received, websockets considers the connection broken and +closes it. + +This mechanism serves two purposes: + +1. It creates a trickle of traffic so that the TCP connection isn't idle and + network infrastructure along the path keeps it open ("keepalive"). +2. It detects if the connection drops or becomes so slow that it's unusable in + practice ("heartbeat"). In that case, it terminates the connection and your + application gets a :exc:`~exceptions.ConnectionClosed` exception. + +Timings are configurable with the ``ping_interval`` and ``ping_timeout`` +arguments of :func:`~client.connect` and :func:`~server.serve`. Shorter values +will detect connection drops faster but they will increase network traffic and +they will be more sensitive to latency. + +Setting ``ping_interval`` to :obj:`None` disables the whole keepalive and +heartbeat mechanism. + +Setting ``ping_timeout`` to :obj:`None` disables only timeouts. This enables +keepalive, to keep idle connections open, and disables heartbeat, to support large +latency spikes. + +.. admonition:: Why doesn't websockets rely on TCP keepalive? + :class: hint + + TCP keepalive is disabled by default on most operating systems. When + enabled, the default interval is two hours or more, which is far too much. + +Keepalive in browsers +--------------------- + +Browsers don't enable a keepalive mechanism like websockets by default. As a +consequence, they can fail to notice that a WebSocket connection is broken for +an extended period of time, until the TCP connection times out. + +In this scenario, the ``WebSocket`` object in the browser doesn't fire a +``close`` event. If you have a reconnection mechanism, it doesn't kick in +because it believes that the connection is still working. + +If your browser-based app mysteriously and randomly fails to receive events, +this is a likely cause. You need a keepalive mechanism in the browser to avoid +this scenario. + +Unfortunately, the WebSocket API in browsers doesn't expose the native Ping and +Pong functionality in the WebSocket protocol. You have to roll your own in the +application layer. + +Latency issues +-------------- + +Latency between a client and a server may increase for two reasons: + +* Network connectivity is poor. When network packets are lost, TCP attempts to + retransmit them, which manifests as latency. Excessive packet loss makes + the connection unusable in practice. At some point, timing out is a + reasonable choice. + +* Traffic is high. For example, if a client sends messages on the connection + faster than a server can process them, this manifests as latency as well, + because data is waiting in flight, mostly in OS buffers. + + If the server is more than 20 seconds behind, it doesn't see the Pong before + the default timeout elapses. As a consequence, it closes the connection. + This is a reasonable choice to prevent overload. + + If traffic spikes cause unwanted timeouts and you're confident that the server + will catch up eventually, you can increase ``ping_timeout`` or you can set it + to :obj:`None` to disable heartbeat entirely. + + The same reasoning applies to situations where the server sends more traffic + than the client can accept. + +The latency measured during the last exchange of Ping and Pong frames is +available in the :attr:`~legacy.protocol.WebSocketCommonProtocol.latency` +attribute. Alternatively, you can measure the latency at any time with the +:attr:`~legacy.protocol.WebSocketCommonProtocol.ping` method. |