Message Boundaries in a TCP Stream¶
TCP carries bytes in order, but it does not preserve your application's message boundaries. Send two length-prefixed messages over the same connection and reconstruct them even when a header spans several reads.
Course examples on GitHub
Before you start¶
Complete Your First TCP Connection. Use two terminals on the Pi in examples/network.
Outcome: decode two complete messages from a stream, retain incomplete data and reject a truncated frame.
The previous lesson's server echoes raw bytes without interpreting them. Reuse it here; the new client constructs frames and parses their echoed bytes.
Define a TCP frame¶
Our small application protocol is:
| Field | Size | Meaning |
|---|---|---|
| Length | 4 bytes | Unsigned payload length in network byte order. |
| Payload | 0–64 bytes | Exactly that many bytes; no terminating zero is added. |
A frame is 4 + N bytes. The limit of 64 is our protocol's rule, not a TCP limit. Length counts bytes, including for UTF-8 text.
The header bytes above are hexadecimal. htonl() converts a 32-bit length to network order, and ntohl() converts it back. We copy those four integer bytes, not an entire C++ struct with possible padding.
Run two messages on one connection¶
Configure and build:
In terminal A, start the existing byte-echo server:
Within 30 seconds, run in terminal B:
Client output:
The server reports echoed bytes=15: eight header bytes plus seven payload bytes. It knows nothing about the two messages.
Buffer first, parse complete frames¶
The client deliberately receives at most three bytes per call. Even a four-byte length field therefore needs more than one read. A successful recv() may also return fewer than three bytes.
The pending buffer keeps everything not yet consumed. After appending new bytes, the parser follows this loop:
A read can finish one frame and begin the next. Removing only the completed frame leaves that next prefix in pending. The inner loop also handles several complete frames arriving together when a larger receive buffer is used.
Read the standalone client¶
Complete file: app/tcp-frames/client.cpp. Socket calls, serialization, parsing and explicit cleanup are all in this one file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | |
Read the code in three passes:
- Build
wire: validate each payload, append its four-byte length, then append its bytes. - Send the stream: advance by each
send()result, then finish the outgoing direction. - Parse
pending: inspect a complete header, wait for the payload and consume exactly one frame at a time.
memcpy() reads the length into a real uint32_t object, avoiding an unaligned integer dereference into the byte buffer. The length is checked before adding it to the header size or allocating storage for that payload.
The string_view into pending is used before erase() changes the buffer. Comparing it with the original message checks this echo exercise; the length prefix itself determines the frame boundary.
Message boundaries are independent of EOF¶
The client prints each complete frame immediately. It need not wait for the peer to close before recognizing a message.
shutdown(SHUT_WR) remains here so the earlier byte-echo server knows when this demonstration session has no more input. It does not separate the two frames.
| At EOF | Result |
|---|---|
| Both frames decoded, no leftover bytes | Success. |
| Some header bytes remain | Truncated header. |
| Header present, payload incomplete | Truncated payload. |
| No leftover bytes, fewer than two frames | Missing frame. |
A zero-length frame still contains its four-byte header. EOF is recv() == 0, not a frame whose declared length happens to be zero. This particular exercise expects two frames; an ongoing service would define its own session rules.
Change the input and the read size¶
Restart the server, then send an empty first message:
The client should print a first frame of zero bytes and a second frame containing Pi.
Next, change std::array<char, 3> to std::array<char, 64> and rebuild. The same output must result. Both default frames now fit in one receive buffer, but TCP still does not promise a particular number of reads. Restore the original buffer afterwards.
To compile the new file directly:
Run it against the server with the same port arguments. Linux per-call I/O timeouts and explicit descriptor cleanup follow the preceding lesson.
Check your understanding¶
A header announces five payload bytes, but the connection ends after only he. Should the receiver emit a two-byte message?
Compare your reasoning
No. The length is the application's contract. The five-byte frame is incomplete, so the receiver reports truncation instead of silently changing the message.
Continue the course¶
Previous: Your First TCP Connection · Next: Disconnection and Timeouts · Course overview
References: TCP stream semantics, receive results, network byte order.