Memra

Keep-alive, chunking, and cookies

◈ 6 cards

The three ways a body can end — Content-Length, Connection: close, chunked — what keep-alive buys and costs, and how Set-Cookie plus CookieManager bolt state onto a stateless protocol.

Three ways to say that the body ends here

A client reading a response has one hard problem: knowing when to stop. TCP is a stream with no message boundaries, so nothing in the bytes themselves says this is the end. HTTP offers exactly three answers, and a well-formed response uses one of them.

  1. Content-Length: n — the body is the next n bytes. Precise, cheap, and the only option that lets the connection be reused. It requires the server to know the size before it starts writing, which for a file on disk it always does.
  2. Connection: close — the body ends at end-of-stream. Simple and always correct, but it burns the connection to deliver the information, so it can be used only once.
  3. Transfer-Encoding: chunked — the body arrives as a series of chunks, each preceded by its own length in hexadecimal on its own line, ending with a chunk of length zero. This is how a server streams output it is still generating: it never has to know the total in advance, and the connection survives.

Keep-alive: what it buys and what it costs

HTTP/1.0 opened a fresh TCP connection for every request. For a page of forty small images that is forty handshakes, and the connection setup can easily outweigh the transfer — dramatically so over TLS, where the handshake is far more work than an ordinary one. HTTP/1.1 therefore leaves the socket open by default; a client may also ask explicitly with Connection: Keep-Alive.

The lockstep does not change. One request, then one response, then the next request, down the same socket. What changes is that end-of-stream no longer means end-of-body, so Content-Length or chunked encoding becomes mandatory rather than merely polite.

Java's URL class does this for you and needs no code to enable it. Its behaviour is tuned with system properties: http.keepAlive turns it off when set to false, and http.maxConnections sets how many sockets are held open at once, defaulting to 5.

Cookies: state on a stateless protocol

HTTP has no memory. Each request is judged on its own contents, which is exactly why it scales and exactly why a shopping cart needs help. Cookies are that help: short strings of non-whitespace ASCII — no commas, no semicolons — passed from server to client and back in the headers.

The server sets one with a response header:

Set-Cookie: cart=A7X29Q; Path=/shop; Domain=.example.org; Max-Age=3600; secure

and the client echoes it on later requests to matching URLs:

Cookie: cart=A7X29Q

The attributes control scope. Domain widens a cookie from one host to a whole subdomain, and a server may only set cookies for domains it belongs to. Path narrows it to a subtree; the default is the setting URL and everything below it. Max-Age (seconds) or Expires (an absolute GMT date) set its lifetime; with neither, it dies when the browser does. secure forbids sending it over an unencrypted connection, and HttpOnly hides it from JavaScript. Note that the value is usually meaningless — a key into a table on the server, not the data itself.

Java's cookie machinery

java.net.CookieHandler is abstract; CookieManager is the concrete implementation, and it is off by default. Install one and the URL class starts storing cookies and returning them to the servers that set them. A CookiePolicy filters what you accept — ACCEPT_ALL, ACCEPT_NONE, or ACCEPT_ORIGINAL_SERVER, which takes first-party cookies and refuses third-party ones. For finer control, implement CookiePolicy yourself and override shouldAccept(URI, HttpCookie). The stored cookies live in a CookieStore, reachable via getCookieStore(), which can add, list and remove HttpCookie objects — useful when you want to persist a session across runs of a program.

Worked example — two GETs down one socket

Ask for /a.html (98 bytes) and then /b.html (41 bytes) on one keep-alive connection. The client writes the first request, reads a status line and headers, sees Content-Length: 98, counts off exactly 98 bytes, and stops — the socket is still open, so nothing else would have told it to. It then writes the second request on the same socket and repeats with 41.

Now suppose the server miscalculates and sends Content-Length: 99. The client consumes the 98 real bytes plus the first byte of the next response, so the second status line reads TTP/1.1 200 OK and parsing fails — or, if there is no second request, the client blocks forever waiting for a byte that does not exist. One wrong integer, and the failure surfaces somewhere else entirely.

t1t2t3t4clientGET /aGET /bserver200 len 98200 len 41keep-aliveopenheldheldcloseHTTP/1.0opencloseopencloseOne handshake instead of two, paid for with an exact length.
The four messages are identical; only the socket policy differs. Keep-alive pays for one TCP handshake instead of two, which is why HTTP/1.1 defaults to it. The price is that the client can no longer wait for end-of-stream to learn where a response ended, so Content-Length or chunked encoding stops being optional.
NORMAL ~/memra/learn/comp-348/http-keep-alive-and-cookies utf-8 LF