Bytes Without Meaning
The stream delivers every byte, in order, exactly once. It is a remarkable thing to have built. And Mira’s program, reading from it, has no idea what any of it means.
You type hi into your chat window and press enter, and your program writes two bytes to the socket. A moment later you type how are you and press enter, and it writes eleven more. Two messages, two writes. Mira’s program then calls read, and it is fair to ask what it gets.
It gets thirteen bytes, hihow are you. Or, if it happened to call read between your two writes, it gets hi, and then on the next read how are you. Or, if a piece boundary fell awkwardly and the second read came a moment early, it gets hih and then ow are you. It may get any of these. The stream makes no promise about which.
Two writes on one side, a sequence of reads on the other. As the timing varies, the same two messages arrive as one read, two reads, or three, split at arbitrary points. Every one of them is the stream working correctly.
This is Chapter 2, and it is Chapter 2 in every detail. Back then, the wire delivered bits with no edges, and Mira’s card had no way to know where a message started or ended. You solved it with a start marker and a length, and the card found the frames. Then, floor by floor, the frames were opened and their contents were glued back together into a stream, which was the whole purpose of Chapter 11, and the stream was handed to the program as a smooth, continuous sequence of bytes with no boundaries in it at all. The card found the edges and then the tower carefully erased them, because the tower’s job was to deliver bytes, and the frame boundaries were the tower’s business rather than the program’s.
So the program has a stream, and a stream has no edges, and the program needs to know where a message starts and ends. You have been here before, and you know what to do. The same two answers are available as before, one floor from the top instead of at the bottom. You can use a delimiter, ending every message with a byte that cannot appear in a message, a newline, say. Mira’s program reads until it sees a newline, and that is one message. This is simple, and it works right up until someone wants to send a message containing a newline, at which point you are escaping, as you were in Chapter 2. Or you can use a length, starting every message with a number that says how many bytes follow. Mira’s program reads the number, then reads exactly that many bytes, and that is one message. There is no scanning and no escaping, and the message can contain anything.
You will pick the length, for the same reasons as before. And you will add one more thing, because a chat is not only text. It is photos, and heartbeats from Chapter 13, and indicators that Mira is typing, and presence updates. Each is a different kind of thing, and the receiver needs to know which kind before it can do anything with the body. So the message begins with its kind.
┌──────┬────────┬──────────────────────┐
│ type │ length │ body │
│ 1 B │ 4 B │ as many as length says│
└──────┴────────┴──────────────────────┘
Type 1 is a text message; type 2, a photo; type 3, a heartbeat with an empty body; type 4, typing. Mira’s program reads five bytes, learns the kind and the size, reads the body, dispatches on the kind, and goes round again. The heartbeat you built in Chapter 13 is a message of type 3. It was always going to live here, and it only needed a place.
This is the shape of every message-based protocol ever built on a stream. The details vary, since the type might be a string and the length might be of variable width, but kind, size and body are the skeleton.
That format carries Courtyard’s chat perfectly. Messages arrive whenever the sender sends them, in either direction, on a connection that stays open as long as you are both around. Nobody asks for a message; it simply comes. This arrangement is called push.
Then Mira adds the feed. Courtyard now keeps photos, and not only the ones sent in chat. There is a gallery with everyone’s pictures, and profiles, and a page of recent posts, and Mira’s server holds all of it. When you want to see one, you do not wait for it to be pushed to you. You ask.
This is a different shape, and it differs in a particular way. In chat, the connection carries an ongoing stream of unprompted messages both ways. In the feed, you send one request and get back one response, and then you might say nothing else for an hour. The server never initiates. It waits to be asked, answers, and waits again. This arrangement is called pull.
On the left, a chat frame of type, length and body, on a long-lived connection carrying messages both ways, unprompted. On the right, a request and a response, each with headers and a body, on a connection that might close after one exchange. Both have a length field, and both have solved Chapter 2 again.
Let us derive the request. You want a photo from Mira’s server, and the message has to contain a few things. It has to say what you want, which is some name for the photo. For now that is a path, /photos/42, and questions about how such names ought to be structured can wait for Chapter 20. It has to say what to do with it. Now you want to fetch it; later you will want to post a new one, or delete one, and the server will need to know which. For now, one word will do: get.
It has to carry a few things about you and your request. What image formats your program can display, so that the server does not send one you cannot show. What language you would prefer. Who you are, once there is a notion of that. These are small, optional and open-ended, and there will be more of them later than you can enumerate now, so rather than fixed fields you make them a list of pairs, each a name and a value, and let the list be as long or as short as the request needs. Such a pair is called a header, which is what everyone calls them. And sometimes the request has a body. A get has none. A post has the photo in it.
The response needs a status, which is to say an answer to whether it worked. The server needs a compact way to say that here it is, or that there is no such photo, or that you are not allowed, or that it broke. A number from a small agreed table does this, the famous ones being 200 for here it is and 404 for no such thing. Then there are headers about the response: how big it is, what kind of image it is, how long you may keep it. Then the body, which is the photo.
What you have derived is HTTP. Not the whole of it, but the whole shape, and everything else is detail.
And now the old problem is back. The response has a body, the body is bytes on a stream, and the stream has no edges. The client needs to know when the photo is over and the next response, if there is one on this connection, begins. Length is the obvious answer, in a header that says this body is 2,481,033 bytes. The client reads that many and stops.
But suppose the server is sending something it is generating, a search result it is still computing, a list it is pulling from a slow disk, a live log. It wants to start sending before it knows how long the whole thing will be, and it cannot put the length in a header it has already sent. Chapter 2 had a fork at this point and chose length. This is the other branch, and it comes back as promised. When the sender does not know the size, it uses a marker. HTTP’s version is a hybrid of the two. The body is sent as a series of chunks, each with its own length, and it ends with a chunk of length zero. Each chunk is length-framed and the end is marker-framed. The sender can start immediately, send as it goes, and stop when it is done. These are the same two tools you have had since Chapter 2, used together.
There is a choice buried in the request format that Chapter 2 did not have to make, and it is a real fork. Your chat frame was binary: a type byte, four length bytes, then the body. It is compact, fast to parse, and unreadable by a human. The real HTTP request looks like this.
GET /photos/42 HTTP/1.1
Host: courtyard.example
Accept: image/jpeg, image/png
It is text. The verb, the path and the headers are ordinary characters, one item per line, with a blank line marking the end. A human can read it, and a human can type it. You can connect to a web server with the simplest possible tool, type those lines by hand, and the server will answer.
One more arrangement belongs here, because it is how a chat application meets the world. In Part III, everything between Courtyard and its users, the front doors, the boxes that spread load, the caches, will understand HTTP and very little else. A custom chat protocol on port 5222 will be invisible to all of it. And yet chat is push and HTTP is pull, and a pull connection that closes after one exchange is no use for a stream of messages both ways.
The answer is a small agreement. Your chat client opens what looks like an ordinary HTTP request, with a header that says, in effect, that once the server has answered this request the two of you should stop speaking HTTP and speak something else on this same connection. The server answers with a status that means it agrees, and from that byte onward the connection carries chat frames, type, length and body, in both directions, indefinitely. It got in the door as HTTP, and it lives as push. This is called an upgrade, and the something else is almost always WebSocket, whose frames look very much like the ones you designed at the top of this chapter. Every browser-based chat you have used works this way.
Your program can finally act on what it receives. Chat messages have edges and kinds. Feed requests have verbs and paths and headers, and responses have statuses and lengths. And the problem you solved in Chapter 2 came back, unchanged, the moment the tower handed you a stream. It will come back again, every time you build on a stream, for the rest of your working life. You will reach for a length or a marker, and you will know why. What you have not decided is what goes inside the body, and what happens when the two sides disagree about it.
Coming up · I·19 — The Shape of the MessageMira’s client is one version behind your server. You added a field to the message. Then you removed one.
→