3. UDP协议基础:报文格式、特点与校验和

聊完TCP/IP协议栈的整体框架,咱们该把目光聚焦到UDP上了。说实话,UDP这个协议在很多人眼里就是个“快枪手”——简单粗暴,不跟你搞那些握手、重传的繁文缛节。但你要是真觉得它没技术含量,那就大错特错了。

我刚开始做网络编程那会儿,也觉得UDP不就是发个数据包嘛,能有多复杂?直到有一次在项目中排查丢包问题,折腾了两天才发现是校验和计算出了偏差。嗯,从那以后我再也不敢小看这个“简单”的协议了。

3.1 UDP报文格式:短小精悍

UDP的报文头有多短?固定只有8个字节。你想想看,TCP头部最少都要20字节,UDP直接省了一大半。这就是它快的原因之一。

来看一下UDP报文的具体结构:

字段 长度(字节) 说明
源端口号 2 发送方的端口,可选(可为0)
目的端口号 2 接收方的端口,必填
UDP长度 2 头部+数据的总长度,单位字节
UDP校验和 2 覆盖头部+数据+伪头部,可选(IPv4中可为0)

就这么4个字段,没了。没有序号、没有确认号、没有窗口大小、没有标志位。说白了,UDP就是“我发我的,你收你的,收不到拉倒”。

关键点:UDP长度字段的最小值是8(只有头部,没有数据)。如果收到一个UDP报文,长度字段小于8,那这个包就是非法的,直接丢弃。

3.2 UDP的特点:无连接与不可靠

这两个特点其实是UDP最核心的设计哲学。咱们一个一个说。

3.2.1 无连接

什么叫无连接?就是发送方不需要跟接收方建立任何“关系”。你不需要像TCP那样先发SYN、等SYN-ACK、再回ACK。UDP的做法很简单:

  • 应用层把数据丢给UDP层
  • UDP加上头部,直接扔给IP层
  • IP层封装后发出去

整个过程没有握手、没有状态维护。我记得有一次做视频直播项目,客户端需要同时向多个服务器发送心跳包。如果用TCP,每个连接都要维护一个socket,还要处理重连逻辑,代码量翻倍都不止。换成UDP,一个socket搞定所有目标地址,省心多了。

实用技巧:UDP的无连接特性特别适合“一对多”或“多对多”场景。比如DNS查询、DHCP、SNMP这些协议,底层都是UDP。

3.2.2 不可靠

UDP的不可靠体现在三个方面:

  1. 不保证送达:数据包发出去了,UDP不关心对方有没有收到
  2. 不保证顺序:先发的包可能后到,后发的包可能先到
  3. 不保证不重复:同一个包可能被接收多次

你可能会问:那UDP还有什么用?

其实,不可靠在某些场景下反而是优点。举个例子,你在打游戏时,每个帧的数据都是实时产生的。如果某个包丢了,你希望的是“算了,用下一帧的数据”,而不是“等我把丢的包重传回来”。重传回来的旧数据,反而会让画面卡顿甚至错乱。

避坑指南:我曾经在一个物联网项目里,直接用UDP传输设备配置数据。结果网络抖动时,设备收到了乱序的配置包,导致配置写入错误。后来我在应用层加了序号和简单的重传机制才解决。记住:UDP不保证可靠,但你可以自己在应用层实现可靠。

3.3 UDP校验和计算

UDP校验和是很多人容易忽略的地方。它虽然叫“校验和”,但计算方式跟你想的可能不太一样。

3.3.1 伪头部(Pseudo Header)

UDP校验和覆盖的范围不仅仅是UDP头部和数据,还包括一个“伪头部”。这个伪头部不是真正发送的,只是在计算校验和时临时拼出来的。

伪头部的结构如下:

字段 长度(字节) 内容
源IP地址 4 IPv4源地址
目的IP地址 4 IPv4目的地址
保留 1 全0
协议号 1 UDP为17(0x11)
UDP长度 2 与UDP头部中的长度字段一致

为什么要加伪头部?说白了,就是为了校验数据有没有送错地方。如果IP层把包发错了目标,UDP校验和就能发现——因为伪头部里的IP地址对不上。

3.3.2 计算步骤

校验和的计算过程其实不复杂,我拆成几步来说:

  1. 拼数据:把伪头部 + UDP头部 + UDP数据拼在一起
  2. 补零:如果总长度是奇数,末尾补一个字节的0
  3. 求和:把所有16位(2字节)的数据相加,用二进制反码求和
  4. 取反:把求和结果取反,得到16位的校验和

这里有个细节:反码求和的意思是,如果相加时有进位,要把进位加到低位继续加,直到没有进位为止。

我写个简单的Python示例,帮你理解这个过程:

def udp_checksum(data):
    # data是字节序列,包含伪头部+UDP头部+UDP数据
    if len(data) % 2 == 1:
        data += b'\x00'  # 奇数长度补零
    
    total = 0
    for i in range(0, len(data), 2):
        # 每两个字节拼成一个16位整数
        word = (data[i] << 8) + data[i+1]
        total += word
        # 反码求和:进位加到低位
        total = (total & 0xFFFF) + (total >> 16)
    
    # 取反得到校验和
    return (~total) & 0xFFFF

重要提醒:在IPv4中,UDP校验和是可选的(可以全0)。但在IPv6中,UDP校验和是强制要求的。如果你在做IPv6相关的开发,千万别把校验和设为0,否则接收方会直接丢弃这个包。

3.3.3 校验和验证

接收方收到UDP报文后,会做同样的计算:把伪头部、UDP头部(包括校验和字段本身)、UDP数据拼在一起,用同样的反码求和算法计算。如果结果全为1(即0xFFFF),说明数据没出错。

为什么会这样?因为发送方在校验和字段里填的是取反后的值,接收方把所有数据(包括校验和)加起来,自然就得到全1了。这个设计很巧妙,你想想看,一次计算就能完成校验,不需要额外的比较操作。

个人经验:我在做嵌入式网络协议栈时,发现有些硬件网卡会自动计算和校验UDP校验和。如果你的平台支持硬件卸载(Hardware Offload),记得在驱动里开启这个功能,能省不少CPU开销。

3.4 UDP协议核心逻辑图

下面这张SVG图把UDP协议的核心逻辑串起来了,从应用层数据到最终的报文发送,每一步都清晰可见。

UDP协议核心逻辑图 应用层数据 UDP封装 源端口(2B) | 目的端口(2B) | 长度(2B) | 校验和(2B) 伪头部(12B) + UDP头部(8B) + 数据 校验和计算 反码求和 → 取反 → 填入校验和字段 IP层封装并发送 无连接 无需握手 不可靠 不保证送达/顺序 校验和 检测数据完整性

从这张图可以看得很清楚:UDP做的事情其实很少——封装、计算校验和、交给IP层。没有TCP那些复杂的拥塞控制、流量控制、重传机制。这就是为什么UDP被称为“轻量级传输协议”。