summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/tools/third_party/websockets/docs/topics/design.rst
blob: f164d29905ee7b982232464143141ea2e3145ae3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
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.