Serve HTTP Yourself · practice
Serve a Few Connections, Then Stop
handle returns the right make_listener bound a serve accepted clients one at a time, with a timeout, until it had served max_connections of them. http.server.HTTPServer is that listener and that accept step in one http_adapter.make_handler_class(handle, state) returns. This lesson writes two small make_server, which builds the server, and serve, which lets it answer a fixed number of connections and then closes it.
Bind like Networks taught
HTTPServer takes an address and a handler class:
handler_class = http_adapter.make_handler_class(handle, state)
server = http.server.HTTPServer(("127.0.0.1", 0), handler_class)
The address follows the rules you learned for listeners. 127.0.0.1 is the 0.0.0.0, which accepts connections from every network the machine is on.
make_server(handle, state, *, timeout=1.0) gets handle as a handle means whatever function was passed in, usually your own, so a test can pass a different one.
One more line sets server.timeout. That attribute bounds how long the server waits for a client to arrive before it gives up on that wait. It is a different timeout from the adapter’s SOCKET_TIMEOUT, which bounds each read on a connection once a client is there.
One call, one connection
server.handle_request() waits for one client, for at most server.timeout seconds. If a client connects in time, it answers that connection, request handler and all, and returns. If nobody comes, it returns quietly when the timeout ends, without raising. Either way, one call handles at most one connection. So serve is a for range(max_connections) with one handle_request() call in it:
max_connections=3, three clients come
handle_request() waits, client 1 connects, answered
handle_request() waits, client 2 connects, answered
handle_request() waits, client 3 connects, answered
server closed
max_connections=3, server.timeout=1.0, nobody comes
handle_request() waits 1 second, returns
handle_request() waits 1 second, returns
handle_request() waits 1 second, returns
server closed about 3 seconds in all
In both runs serve makes exactly three calls and then stops; a call that timed out still counts. That is what makes the run bounded: it ends after at most max_connections waits, whatever clients do.
HTTPServer also has a serve_forever(). It does what its name says and returns only when another part of the program tells it to stop. Your server never uses it.
serve(server, max_connections=3) runs with server.timeout = 1.0, and no client ever connects. What happens?
Close it
make_server returns the server open, and serve closes it once the loop is done, so the listening socket never outlives the run. A server works with with the way your sockets did: leaving a with server: block calls server.server_close(), whether the block finished or raised. Calling server.server_close() after the loop works too.
pl_http.py
You have one pl_http.py, in your workspace, is a helper written for this course that plays the client. It calls your make_server with your handle and the books from sample_state(), runs your serve in the background, sends the requests of the scenario you name, and prints the bytes that went each way. You run it; you never edit it. The scenario for this lesson is books, and its client is wire.fetch, the function you finished in Chapter 2. The wire.py in your workspace is the finished Chapter 3 version, there to read and run, not to edit:
python pl_http.py books
With both functions written, you see this, with three lines changed here: the reply and the headers line are cut short with ..., and the log line’s time is replaced by words.
pl_http: books, your server on 127.0.0.1 (a port the system chose)
--- requests and replies ---
client -> server b'GET /books HTTP/1.1\r\nHost: books.test\r\n\r\n' (41 bytes)
server -> client b'HTTP/1.0 200 OK\r\nServer: ...
--- wire.fetch returned ---
version: 'HTTP/1.0'
status: 200
reason: 'OK'
headers: {'server': [...], 'date': [...], 'content-type': ['application/json'], 'content-length': ['132']}
body: b'{"books": [{"id": 1, "title": "Kindred", "author": "Octavia E. Butler"}, {"id": 2, "title": "Caf\xc3\xa9 Society", "author": "Ana Ruiz"}]}' (132 bytes)
--- your server's log ---
127.0.0.1 - - [the date and time] "GET /books HTTP/1.1" 200 -
--- result ---
serve() returned after 1 connection; the server is closed
pl_http: every thread has stopped
Read it from the top. The first line names the address your server bound; pl_http finds the port itself and never prints it. client -> server is the request fetch wrote. server -> client is the reply, cut after its first 200 bytes: the status line HTTP/1.0 200 OK, then Server, naming BaseHTTP and the Python version, and Date, both added by http.server, then your two headers, the blank line, and the start of your body. wire.fetch returned is the record your Chapter 1 client built from those bytes. The log is the line http.server writes to standard error for each request. The result says whether serve came back and whether the server was closed.
When something is wrong, the same report says so. If make_server or serve is not written, it says not written yet: Lesson 3. If serve returns without closing the server, the result says the server is still open, and pl_http exits with status 1. If serve never returns, pl_http stops waiting about 2 seconds after the client is done, says that serve() was still running, and exits with status 3; no server is left behind. If binding fails, it calls that a site problem, but check the address in your make_server first: an address the machine does not have fails the same way.
wire.fetch sent HTTP/1.1, but the reply says HTTP/1.0. Why?
Your task
Write
make_server(handle, state, *, timeout=1.0): build the handler class withhttp_adapter.make_handler_class(handle, state), create the server withhttp.server.HTTPServer(("127.0.0.1", 0), handler_class), setserver.timeout = timeout, and return the server.Write
serve(server, *, max_connections): insidewith server:, callserver.handle_request()max_connectionstimes with aforloop.Run
python pl_http.py booksand read the transcript.
Keep import http.server at the top of the file and write http.server.HTTPServer in full, as above.
Then press Check my work. Your server now answers every request with the whole /nowhere included. The next lesson makes it say no.
Task
Write two reading_server.py.
make_server(handle, state, *, timeout=1.0)builds the handler class withhttp_adapter.make_handler_class(handle, state), using its ownhandleandstate, creates the server with http.server.HTTPServer(("127.0.0.1", 0), handler_class), setsserver.timeout = timeout, and returns the server, still open.serve(server, *, max_connections)callsserver.handle_request()exactlymax_connectionstimes and then closes the server, withwith server:around theor server.server_close()after it.
Keep the line import http.server and write http.server.HTTPServer(...) in full; do not use a from http.server import line. Do not call serve_forever().
Run python pl_http.py books in the wire.fetch.
Then press Check my work. It reads reading_server.py for a from http.server import line and a serve_forever call. It runs make_server with http.server replaced, through the HTTPServer on ("127.0.0.1", 0), that server.timeout is the timeout passed (1.0 when none is), and that one request through the handler class you passed reaches the handle and the state that make_server was given. It runs serve on a stand-in server with 3, 1, and 5 connections and checks that handle_request was called exactly that many times and that the server was closed after the last call; a serve that keeps calling fails after 50 calls instead of running on. Nothing from your terminal is graded.