Skip to content

Commit 3b2356d

Browse files
kovanclaude
andcommitted
gh-79012: Restructure HOWTO to explain concepts before code
- Move write/drain and close/wait_closed explanations above the echo server example - Explain async with server, serve_forever, and asyncio.run - Break chat server into subsections: client tracking, broadcasting, then the complete example - Show broadcast function separately before the full listing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3f5b5ae commit 3b2356d

File tree

1 file changed

+46
-13
lines changed

1 file changed

+46
-13
lines changed

Doc/howto/asyncio-chat-server.rst

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,19 @@ asyncio calls your callback with two arguments: a
3535
:class:`~asyncio.StreamWriter` for sending data back. Each connection runs
3636
as its own coroutine, so multiple clients are handled concurrently.
3737

38+
The :meth:`~asyncio.StreamWriter.write` method buffers data without sending
39+
it immediately. Awaiting :meth:`~asyncio.StreamWriter.drain` flushes the
40+
buffer and applies back-pressure if the client is slow to read. Similarly,
41+
:meth:`~asyncio.StreamWriter.close` initiates shutdown, and awaiting
42+
:meth:`~asyncio.StreamWriter.wait_closed` waits until the connection is
43+
fully closed.
44+
45+
Using the server as an async context manager (``async with server``) ensures
46+
it is properly cleaned up when done. Calling
47+
:meth:`~asyncio.Server.serve_forever` keeps the server running until the
48+
program is interrupted. Finally, :func:`asyncio.run` starts the event loop
49+
and runs the top-level coroutine.
50+
3851
Here is a complete echo server::
3952

4053
import asyncio
@@ -65,13 +78,6 @@ Here is a complete echo server::
6578

6679
asyncio.run(main())
6780

68-
The :meth:`~asyncio.StreamWriter.write` method buffers data without sending
69-
it immediately. Awaiting :meth:`~asyncio.StreamWriter.drain` flushes the
70-
buffer and applies back-pressure if the client is slow to read. Similarly,
71-
:meth:`~asyncio.StreamWriter.close` initiates shutdown, and awaiting
72-
:meth:`~asyncio.StreamWriter.wait_closed` waits until the connection is
73-
fully closed.
74-
7581
To test, run the server in one terminal and connect from another using ``nc``
7682
(or ``telnet``):
7783

@@ -88,13 +94,40 @@ Building the chat server
8894
The chat server extends the echo server with two additions: tracking connected
8995
clients and broadcasting messages to everyone.
9096

91-
We store each client's name and :class:`~asyncio.StreamWriter` in a dictionary.
92-
When a message arrives, we broadcast it to all other connected clients.
93-
:class:`asyncio.TaskGroup` sends to all recipients concurrently, and
94-
:func:`contextlib.suppress` silently handles any :exc:`ConnectionError` from
95-
clients that have already disconnected.
97+
Client tracking
98+
---------------
99+
100+
We store each connected client's name and :class:`~asyncio.StreamWriter` in a
101+
module-level dictionary. When a client connects, ``handle_client`` prompts for
102+
a name and adds the writer to the dictionary. A ``finally`` block ensures the
103+
client is always removed on disconnect, even if the connection drops
104+
unexpectedly.
105+
106+
Broadcasting messages
107+
---------------------
108+
109+
To send a message to all clients, we define a ``broadcast`` function.
110+
:class:`asyncio.TaskGroup` sends to all recipients concurrently rather than
111+
one at a time. :func:`contextlib.suppress` silently handles any
112+
:exc:`ConnectionError` from clients that have already disconnected::
113+
114+
async def broadcast(message, *, sender=None):
115+
"""Send a message to all connected clients except the sender."""
116+
async def send(writer):
117+
with contextlib.suppress(ConnectionError):
118+
writer.write(message.encode())
119+
await writer.drain()
120+
121+
async with asyncio.TaskGroup() as tg:
122+
# Iterate over a copy: clients may leave during the broadcast.
123+
for name, writer in list(connected_clients.items()):
124+
if name != sender:
125+
tg.create_task(send(writer))
126+
127+
The complete chat server
128+
------------------------
96129

97-
::
130+
Putting it all together::
98131

99132
import asyncio
100133
import contextlib

0 commit comments

Comments
 (0)