Chapter Orientation

The UDP module fits datagrams into the engine model: dual shape — a connectionless endpoint (recvfrom carries the source address — any source, you filter) and a connected one (after connect, receive only that peer — the client shape); event-driven send/receive (engine callbacks — the same callback face as TCP); the batch family (the udp_batch example — amortizing multi-packet send/receive in one call); multicast (join/leave groups — the multicast example); and PMTU and asynchronous errors (udp_introspect/udp_errors examples — ICMP unreachable and other platform capabilities). UDP has no flow control and no state machine — but the engine's callbacks, reference counting, and shutdown protocol are all the same.

69.1引入

The essential difference between UDP and TCP is not "reliable versus unreliable" — it is the data boundary: TCP is a byte stream (message boundaries are your problem — Chapter 65's buffer chain and Chapter 72's framing exist for this); UDP is datagrams (one send = one message, one recv = one complete message — the boundary is kernel-kept). This difference makes UDP's API shape entirely different: no send budget (a message either queues or it doesn't), no receive chain (one message, one buffer), no flow control (unreliability is a feature — real-time audio/video would rather drop than back up).

Choosing between the two shapes: the connectionless endpoint (server shape) — after bind, receive from any source (recvfrom carries the source; replies specify the destination with sendto); the connected form (client shape) — after connect "locks" the peer, receive only from it (the kernel filters) and send directly (no per-call destination). UDP's connect does no handshake (it is just a filter + default destination) — semantically a completely different thing from TCP's connect (a real connection setup).

69.2概念

The dual shape and lifecycle

1Createan xnetudp object -> bind (connectionless) or connect (connected)
2Statethe UDP state machine (BINDING/BOUND/CONNECTING/CONNECTED — transitional states of async bind/connect)
3Send/receiveconnectionless recvfrom (with source) / sendto (destination given); connected recv/send (fixed peer)
4CloseClose -> CLOSED -> Destroy — reference counting as with Stream (Chapter 67's contract continued)

Asynchronous bind/connect: UDP's binding and connecting are engine operations too (state-machine transitions — poll xrtNetUdpState or wait on events; the udp example's WaitState helper demonstrates deadline-bounded polling).

The event callback face

UDP's event table (same shape as Stream's): Read (message arrived — recvfrom/recv inside the callback), Write (sendable), Close (final-state notice). Send/receive in the callback: a datagram arrival triggers the callback, and the callback pulls (pull-style recv — messages in a queue, the callback is a notification); batch pulling (pull several per callback — the efficient shape for draining backlogs). Worker constraints match Stream's (callbacks on Workers, cross-thread via Post — Chapter 68's contract, UDP edition).

The batch family and the packet structure

xnetudppacket (the carrier of message + source address); batch variants operate on several messages at once (array submit/receive) — the amortizing entry for high-PPS scenarios (DNS servers, monitoring beacons). The Msg family (SendMsg/RecvMsg — kin of Chapter 64's port layer): vector sending with a target address — UDP's multi-buffer variant.

Multicast and PMTU

Multicast (the multicast example): join/leave multicast groups (group address + local interface), TTL control (hops across network segments); send/receive use the same API as unicast (the group address as destination). PMTU probing and asynchronous errors: an oversized send is dropped en route (when fragmentation is disabled) — ICMP "frag needed" is reported asynchronously (RecvError per platform capability — Chapter 64's capability gating applied to UDP); the udp_errors example demonstrates error reception, and udp_introspect demonstrates PMTU/statistics queries.

69.3示例

Complete program: dual-shape loopback

From the repository example examples/network/udp/main.c — connectionless endpoint + connected form side by side:

examples/network/udp/main.c源码 ↗















 */


#include <stdio.h>
#include <string.h>
#include <xrt.h>

#include <math.h>
static inline double exampleTimerLimit(int64 Timeout)
{
    return Timeout == XRT_WAIT_FOREVER ? INFINITY : xrtTimer() + (double)Timeout / 1000.0;
}
static inline bool exampleTimerExpired(double Limit)
{
    return xrtTimer() >= Limit;
}
static inline int64 exampleTimerRemaining(double Limit)
{
    double Ms;
    if (Limit == INFINITY) return XRT_WAIT_FOREVER;
    Ms = ceil((Limit - xrtTimer()) * 1000.0);
    return Ms <= 0 ? 0 : Ms >= 0x1p63 ? INT64_MAX : (int64)Ms;
}



/* 在截止时间内等待 UDP 状态转换。 */
static bool exampleUdpWaitState(xnetudp* pUdp, xnetudpstate State)
{
	double iDeadline = exampleTimerLimit(3000);

	while ( xrtNetUdpState(pUdp) != State ) {
		if ( exampleTimerExpired(iDeadline) ) {
			return false;
		}
		xrtThreadYield();
	}
	return true;
}



/* 在截止时间内拉取一个 UDP 包。 */
static xnetudppacket* exampleUdpReceive(xnetudp* pUdp)
{
	double iDeadline = exampleTimerLimit(3000);
	xnetudppacket* pPacket;

	for ( ;; ) {
		pPacket = xrtNetUdpReceive(pUdp);
		if ( pPacket != NULL ) {
			return pPacket;
		}
		if ( exampleTimerExpired(iDeadline) ) {
			return NULL;
		}
		xrtThreadYield();
	}
}



/* 建立绑定端和连接端,完成一次 UDP 回环收发。 */
int main(void)
{
	xnetengineconfig EngineConfig;
	xnetudpconfig UdpConfig;
	xnetengine* pEngine = NULL;
	xnetudp* pServer = NULL;
	xnetudp* pClient = NULL;
	xnetudppacket* pPacket = NULL;
	xnetaddr Address;
	xnetdgramcontrol Control;
	str sEndpoint = NULL;
	int iResult = 1;

	xrtNetEngineConfigInit(&EngineConfig);
	EngineConfig.Workers = 2;
	xrtNetUdpConfigInit(&UdpConfig);
	UdpConfig.ReceiveMeta = XNET_DGRAM_META_DESTINATION;
	pEngine = xrtNetEngineCreate(&EngineConfig);
	if ( (pEngine == NULL) || !xrtNetEngineStart(pEngine) ) {
		goto Cleanup;
	}
	(void)xrtNetAddrLoopback(&Address, XNET_FAMILY_IPV4, 0);
	pServer = xrtNetUdpBind(pEngine, &Address, 0,
		&UdpConfig, NULL, NULL);
	if ( (pServer == NULL) || !xrtNetUdpLocal(pServer, &Address) ) {
		iResult = 2;
		goto Cleanup;
	}
	sEndpoint = xrtNetAddrEndpointString(&Address);
	if ( sEndpoint == NULL ) {
		iResult = 3;
		goto Cleanup;
	}
	printf("UDP server: %s\n", sEndpoint);
	xrtFree(sEndpoint);
	sEndpoint = NULL;
	pClient = xrtNetUdpConnect(pEngine, &Address, 1,
		NULL, NULL, NULL);
	if ( (pClient == NULL) ||
		 !exampleUdpWaitState(pServer, XNET_UDP_OPEN) ||
		 !exampleUdpWaitState(pClient, XNET_UDP_OPEN) ) {
		iResult = 4;
		goto Cleanup;
	}
	if ( xrtNetUdpSend(pClient, "hello UDP", 9) !=
		 XNET_RESULT_OK ) {
		iResult = 5;
		goto Cleanup;
	}
	pPacket = exampleUdpReceive(pServer);
	if ( pPacket == NULL ) {
		iResult = 6;
		goto Cleanup;
	}
	printf("received: %.*s\n",
		(int)xrtNetUdpPacketSize(pPacket),
		(cstr)xrtNetUdpPacketData(pPacket));
	if ( ((xrtNetUdpPacketMeta(pPacket)->Flags &
		  XNET_DGRAM_META_DESTINATION) == 0) ||
		 ((xrtNetUdpSendControlAvailable(pServer) &
		  XNET_DGRAM_CONTROL_SOURCE) == 0) ) {
		iResult = 7;
		goto Cleanup;
	}
	memset(&Control, 0, sizeof(Control));
	Control.Flags = XNET_DGRAM_CONTROL_SOURCE;
	Control.Source = xrtNetUdpPacketMeta(pPacket)->Destination;
	Control.Source.Port = 0;
	if ( xrtNetUdpSendMsg(
		pServer,
		xrtNetUdpPacketRemote(pPacket),
		&Control,
		"reply",
		5
	) != XNET_RESULT_OK ) {
		iResult = 8;
		goto Cleanup;
	}
	xrtNetUdpPacketDestroy(pPacket);
	pPacket = exampleUdpReceive(pClient);
	if ( (pPacket == NULL) || (xrtNetUdpPacketSize(pPacket) != 5) ||
		 (memcmp(xrtNetUdpPacketData(pPacket), "reply", 5) != 0) ) {
		iResult = 9;
		goto Cleanup;
	}
	printf("response: %.*s\n",
		(int)xrtNetUdpPacketSize(pPacket),
		(cstr)xrtNetUdpPacketData(pPacket));
	iResult = 0;

Cleanup:
	xrtNetUdpPacketDestroy(pPacket);
	xrtFree(sEndpoint);
	if ( pClient != NULL ) {
		(void)xrtNetUdpClose(pClient);
		(void)exampleUdpWaitState(pClient, XNET_UDP_CLOSED);
	}
	if ( pServer != NULL ) {
		(void)xrtNetUdpClose(pServer);
		(void)exampleUdpWaitState(pServer, XNET_UDP_CLOSED);
	}
	xrtNetUdpDestroy(pClient);
	xrtNetUdpDestroy(pServer);
	if ( (pEngine != NULL) && !xrtNetEngineDestroy(pEngine) &&
		 (iResult == 0) ) {
		iResult = 10;
	}
	return iResult;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/network/udp/main.c -lws2_32 -liphlpapiUDP server: 127.0.0.1:NNNNNreceived: hello UDP

What just happened. (1) The server's connectionless endpoint binds (port 0 dynamic — Local retrieves and prints 127.0.0.1:NNNNN); state waiting (the WaitState helper polls BOUND within a deadline — managing the async bind's transitional state). (2) The client's connected form connects to the server — receiving only that peer (the server's reply). (3) The client sends hello UDP — no destination parameter (connect already locked it); (4) the server's Read event fires, recvfrom pulls (the source is the client) → prints received: hello UDP — each shape in its place. (5) The reply sendtos to the client's address — a connectionless reply must specify the destination.

Complete program: batching and multicast

From examples/network/udp_batch/main.c and examples/network/udp_multicast/main.c (read together):

examples/network/udp_batch/main.c源码 ↗



































 */

#include <stdio.h>
#include <string.h>
#include <xrt.h>

#include <math.h>
static inline double exampleTimerLimit(int64 Timeout)
{
    return Timeout == XRT_WAIT_FOREVER ? INFINITY : xrtTimer() + (double)Timeout / 1000.0;
}
static inline bool exampleTimerExpired(double Limit)
{
    return xrtTimer() >= Limit;
}
static inline int64 exampleTimerRemaining(double Limit)
{
    double Ms;
    if (Limit == INFINITY) return XRT_WAIT_FOREVER;
    Ms = ceil((Limit - xrtTimer()) * 1000.0);
    return Ms <= 0 ? 0 : Ms >= 0x1p63 ? INT64_MAX : (int64)Ms;
}



/* 等 Future 成功解析。 */
static bool exampleFutureWait(xfuture* pFuture)
{
	return (pFuture != NULL) &&
		(xrtFutureWaitFor(pFuture, INT64_C(3000)) == XWAIT_OK) &&
		(xrtFutureState(pFuture) == XFUTURE_RESOLVED);
}



/* 在截止时间内等 UDP 进入指定状态。 */
static bool exampleWaitState(xnetudp* pUdp, xnetudpstate State)
{
	double iDeadline = exampleTimerLimit(3000);

	while ( xrtNetUdpState(pUdp) != State ) {
		if ( exampleTimerExpired(iDeadline) ) {
			return false;
		}
		xrtThreadYield();
	}
	return true;
}



/* 收满 count 条(普通单包拉取,等待全部到齐)。 */
static size_t exampleDrainCount(xnetudp* pUdp, size_t iWant)
{
	size_t iGot = 0;
	double iDeadline = exampleTimerLimit(3000);

	while ( iGot < iWant ) {
		xnetudppacket* pPacket = xrtNetUdpReceive(pUdp);

		if ( pPacket != NULL ) {
			xrtNetUdpPacketDestroy(pPacket);
			++iGot;
			iDeadline = exampleTimerLimit(3000);
			continue;
		}
		if ( exampleTimerExpired(iDeadline) ) {
			break;
		}
		xrtThreadYield();
	}
	return iGot;
}



/* 服务端绑小接收槽演示截断;客户端连发演示三种批量接收。 */
int main(void)
{
	xnetengineconfig EngineConfig;
	xnetudpconfig UdpConfig;
	xnetengine* pEngine = NULL;
	xnetudp* pServer = NULL;
	xnetudp* pClient = NULL;
	xnetaddr Address;
	xnetudppacket* pPackets[8];
	xnetudppacket* pKept = NULL;
	xnetudpbatch* pBatch = NULL;
	xnetudpbatch* pBatchRef = NULL;
	xfuture* pFuture = NULL;
	xfuture* pWritable = NULL;
	xfuture* pErrorFuture = NULL;
	char Big[64];
	size_t iCount;
	size_t i;
	int iResult = 1;

	xrtNetEngineConfigInit(&EngineConfig);
	EngineConfig.Workers = 1;
	xrtNetUdpConfigInit(&UdpConfig);
	UdpConfig.ReceiveSize = 16u;  /* 对端发 64 字节 → 截断前缀 */
	UdpConfig.Truncation = XNET_UDP_TRUNCATE_DELIVER;
	pEngine = xrtNetEngineCreate(&EngineConfig);
	if ( (pEngine == NULL) || !xrtNetEngineStart(pEngine) ) {
		goto Cleanup;
	}
	(void)xrtNetAddrLoopback(&Address, XNET_FAMILY_IPV4, 0);
	pServer = xrtNetUdpBind(pEngine, &Address, 0, &UdpConfig,
		NULL, NULL);
	if ( (pServer == NULL) || !xrtNetUdpLocal(pServer, &Address) ) {
		iResult = 2;
		goto Cleanup;
	}
	pClient = xrtNetUdpConnect(pEngine, &Address, 0, NULL,
		NULL, NULL);
	if ( (pClient == NULL) ||
		 !exampleWaitState(pServer, XNET_UDP_OPEN) ||
		 !exampleWaitState(pClient, XNET_UDP_OPEN) ) {
		iResult = 3;
		goto Cleanup;
	}

	/* 截断演示:64 字节载荷到达 16 字节接收槽。 */
	memset(Big, 'A', sizeof(Big));
	if ( xrtNetUdpSend(pClient, Big, sizeof(Big)) != XNET_RESULT_OK ) {
		goto Cleanup;
	}
	iCount = exampleDrainCount(pServer, 1);
	if ( iCount != 1u ) {
		iResult = 4;
		goto Cleanup;
	}
	{
		/* 重发一条并直接取包核对截断标志与前缀长度。 */
		xnetudppacket* pPacket = NULL;
		double iDeadline = exampleTimerLimit(3000);

		if ( xrtNetUdpSend(pClient, Big, sizeof(Big)) !=
			 XNET_RESULT_OK ) {
			goto Cleanup;
		}
		while ( (pPacket = xrtNetUdpReceive(pServer)) == NULL ) {
			if ( exampleTimerExpired(iDeadline) ) {
				break;
			}
			xrtThreadYield();
		}
		if ( (pPacket == NULL) || !xrtNetUdpPacketTruncated(pPacket) ||
			 (xrtNetUdpPacketSize(pPacket) != 16u) ) {
			xrtNetUdpPacketDestroy(pPacket);
			iResult = 5;
			goto Cleanup;
		}
		printf("batch: truncated prefix=%zu of %zu\n",
			xrtNetUdpPacketSize(pPacket), sizeof(Big));
		xrtNetUdpPacketDestroy(pPacket);
	}

	/* 拉取批量:三条齐发,等队列到齐后一次 ReceiveBatch 全取。 */
	for ( i = 0; i < 3u; ++i ) {
		if ( xrtNetUdpSend(pClient, "bb", 2) != XNET_RESULT_OK ) {
			goto Cleanup;
		}
	}
	memset(pPackets, 0, sizeof(pPackets));
	{
		double iDeadline = exampleTimerLimit(3000);

		while ( xrtNetUdpQueued(pServer) < 3u ) {
			if ( exampleTimerExpired(iDeadline) ) {
				iResult = 6;
				goto Cleanup;
			}
			xrtThreadYield();
		}
		iCount = xrtNetUdpReceiveBatch(pServer, pPackets, 8);
		if ( iCount != 3u ) {
			iResult = 6;
			goto Cleanup;
		}
	}
	printf("pull batches: %zu packets in 1 batch\n", iCount);
	for ( i = 0; i < iCount; ++i ) {
		xrtNetUdpPacketDestroy(pPackets[i]);
	}

	/* 阻塞批量:两条齐发,BatchPacket 借用 + BatchTake 转移。 */
	for ( i = 0; i < 2u; ++i ) {
		if ( xrtNetUdpSend(pClient, "ww", 2) != XNET_RESULT_OK ) {
			goto Cleanup;
		}
	}
	{
		double iDeadline = exampleTimerLimit(3000);

		while ( xrtNetUdpQueued(pServer) < 2u ) {
			if ( exampleTimerExpired(iDeadline) ) {
				iResult = 7;
				goto Cleanup;
			}
			xrtThreadYield();
		}
	}
	pBatch = xrtNetUdpReceiveBatchWait(pServer, 4,3000, NULL);
	if ( (pBatch == NULL) || (xrtNetUdpBatchCount(pBatch) < 1u) ) {
		iResult = 7;
		goto Cleanup;
	}
	{
		xnetudppacket* pBorrowed = xrtNetUdpBatchPacket(pBatch, 0);
		xnetudppacket* pTaken = xrtNetUdpBatchTake(pBatch,
			xrtNetUdpBatchCount(pBatch) - 1u);

		if ( (pBorrowed == NULL) || (pTaken == NULL) ||
			 (xrtNetUdpPacketSize(pBorrowed) != 2u) ||
			 (xrtNetUdpBatchPacket(pBatch,
				xrtNetUdpBatchCount(pBatch) - 1u) != NULL) ) {
			xrtNetUdpPacketDestroy(pTaken);
			iResult = 8;
			goto Cleanup;
		}
		printf("wait batch: %zu packets (borrow=take=ok)\n",
			xrtNetUdpBatchCount(pBatch));
		xrtNetUdpPacketDestroy(pTaken);
	}
	xrtNetUdpBatchDestroy(pBatch);
	pBatch = NULL;

	/* Future 批量:两条齐发 + 双引用把包与批量带出生命周期。 */
	for ( i = 0; i < 2u; ++i ) {
		if ( xrtNetUdpSend(pClient, "ff", 2) != XNET_RESULT_OK ) {
			goto Cleanup;
		}
	}
	{
		double iDeadline = exampleTimerLimit(3000);

		while ( xrtNetUdpQueued(pServer) < 2u ) {
			if ( exampleTimerExpired(iDeadline) ) {
				iResult = 9;
				goto Cleanup;
			}
			xrtThreadYield();
		}
	}
	pFuture = xrtNetUdpReceiveBatchAsync(pServer, 4);
	if ( !exampleFutureWait(pFuture) ) {
		iResult = 9;
		goto Cleanup;
	}
	pBatch = (xnetudpbatch*)xrtFutureValue(pFuture);
	if ( (pBatch == NULL) || (xrtNetUdpBatchCount(pBatch) < 1u) ) {
		iResult = 10;
		goto Cleanup;
	}
	
	 * 必须自己 Ref(包与整批各一份),不能替 Future 释放。 */
	pKept = xrtNetUdpPacketRef(xrtNetUdpBatchPacket(pBatch, 0));
	pBatchRef = xrtNetUdpBatchRef(pBatch);
	if ( (pKept == NULL) || (pBatchRef == NULL) ) {
		iResult = 11;
		goto Cleanup;
	}
	printf("future batch: %zu packets, ref kept size=%zu\n",
		xrtNetUdpBatchCount(pBatch),
		xrtNetUdpPacketSize(pKept));
	/* 先销毁 Future(释放其引用):两份 Ref 让包与批量继续存活。 */
	xrtFutureDestroy(pFuture);
	pFuture = NULL;
	if ( (xrtNetUdpBatchCount(pBatchRef) < 1u) ||
		 (xrtNetUdpPacketSize(pKept) != 2u) ) {
		iResult = 12;
		goto Cleanup;
	}
	xrtNetUdpPacketDestroy(pKept);
	pKept = NULL;
	xrtNetUdpBatchDestroy(pBatchRef);
	pBatchRef = NULL;
	pBatch = NULL;

	/* 错误队列空路径:拉取 NULL/0 + Future 由关闭终结。 */
	{
		xnetudperrorpacket* pErrors[4];

		printf("error queue: empty pull=%zu/%zu",
			(size_t)(xrtNetUdpReceiveError(pServer) != NULL ?
				1u : 0u),
			xrtNetUdpReceiveErrorBatch(pServer, pErrors, 4));
	}
	pErrorFuture = xrtNetUdpReceiveErrorAsync(pServer);
	(void)xrtNetUdpAbort(pServer);
	if ( !exampleWaitState(pServer, XNET_UDP_CLOSED) ) {
		iResult = 13;
		goto Cleanup;
	}
	/* 关闭使未决错误 Future 进入终态(RESOLVED=1..CLOSED=4)。 */
	(void)xrtFutureWaitFor(pErrorFuture, INT64_C(3000));
	printf(", async terminal=%d\n",
		(int)xrtFutureState(pErrorFuture));

	/* 发送条件:空闲队列立即可写(同步 + Future 双形态)。 */
	printf("writable: sync=%d",
		xrtNetUdpWritable(pClient, 64,3000,
			NULL) ? 1 : 0);
	pWritable = xrtNetUdpWritableAsync(pClient, 64);
	printf(" async=%d\n", exampleFutureWait(pWritable) ? 1 : 0);
	iResult = 0;

Cleanup:
	xrtFutureDestroy(pWritable);
	xrtFutureDestroy(pErrorFuture);
	xrtFutureDestroy(pFuture);
	xrtNetUdpBatchDestroy(pBatch);
	xrtNetUdpBatchDestroy(pBatchRef);
	xrtNetUdpPacketDestroy(pKept);
	if ( pClient != NULL ) {
		(void)xrtNetUdpAbort(pClient);
		(void)exampleWaitState(pClient, XNET_UDP_CLOSED);
	}
	if ( pServer != NULL ) {
		(void)xrtNetUdpAbort(pServer);
		(void)exampleWaitState(pServer, XNET_UDP_CLOSED);
	}
	xrtNetUdpDestroy(pClient);
	xrtNetUdpDestroy(pServer);
	if ( (pEngine != NULL) && !xrtNetEngineDestroy(pEngine) &&
		 (iResult == 0) ) {
		iResult = 14;
	}
	return iResult;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/network/udp_batch/main.c -lws2_32 -liphlpapibatch: truncated prefix=16 of 64pull batches: 3 packets in 1 batchwait batch: 2 packets (borrow=take=ok)future batch: 2 packets, ref kept size=2error queue: empty pull=0/0, async terminal=4writable: sync=1 async=1

What just happened. (1) truncated prefix=16 of 64 — truncated-prefix semantics (a small buffer receiving a big message: truncated to what fits, message size queryable — UDP's within-boundary truncation shape). (2) pull batches: 3 packets in 1 batch — batch pulling: three messages in one call (proof of amortized backlog draining in a single callback). (3) The wait/future lines: blocking wait and Future forms each do one batch receive (borrow=take=ok — both takeaway ownerships of message buffers verified). (4) The error-queue line: empty pull and asynchronous terminal states of the error queue — the RecvError family tested on UDP. (5) The writable line: synchronous and asynchronous sendability probes. The multicast example adds the multicast slot (join → group-address send → receive → leave); the sync/future examples are the blocking/Future siblings — four shapes aligned with the TCP family.

69.4契约

  • Data boundaries: one message, one buffer; no chain, no budget — the fundamental difference from TCP; boundaries are never lost, and neither is ordering (ordering was never promised).
  • Dual shape: connectionless (any source + sendto replies) / connected (connect filter + fixed destination) — choose by role.
  • Asynchronous lifecycle: bind/connect transitional-state management (State polling or event waiting + deadline).
  • Same callback face: Read/Write/Close events; pull-style send/receive; Worker constraints and Post as with Stream.
  • Multicast: join/leave + TTL; send/receive as unicast. The batch family amortizes high PPS.
  • Asynchronous errors: ICMP/PMTU-class errors gated by platform capability (RecvError) — the "unreachable" feedback of unreliable messages.

From examples to engineering: UDP's three hosts

Discovery and registration class (multicast's home turf): service discovery (the multicast example's challenge edition), configuration distribution, heartbeat broadcast — small messages + multicast + connectionless low overhead; multicast TTL controls scope (TTL=1 confines to the local segment). Real-time streaming class (unreliability as a feature): audio/video, games, telemetry — fixed-cadence sending, no retransmission of drops (retransmitting stale data is meaningless), seq numbers for reordering detection. Request-response class (reliability built at the application layer): DNS-style queries, simple RPC — timeout + retry + request-ID pairing (the standard layer of the advanced exercise). The common divide of the three hosts is the cost of packet loss: discovery doesn't care, real-time must not retransmit, request-response must retry — build the application layer by cost, rather than defaulting to all-TCP or bare-UDP recklessness.

UDP's standing in the QUIC era

UDP's standing in the modern network stack deserves a chapter-level cognitive update: QUIC/HTTP3 all run on UDP — reliable transport, flow control, and encryption rebuilt at the application layer (above UDP). Why: the kernel rigidity of TCP (middlebox tampering, slow upgrades, head-of-line blocking) forced innovation down onto the UDP carrier. XRT's UDP module supplies the engine-shaped foundation for this (callback face/batching/multicast/PMTU) — raw material for QUIC-class protocols. It complements XRT's own TCP stack (an engine wrapper over kernel TCP): standard web uses TCP (the gold-standard chapter); custom protocols and low-latency scenarios start from UDP raw material. The modern answer to TCP versus UDP: standard-protocol needs take TCP; controllable protocol evolution takes UDP raw material — no longer just the old "reliable versus fast" dichotomy.

Observation and security: UDP's two weak spots

A UDP service's weak observation spots need proactive shoring (Chapter 50's pipeline). Loss is invisible: UDP has no retransmission — only the application layer knows how much was lost (seq-gap analysis — seq continuity statistics into logs); the security face: UDP is easy to spoof (no handshake verification) — the defense points against amplification reflection attacks (DNS amplification is the classic): response-size limits (a 64-byte request must not earn a 4000-byte reply), destination verification (connected-form filtering), rate limiting. Walk both weak spots before a service ships — UDP's "simplicity" is API simplicity; the operations and security homework only grows.

Common Mistakes

Pitfall 1: reading UDP connect as TCP connect:Pitfall 1: reading UDP connect as TCP connect

Symptoms: believing "connect succeeded = the peer is reachable" — in fact connect succeeds even when the peer doesn't exist (UDP connect only sets a filter); or believing post-connect reliability — drops continue as usual.

Cause: UDP connect is a local filter setting — no handshake, no confirmation, no change to the unreliable nature.

错误写法
if ( xrtNetUdpConnect(Udp, &Peer) ) {
	AssumeReachable(Peer);   /* wrong — UDP connect does not verify the peer exists */
}
正确写法
xrtNetUdpConnect(Udp, &Peer);   /* sets filter + default destination — a local operation */
SendRequest(Udp);
if ( !WaitResponse(Udp, Deadline) ) {   /* reachability is verified by application-layer request-response */
	RetryOrFail();
}
Pitfall 2: message size exceeding the path MTU:Pitfall 2: message size exceeding the path MTU

Symptoms: fine on local loopback, dropped across network segments — big messages discarded (fragmentation disabled or an intermediate router's smaller MTU); occasional partial arrival (partial fragment loss).

Cause: the UDP message exceeds the path MTU — kernel fragmentation (partial loss = whole message lost) or a PMTU black hole (DF bit + filtered ICMP).

错误写法
char Big[65000];
Fill(Big);
xrtNetUdpSend(Udp, Big, sizeof(Big));   /* over MTU — near-certain whole-message loss across segments */
正确写法
/* application-layer limit within a safe size (commonly below 1200-1400 bytes); big messages go over TCP or are app-fragmented */
char Packet[1200];
xrtNetUdpSend(Udp, Packet, sizeof(Packet));

Programming Exercises

Basic: dual-shape loopback reproduced

Reproduce the main example; then add a "second client" sending to the server — verify the connectionless endpoint receives from any source while the connected form receives only its locked peer (the second client's packets are filtered).

Advanced: request-response timeout retry

The standard reliability layer for a UDP client: send request → wait with deadline → retry on timeout (exponential backoff) → report after N failures; the server simulates loss (randomly dropping 50%) to verify retry convergence. Hint: the deadline family + Chapter 54's cancellation.

Challenge: a multicast discovery service

LAN service discovery: servers join a multicast group and broadcast themselves periodically; clients join the group to receive discovery messages; unicast connection confirms. Acceptance: with multiple servers broadcasting concurrently, the client enumerates all; join/leave clean (no leaks); broadcast-storm control (interval + TTL); the whole discover-to-connect chain logged and auditable.

Summary

TopicQuick reference
Data boundariesone message, one buffer — the kernel keeps boundaries; no chain, no budget
Dual shapeconnectionless (any source + sendto) / connected (connect filter + fixed destination)
Lifecycleasync bind/connect state machine + deadline polling / event waiting
Callback faceRead/Write/Close as with Stream; pull-style send/receive; same Worker constraints
Batch familymulti-message send/receive at once — high-PPS amortization; Msg family with address vectors
Multicastjoin/leave + TTL; send/receive as unicast
MTUkeep messages within a safe size (~1200); PMTU errors gated by capability

📖 API references in this chapter:udp (87 entries)net (247 entries)

Click for full signatures, parameter constraints, return values and error notes