TT Lab
Get started
Learn Learning paths Courses

My TCP Parcel Arrived in Pieces

Find the Missing Message Boundaries

Continue in TT Lab

In one line

TCP preserves byte order, but it does not carry the boundaries of the messages you define.

Why this was needed

You send two orders to the parcel server, "cat treats" and "charger cable", and one shows up as half a message while the other two appear glued together. It is tempting to conclude that the network lost bytes. But the two sends on the sending side and the two recvs on the receiving side do not correspond one to one. Depending on the state of the operating system buffers at that moment, the same data can arrive in different chunks. Having received everything in one piece by chance in a short local test is not evidence of a message contract.

The parcels in this course are byte messages for teaching. They are not a real order or payment system or an authenticated service, and you do not need to open any outside network. What we will build is a frame with a length header and a small program that restores that frame. It suits Python learners who know strings, classes, loops, and exceptions.

How it works

The 4 bytes at the front of the envelope give the body length as an unsigned big-endian number. b"cat" is 00 00 00 03 followed by 63 61 74. The whole frame is 7 bytes, but the header value is 3. A string is first converted to UTF-8, and only then is len calculated. Two characters and two bytes to transmit are not the same thing. The receiving parser does not interpret characters too early and keeps the bytes. Even if a chunk is cut in the middle of a UTF-8 character, you can interpret the body after collecting all of it.

The Decoder repeats three questions. Are the 4 header bytes here? Is the body that the header asked for here as well? After taking out a completed body, is the next frame here too? If the answer to the first question is no, it keeps the leftover and returns. The second is the same. The third needs a loop, not an if that checks only once. A single chunk may hold two complete frames and half of a third header.

feed 1: [길이 헤더 앞 2바이트]             → []
feed 2: [헤더 뒤 2바이트][본문 앞부분]      → []
feed 3: [나머지 본문][다음 프레임 전체]    → [본문1, 본문2]

An empty list means there is no completed message yet. [b""] means one message of length 0 was completed. If you treat these two as the same thing, you lose empty messages such as heartbeats. feed(b"") also just means "no input this time" and is not defined as a connection close. Only when recv on the network returns b"" do you know that EOF has occurred, and you pass that event to finish.

If the connection ends with half a header left over, that is not a normal close with no next order. The other side started an order and did not complete it. The same applies when it promised 8 body bytes and sent only 5. Distinguishing a close with no leftover from a close with an incomplete frame keeps the application from processing an incomplete order. However, successful protocol parsing does not replace validating the order itself.

There is one parser per connection. If you gather the leftovers of different clients in a global buffer, B's body gets attached after A's header. That is why the state belongs in an instance attribute rather than a global variable in the file. Conversely, if you create a new instance every time on the same connection, a header that is not yet complete disappears on every call. Where you keep the state is exactly who owns the data.

What it looks like in the field

RPC, device communication, and game servers use different frame formats, but they share the problem of "only part of it arrived". HTTP and WebSocket already have their own framing rules, so you do not have to add this course's 4-byte header to them unconditionally. If a library gives you a message-level API, first check which layer handles the boundaries. The trap is to assume that because TCP is reliable, application message boundaries also appear automatically.

The byte sequence made by joining A of length 1 and BC of length 2 is 00 00 00 01 41 00 00 00 02 42 43. If you give the first feed only the first 6 bytes, it returns one A and keeps the first byte of the next header, 00. If you then give it the last 5 bytes, the remaining 3 header bytes and the body BC are completed. If you give the same byte sequence all at once, the result must be [b"A", b"BC"]. If the result changes when you only change where you split, suspect the parser's state transitions before the network.

It is also useful to create two Decoders for connections A and B, feed A only the beginning of a header and feed B a complete frame. If B's completion changes A's leftover, the state is being shared. This kind of crossed input exposes ownership errors even without concurrent execution. The purpose of the test is not to reproduce the chance behavior of the operating system scheduler but to systematically check the input orders that the code allows.

This teaching implementation is the simple approach of deleting the front of a bytearray. With heavy traffic, copy and move costs become important, and you can consider alternatives such as a read cursor or a ring buffer. First prove that messages are restored correctly, and then optimize with profiling. You should also distinguish that a single length limit does not cap the total number of connections or the size of the chunk fed at one time.

What you will do in the next check

In the quiz right after this, you distinguish boundaries, empty messages, and EOF. In the lab in the last module, you pass a deterministic test that feeds one byte at a time and a test that feeds several frames bundled together. You do not wait for a particular split to appear by chance on real TCP. Given the same original, the list of restored messages must be the same however you split the feed.