The Async Server
One Shopkeeper, Many Counters
Imagine a small shop with one shopkeeper and lots of counters. A customer walks in and steps up to a counter. While that customer is deciding what to buy, the shopkeeper doesn't stand and stare -- they go and help someone at another counter, then come back when the first customer is ready.
AsyncServer is that shop:
| In the shop | In PyWeb |
|---|---|
| The front door | The listening socket on a port |
| A customer at a counter | One connection from a browser |
| The shopkeeper | PyNet's event loop |
| The stock room | Your App (routes, middleware, handlers) |
| "Come back later, we're full!" | 503 Service Unavailable |
| Opening and closing up | on_startup and on_shutdown hooks |
Every connection gets its own task, so a slow customer at counter A
never holds up a quick one at counter B -- as long as the handler
awaits while it waits (see Handlers and Threads).
Try It
This starts a real server on a free port, talks to it with raw bytes -- exactly what a browser would send -- and shuts it down again:
from pynet.loop import run
from pynet.streams import open_connection
from pyweb.aserver import AsyncServer
from pyweb.app import App
app = App()
@app.on_startup
def opening():
print("The shop is open!")
@app.on_shutdown
def closing():
print("The shop is closed.")
@app.get("/hello/<name>")
async def hello(request, name):
return {"greeting": f"Hello, {name}!"}
async def main():
async with AsyncServer(app, access_log=False) as server:
print("listening:", server.port > 0)
reader, writer = await open_connection("127.0.0.1", server.port)
writer.write(b"GET /hello/Sam HTTP/1.1\r\nHost: shop\r\nConnection: close\r\n\r\n")
await writer.drain()
reply = await reader.read() # read until the server closes
writer.close()
head, _, body = reply.partition(b"\r\n\r\n")
print(head.split(b"\r\n")[0].decode())
print(body.decode())
run(main())
Output:
async with starts the server on a free port (port 0 means "any free
port, please") and closes it at the end. You can also call
await server.start(host, port) and await server.close() yourself, or
await server.serve_forever() to keep serving until the task is
cancelled (for example with Ctrl+C).
What the Server Adds
Your handler only builds the parcel. The server sticks on the postage:
Date-- when the reply was sent.Server: PyWeb-- who sent it.Content-Length-- how many bytes the body has, so the browser knows where the reply ends. (Not on204 No Contentor304, which never have a body.)Connection: close-- only when the server is about to hang up.
For a HEAD request the server sends the same headers as GET --
including the Content-Length the GET body would have had -- but no
body.
The server also refuses to send a header containing a newline. A sneaky
newline could split one reply into two ("response splitting"), so
instead the visitor gets a plain 500.
Closing Up Gracefully
await server.close(grace=5.0) works like closing a shop at the end of
the day:
- Lock the door -- stop accepting new connections.
- Tell browsers to leave -- idle connections close straight away.
- Let people finish paying -- requests already being handled get up
to
graceseconds to finish. Their reply saysConnection: close. - Turn out the lights -- anything still running after
graceis cancelled, then the app'son_shutdownhooks run.
The Access Log
Every request writes one line to the pyweb.access logger, like:
That is method, path, status, body bytes and how long it took. The query
string is left out on purpose -- it might hold a secret token. Turn it
off with AsyncServer(app, access_log=False).
What We Test
- A
GETand aPOSTwith a body round-trip over a real socket, withDate,ServerandContent-Lengthadded. - A slow handler on one connection does not delay a fast request on another.
HEADsends theGETlength but no body;HEADon a stream sends only the head.- A response header with a newline becomes a
500, never two replies. - With
max_connections=2, a third connection gets503and is closed. close(grace=...)lets an in-flight request finish, cuts off one that runs too long, closes idle connections at once, ends open event streams, and runs the startup and shutdown hooks. Closing twice is safe, even when the first close's shutdown raised.- The access log line has the right shape and never includes the query.
Next Up
How can one connection carry many requests? See Keep-Alive.