The XTP2 extension: speaking binary over TCP
TCP gives you a byte stream; XTP2 gives you message boundaries. This chapter installs the framing library (ch. 23's foreshadowing) into EventData, yielding a protocol service immune to sticky packets and malicious frames.
Overview
33.1 Intro
The pain of binary protocols was never "designing the frame format" — it's implementation discipline: boundary handling (sticky/partial packets), length validation (malicious oversized frames), type safety (view lifetimes). xtp2 turned all three into a library: a 32-byte fixed header + four message types (REQUEST/RESPONSE/PUSH/EVENT) + a streaming parser; you only feed it bytes and handle the messages it produces. Prerequisite: the build.bat xtp variant.
33.2 Frames & session buffers
Assembly starts with the session structure: one processor per connection plus a partial-packet carry buffer (ch. 21's registry pattern, TCP edition):
typedef struct XtpSession {
xtp2_processor* pRx; /* one parser per connection */
xbuffer* pPending; /* partial carry (the tail feed couldn't digest) */
} XtpSession;
/* created on Open (into the registry) · removed on Close (processor and buffer destroyed) */
Why pPending exists: xtp2_feed yields at most one message per call — one TCP delivery may hold "one and a half" or several; whatever remains at XTP2_MORE stays for the next EventData.
33.3 Assembling the protocol in EventData
static void EventData(XS_HostInfo* pHost, XS_StreamConn* pConn, xnetbuf* pBuffer)
{
XtpSession* pS = SessionFind(pConn); /* session from the registry */
xbuffer* pIn = pS->pPending;
BufferAppendNetBuf(pIn, pBuffer); /* buffer chain → contiguous buffer */
for ( ;; ) {
size_t iUsed = 0;
xtp2_message tMsg;
xtp2_result r = xtp2_feed(pS->pRx,
BufferData(pIn), BufferSize(pIn), &iUsed, &tMsg);
BufferConsume(pIn, iUsed); /* drop the consumed part */
if ( r < 0 ) { /* bad frame: sticky error */
SessionDrop(pConn); /* disconnect: the 33.4 decision */
return;
}
if ( r == XTP2_MESSAGE ) {
DispatchMessage(pConn, &tMsg); /* a use-and-drop view */
continue; /* the buffer may hold more whole frames */
}
break; /* XTP2_MORE: keep the tail for next round */
}
}
What just happened — all four assembly points are in there: the carry buffer bridging rounds, precise consumed-based consumption, message views used up within the round, and errors breaking immediately (a sticky error has no "try again"). The response direction packs with xtp2_measure + encode then xrtNetStreamSend — echoing the REQUEST's id into the RESPONSE gives correlation.
33.4 limits & security
- Length defense: max_packet (default 1MiB) takes effect during header parsing — a malicious oversized frame is rejected before its body is read; memory can't be tricked by a header "declaring 4GB"
- Structure defense: cross-checks of parameter table lengths, bounds checks on header fields, and unsigned conversions for status avoiding implementation-defined behavior
- Disconnection policy: a sticky protocol error means the stream is untrusted — the fuzz-verified round-trip invariant guarantees "whatever decodes is a legal frame"; conversely, feeding on after a bad frame only compounds errors. Disconnect + log is the most honest handling for binary protocols
- Time defense: TCP idle_timeout catches the slow-loris "connect and send half a header"
33.5 Contract
- Session: one processor per connection; the carry buffer bridges rounds; Close's terminal destroys everything uniformly.
- Messages: views from feed are used up in the round; copy before keeping.
- Errors: a sticky error = disconnect; tighten limits to what the business actually needs.
Common mistakes
- Symptom
- Messages processed twice, or parsing misaligned.
- Cause
- feed's consumed tells you how many bytes this call ate — without consuming them, the next round re-parses from the start. Buffer operations must advance strictly by consumed.
- Symptom
- After reset clears the error, feeding the same stream yields garbled messages.
- Cause
- reset only clears parser state — it doesn't repair the already-misaligned byte stream; the second half of a broken message gets read as a new header. reset's proper use is "reusing the processor object" (new connection/new stream).
Exercises
Implement a minimal PING REQUEST → PONG RESPONSE service (the client can build frames with xtp2_encode).
Have the client send one frame in 1-byte fragments, verifying the partial-packet buffer.
Construct three attacks — oversized length headers, truncated frames, illegal magic — and confirm the server's memory stays flat and disconnects are clean.
Summary
| Point | Conclusion |
|---|---|
| Positioning | a codec primitive: frame format + parsing discipline as a library |
| Assembly | session = processor + carry buffer; consume precisely by consumed |
| Messages | feed views used up per round; REQUEST ids echoed for correlation |
| Security | limits effective at header time; bad frame = disconnect; idle as backstop |
- xrt reference · buffer — the consume/append model for session carry buffers