ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

深入UDP:报文头、IP分片与C#分包组包实战

深入UDP:报文头、IP分片与C#分包组包实战 1. 为什么先聊UDP一个“不可靠协议”撑起了半个互联网做网络排查做到今天我手机里存得最多的不是TCP抓包反而是UDP那一堆看上去“没头没尾”的数据报。之所以这么说是因为TCP出了问题往往有重传、有状态、有日志可循而UDP出问题只有一个症状数据丢了而且丢得悄无声息。传输层协议的UDPUser Datagram Protocol用户数据报协议是TCP/IP协议族里最容易被忽略、却又最离不开的一个。本文不是教科书式的讲解而是结合我做过的真实排障、打流测试和C#开发经验把UDP从报文结构一直拆到分包组包实战。1.1 一次真实故障排查的起点去年有个项目客户视频会议系统频繁出现画面马赛克、声音断续网络工程师查了一圈路由和防火墙都说“链路没问题”。我上去之后没看别的直接在两台主机之间跑了三分钟UDP打流结果丢包率稳定在8%左右。TCP测速看起来一切正常因为TCP有重传、有窗口调整把丢包都“消化”掉了而UDP把所有问题赤裸裸地摆到桌面上。从那时起我就养成了一个习惯任何“测了TCP没问题但业务体验差”的场景先补一轮UDP测试。这也是我写这篇文章的初衷。UDP不是“简化版的TCP”它是一个独立的、面向数据报的传输层协议有自己的头部结构、校验规则、分片逻辑和socket行为。理解了它你才能真正理解DNS查询为什么快、语音为什么延迟敏感、游戏同步为什么用UDP不用TCP、打流工具为什么会报出那么多丢包数。1.2 这篇文章覆盖哪些内容围绕“传输层协议UDP”我按自己平时工作的顺序来组织先拆报文头再对比TCP然后看IP分片、协议栈行为接着用iperf3实测网络质量最后落到C#程序里的分包组包和排障工具。每一部分都会给出可直接复用的参数、命令和代码也会聊聊那些文档里不写、只有动手踩过坑才知道的细节。2. 报文结构拆解8字节头里的四个字段UDP最让我感慨的地方是它用8个字节的头部承载了与TCP完全不同的设计哲学。TCP头部最短20字节塞满了序号、确认号、标志位、窗口大小为的是构建一个复杂的“状态机”UDP则把所有状态统统扔掉只留下四个字段源端口、目的端口、长度、校验和。2.1 四个字段各自干什么字段长度含义与注意点源端口16 bit发送方端口可为0比如某些探测报文不关心回包端口目的端口16 bit接收方端口用来把数据报交给上层对应应用长度16 bitUDP头部数据的总字节数最小值是8只有头部、没有数据校验和16 bit覆盖伪首部、UDP头和数据用于差错检测这里有个初学者容易忽略的点UDP的“长度”字段是冗余的因为IP头里已经有总长度IP层能算出来UDP数据报到哪里结束。为什么还要再写一遍主要是当年设计时考虑到了UDP数据报可以被多个进程复用同一IP包等情况让接收方能独立解析不依赖IP层信息。你把它理解成一个“给上层协议的便利贴”就行。2.2 校验和伪首部的特殊玩法UDP校验和不是只算UDP头和数据它会把IP层的一些信息也拉进来这部分叫“伪首部”pseudo header。伪首部内容包括源IP地址、目的IP地址、协议号UDP是17、UDP长度。校验时把这12个字节临时拼在UDP报文前面按16位逐字累加、取反码。为什么要把IP地址也算进来因为UDP本身没有连接概念收方不知道这个报文到底是谁发给自己的。如果IP层转发过程中地址出错而UDP又不校验IP地址接收方可能把发给别人的数据当自己的收下。伪首部就是UDP“占IP的便宜”做的一次地址合法性校验。IPv4下UDP校验和允许为0表示不校验但实际网络中几乎都填了IPv6下校验和则是强制的因为IPv6头里没有校验和字段链路层又不保证可靠只能靠UDP或上层协议兜底。我自己抓包时如果看到checksum为0的UDP报文第一反应就是抓包工具的校验和卸载checksum offload导致显示问题而不是报文真没算。2.3 用Wireshark看一个真实的UDP报文打开Wireshark随便过滤一个DNS查询udp.port 53点开一条能看到这样的结构User Datagram Protocol, Src Port: 54321, Dst Port: 53 Source Port: 54321 Destination Port: 53 Length: 44 Checksum: 0x4a2b [unverified] [Checksum Status: Unverified]这44 8字节UDP头 36字节DNS查询数据。抓包工具经常显示“Unverified”是因为本机网卡开启了checksum offload真实报文里的校验和由网卡硬件计算软件抓到的可能是占位值。这是新手经常被吓到的地方不是协议出了问题。3. TCP与UDP的本质差异从“寄快递和打视频电话”说起很多文章喜欢用“TCP可靠、UDP不可靠”来概括这话没错但容易让人误解成“UDP是劣化的TCP”。我更喜欢用两个生活场景来类比TCP像寄快递有单号、可追踪、丢了包会补发、顺序乱了会重新排列UDP像打视频电话每一帧画面都是“现在这一刻”的内容如果这一帧丢了补发一帧旧的毫无意义不如直接跳过继续播下一帧。3.1 六张对比表看懂两者差在哪对比项TCPUDP连接状态面向连接需要三次握手无连接直接发包可靠性确认、重传、超时尽力而为不保证送达顺序性按序号重组保序交付不保序乱序要靠应用处理流量控制滑动窗口、慢启动无拥塞控制有遇丢包主动降速无照样发头部大小20~60字节8字节固定传输模式字节流数据报保留消息边界注意最后一行TCP是字节流你send两次8字节对方可能一次收到16字节也可能收到三次而UDP是数据报模型一次sendto对应一次recvfrom的边界对端recvfrom一次就能取到完整的那次发送内容前提是报文没被分片拆乱、也没超过接收缓冲区。这个“消息边界”特性是很多人选UDP做自定义协议的根本原因。3.2 流量控制、拥塞控制与顺序问题TCP维护序号和确认号接收方能检测丢失、重复、乱序并请求重传UDP的头部里压根没有序号这个字段。所以UDP接收方无法判断数据报谁先谁后丢没丢也不知道。这不是UDP的缺陷而是设计取舍把这些开销全部移交给应用层换取极低的解析延迟和极少的协议开销。以语音通话为例一个G.711语音包大约几十毫秒的采集量。如果走TCP一旦网络抖动导致某个包重传接收方为了保持顺序就得缓存后续数据通话延迟会越积越大最终彻底无法对话。UDP配合RTP头部里的时间戳和序号让接收方可以独立处理每一帧丢了就丢延迟始终可控。这就是“实时性优先”场景下UDP的不可替代性。3.3 典型应用场景与选型思路必须用UDP的DNS域名解析、DHCP动态获取IP、TFTP简单文件传输、SNMP网络管理、syslog日志上报。基于UDP构建上层协议的RTP/RTSP音视频、QUICHTTP/3的传输底座本质是基于UDP实现可靠传输、游戏同步协议。必须用TCP的HTTP网页加载、文件下载、邮件传输、需要数据完整性的接口调用。选型时我一般问三个问题数据丢一帧能不能忍受延迟敏感还是吞吐敏感消息有没有明确的边界如果三个问题的答案分别是“能忍”“延迟敏感”“有边界”那就大胆用UDP然后自己在应用层做可靠性和顺序处理。4. UDP与IP分片一个数据报是怎么被“切碎”的“udp划分ip数据报片”是热搜词里出现频率很高的一个说法翻译过来就是UDP数据报的IP分片问题。这也是UDP排障里最容易出幺蛾子的地方值得单独拎出来讲透。4.1 IP分片基础MTU、标识、标志与片偏移IP层面对不同链路有最大传输单元MTU限制以太网通常1500字节。如果IP包超过MTU就必须切成多片发送。分片主要依赖IP头里三个字段标识Identification、标志Flags、片偏移Fragment Offset。标识同一个原始数据报的所有分片共享同一个标识值接收端据此归组。标志DFDont Fragment禁止分片和MFMore Fragments还有后续分片。片偏移以8字节为单位标明本片在原始数据报中的位置。对于UDP来说应用层一次sendto的数据加上IP头、UDP头如果超过了MTU就会触发IP分片。一个常见误区是“UDP自己会分片”其实UDP头里没有分片字段分片完全由IP层完成UDP对此一无所知。4.2 一个2000字节UDP报文的切分全过程假设应用层发送了2000字节的UDP数据头部开销20字节IP头8字节UDP头整个IP包总长2028字节超过1500的MTU。IP层会这样处理第一个分片IP头20字节 UDP头8字节 1472字节数据 1500字节正好MTU大小MF置1片偏移0。剩余数据2000 - 1472 528字节第二个分片IP头20字节 528字节数据 548字节MF置0片偏移为1480/8185。这里有一个数值值得记住以太网MTU 1500下UDP单次能携带的最大数据是1472字节。超过这个数就要承担被IP分片的风险超过65507字节65535-20-8sendto直接报错内核根本不接受。抓包时可以用Wireshark的过滤器快速识别分片ip.flags.mf 1 || ip.frag_offset 0然后按ip.id分组看同一标识的所有分片。4.3 分片丢失的连锁反应为什么大家都不想分片IP分片最麻烦的地方在于“全有或全无”分片各自独立传输只要其中一片丢失接收方的IP层就无法组装出完整数据报UDP层只能丢掉这个不完整的报文其他所有分片全部白传。TCP遇到分片丢失还能靠重传兜底UDP丢了一片就等于整个应用层数据消失连个通知都没有。所以做UDP传输时我通常直接把应用层数据控制在1400字节以内给IP头、UDP头和各种隧道封装留余量主动避开分片。如果是视频、日志这种大块数据就在应用层分包而不是把大报文丢给IP层去切。这样既能避免分片导致的级联丢失也能减少重组开销。5. UDP协议栈与组播内核里的事和IGMP的边界光看报文不够还得知道UDP在操作系统协议栈里怎么走。很多“奇怪现象”其实是内核缓冲、校验和卸载或组播机制造成的。5.1 UDP在协议栈中的位置从应用层看UDP就是socket的一个类型SOCK_DGRAM。调用sendto时数据逐个穿过应用层缓冲区、UDP层、IP层、链路层调用recvfrom时内核把收到的UDP数据报按目的端口投递到对应socket的接收队列。关键区别在于TCP的发送有拥塞控制发不动时会阻塞在发送缓冲区UDP没有拥塞控制发送端只要socket发送缓冲区放得下就发送放不下会返回EWOULDBLOCK或ENOBUFS对应C#里的WSAENOBUFS。接收端也一样如果应用来不及取数据内核接收队列满了之后新报文直接丢弃。5.2 内核接收缓冲与丢包统计Linux下UDP的丢包数据藏在两个地方cat /proc/net/snmp | grep Udp netstat -su输出的RcvbufErrors和SndbufErrors分别代表接收缓冲满和发送缓冲满导致的丢包次数InErrors包含各种接收错误。排查时如果看到RcvbufErrors持续增长说明应用层处理速度跟不上报文到达速率需要调大rmem或优化收包逻辑。实测排障时有个小技巧先用ss -unap查看每个UDP socket的接收队列大小和积压字节数。如果积压一直在涨不用看抓包就知道是应用消费太慢如果积压为0但抓包显示有包到达那多半是内核已经丢掉了。5.3 组播和IGMP与UDP相关的另一层协议热搜词里还有IGMP这里顺带把边界讲清楚。IGMP是网络层协议负责管理主机与组播路由器之间的组成员关系而UDP是传输层协议负责承载数据。两者常被放在一起提是因为UDP是组播数据的典型传输载体应用把UDP报文发往组播地址224.0.0.0/4组播路由器通过IGMP感知组成员并转发副本。在实际项目中视频分发经常用UDP组播服务端只发一份数据组内所有成员都能收到节省带宽。排障重点则有三处主机是否成功加入组播组ip maddr可以看到、路由器IGMP查询和报告是否正常、交换机是否开启了组播侦听。我遇到过很多次“单播正常、组播不通”的情况最后都是IGMP snooping配置问题跟UDP本身没有关系。6. iperf3 UDP打流实测网络到底能收多少包聊完原理进入我最喜欢干的环节用iperf3打流验证网络真实质量。TCP测吞吐容易掩盖丢包UDP打流则把带宽、抖动、丢包率三项指标直接打出来是判断网络健康度最狠的工具。6.1 命令速查服务端与客户端先启动服务端注意UDP模式下服务端也必须带-u参数# 服务端监听5201端口允许UDP测速 iperf3 -s -u客户端按需指定带宽和数据包大小我常用的基准测试是这样# 客户端打100Mbps的UDP流持续30秒每个包1400字节 iperf3 -u -c 192.168.1.10 -b 100M -t 30 -l 1400如果只想测带宽上限把-b调大例如-b 1G如果想贴近真实业务就把-b设为业务码率并用-R--reverse测上行方向。多线程UDP测试用-P 4也能开但要注意多流之间可能互相影响丢包率解读结果时别混为一谈。6.2 输出解读带宽、抖动、丢包率打完流后客户端末尾几行是关键[ ID] Interval Transfer Bitrate Jitter Lost/Total Datagrams [ 5] 0.00-30.00 sec 334 MBytes 93.5 Mbits/sec 0.035 ms 10492/246014 (4.3%)Bitrate实际测得的接收速率。如果设置-b 100M结果只有93.5M说明网络或中间设备开始丢包限速。Jitter抖动单位毫秒。它反映数据报到达间隔的离散程度语音和视频业务对抖动非常敏感一般超过20~30ms体验就会明显变差。Lost/Total Datagrams丢包数/总包数和丢包率。4.3%的UDP丢包率对于文件传输可能勉强能忍但对实时音视频已经不可接受了。输出里还有一条容易被忽略的datagrams received out-of-order乱序包数量。它不在标准表格里但出现大量乱序时说明网络里存在负载均衡或路径不一致应用层要处理乱序。6.3 把UDP打流用在真实排障中我的标准打法分三步。第一步先打低带宽比如-b 1M确认基线是否干净第二步按业务码率打比如视频会议2M观察丢包第三步逐步加码到链路标称带宽的80%看在哪一档开始丢包基本就能定位瓶颈。有一次客户投诉“专线带宽明明买了100M视频还是卡”我打流到50M就出现1%丢包到80M丢包率飙到12%。最后查出来是防火墙开启的QoS策略把UDP流量限速了TCP不受影响。这个案例说明UDP打流的价值不在“测速”而在“暴露中间设备对UDP流量的特殊待遇”。7. C# UDP发送分包组包实战大包传输的正确姿势热搜词里“c# udp 发送 分包 组包”基本是开发者的共同痛点。UDP想传超过1472字节的数据要么承担IP分片风险要么自己在应用层切包。我个人的实践是大文件、大批量数据一律应用层分包原因前面讲过——IP分片丢一片全丢应用层分包至少能定位到哪一片丢了。7.1 为什么不能一次发一个10MB的byte数组很多新手会写出这样的代码udpClient.Send(largeBuffer, largeBuffer.Length, remoteEndPoint);如果largeBuffer.Length只有几百字节没问题一旦超过65507字节Send会直接抛SocketException错误码通常是“消息太长”。即使没超过65507只要超过1472字节也会被IP分片网络稍有波动整个报文就没了。所以正确的姿势是在应用层把大消息切成一堆不超过1400字节的分片每个分片独立发送收端根据分片头重组。7.2 应用层分包协议设计我给项目设计过一个16字节的分片头包含消息标识、分片序号、分片总数和数据长度// 分片头定义Magic(2) Version(1) MsgId(4) FragIndex(2) FragCount(2) DataLen(2) Reserved(3) class FragmentHeader { public ushort Magic; // 固定值比如0x5AA5用于快速识别 public byte Version; // 协议版本 public int MsgId; // 消息唯一标识用时间戳随机数生成 public ushort FragIndex; // 当前分片编号从0开始 public ushort FragCount; // 总分片数 public ushort DataLen; // 本片数据有效长度 public byte[] Reserved; // 预留可放标志位 }分包发送时逻辑很直接int maxPayload 1400 - 16; // 分片头16字节 int fragCount (int)Math.Ceiling((double)data.Length / maxPayload); for (int i 0; i fragCount; i) { int offset i * maxPayload; int len Math.Min(maxPayload, data.Length - offset); // 依次填充header字段把data拷贝到缓冲区偏移16处 // 然后udpClient.Send(buffer, 16 len, remote); }这个方案的核心价值是丢掉某个分片时接收方明确知道缺的是哪一片可以做选择性重传而不是整个消息重来。7.3 收端组装算法与乱序处理UDP不保证顺序所以收端不能假设“先到的就是第一片”。我一般用字典缓存分片Dictionaryint, byte[] fragCache new Dictionaryint, byte[](); int expectedCount 0; DateTime firstArriveTime DateTime.UtcNow;每收到一个分片先检查MsgId是否是新的如果是就创建缓存区域然后把分片按FragIndex写入对应位置当缓存分片数量等于FragCount时把各片拼接起来再交给上层处理。关键在超时处理UDP丢片不会通知你必须自己定一个“等待超时时间”比如500毫秒或1秒超时后丢弃整条消息并记录一条日志。这个超时值的选取要权衡业务实时性和完整性——视频可以考虑丢消息继续文件传输则应该触发重传请求。乱序处理上我的经验是不要尝试在组装层做复杂的排序算法直接按FragIndex填到位即可因为组装的前提是拿到全部分片如果确实出现分片严重乱序导致接收缓冲区积压优先排查网络负载均衡或中间设备而不是在代码里加排序消耗CPU。7.4 常见坑缓冲区不足、端口冲突与粘包C#用UdpClient排障时我踩过几个坑值得单独列一下接收缓冲区太小默认Client.ReceiveBufferSize可能不够高码率下会丢包。建议显式调大udpClient.Client.ReceiveBufferSize 1024 * 1024; // 1MB udpClient.Client.SendBufferSize 1024 * 1024;多个UdpClient绑定同一端口Windows下没有SO_REUSEADDR的话会抛“地址已被使用”。如果是组播接收要设置MulticastLoopback并加入组播组。UDP“粘包”误判虽然UDP有消息边界但如果对方一次发送的数据超过了接收端设置的缓冲区大小Receive会截断并丢掉剩余部分。所以接收缓冲区必须大于等于单个数据报最大长度。8. 排障工具链与个人经验最后把我平时查UDP问题用到的工具串一遍算是给前面所有内容收个尾。8.1 几个我用得很顺手的UDP排查命令工具/命令用途典型用法iperf3UDP带宽、抖动、丢包测试iperf3 -u -c 目标 -b 10M -t 10netstat / ss查看UDP端口监听和收发统计netstat -su、ss -unapWireshark抓包看分片、乱序、checksum过滤udp、ip.frag_offset 0tcpdumpLinux命令行抓UDPtcpdump -i eth0 udp port 5201 -w cap.pcapping -f附带测试网络丢包基线ping -f -l 1400 目标Windows说明一下ping -f测的是ICMP和UDP不完全等价但用来判断链路整体丢包率是一个低成本的前置检查。真正的UDP行为还是以iperf3结果为准。8.2 实测体会UDP“不可靠”不是缺陷是特性做了这么多年UDP相关的项目我最深的体会是UDP所谓“不可靠”其实是一种透明——它不替你隐藏网络问题而是把选择权交还给你。TCP替你重传、替你排序、替你控速但代价是你无法感知真实链路状态也无法实现毫秒级实时控制。UDP恰好相反它把可靠性、顺序性、拥塞控制这些事全部“下放”到应用层让设计者按业务需求定制。我见过把UDP用好的团队也见过在UDP上强行实现TCP那一套导致代码臃肿的团队。关键不在于“用不用UDP”而在于你是否清楚自己的数据丢掉没关系、延迟有上限、消息边界必须保留。如果这三个条件成立UDP就是最优解。反过来如果你要的是完整不落一个字节的文件传输又没有能力处理重传和乱序那就老实选TCP别为了“性能”去折腾UDP。另外提醒一句UDP打流是网络排障利器但别在生产网络高峰期乱跑尤其别拿大带宽去冲击别人的语音和视频业务。我一般只在维护窗口或测试网段里做高负载UDP测试这是基本素养。这篇文章从报文头、TCP对比、IP分片、协议栈、组播、iperf3打流到C#分包组包基本覆盖了我日常接触UDP的完整路径。希望这里的经验和代码能帮你少走一些我当年走过的弯路。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进