Chapter 23 / 51
Volume 3 · Protocol Development

TCP stream services: three callbacks in the raw-byte world

Game gateways, IoT intake, private protocols — they don't speak HTTP. The tcp/tcps class hands you a raw stream; everything else is yours to define.

Overview

Positioning: a simpler callback surface than HTTP (just three), but a rawer data shape (buffer chains). The chapter's end connects to ch. 33 (XTP2): building your own protocol over TCP is exactly project 2's theme.

23.1 Intro

HTTP's value is "agreeing what a request looks like"; TCP's value is "agreeing on nothing". So the host's duty for TCP shrinks to three things: establish the stream, hand over data, report the terminal state — protocol design stays entirely yours. TLS (tcps) merely plugs the encryption layer into the stream; the callback surface is identical.

23.2 The view & callbacks

Three callbacks + the view (contract source style)
/* XS_StreamConn: exactly one of tcp/tls non-null (under tcps never bypass tls to write tcp) */
static void EventOpen(XS_HostInfo* pHost, XS_StreamConn* pConn);
static void EventData(XS_HostInfo* pHost, XS_StreamConn* pConn, xnetbuf* pBuffer);
static void EventClose(XS_HostInfo* pHost, XS_StreamConn* pConn,
    xnetresult iResult, const xerror* pError);
  • EventOpen: connection established — the moment for a welcome banner and session context
  • EventData: data arrived. pBuffer is a zero-copy buffer chain (borrowed in the callback): size via xrtNetBufSize(pBuffer), data consumed through xrt's buffer iteration APIs — it is not one contiguous memory block; don't treat it as char*
  • EventClose: the terminal — iResult gives the network verdict, pError the details (may be NULL)
  • Sending: xrtNetStreamSend(pConn->tcp, ...) / xrtTlsStreamSend(pConn->tls, ...) by whichever side is non-null

23.3 Banner & echo

A minimal TCP service
static void EventOpen(XS_HostInfo* pHost, XS_StreamConn* pConn)
{
    if ( pConn->tcp != NULL )
        (void)xrtNetStreamSend(pConn->tcp, "hello\n", 6);
    /* tcps: test pConn->tls + xrtTlsStreamSend instead (check written) */
}

static void EventData(XS_HostInfo* pHost, XS_StreamConn* pConn, xnetbuf* pBuffer)
{
    /* echo: send the received data back the way it came.
     * real implementation: walk each block of the chain and Send per segment;
     * size query: xrtNetBufSize(pBuffer) */
}
Terminal · real shape
$ telnet 127.0.0.1 9000
hello
(every line you type is echoed back verbatim)

What just happened: banner-on-connect is the convention of many text protocols (SMTP/FTP/IRC); echo is the stream service's "hello world". A real protocol (frame header + length + body) adds a state machine on top — ch. 33's XTP2 is a ready-made framing library.

23.4 Lifecycle & limits

  • The host owns the connection lifecycle: Accept takeover → registry → destruct on Close — the application never Destroys a connection (unlike the custom class where everything is manual)
  • SNI: under tcps, host certificates build the SNI table; each connection gets its certificate by SNI
  • idle_timeout: idle connections close server-side on expiry (func_test has a measured 4.5s-close case)
  • recv_limit: below the default read block, the read size shrinks in step (preventing config-induced generic failures)
  • Session context: isomorphic with WS — your own registry (connection → context), insert on Open, remove on Close

23.5 Contract

  • Borrowing: pConn/pBuffer/pError are valid inside the callback; consume chain data as frames, never as persistent storage.
  • Direction: under tcps never bypass tls to write tcp — plaintext bytes read as TLS-record noise on the other side.
  • Lifecycle: connections belong to the host; your session context belongs to you, cleaned in the Close terminal.

Common mistakes

Treating xnetbuf as contiguous memory
Symptom
Garbled reads / out-of-bounds access.
Cause
It is a buffer chain (a struct with Head/Tail/Size fields) — consumption must go through xrt's buffer iteration APIs block by block; there is no single "whole data pointer".
Saving pConn across callbacks and sending on your own
Symptom
Sending/receiving on the connection pointer from another thread crashes or garbles ordering.
Cause
Active I/O outside callbacks belongs to the "takeover" mode (HTTP's XS_TAKEOVER / your own threads need care); in normal mode your logic is event-driven — done within the Open/Data/Close trio.

Exercises

BasicA banner echo service

Implement 23.3's service and verify with telnet.

Acceptance: hello on connect; typed input echoed; the EventClose log line appears after Ctrl+] quits.
AdvancedA line-protocol parser

Frame by \n across multiple EventData rounds (half-line buffering) and build a mini KV with GET/SET/QUIT commands.

Acceptance: a command split across two sends ("GE" + "T key\n") parses correctly.
ChallengeA tcps dual stack

Enable a TLS port with a certificate; the same callbacks serve both plaintext and encrypted endpoints.

Acceptance: telnet on plaintext and openssl s_client on TLS behave identically; two bound lines in the log.

Summary

PointConclusion
Callback surfacethe Open/Data/Close trio; protocol design is all yours
Data shapexnetbuf zero-copy chain, borrowed per callback, consumed block-wise
tcpsone-of-two view; never bypass tls; SNI per-connection certificates
Lifecyclehost owns connections; you own context (registry + Close cleanup)
Limitsidle_timeout closes idle; recv_limit shrinks read blocks
xrt further reading