本章导读
UDP 模块把数据报装进引擎模型:双形态——无连接端(recvfrom 带来源地址——任意来源、自己过滤)与连接式(connect 后只收该对端——客户端形态);事件驱动收发(引擎回调——与 TCP 同一的回调面);批量族(udp_batch 范例——多报一次收发的摊还);组播(加入/离开组——multicast 范例);PMTU 与异步错误(udp_introspect/udp_errors 范例——ICMP 不可达等平台能力)。UDP 无流控无状态机——但引擎的回调、引用计数、停机协议全部同款。
69.1引入
UDP 与 TCP 的本质差异不是"可靠 vs 不可靠",是数据边界:TCP 是字节流(消息边界自管——第 65 章缓冲链+第 72 章分帧为此存在);UDP 是数据报(一次 send 一个报文、一次 recv 一个完整报文——边界内核保管)。这个差异让 UDP 的 API 形态完全不同:没有发送预算(报文要么进队列要么不进)、没有接收链(一个报文一块缓冲)、没有流控(不可靠是特性——实时音视频宁可丢也不积压)。
两个形态的选择:无连接端(server 形态)——bind 后收任意来源(recvfrom 附来源地址——回复时 sendto 指定目的地);连接式(client 形态)——connect"锁定"对端后只收它(过滤内核做)、send 直接发(无需每次指定)。UDP 的 connect 不握手(只是过滤器+默认目的地)——与 TCP connect(真连接建立)语义完全不同。
69.2概念
双形态与生命周期
异步 bind/connect:UDP 的绑定与连接也是引擎操作(状态机过渡——xrtNetUdpState 轮询或事件等待;udp 范例的 WaitState 辅助演示截止时间轮询)。
事件回调面
UDP 的事件表(与 Stream 同款形态):Read(报文到达——recvfrom/recv 在回调里)、Write(可发)、Close(终态通知)。收发在回调:数据报到达回调触发、回调里拉取(拉取式 recv——报文在队列、回调是通知);批量拉取(一次回调拉多个——积压清理的高效形态)。Worker 约束同 Stream(回调在 Worker、跨线程 Post——第 68 章契约的 UDP 版)。
批量族与报文结构
xnetudppacket(报文+来源地址的载体);批量变体一次操作多个报文(数组提交/收取)——高 PPS 场景(DNS 服务器、监控上报)的摊还入口。Msg 族(SendMsg/RecvMsg——第 64 章端口层同族):带目标地址的向量发送——UDP 的多缓冲变体。
组播与 PMTU
组播(multicast 范例):加入/离开组播组(组地址 + 本地接口)、TTL 控制(跨网段跳数);收发与单播同 API(组地址做目的地)。PMTU 探测与异步错误:send 的报文过大被中途丢弃(分片禁用时)——ICMP "frag needed" 异步回报(RecvError 按平台能力——第 64 章能力门控的 UDP 应用);udp_errors 范例演示错误接收、udp_introspect 演示 PMTU/统计查询。
69.3示例
完整程序:双形态回环
来自仓库范例 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; }
刚才发生了什么。 ① 服务端无连接端 bind(端口 0 动态分配——Local 取回打印 127.0.0.1:NNNNN);状态等待(WaitState 辅助在截止时间内轮询 BOUND——异步 bind 的过渡态管理)。② 客户端连接式 connect 到服务端——只收该对端(服务端的回包)。③ 客户端 send hello UDP——无目的地参数(connect 已锁定);④ 服务端 Read 事件触发、recvfrom 拉取(来源是客户端)→ 打印 received: hello UDP——双形态各在其位。⑤ 回包 sendto 客户端地址——无连接端的回复要指定目的地。
完整程序:批量与组播
来自 examples/network/udp_batch/main.c 与 examples/network/udp_multicast/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; }
刚才发生了什么。 ① truncated prefix=16 of 64——截断前缀语义(小缓冲收大报:截到能装的、报尺寸可查——UDP 的边界内截断形态)。② pull batches: 3 packets in 1 batch——批量拉取:一次调用三报(单次回调消费积压的摊还实证)。③ wait/future 两行:阻塞等待与 Future 形态各做一次批量收(borrow=take=ok——报文缓冲的两种取出所有权都验证)。④ error queue 行:错误队列的空拉与异步终态——RecvError 族的 UDP 实测。⑤ writable 行:同步与异步的可写探测。multicast 范例补组播位(join→组地址 send→收到→leave);sync/future 范例是阻塞/Future 姊妹形态——四形态与 TCP 家族对齐。
69.4契约
- 数据边界:一报一块缓冲、无链无预算——与 TCP 的根本差异;不丢边界也不丢序(序本来不保)。
- 双形态:无连接端(任意来源+sendto 回复)/ 连接式(connect 过滤+固定目的地)——按角色选。
- 异步生命周期:bind/connect 过渡态管理(State 轮询或事件等待+deadline)。
- 回调面同款:Read/Write/Close 事件;拉取式收发;Worker 约束与 Post 同 Stream。
- 组播:join/leave+TTL;收发同单播。批量族高 PPS 摊还。
- 异步错误:ICMP/PMTU 类错误按平台能力门控(RecvError)——不可靠报文的"不可达"反馈。
从示例到工程:UDP 的三个宿主
发现与注册类(组播主场):服务发现(multicast 范例的挑战版)、配置分发、心跳多播——小报文+组播+无连接的低开销形态;组播 TTL 控制范围(TTL=1 限本网段)。实时流类(不可靠是特性):音视频/游戏/遥测——固定节奏发送、丢包不重传(重传过期数据无意义)、seq 号做乱序检测。请求-响应类(可靠性应用层自建):DNS 类查询、简单 RPC——超时+重试+请求 ID 配对(进阶练习的标准层)。三宿主的共同分野是丢包的代价:发现类无所谓、实时类不要重传、请求类必须重试——按代价建应用层,而不是默认 TCP 一把梭或裸 UDP 裸奔。
QUIC 时代的 UDP 地位
UDP 在现代网络栈的地位值得一章级别的认知更新:QUIC/HTTP3 全部跑在 UDP 上——可靠传输、流控、加密在应用层(UDP 之上)重建。为什么:TCP 的内核僵化(中间盒修改、升级慢、队头阻塞)让创新被迫下沉到 UDP 载体。XRT 的 UDP 模块为此提供引擎形态的底座(回调面/批量/组播/PMTU)——QUIC 类协议的实现原料。与 XRT 自身 TCP 栈(内核 TCP 的引擎封装)互补:标准 Web 用 TCP(金标准章)、自定义协议与低延迟场景从 UDP 原料起步。选 TCP 还是 UDP 的现代答案:标准协议需求 TCP、可控协议演进需求 UDP 原料——不再只是"可靠 vs 快"的旧二分。
观测与安全:UDP 的两个薄弱位
UDP 服务观测的薄弱位要主动补强(第 50 章管线)。丢包不可见:UDP 没有重传——丢了多少只有应用层知道(seq 号缺口分析——seq 连续性统计进日志);安全面:UDP 易被伪造源地址(无握手验证)——放大反射攻击(DNS 放大是经典)的防御位:响应尺寸限制(请求 64 字节不回 4000 字节)、目的验证(连接式过滤)、速率限制。两个薄弱位在服务上线前过一遍——UDP 的"简单"是 API 简单,运维与安全的功课不减反增。
常见错误
症状:以为"connect 成功=对端可达"——实际对端不存在 connect 也成功(UDP connect 只设过滤器);或以为 connect 后可靠——丢包照旧。
原因:UDP connect 是本地过滤设置——不握手、不确认、不改变不可靠本性。
if ( xrtNetUdpConnect(Udp, &Peer) ) { AssumeReachable(Peer); /* 错——UDP connect 不验证对端存在 */ }
xrtNetUdpConnect(Udp, &Peer); /* 设置过滤+默认目的地——本地操作 */ SendRequest(Udp); if ( !WaitResponse(Udp, Deadline) ) { /* 可达性靠应用层请求-响应验证 */ RetryOrFail(); }
症状:本机回环测试正常、跨网段丢包——大报文被丢弃(分片禁用或中间路由 MTU 小);偶发部分到达(分片部分丢)。
原因:UDP 报文超过路径 MTU——内核分片(部分丢=整报丢)或 PMTU 黑洞(DF 位+ICMP 被过滤)。
char Big[65000]; Fill(Big); xrtNetUdpSend(Udp, Big, sizeof(Big)); /* 超 MTU——跨网段大概率整报丢弃 */
/* 应用层限制在安全尺寸内(常见 1200-1400 字节以下);大消息走 TCP 或应用分片 */ char Packet[1200]; xrtNetUdpSend(Udp, Packet, sizeof(Packet));
编程练习
复现主示例;再加一个"第二客户端"向服务端发报——验证无连接端收任意来源、连接式只收锁定对端(第二客户端的包被过滤)。
UDP 客户端的标准可靠性层:发请求→限时等响应→超时重试(指数退避)→N 次失败报告;服务端模拟丢包(随机丢弃 50%)验证重试收敛。提示:deadline 族+第 54 章取消。
局域网服务发现:服务端 join 组播组定期广播自己、客户端 join 组收发现报、单播连接确认。验收标准:多服务端并发广播时客户端能枚举全部;join/leave 干净(无泄漏);广播风暴控制(间隔+TTL);发现到连接的全链路进日志可审计。
小结
| 知识点 | 速查 |
|---|---|
| 数据边界 | 一报一缓冲——内核保管边界;无链无预算 |
| 双形态 | 无连接端(任意来源+sendto)/ 连接式(connect 过滤+固定目的地) |
| 生命周期 | 异步 bind/connect 状态机+deadline 轮询/事件等待 |
| 回调面 | Read/Write/Close 同 Stream;拉取式收发;Worker 约束同款 |
| 批量族 | 多报一次收发——高 PPS 摊还;Msg 族带地址向量 |
| 组播 | join/leave+TTL;收发同单播 |
| MTU | 报文限安全尺寸(~1200);PMTU 错误按能力门控 |
📖 本章涉及的 API 参考手册:udp(87 个条目)net(247 个条目)
点击查看完整签名、参数约束、返回值与错误说明