0%

Serve HTTP Yourself · practice

Serve a Few Connections, Then Stop

handle returns the right when you call it, but no client has reached it. In Networks with Python you wrote the server side by hand: make_listener bound a and 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 . Each time it accepts a connection, it makes a request handler for it from a class you give it, and that class is what 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 , so only programs on the same machine can connect; never bind 0.0.0.0, which accepts connections from every network the machine is on. 0 asks the system for any free port, so the server never fails because a fixed port is taken, and no port number ever needs to appear in your code. The server binds and starts listening the moment it is created. From then on a client can connect, even before the server is ready to accept it: the system holds the connection until the next accept takes it.

make_server(handle, state, *, timeout=1.0) gets handle as a . Inside the function, 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 over 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 called 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 , and a server needs a client. 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

  1. Write make_server(handle, state, *, timeout=1.0): build the handler class with http_adapter.make_handler_class(handle, state), create the server with http.server.HTTPServer(("127.0.0.1", 0), handler_class), set server.timeout = timeout, and return the server.

  2. Write serve(server, *, max_connections): inside with server:, call server.handle_request() max_connections times with a for loop.

  3. Run python pl_http.py books and 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 in reading_server.py.

  • make_server(handle, state, *, timeout=1.0) builds the handler class with http_adapter.make_handler_class(handle, state), using its own handle and state , creates the server with http.server.HTTPServer(("127.0.0.1", 0), handler_class), sets server.timeout = timeout, and returns the server, still open.

  • serve(server, *, max_connections) calls server.handle_request() exactly max_connections times and then closes the server, with with server: around the or 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 to see your server answer 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 , by a stand-in that records what your code asked for, so no is bound: that the server was an 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.