SkyRaun / Curriculum / Lab 01

TCP is a byte stream.

One question: when a program sends messages over TCP, how does the receiver know where each message ends? You will expose a real protocol defect, learn why a friendly local run hides it, repair the decoder yourself, and prove the repair against boundaries it has never seen.

free · published in full module 01 · foundations needs: python 3.8+, a browser no cloud account

What TCP gives your program

TCP is a transport protocol implemented by the operating systems at both ends of a connection. Applications use it when two programs need an ordered flow of data — web servers, databases and SSH all build application protocols on top of it.

For this lab, remember exactly one contract:

the contract

TCP preserves byte order. It does not label application messages.

Sockets, addresses and ports

A socket is your program's endpoint for a network connection. The client socket connects to the server socket. Both programs run on your machine, so they talk over the loopback address 127.0.0.1, which means “this computer”. A port identifies which listening program should receive a connection at that address — the raw server uses 9001, and the framed server later uses 9002 so the two examples cannot collide.

client program                                server program
      │                                             │
      │ send bytes                       recv bytes │
      ▼                                             ▲
 client socket ───── TCP byte stream ─────── server socket
                         127.0.0.1
Two sockets, one ordered stream of bytes. Nothing in this picture knows what a “message” is.

Bytes are not messages

Text such as ALPHA is encoded to bytes before it travels through a socket. TCP presents those bytes to the application as one ordered sequence with no built-in separators. Suppose the client makes three application writes:

send("ALPHA\n")
send("BRAVO\n")
send("CHARLIE\n")

TCP carries this ordered stream:

ALPHA\nBRAVO\nCHARLIE\n

The server might receive that as [ALPHA\nBR] [AVO\nCHAR] [LIE\n], or as [AL] [PHA\n] [BRAVO\nCHARLIE\n]. Both are valid. A client send() boundary and a server recv() boundary do not have to line up. The server's recv(read_size) call means “give me up to this many bytes that are available now” — it does not ask TCP for “the next message”.

Framing creates messages

An application message is a unit your program cares about: one command, one record, one JSON document. Because TCP does not mark those units, your application protocol needs a framing rule. This lab's rule is deliberately small:

One newline byte (\n) ends one message.

A decoder is the code that applies that rule. It consumes byte chunks from the socket and emits complete application messages. A correct decoder must produce ALPHA, BRAVO and CHARLIE no matter where recv() happens to split or combine the bytes.

What the experiments will prove

  1. A single write can require several reads.
  2. Friendly timing can make reads resemble writes without creating a guarantee.
  3. Code that calls each read a message produces the wrong application data.

Experiment 1 — one write, several reads

Can one application write turn into several socket reads? The client makes one 20-byte write. The server's buffer holds at most eight bytes, so a single recv() cannot return the whole stream.

predict before you run

How many reads are minimally required? Where do you expect the boundaries to fall?

Start the raw server, which reports each chunk exactly as recv() returns it. Wait until it says listening, then run the client.

Server

$ python raw_server.py --read-size 8

Client

$ python client.py --mode together

Read the evidence

The client terminal shows one sendall containing all 20 bytes. The server needs at least three reads because its buffer holds only eight. A typical run is 8, 8 and 4 bytes:

client write   [ALPHA\nBRAVO\nCHARLIE\n]
server reads   [ALPHA\nBR] [AVO\nCHAR] [LIE\n]
app messages   [ALPHA\n] [BRAVO\n] [CHARLIE\n]
Three different sets of boundaries over the same bytes. Only the middle row is what your socket actually hands you.
what this proves

The client's one-write boundary did not become a server read boundary. Your exact chunks may differ — byte order will not.

Experiment 2 — watch the bug hide, then surface

If TCP does not preserve write boundaries, why does one recv() = one message code so often survive development and code review?

Because a quiet local machine produces friendly timing. This experiment runs the same starter decoder twice: first under conditions that flatter it, then under conditions that expose it. The same server process handles both.

broken rule + friendly timing    -> output looks right once -> false confidence
broken rule + different timing   -> messages split or combine -> visible defect

Create the friendly conditions

The broken framed server gets a large buffer and waits for two client connections. The first client makes three writes with half a second between them — each pause gives the server time to read before the next write arrives.

predict before you run

Will the reads probably resemble ALPHA, BRAVO and CHARLIE? If they do, what has changed — TCP's contract, or only this run's timing?

Server

$ python framed_server.py --read-size 1024 --connections 2

Client

$ python client.py --port 9002 --mode separate --pause 0.5

The server will probably report message #1 as ALPHA, #2 as BRAVO and #3 as CHARLIE. The broken decoder looks correct. This is the exact moment a bug like this gets approved and merged.

After the first client closes, do not restart the server. It prints waiting for connection 2/2. The process, the decoder and the read size have not changed.

Make the same bug visible

Now a second client sends the same three logical messages as awkward partial writes:

[AL] [P] [HA\nB] [RAVO\nCH] [AR] [LIE\n]
predict before you run

The intended messages are still ALPHA, BRAVO and CHARLIE. What might the broken decoder print when those partial writes arrive separately?

Client

$ python client.py --port 9002 --mode fragmented --pause 0.2

Server — still running

# the same process, untouched

Instead of three messages, the server will likely claim fragments such as AL, P or HA\nB are complete messages. Exact chunks vary, because this is a real TCP run. The invariant is what matters: anything other than exactly ALPHA, BRAVO and CHARLIE violates the newline framing rule.

what changed

Only the client's write shape and timing. The server process, decoder, buffer size, byte order and intended messages all stayed the same. Friendly timing hid the defect; another equally valid delivery exposed it.

the point

Passing the first connection was not proof. Observed timing describes one run. TCP's contract describes every valid run your decoder must handle.

Repair it, then prove it

The fix is not a bigger buffer and it is not a delay. A correct decoder keeps a buffer of bytes it has not yet been able to frame, and only emits a message when it has actually seen the terminator. The partial tail has to survive until the next read.

You write that decoder yourself in framing.py. The lab does not hand you the answer — it hands you a test that does not care how you got there:

test_framing.py before → after

Starter decoder

$ python test_framing.py
FAIL split mid-message
FAIL two messages, one read
FAIL terminator arrives alone
FAIL partial tail across three reads

After your repair

$ python test_framing.py
PASS split mid-message
PASS two messages, one read
PASS terminator arrives alone
PASS partial tail across three reads

The test feeds your decoder boundaries you never chose and never saw. That is the difference between “it worked on my machine” and a repair you can defend.

The interview question this lab answers

you should now be able to answer this cold

“Our service reads from a socket and processes one message per read. It has worked in staging for six months. Under load in production it started producing corrupted records. What happened, and why did staging not catch it?”

If you can answer that with a mechanism rather than a guess — and say what evidence would confirm it — this lab did its job.

That was one lab, in full, with nothing held back.

Foundations is fourteen more of these, plus the incident challenges where nobody tells you which layer is lying. Same voice, same structure, same refusal to hand you the answer.