Skip to content

Commit 667e418

Browse files
committed
Review how-to guides, notably the patterns guide.
Fix #1209.
1 parent e934680 commit 667e418

8 files changed

+113
-87
lines changed

docs/conf.py

+1
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484

8585
intersphinx_mapping = {
8686
"python": ("https://docs.python.org/3", None),
87+
"sesame": ("https://django-sesame.readthedocs.io/en/stable/", None),
8788
"werkzeug": ("https://werkzeug.palletsprojects.com/en/stable/", None),
8889
}
8990

docs/howto/autoreload.rst

+9-8
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
Reload on code changes
22
======================
33

4-
When developing a websockets server, you may run it locally to test changes.
5-
Unfortunately, whenever you want to try a new version of the code, you must
6-
stop the server and restart it, which slows down your development process.
4+
When developing a websockets server, you are likely to run it locally to test
5+
changes. Unfortunately, whenever you want to try a new version of the code, you
6+
must stop the server and restart it, which slows down your development process.
77

8-
Web frameworks such as Django or Flask provide a development server that
9-
reloads the application automatically when you make code changes. There is no
10-
such functionality in websockets because it's designed only for production.
8+
Web frameworks such as Django or Flask provide a development server that reloads
9+
the application automatically when you make code changes. There is no equivalent
10+
functionality in websockets because it's designed only for production.
1111

12-
However, you can achieve the same result easily.
12+
However, you can achieve the same result easily with a third-party library and a
13+
shell command.
1314

1415
Install watchdog_ with the ``watchmedo`` shell utility:
1516

@@ -27,4 +28,4 @@ Run your server with ``watchmedo auto-restart``:
2728
python app.py
2829
2930
This example assumes that the server is defined in a script called ``app.py``
30-
and exits cleanly when receiving the ``SIGTERM`` signal. Adapt it as necessary.
31+
and exits cleanly when receiving the ``SIGTERM`` signal. Adapt as necessary.

docs/howto/debugging.rst

+6-5
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ Enable debug logs
33

44
websockets logs events with the :mod:`logging` module from the standard library.
55

6-
It writes to the ``"websockets.server"`` and ``"websockets.client"`` loggers.
6+
It emits logs in the ``"websockets.server"`` and ``"websockets.client"``
7+
loggers.
78

8-
Enable logs at the ``DEBUG`` level to see exactly what websockets is doing.
9+
You can enable logs at the ``DEBUG`` level to see exactly what websockets does.
910

1011
If logging isn't configured in your application::
1112

@@ -24,10 +25,10 @@ If logging is already configured::
2425
logger.setLevel(logging.DEBUG)
2526
logger.addHandler(logging.StreamHandler())
2627

27-
Refer to the :doc:`logging guide <../topics/logging>` for more details on
28+
Refer to the :doc:`logging guide <../topics/logging>` for more information about
2829
logging in websockets.
2930

30-
In addition, you may enable asyncio's `debug mode`_ to see what asyncio is
31-
doing.
31+
You may also enable asyncio's `debug mode`_ to get warnings about classic
32+
pitfalls.
3233

3334
.. _debug mode: https://docs.python.org/3/library/asyncio-dev.html#asyncio-debug-mode

docs/howto/django.rst

+29-28
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ call its APIs in the websockets server.
101101
Now here's how to implement authentication.
102102

103103
.. literalinclude:: ../../example/django/authentication.py
104+
:caption: authentication.py
104105

105106
Let's unpack this code.
106107

@@ -113,23 +114,25 @@ your settings module.
113114

114115
The connection handler reads the first message received from the client, which
115116
is expected to contain a django-sesame token. Then it authenticates the user
116-
with ``get_user()``, the API for `authentication outside a view`_. If
117-
authentication fails, it closes the connection and exits.
117+
with :func:`~sesame.utils.get_user`, the API provided by django-sesame for
118+
`authentication outside a view`_.
118119

119120
.. _authentication outside a view: https://django-sesame.readthedocs.io/en/stable/howto.html#outside-a-view
120121

121-
When we call an API that makes a database query such as ``get_user()``, we
122-
wrap the call in :func:`~asyncio.to_thread`. Indeed, the Django ORM doesn't
123-
support asynchronous I/O. It would block the event loop if it didn't run in a
124-
separate thread.
122+
If authentication fails, it closes the connection and exits.
123+
124+
When we call an API that makes a database query such as
125+
:func:`~sesame.utils.get_user`, we wrap the call in :func:`~asyncio.to_thread`.
126+
Indeed, the Django ORM doesn't support asynchronous I/O. It would block the
127+
event loop if it didn't run in a separate thread.
125128

126129
Finally, we start a server with :func:`~websockets.asyncio.server.serve`.
127130

128131
We're ready to test!
129132

130-
Save this code to a file called ``authentication.py``, make sure the
131-
``DJANGO_SETTINGS_MODULE`` environment variable is set properly, and start the
132-
websockets server:
133+
Download :download:`authentication.py <../../example/django/authentication.py>`,
134+
make sure the ``DJANGO_SETTINGS_MODULE`` environment variable is set properly,
135+
and start the websockets server:
133136

134137
.. code-block:: console
135138
@@ -169,7 +172,7 @@ following code in the JavaScript console of the browser:
169172
websocket.onmessage = (event) => console.log(event.data);
170173
171174
If you don't want to import your entire Django project into the websockets
172-
server, you can build a separate Django project with ``django.contrib.auth``,
175+
server, you can create a simpler Django project with ``django.contrib.auth``,
173176
``django-sesame``, a suitable ``User`` model, and a subset of the settings of
174177
the main project.
175178

@@ -184,11 +187,11 @@ action was made. This may be used for showing notifications to other users.
184187

185188
Many use cases for WebSocket with Django follow a similar pattern.
186189

187-
Set up event bus
188-
................
190+
Set up event stream
191+
...................
189192

190-
We need a event bus to enable communications between Django and websockets.
191-
Both sides connect permanently to the bus. Then Django writes events and
193+
We need an event stream to enable communications between Django and websockets.
194+
Both sides connect permanently to the stream. Then Django writes events and
192195
websockets reads them. For the sake of simplicity, we'll rely on `Redis
193196
Pub/Sub`_.
194197

@@ -219,14 +222,15 @@ change ``get_redis_connection("default")`` in the code below to the same name.
219222
Publish events
220223
..............
221224

222-
Now let's write events to the bus.
225+
Now let's write events to the stream.
223226

224227
Add the following code to a module that is imported when your Django project
225-
starts. Typically, you would put it in a ``signals.py`` module, which you
226-
would import in the ``AppConfig.ready()`` method of one of your apps:
228+
starts. Typically, you would put it in a :download:`signals.py
229+
<../../example/django/signals.py>` module, which you would import in the
230+
``AppConfig.ready()`` method of one of your apps:
227231

228232
.. literalinclude:: ../../example/django/signals.py
229-
233+
:caption: signals.py
230234
This code runs every time the admin saves a ``LogEntry`` object to keep track
231235
of a change. It extracts interesting data, serializes it to JSON, and writes
232236
an event to Redis.
@@ -256,13 +260,13 @@ We need to add several features:
256260

257261
* Keep track of connected clients so we can broadcast messages.
258262
* Tell which content types the user has permission to view or to change.
259-
* Connect to the message bus and read events.
263+
* Connect to the message stream and read events.
260264
* Broadcast these events to users who have corresponding permissions.
261265

262266
Here's a complete implementation.
263267

264268
.. literalinclude:: ../../example/django/notifications.py
265-
269+
:caption: notifications.py
266270
Since the ``get_content_types()`` function makes a database query, it is
267271
wrapped inside :func:`asyncio.to_thread()`. It runs once when each WebSocket
268272
connection is open; then its result is cached for the lifetime of the
@@ -273,13 +277,10 @@ The connection handler merely registers the connection in a global variable,
273277
associated to the list of content types for which events should be sent to
274278
that connection, and waits until the client disconnects.
275279

276-
The ``process_events()`` function reads events from Redis and broadcasts them
277-
to all connections that should receive them. We don't care much if a sending a
278-
notification fails — this happens when a connection drops between the moment
279-
we iterate on connections and the moment the corresponding message is sent —
280-
so we start a task with for each message and forget about it. Also, this means
281-
we're immediately ready to process the next event, even if it takes time to
282-
send a message to a slow client.
280+
The ``process_events()`` function reads events from Redis and broadcasts them to
281+
all connections that should receive them. We don't care much if a sending a
282+
notification fails. This happens when a connection drops between the moment we
283+
iterate on connections and the moment the corresponding message is sent.
283284

284285
Since Redis can publish a message to multiple subscribers, multiple instances
285286
of this server can safely run in parallel.
@@ -290,4 +291,4 @@ Does it scale?
290291
In theory, given enough servers, this design can scale to a hundred million
291292
clients, since Redis can handle ten thousand servers and each server can
292293
handle ten thousand clients. In practice, you would need a more scalable
293-
message bus before reaching that scale, due to the volume of messages.
294+
message stream before reaching that scale, due to the volume of messages.

docs/howto/extensions.rst

+24-15
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,39 @@
11
Write an extension
22
==================
33

4-
.. currentmodule:: websockets.extensions
4+
.. currentmodule:: websockets
55

66
During the opening handshake, WebSocket clients and servers negotiate which
7-
extensions_ will be used with which parameters. Then each frame is processed
8-
by extensions before being sent or after being received.
7+
extensions_ will be used and with which parameters.
98

109
.. _extensions: https://datatracker.ietf.org/doc/html/rfc6455.html#section-9
1110

12-
As a consequence, writing an extension requires implementing several classes:
11+
Then, each frame is processed before being sent and after being received
12+
according to the extensions that were negotiated.
1313

14-
* Extension Factory: it negotiates parameters and instantiates the extension.
14+
Writing an extension requires implementing at least two classes, an extension
15+
factory and an extension. They inherit from base classes provided by websockets.
1516

16-
Clients and servers require separate extension factories with distinct APIs.
17+
Extension factory
18+
-----------------
1719

18-
Extension factories are the public API of an extension.
20+
An extension factory negotiates parameters and instantiates the extension.
1921

20-
* Extension: it decodes incoming frames and encodes outgoing frames.
22+
Clients and servers require separate extension factories with distinct APIs.
23+
Base classes are :class:`~extensions.ClientExtensionFactory` and
24+
:class:`~extensions.ServerExtensionFactory`.
2125

22-
If the extension is symmetrical, clients and servers can use the same
23-
class.
26+
Extension factories are the public API of an extension. Extensions are enabled
27+
with the ``extensions`` parameter of :func:`~asyncio.client.connect` or
28+
:func:`~asyncio.server.serve`.
2429

25-
Extensions are initialized by extension factories, so they don't need to be
26-
part of the public API of an extension.
30+
Extension
31+
---------
2732

28-
websockets provides base classes for extension factories and extensions.
29-
See :class:`ClientExtensionFactory`, :class:`ServerExtensionFactory`,
30-
and :class:`Extension` for details.
33+
An extension decodes incoming frames and encodes outgoing frames.
34+
35+
If the extension is symmetrical, clients and servers can use the same class. The
36+
base class is :class:`~extensions.Extension`.
37+
38+
Since extensions are initialized by extension factories, they don't need to be
39+
part of the public API of an extension.

docs/howto/index.rst

+3-6
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,19 @@ Configure websockets securely in production.
1414

1515
encryption
1616

17-
If you're stuck, perhaps you'll find the answer here.
17+
These guides will help you design and build your application.
1818

1919
.. toctree::
20+
:maxdepth: 2
2021

2122
patterns
22-
23-
This guide will help you integrate websockets into a broader system.
24-
25-
.. toctree::
26-
2723
django
2824

2925
Upgrading from the legacy :mod:`asyncio` implementation to the new one?
3026
Read this.
3127

3228
.. toctree::
29+
:maxdepth: 2
3330

3431
upgrade
3532

docs/howto/patterns.rst

+39-25
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,52 @@
1-
Patterns
2-
========
1+
Design a WebSocket application
2+
==============================
33

44
.. currentmodule:: websockets
55

6-
Here are typical patterns for processing messages in a WebSocket server or
7-
client. You will certainly implement some of them in your application.
6+
WebSocket server or client applications follow common patterns. This guide
7+
describes patterns that you're likely to implement in your application.
88

9-
This page gives examples of connection handlers for a server. However, they're
10-
also applicable to a client, simply by assuming that ``websocket`` is a
11-
connection created with :func:`~asyncio.client.connect`.
9+
All examples are connection handlers for a server. However, they would also
10+
apply to a client, assuming that ``websocket`` is a connection created with
11+
:func:`~asyncio.client.connect`.
1212

13-
WebSocket connections are long-lived. You will usually write a loop to process
14-
several messages during the lifetime of a connection.
13+
.. admonition:: WebSocket connections are long-lived.
14+
:class: tip
1515

16-
Consumer
17-
--------
16+
You need a loop to process several messages during the lifetime of a
17+
connection.
18+
19+
Consumer pattern
20+
----------------
1821

1922
To receive messages from the WebSocket connection::
2023

2124
async def consumer_handler(websocket):
2225
async for message in websocket:
23-
await consumer(message)
26+
await consume(message)
2427

25-
In this example, ``consumer()`` is a coroutine implementing your business
26-
logic for processing a message received on the WebSocket connection. Each
27-
message may be :class:`str` or :class:`bytes`.
28+
In this example, ``consume()`` is a coroutine implementing your business logic
29+
for processing a message received on the WebSocket connection.
2830

2931
Iteration terminates when the client disconnects.
3032

31-
Producer
32-
--------
33+
Producer pattern
34+
----------------
3335

3436
To send messages to the WebSocket connection::
3537

38+
from websockets.exceptions import ConnectionClosed
39+
3640
async def producer_handler(websocket):
37-
while True:
38-
message = await producer()
39-
await websocket.send(message)
41+
try:
42+
while True:
43+
message = await produce()
44+
await websocket.send(message)
45+
except ConnectionClosed:
46+
break
4047

41-
In this example, ``producer()`` is a coroutine implementing your business
42-
logic for generating the next message to send on the WebSocket connection.
43-
Each message must be :class:`str` or :class:`bytes`.
48+
In this example, ``produce()`` is a coroutine implementing your business logic
49+
for generating the next message to send on the WebSocket connection.
4450

4551
Iteration terminates when the client disconnects because
4652
:meth:`~asyncio.server.ServerConnection.send` raises a
@@ -51,8 +57,12 @@ Consumer and producer
5157
---------------------
5258

5359
You can receive and send messages on the same WebSocket connection by
54-
combining the consumer and producer patterns. This requires running two tasks
55-
in parallel::
60+
combining the consumer and producer patterns.
61+
62+
This requires running two tasks in parallel. The simplest option offered by
63+
:mod:`asyncio` is::
64+
65+
import asyncio
5666

5767
async def handler(websocket):
5868
await asyncio.gather(
@@ -99,6 +109,10 @@ connect and unregister them when they disconnect::
99109
This example maintains the set of connected clients in memory. This works as
100110
long as you run a single process. It doesn't scale to multiple processes.
101111

112+
If you just need the set of connected clients, as in this example, use the
113+
:attr:`~asyncio.server.Server.connections` property of the server. This pattern
114+
is needed only when recording additional information about each client.
115+
102116
Publish–subscribe
103117
-----------------
104118

docs/project/changelog.rst

+2
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ New features
6868
Improvements
6969
............
7070

71+
* Refreshed several how-to guides and topic guides.
72+
7173
* Added type overloads for the ``decode`` argument of
7274
:meth:`~asyncio.connection.Connection.recv`. This may simplify static typing.
7375

0 commit comments

Comments
 (0)