首页 / 资讯中心 / 文章详情

UDP协议排查实战:从报文解析到Zynq嵌入式应用

UDP协议排查实战:从报文解析到Zynq嵌入式应用 ★ FEATURED ARTICLE
做网络这行谁还没被UDP坑过几次表面上UDP协议就一个头加一段数据比TCP简单得多可真到了排查问题的时候丢包、乱序、校验和报错、端口假通……一个比一个头大。我整理这套UDP用户数据报练习题不是为了出几道笔试填空而是把日常抓包、端口探测、性能压测、协议栈排错这些活儿还原成一个个能上手操作的题目。适合正在啃《计算机网络》的学生、刚接手服务器网络排查的运维以及搞Xilinx Zynq这类嵌入式平台的朋友。刷完这套题你至少能独立搞定UDP报文解析、连通性验证、iperf3打流、Wireshark分析这几个高频场景踩坑少一半。1. 先来一道报文解析题UDP头里的门道1.1 题目拆开一个UDP报文的五脏六腑拿到一串十六进制报文比如从Wireshark里拷出来的这几行00 35 12 34 00 1c 7c 4b这正好是一个UDP数据报的前8个字节。我们逐字段拆00 35源端口十六进制0x0035十进制53也就是DNS端口。12 34目的端口十进制4660。00 1cUDP长度0x001c 28字节。这个28指的是UDP头8字节加上数据20字节。别惊讶DNS请求通常就二三十字节。7c 4b校验和。这个校验和是伪首部加UDP头加数据一起算出来的不是只算UDP头。做这种解析题最容易错的是长度字段。很多人把UDP长度理解成数据长度实际上是UDP数据报长度 8字节头 payload。如果长度字段小于8那就是非法报文直接丢弃。而且IPv4的UDP长度字段最小值是8代表不携带任何数据的空包。再往深一层为什么要引入伪首部因为UDP校验和必须覆盖源IP、目的IP和协议号否则一个UDP数据报在IP层被转发了IP地址变了校验和却不变接收端就没法发现这个包根本不是发给我的。伪首部就是借IP头里的地址信息参与校验保证端到端完整性。这是练习题里最值得琢磨的点。1.2 为什么UDP头没有序号和确认号——与TCP对比有同学问“TCP头那么长UDP头才8字节凭啥少这么多”凭的就是UDP不需要可靠传输。TCP为了实现有序、可靠、流量控制和拥塞控制加了序号、确认号、窗口、标志位一加就是20字节起步。UDP把这些全砍了所以它无连接、不可靠、没有拥塞控制但换来的是低延迟和简单。对比表如下对比项UDPTCP连接状态无连接发完就不管面向连接三次握手和四次挥手头部最小长度8字节20字节可靠性尽力而为不重传确认、重传、序号保证可靠数据边界保留消息边界一次性读取一个数据报字节流需要自行分包拥塞控制没有粗暴地按应用速率发有慢启动、拥塞避免等广播/多播支持不支持典型应用DNS、RTP语音视频、DHCP、TFTPHTTP、FTP、SSH、数据库连接练习题里我常让学员回答为什么视频通话抖动这么厉害还是用UDP因为视频对实时性要求高偶尔丢一帧还能忍但要是因为丢包重传等两三百毫秒整个画面就卡死了。TCP的重传机制在这种场景反而是劣势。搞明白这个对比做后面的端口测试和性能测试题时心里就有底了——UDP测试里看到的丢包TCP测试根本看不到因为TCP把丢失的包默默重传了延迟变大但表面上通。2. 把理论变成命令UDP端口连通性测试2.1 练习题用nc测试一个UDP端口是否通目标确认服务器10.0.0.5上的UDP 5353端口是否接受数据。第一反应是直接用ncnc -uzv 10.0.0.5 5353参数解释-u指定UDP协议。-z扫描模式不发送实际数据。-v输出详细过程。跑完如果输出“Connection to 10.0.0.5 5353 port [udp/*] succeeded!”看着是通了。但这里有个大坑UDP是无连接的nc -z所谓的通其实是收到了对方返回的“ICMP Port Unreachable”才算失败如果对方静默丢包nc就卡住超时最后结果可能是“succeeded”或“timeout”混合状态。所以nc -uz的“成功”不一定可靠。更靠谱的验证是发一个符合预期的应用数据然后看有没有应答。比如目标端口可能是个DNS服务那就发一个DNS querydig 10.0.0.5 example.com A如果对方响应了才算真正通。如果没有dig就用python一发import socket s socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.settimeout(2) try: s.sendto(b\x01\x02, (10.0.0.5, 5353)) data, addr s.recvfrom(1024) print(got response:, data) except socket.timeout: print(no response / port may be filtered)这道练习的核心是UDP很难只靠TCP式探测判断通不通必须结合应用层回应或抓包。2.2 练习题用socat搭建一个UDP echo服务来验证通信手头没有现成的UDP服务时自己造一个。socat是个瑞士军刀一条命令就能起UDP echosudo socat -v UDP-LISTEN:9876,fork EXEC:/bin/catUDP-LISTEN:9876监听UDP9876端口fork让每来一个数据报启动一个子进程处理EXEC:/bin/cat把收到的数据原样回给对端。另一台机器或本机发送echo hello udp | socat - UDP:127.0.0.1:9876注意socat的UDP监听默认不会自动回包必须通过EXEC指定一个能回显的程序。-v选项会把交换的原始数据打印出来方便对照。如果echo收到了原始数据说明从发送端到服务端的路径是通的双向通信也没问题。我一般会再抓个包确认tcpdump -i lo udp port 9876 -vv在另一个终端跑echo测试tcpdump应能看到包从127.0.0.1.9876进又从9876发回去。这样就把“端口通不通”从玄学变成了证据链。做这道题时记住UDP测试的核心不是“连上”而是“来回有数据”。没有应用层回执的通都不算真通。3. 性能测试必修课iperf3 UDP打流3.1 题目用iperf3测试UDP带宽和丢包率iperf3是测网络性能的标配工具支持TCP但也特别适合UDP打流。因为TCP会重传掩盖网络丢包UDP不打不重传测出来的丢包率是真实网络的“裸性能”。服务端先起UDP模式iperf3 -s -u -p 5201-u让socket用SOCK_DGRAM-p指定端口。客户端往服务端灌流量iperf3 -c 10.0.0.5 -u -b 100M -t 30 -i 1-b 100M目标带宽100Mbps。这里注意UDP打流如果不指定-biperf3默认只发1Mbps很多人跑完看结果傻眼以为带宽就1M其实是没设速率。-t 30测30秒。-i 1每秒打印一次统计数据。跑完后屏幕末尾会给出汇总[ ID] Interval Transfer Bitrate Jitter Lost/Total Datagrams [ 5] 0.00-30.00 sec 350 MBytes 98.0 Mbits/sec 1.234 ms 1234/334567 (0.37%)这里要会读三个关键值Bitrate实际吞吐接近-b设置值说明网络能扛住。Jitter抖动单位毫秒语音视频场景这个值越稳定越低越好。Lost/Total Datagrams丢包统计。括号里的0.37%就是丢包率。丢包率太高就要结合网络路径、防火墙限速、网卡队列等逐个排查。练习题变体把-b从10M逐步调到500M同时观察丢包率拐点。找到那个丢包率突然飙升的速率基本就是当前路径的UDP稳定临界值。这个拐点对规划视频码率、避免链路拥塞很有参考价值。3.2 练习题调整发送速率和包大小对结果的影响iperf3除了调带宽还有个-l参数控制UDP负载长度。比如iperf3 -c 10.0.0.5 -u -b 100M -l 1400 -t 10-l 1400是典型的接近MTU大小。如果设成1500以上比如-l 2000UDP数据报会超过以太网MTU触发IP分片。在以太网里IP头20字节UDP头8字节负载1472字节刚好凑1500字节MTU超过就会分片。而有些防火墙或中间设备会丢弃分片包导致明明没有丢帧iperf3却报大量丢包。为了测出真实链路可以先探测MTUping -M do -s 1472 10.0.0.5-M do禁止分片-s 1472发送1472字节ICMP负载加上ICMP头8字节就是1480IP层总共1500。如果ping通说明路径MTU至少1500如果不通则要减小-s逐步摸清临界值。iperf3的-l最好不要超过探测到的MTU这样测出的丢包率才是链路真实品质而不是分片/丢弃造成的假象。另外-b给的太高也可能导致本机发送队列溢出。Linux下可以通过netstat -su查看UDP发送缓冲区溢出计数如果SendBufferErrors或RcvbufErrors非零说明是本地socket缓冲问题而不是网络问题。这套题做完基本就能区分丢包是链路、分区还是本机的锅。4. 深入协议栈抓包与UDP校验和诊断4.1 练习题用Wireshark分析一个UDP包抓包是UDP排错的第一步没有抓包全靠猜效率极低。打开Wireshark设置过滤器udp.port 5353然后触发一次mDNS或DNS查询会看到对应UDP包。展开Ethernet II、IP、UDP三层可以看到UDP Source Port: 5353UDP Destination Port: 5353UDP Length: 41UDP Checksum: 0x1234 [unverified]注意Wireshark里显示的[unverified]。这是因为很多现代网卡开启了硬件校验和卸载Checksum Offload数据包在抓取时校验和字段还没有被网卡填充或者抓包工具拿到的校验和是网卡计算后的原始值不一定和实际发到线上的完全一致。所以看到一个“unverified”不要慌不代表包真的坏了。要确认校验和到底对不对最可靠的办法是关掉网卡的checksum offload或者发送端软件强制计算。另一个常见现象Wireshark里能看到UDP包但应用层收不到。这时要检查IP层有没有分片以及目的端口是否有程序监听。抓包过滤器可以加ip.addr 10.0.0.5和udp组合把所有UDP流量都捞出来。4.2 练习题编写Python脚本校验UDP校验和理解了抓包后手算一遍校验和比看十遍文档都有用。UDP校验和算法是“反码求和”。简单说把所有参与计算的16位字加起来如果结果溢出就回卷最后取反。参与计算的包括伪首部、UDP头和数据。下面这段Python代码演示了完整流程import struct def checksum(data): # data 为已按16位拆好的整数列表 total 0 for ch in data: total ch total (total 0xffff) (total 16) # 回卷 return (~total) 0xffff def udp_checksum(src_ip, dst_ip, udp_header, payload): src struct.unpack(!4H, socket.inet_aton(src_ip)) dst struct.unpack(!4H, socket.inet_aton(dst_ip)) proto 17 # UDP udp_len len(udp_header) len(payload) pseudo list(src) list(dst) [proto, udp_len] # 将数据按16位拆分 udp_total list(udp_header) [ (payload[i] 8) | payload[i1] for i in range(0, len(payload) - 1, 2) ] if len(payload) % 2 1: # 奇数长度补零 udp_total.append(payload[-1] 8) data16 pseudo udp_total return checksum(data16)几个必须注意的细节伪首部里的长度字段是UDP长度不是IP总长。数据长度奇数时最后一个字节后面补一个0字节但不在网络上传输只是计算用。计算结果为0x0000时在IPv4里表示“未使用校验和”发送时应该填0xFFFF表示实际算出来的校验和是0避免和“未使用”混淆。验证方法构造一个DNS包和Wireshark抓到的实际校验和对一下。如果对不上先检查伪首部的IP地址是不是源和目的的直连地址别把经过NAT后的地址也算了进去。这道题做完你对“为什么抓包显示校验和错误”就有自己的判断了要么是计算方式不对要么是硬件offload在捣乱要么是传输真的出了问题。5. 嵌入式实战Zynq以太网UDP测试5.1 题目在Zynq上跑一个UDP回环测试Zynq是Xilinx的SoC内部ARM Cortex-A系列和FPGA在同一个芯片里。做以太网UDP测试有两条路一是直接在ARM Linux上写socket程序二是用裸机SDK配lwIP协议栈。不管哪条一个最小的回环服务端就能验证底层链路和收发通路。以Linux为例朴素UDP回环#include stdio.h #include string.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h int main() { int sock socket(AF_INET, SOCK_DGRAM, 0); struct sockaddr_in addr { .sin_family AF_INET, .sin_addr.s_addr htonl(INADDR_ANY), .sin_port htons(5005) }; bind(sock, (struct sockaddr*)addr, sizeof(addr)); char buf[2048]; struct sockaddr_in cli; socklen_t len sizeof(cli); while (1) { ssize_t n recvfrom(sock, buf, sizeof(buf), 0, (struct sockaddr*)cli, len); sendto(sock, buf, n, 0, (struct sockaddr*)cli, len); } }在PC上用nc或之前说的echo命令往Zynq的IP发送udp到5005端口能收到回包说明ARM Linux侧网络通路没问题。如果没回包优先tcpdump -i eth0 udp port 5005看包有没有到内核到了但应用没收到检查防火墙是不是把UDP拦了。用裸机SDK更常见的是用lwIP例程“lwIP Echo Server”里面自带一个UDP echo任务直接用工具发数据就行。Zynq的SDK里还能配置MAC地址这里有个坑默认工程里的MAC地址往往是一模一样的比如唯一标识符没填。如果多块板子接到同一网络就会产生IP地址或MAC冲突表现就是时通时不通。测试前务必改掉MAC地址和静态IP。5.2 嵌入式UDP测试的几个深坑嵌入式的UDP测试与纯PC环境最大的区别在于协议栈跑在ARM上DMA和缓存一致性要做对。我碰到过几次典型的坑DMA描述符没初始化好裸机下用AXI Ethernet或emacps驱动时DMA描述符数量、地址对齐没配置会导致发一个包丢一个包甚至直接卡死。排查时先看驱动里描述符池有没有初始化、接收BD地址是否合理。缓存一致性问题ARM CPU和DMA缓存不一致CPU读到的数据可能还是旧内容表现为收到的UDP包内容不对或者校验和错。解决方法是把接收缓冲区设为非缓存如使用dma_alloc_coherent分配或者在接收前后做cache flush/invalidate。巨帧Jumbo Frame不支持Zynq的GEM控制器有些配置默认MTU1500但有人想提高吞吐强行发9000字节UDP帧结果硬件支持不了包被驱动器丢弃。这时用ifconfig eth0 mtu 9000验证但前提是PHY和交换机能支持。ARP解析失败嵌入式端要发UDP必须先发ARP请求找网关或对端MAC。如果ARP表没建立socket发送就返回“Network is unreachable”或“No ARP entries”。测试前先ping一下对端把ARP缓存填上再跑UDP。这个最简单也最容易忽略。处理这类问题时我有两个常用工具在Zynq里用cat /proc/net/udp看socket收发队列用ethtool -S eth0看收发计数和丢包计数。把这些数值和抓包结果对应起来基本能定位到是MAC层、DMA层还是协议栈层的问题。嵌入式不比PC不能随时重启网卡所以每改一处寄存器配置都要回到最小测试用例验证别同时改一堆东西。6. 常见问题与排查技巧收录6.1 问题速查表现象可能原因快速排查方法UDP端口“通”但应用无响应防火墙丢弃ICMP端口不可达服务不回复UDP抓包看对方是否收到确认服务是否在监听UDP丢包率很高网络拥塞发送速率超链路中间设备限速用iperf3降速测试netstat -su看本机溢出计数收到包但应用层无法读取接收缓冲区太小应用阻塞在读操作sysctl net.core.rmem_max调整ss -ulnp确认端口包内容损坏链路错误网卡校验和offload未开关闭checksum offload后重抓包重传可靠数据应改用TCP多个包到达顺序混乱UDP不保证有序多路径分发应用层自行处理包序改用TCP或增加序列号Zynq收不到包DMA描述符不足ARP未完成MAC冲突查看ethool统计先ping通再发UDP检查缓存一致性6.2 排查套路从下往上还是从上往下我调试UDP问题一贯的思路是“先抓包再定范围”。最忌讳一开始就怀疑芯片或驱动绝大多数UDP异常其实死在应用层或配置上。第一步tcpdump -i any udp and host 10.0.0.5看包有没有到本机网卡。第二步包到了用ss -ulnp确认端口有没有被进程监听。第三步监听没错把接收端loopback一下排除协议栈问题。第四步跨主机测试时确认中间设备有没有对UDP做策略或NAT改写可以对比两端抓包的校验和和端口。另外pingICMP只能证明三层通不能代表UDP通UDP是个轻量协议但排查它的故障往往比TCP更费劲因为一切都“尽力而为”没有重传确认来帮你定位。所以每次测试我都要求自己留下抓包文件不然过两天复现问题时什么证据都没有只能重来一遍。最后分享一个我觉得很顺手的经验排查UDP问题前先在本地回环127.0.0.1上把程序自测一遍。回环走的是虚拟接口能排除网卡、线缆、交换机的干扰。如果回环都收发不正常那肯定是socket代码写错了回环没问题再放到真机、真线、真网络上跑。用这个顺序我至少过滤掉了九成的“它突然就不通了”的假故障。剩下的就交给抓包说话。
阅读完成 · 觉得有帮助?
咨询建站