Engineering note
Framing messages on a byte stream
Application framing and bounded accumulation on TCP streams.
Networking / Resource limits
TCP read boundaries
A TCP read may contain a partial message, several complete messages or a combination of both. Framing must preserve message order independently of read boundaries.
TCP provides an ordered byte stream. The application protocol defines the message boundaries. The TCP specification makes that distinction explicit.
Fragmented input example
Synthetic example / ASCII bytes / LF-delimited messages
In this illustrative format, \n represents one line-feed byte and ends a message. The byte stream is RED\nBLUE\n.
| Read | Received bytes | Emitted messages | Retained bytes |
|---|---|---|---|
| A1 | RED\nBLUE\n |
RED, BLUE |
None |
| B1 | RE |
None | RE |
| B2 | D\nBL |
RED |
BL |
| B3 | UE\n |
BLUE |
None |
This is a fictional ASCII format with a line-feed delimiter. A real format also needs rules for delimiters inside payloads, encoding, empty messages and incomplete input when the connection ends.
XML stream framing
The networking application accepts continuous XML events and legacy framed input. Per-read delimiter searches cannot detect boundaries split across reads. Repeated conversion of the accumulated buffer into strings also introduces avoidable allocations.
The framer scans bytes directly and accounts for quoted attributes, comments and CDATA. It extracts candidate frames for subsequent XML validation.
Accumulation limits
The accumulation limit applies before storage grows beyond the permitted candidate size. Completed frames require the same size check, including those whose delimiter arrives in the current read. Checking only the incomplete remainder permits oversized completed frames to bypass the limit.
Bounded LF framing model
For each incoming byte:
If it is LF:
emit the current candidate; reset its length
Otherwise:
if candidate length equals the limit: reject the stream
append the byte to bounded candidate storage
At EOF:
reject a non-empty incomplete candidate
The model checks capacity before appending a payload byte. Slice-based implementations must preserve the same semantics. Copies into returned arrays still allocate, even where boundary scanning avoids string conversion.
Executable C# example
The independent example below implements the fictional LF format. Candidate storage is allocated once at the configured positive payload limit. LF is excluded from that limit, empty payloads are accepted, and incomplete input is retained between calls.
Framing method and complete source
public IReadOnlyList<byte[]> Append(ReadOnlySpan<byte> input)
{
ObjectDisposedException.ThrowIf(_stopped, this);
var frames = new List<byte[]>();
foreach (byte value in input)
{
if (value == (byte)'\n')
{
frames.Add(_candidate.AsSpan(0, _length).ToArray());
_length = 0;
continue;
}
if (_length == _candidate.Length)
Reject("Payload exceeds the configured limit.");
_candidate[_length++] = value;
}
return frames;
}
Download Program.cs, including executable checks and the project file. Save both in one directory, then run:
dotnet run --project FramingExample.csproj --configuration Release
The example compiled and ran locally with the .NET 10 SDK and warnings treated as errors. Its checks cover all 256 partitions of RED\nBLUE\n, exact-limit and empty payloads, oversized completed and incomplete payloads, fragmented overflow, incomplete EOF and instance reuse.
Returned frames and the result list allocate. The framer is single-consumer and excludes transport, XML parsing and queue management. An input error terminates the instance; a failing call does not return its partial batch. Completion rejects retained incomplete input. Callers must also bound read sizes and consumption of returned frames.
Verification cases
- Feed the same stream whole, one byte at a time and split at every possible position. Compare ordered output.
- Combine several complete messages with a trailing partial message.
- Accept a payload exactly at the limit and reject one byte beyond it, both with and without a delimiter in the same read.
- Define empty-message and EOF behaviour, then check both explicitly.
- For XML, split quoted attributes, comments and CDATA as well as opening and closing tags.
Resource controls must also address connection counts, queue growth, scan work and processing time. Repeated scans from the start of an incomplete candidate can produce excessive CPU work despite bounded memory.
Related case study: networking and message processing.
Examples are generalised or synthetic and omit operational details and identifying information.
← All engineering notes