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
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:
*/ #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; }
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):
*/ #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; }
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
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(); }
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
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).
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.
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
| Topic | Quick reference |
|---|---|
| Data boundaries | one message, one buffer — the kernel keeps boundaries; no chain, no budget |
| Dual shape | connectionless (any source + sendto) / connected (connect filter + fixed destination) |
| Lifecycle | async bind/connect state machine + deadline polling / event waiting |
| Callback face | Read/Write/Close as with Stream; pull-style send/receive; same Worker constraints |
| Batch family | multi-message send/receive at once — high-PPS amortization; Msg family with address vectors |
| Multicast | join/leave + TTL; send/receive as unicast |
| MTU | keep 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