mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
5246 字
15 分钟
UDP与传输基础:最简传输协议
2022-06-05

数据包穿越了 NAT与中间盒 的地址翻译,经过 域内路由BGP与域间路由 的逐跳转发,跨过运营商骨干网和 IXP与互联网交换,终于到达了目的主机的 IP 层。但一台主机上同时跑着浏览器、SSH、DNS 客户端、视频会议——这个数据包该交给谁?

IP 协议只管把数据包送到主机,不管主机上哪个进程来收。从”到主机”到”到进程”这一步跨越,就是传输层的职责。传输层是网络层与应用层之间的桥梁——网络层提供主机到主机的交付,传输层提供进程到进程的交付。

本章从传输层的角色出发,先看最简单的传输层协议 UDP 的报文格式与校验和机制,再深入端口号与多路复用的实现,最后分析 UDP 的应用场景、局限与增强方案,以及 UDP 在 NAT 穿透中的关键角色。

一、传输层的角色#

1.1 从网络层到传输层#

IP 协议的核心抽象是主机到主机的交付——目的 IP 地址标识一台主机,不标识主机上的进程。但实际通信发生在进程之间:浏览器和 Web 服务器通信,SSH 客户端和 SSH 守护进程通信。传输层要解决的就是”数据包到了主机之后,怎么交给正确的进程”。

flowchart TB subgraph 网络层["网络层:主机到主机"] IP1["源主机 10.0.1.5"] IP2["目的主机 10.0.2.10"] IP1 -->|"IP: 10.0.1.5 → 10.0.2.10"| IP2 end subgraph 传输层["传输层:进程到进程"] P1["浏览器<br/>端口 52840"] P2["SSH客户端<br/>端口 49152"] P3["DNS客户端<br/>端口 34567"] P4["Web服务器<br/>端口 443"] P5["SSH守护进程<br/>端口 22"] P6["DNS服务器<br/>端口 53"] end P1 -->|"10.0.1.5:52840 → 10.0.2.10:443"| P4 P2 -->|"10.0.1.5:49152 → 10.0.2.10:22"| P5 P3 -->|"10.0.1.5:34567 → 10.0.2.10:53"| P6 style 网络层 fill:#e3f2fd,stroke:#1565c0 style 传输层 fill:#e8f5e9,stroke:#2e7d32

传输层用端口号区分同一主机上的不同进程。IP 地址 + 端口号的组合就是套接字(socket),唯一标识一个通信端点。一对套接字 (源IP:源端口, 目的IP:目的端口) 唯一标识一条传输层连接。

1.2 传输层的功能#

传输层提供四个核心功能:

  1. 多路复用与分用(Multiplexing/Demultiplexing):多个进程共享同一个网络层接口发送数据(复用),接收端根据端口号将数据分发给正确的进程(分用)
  2. 可靠性(Reliability):确认、重传、排序——保证数据完整有序地到达。UDP 不提供,TCP 提供
  3. 流量控制(Flow Control):防止发送方淹没接收方。UDP 不提供,TCP 用滑动窗口实现
  4. 拥塞控制(Congestion Control):防止发送方淹没网络。UDP 不提供,TCP 用慢启动/拥塞避免实现
Note

UDP 只实现了第 1 项功能——多路复用与分用,外加一个可选的校验和。TCP 实现了全部四项。这不是 UDP 的缺陷,而是设计选择——有些应用不需要可靠性,强加可靠性反而增加延迟。

1.3 传输层协议对比#

维度UDPTCPSCTPQUIC
RFC768929349609000
连接建立三次握手四次握手1-RTT/0-RTT
可靠性确认+重传确认+重传确认+重传
有序交付部分有序流内有序
流量控制滑动窗口滑动窗口基于信用
拥塞控制Cubic/BBR类TCPCubic/BBR
多流
头部大小8 字节20-60 字节12+ 字节变长
传输层IP 之上IP 之上IP 之上UDP 之上
典型应用DNS/视频/游戏Web/SSH/邮件信令/电信HTTP/3

SCTP 在电信信令领域有应用,QUIC 是 HTTP/3 的传输基础(在 QUIC与HTTP/3 中展开)。本章聚焦 UDP——最简单的传输层协议,理解它是理解更复杂协议的基础。

二、UDP报文格式#

2.1 UDP头部结构#

UDP 的报文格式极其简洁——头部只有 8 字节,4 个字段,每个字段 2 字节:

0 7 8 15 16 23 24 31
+--------+--------+--------+--------+
| 源端口 | 目的端口 |
+--------+--------+--------+--------+
| 长度 | 校验和 |
+--------+--------+--------+--------+
| 数据 ... |
+-----------------------------------+
字段长度说明
源端口2 字节发送方端口号,可选(填 0 表示不需要回复)
目的端口2 字节接收方端口号,必填
长度2 字节UDP 头部 + 数据的总长度(最小值 8,即只有头部)
校验和2 字节可选的完整性校验(IPv4 中可填 0 表示不校验,IPv6 中必填)

8 字节头部意味着 UDP 的协议开销极低。对比 TCP 最少 20 字节的头部,UDP 在小包场景下效率优势明显——一个 50 字节的 DNS 查询,UDP 的协议开销占比 16%,TCP 则高达 40%。

2.2 校验和计算#

UDP 校验和覆盖三个部分:伪头部 + UDP 头部 + 数据。伪头部不是 UDP 报文的一部分,而是从 IP 头部中提取的地址信息,用于校验数据包是否送达了正确的目的地址和端口。

#!/usr/bin/env python3
"""UDP 校验和计算演示"""
import struct
import socket
def udp_checksum(src_ip, dst_ip, src_port, dst_port, payload):
"""计算 UDP 校验和"""
# 构造伪头部
src_addr = socket.inet_aton(src_ip)
dst_addr = socket.inet_aton(dst_ip)
protocol = 17 # UDP 协议号
udp_length = 8 + len(payload)
pseudo_header = struct.pack(
'!4s4sBBH',
src_addr, dst_addr, 0, protocol, udp_length
)
# 构造 UDP 头部(校验和字段填 0)
udp_header = struct.pack('!HHHH', src_port, dst_port, udp_length, 0)
# 拼接所有数据
data = pseudo_header + udp_header + payload
# 如果数据长度为奇数,补一个零字节
if len(data) % 2:
data += b'\x00'
# 按 16 位字求和
total = 0
for i in range(0, len(data), 2):
word = (data[i] << 8) + data[i + 1]
total += word
# 回卷进位
while total >> 16:
total = (total & 0xFFFF) + (total >> 16)
# 取反
return ~total & 0xFFFF
# 示例:计算一个 DNS 查询的校验和
src_ip = "192.168.1.100"
dst_ip = "8.8.8.8"
src_port = 52840
dst_port = 53
payload = b'\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00' \
b'\x06google\x03com\x00\x00\x01\x00\x01'
checksum = udp_checksum(src_ip, dst_ip, src_port, dst_port, payload)
print(f"UDP 校验和: 0x{checksum:04x}")

校验和的计算过程:将伪头部、UDP 头部和数据按 16 位字求和,回卷进位,最后取反。接收方将同样的数据(含校验和)求和,结果应为全 1(0xFFFF)——否则说明数据在传输中被篡改。

Warning

IPv4 允许 UDP 校验和填 0(表示不校验),这是历史遗留。IPv6 强制要求 UDP 校验和,因为 IPv6 去掉了 IP 头部校验和,传输层校验成了唯一的端到端完整性保障。生产环境中绝对不要在 IPv4 中关闭 UDP 校验和——链路层校验(如以太网 FCS)只保证逐跳完整性,端到端的中间路由器可能引入错误。

2.3 UDP vs IP校验和#

维度IP 校验和UDP 校验和
覆盖范围仅 IP 头部伪头部 + UDP 头部 + 数据
是否校验数据
是否校验地址仅源/目的 IP源/目的 IP + 源/目的端口
每跳行为每台路由器重新计算(TTL 变化)端到端不变
可选性必填IPv4 可选,IPv6 必填

IP 校验和只保护 IP 头部,每经过一台路由器都要重新计算(因为 TTL 减 1)。UDP 校验和是端到端的——中间路由器不碰传输层头部,所以校验和从源端到目的端不变。这正是 UDP 校验和包含伪头部的原因:如果 IP 头部中的地址被中间路由器篡改(正常转发除外),UDP 校验和能检测出来。

2.4 Wireshark抓包分析#

# 抓取 UDP 流量
sudo tshark -i eth0 -f "udp" -c 10
# 典型输出:
# 1 0.000 192.168.1.100 → 8.8.8.8 UDP 62 52840 → 53 Len=30
# 2 0.032 8.8.8.8 → 192.168.1.100 UDP 78 53 → 52840 Len=46
# 3 0.105 192.168.1.100 → 192.168.1.1 UDP 54 34567 → 53 Len=22
# 过滤特定端口的 UDP
sudo tshark -i eth0 -f "udp port 53" -Y "dns"
# 显示 UDP 头部详细信息
sudo tshark -i eth0 -f "udp" -T fields \
-e ip.src -e ip.dst -e udp.srcport -e udp.dstport \
-e udp.length -e udp.checksum
# 统计 UDP 流量
sudo tshark -i eth0 -z conv,udp -c 1000

三、端口号与多路复用#

3.1 端口号分类#

端口号是 16 位无符号整数,范围 0-65535。IANA(互联网号码分配机构)将端口分为三段:

范围名称说明
0-1023熟知端口(Well-Known)系统级服务,需要 root 权限绑定
1024-49151注册端口(Registered)IANA 注册的应用协议
49152-65535动态/私有端口(Ephemeral)客户端临时使用

常见熟知端口和注册端口:

端口协议传输层说明
20/21FTPTCP文件传输
22SSHTCP远程登录
25SMTPTCP邮件发送
53DNSUDP/TCP域名解析
67/68DHCPUDP地址分配
80HTTPTCPWeb 服务
123NTPUDP时间同步
161/162SNMPUDP网络管理
443HTTPSTCP安全 Web
514SyslogUDP系统日志
1194OpenVPNUDPVPN 隧道
5060SIPUDP/TCPVoIP 信令

注意 DNS 同时使用 UDP 和 TCP——通常查询用 UDP(快),区域传送和超长响应用 TCP(可靠)。NTP、SNMP、Syslog 这类”小包、允许丢”的协议只用 UDP。

3.2 多路复用与分用#

多路复用是发送端的行为:多个进程通过不同的源端口共享同一个 IP 层发送数据。分用是接收端的行为:操作系统根据目的端口将收到的 UDP 数据报分发给对应的进程。

flowchart LR subgraph 发送端复用["发送端:多路复用"] APP1["浏览器<br/>:52840"] APP2["DNS客户端<br/>:34567"] APP3["NTP客户端<br/>:49200"] end subgraph IP层["IP 层"] IP["IP: 192.168.1.100<br/>封装 UDP 头部"] end subgraph 接收端分用["接收端:多路分用"] WEB["Web服务器<br/>:443"] DNS["DNS服务器<br/>:53"] NTPD["NTP守护进程<br/>:123"] end APP1 --> IP APP2 --> IP APP3 --> IP IP -->|"目的端口 443"| WEB IP -->|"目的端口 53"| DNS IP -->|"目的端口 123"| NTPD style 发送端复用 fill:#e3f2fd,stroke:#1565c0 style IP层 fill:#fff3e0,stroke:#e65100 style 接收端分用 fill:#e8f5e9,stroke:#2e7d32

分用的具体过程:操作系统维护一个端口号 → 套接字的映射表。收到 UDP 数据报后,内核提取目的端口号,在映射表中查找对应的套接字,将数据放入该套接字的接收缓冲区。如果没有进程绑定该端口,内核回复 ICMP Port Unreachable(类型 3,代码 3)。

3.3 /etc/services与IANA注册#

Linux 系统的 /etc/services 文件记录了端口与协议的对应关系:

# 查看常见 UDP 服务端口
grep -i udp /etc/services | head -20
# 典型输出:
# domain 53/tcp nameserver # name-domain server
# domain 53/udp nameserver
# bootps 67/udp # BOOTP/DHCP server
# bootpc 68/udp # BOOTP/DHCP client
# tftp 69/udp # Trivial File Transfer
# ntp 123/udp # Network Time Protocol
# snmp 161/udp # Simple Net Mgmt Protocol
# snmptrap 162/udp # Traps for SNMP
# syslog 514/udp # BSD syslog
# 查询特定端口的注册信息
getent services 53
# domain 53/tcp udp

IANA 维护的正式注册表在 https://www.iana.org/assignments/service-names-port-numbers/ 。但端口注册是”建议”而非”强制”——你完全可以在 53 端口跑自己的服务,只是不推荐,因为客户端习惯性地把 53 端口和 DNS 关联。

3.4 端口扫描与安全#

# UDP 端口扫描(需要 root 权限)
sudo nmap -sU -p 53,67,68,123,161,514 192.168.1.1
# 典型输出:
# PORT STATE SERVICE
# 53/udp open domain
# 67/udp open|filtered dhcps
# 68/udp open|filtered dhcpc
# 123/udp open ntp
# 161/udp open snmp
# 514/udp open syslog
# 全 UDP 端口扫描(非常慢,因为 UDP 无响应可能是过滤也可能是开放)
sudo nmap -sU -p 1-65535 --max-retries 1 192.168.1.1
# 快速扫描常见 UDP 端口
sudo nmap -sU -F --version-intensity 0 192.168.1.1

UDP 端口扫描比 TCP 慢得多——TCP 的 SYN 扫描能从 SYN/ACK 或 RST 明确判断端口状态,UDP 只能从 ICMP Port Unreachable 判断关闭,没有响应可能是开放也可能是被防火墙过滤(open|filtered)。

四、UDP的应用场景#

4.1 DNS查询#

DNS 是 UDP 最典型的应用。一个 DNS 查询通常只有几十字节,响应也不超过 512 字节(传统 DNS over UDP 的限制)。用 UDP 发一个查询,收一个响应,不需要建立连接——一次 DNS 查询只需 1 个 RTT,TCP 则需要 3 次握手 + 查询 + 响应 = 至少 2 个 RTT。

# 用 dig 抓取 DNS 查询的 UDP 包
sudo tshark -i eth0 -f "udp port 53" -w dns_capture.pcap &
dig @8.8.8.8 google.com A
sudo kill %1
# 分析抓包结果
tshark -r dns_capture.pcap -Y "dns" -T fields \
-e frame.time_relative -e ip.src -e ip.dst \
-e dns.qry.name -e dns.a
# 典型输出:
# 0.000 192.168.1.100 → 8.8.8.8 google.com
# 0.032 8.8.8.8 → 192.168.1.100 google.com 142.250.80.46
# 观察 UDP 长度
tshark -r dns_capture.pcap -Y "dns" -T fields \
-e udp.length -e dns.qry.name
# 典型:查询 40 字节,响应 56 字节

DNS 什么时候用 TCP?两种情况:响应超过 512 字节时(DNS 标志位 TC=1 表示截断,客户端改用 TCP 重试),以及 DNS 区域传送(zone transfer,AXFR/IXFR)时。EDNS0 扩展允许 UDP 承载更大的 DNS 响应(通常 4096 字节),但超过 MTU 会触发 IP 分片,得不偿失。

4.2 实时音视频#

实时音视频是 UDP 的核心应用场景。RTP(Real-time Transport Protocol,RFC 3550)运行在 UDP 之上,承载音视频数据;RTCP(RTP Control Protocol)承载质量反馈。

sequenceDiagram participant A as 发送端<br/>192.168.1.100:5004 participant R as 路由器 participant B as 接收端<br/>10.0.2.10:5004 A->>R: RTP #1 (视频帧 N) A->>R: RTP #2 (视频帧 N+1) R->>B: RTP #1 Note over R,B: RTP #2 丢失(网络拥塞) A->>R: RTP #3 (视频帧 N+2) R->>B: RTP #3 B->>A: RTCP RR (丢包率 33%) Note over A: 收到反馈,调整编码参数 A->>R: RTP #4 (降低码率后) R->>B: RTP #4

音视频选择 UDP 的原因:

  1. 延迟敏感:一个视频帧迟到 200ms 就没用了——播放器要么跳过,要么卡顿。TCP 的重传机制会让后续帧等待丢失帧的重传,造成更大的延迟
  2. 允许丢包:丢一帧视频,播放器插值或跳过,人眼几乎察觉不到。丢一个音频包,播放器用前一个包的样本来填补
  3. 速率可控:编码器可以根据网络状况调整码率,不需要 TCP 的拥塞窗口来限速

4.3 游戏与IoT#

应用类型包大小发送频率延迟要求丢包容忍典型协议
FPS 游戏50-200B20-60Hz<50ms高(预测补偿)自定义 UDP
MOBA 游戏100-500B10-30Hz<100ms自定义 UDP
IoT 传感器10-100B0.1-1Hz秒级CoAP/UDP
IoT 执行器10-50B事件驱动<500ms极低CoAP/DTLS
流媒体推送1-4KB25-60fps<200msRTP/UDP
VPN 隧道变长持续OpenVPN/UDP

游戏选择 UDP 的核心原因:TCP 的队头阻塞不可接受。一个 TCP 包丢失,后续所有包都在内核缓冲区等待重传——即使后续包已经到达。游戏需要最新的状态,旧状态可以直接丢弃。IoT 选择 UDP 则是因为开销小——一个 10 字节的传感器读数,TCP 的 20 字节头部 + 三次握手完全不值得。

4.4 QUIC的UDP基础#

QUIC(RFC 9000)选择在 UDP 之上而非直接在 IP 之上构建,原因有三个:

  1. 中间盒友好:NAT、防火墙、流量整形设备通常只识别 TCP 和 UDP。一个新的 IP 协议号会被大量中间盒丢弃。UDP 是”通行证”——几乎所有网络设备都放行 UDP 流量
  2. 内核无需修改:TCP 在操作系统内核中实现,修改 TCP 协议栈需要升级内核。QUIC 在用户态实现,部署和迭代不需要改操作系统
  3. 避免 TCP 的历史包袱:TCP 的拥塞控制、重传机制、连接状态都在内核中,应用无法定制。QUIC 可以在用户态实现更灵活的拥塞控制算法
Tip

QUIC 本质上是”在 UDP 之上重新实现了 TCP 的可靠性 + 增加了多流和加密”。理解 UDP 是理解 QUIC 的前提——QUIC 的连接 ID、流多路复用、0-RTT 握手等特性,都是在 UDP 的”无连接”基础上构建的。详见 QUIC与HTTP/3

五、UDP的局限与增强#

5.1 UDP的不可靠性#

UDP 不提供任何可靠性保证——数据报可能丢失、重复、乱序到达。这不是缺陷,而是设计选择:可靠性需要确认和重传机制,这些机制引入延迟和状态,与 UDP 的”简单快速”目标矛盾。

UDP 不可靠的三种表现:

  1. 丢包:网络拥塞时路由器丢包,UDP 不重传。接收方毫无感知
  2. 乱序:数据报经过不同路径到达,UDP 不排序。应用收到的顺序可能和发送顺序不同
  3. 重复:极少数情况下(如链路层重传),同一个数据报可能到达两次。UDP 不去重

对于 DNS 查询这种”发一个、收一个”的场景,不可靠性不是问题——超时重发即可。但对于文件传输、消息队列等场景,不可靠性不可接受。

5.2 可靠UDP方案#

既然 UDP 不提供可靠性,而 TCP 的可靠性又有性能代价(队头阻塞、内核态实现),很多应用选择在 UDP 之上自己实现可靠性:

方案可靠性有序性拥塞控制多流典型应用
QUICARQ流内有序Cubic/BBRHTTP/3
KCPARQ可选游戏、加速器
UDTARQUDT 专用高带宽传输
SCTPARQ部分有序类TCP电信信令
WebRTC DataChannelSCTP部分有序类TCP浏览器 P2P
ENETARQ可选游戏

KCP 是中国开发者林伟(skywind3000)创建的可靠 UDP 协议,核心思想是”用 10%-20% 的带宽代价换取比 TCP 低 30%-40% 的延迟”。KCP 通过更激进的重传策略(不等超时就重传)和更灵活的拥塞控制(可以关闭拥塞控制,由应用层决定发送速率)来降低延迟。

5.3 UDP拥塞控制#

RFC 8085 明确指出:使用 UDP 的应用应该实现拥塞控制。没有拥塞控制的 UDP 流在丢包时会持续以全速发送,挤占 TCP 流的带宽——TCP 遇到丢包就降速,UDP 不降速,最终 UDP 流”饿死”TCP 流。

UDP 拥塞控制的实现方式:

  1. 应用层实现:QUIC 在用户态实现了 Cubic/BBR 等拥塞控制算法
  2. TFRC(RFC 5348):TCP 友好速率控制,根据丢包率计算允许的发送速率,公式为 X = s / (R * sqrt(2p/3) + t_RTO * (3 * sqrt(3p/8) * p * (1+32p^2))),其中 s 是包大小,R 是 RTT,p 是丢包率
  3. DCCP(RFC 4340):提供拥塞控制但不提供可靠性的传输协议,但部署量极小
Warning

不实现拥塞控制的 UDP 应用在公网上是危险的。2008 年澳大利亚一个研究实验发现,无拥塞控制的 UDP 流可以将同一链路上的 TCP 吞吐量压缩到原来的 1/100。如果你的 UDP 应用需要高带宽,务必实现某种形式的拥塞控制。

5.4 UDP缓冲区管理#

UDP 没有流量控制——发送方不知道接收方的缓冲区是否已满。当接收方的 UDP 套接字接收缓冲区溢出时,内核直接丢弃新到达的数据报,不通知发送方。

# 查看 UDP 套接字统计
ss -u -a -n
# 典型输出:
# State Recv-Q Send-Q Local Address:Port Peer Address:Port
# UNCONN 0 0 *:53 *:*
# UNCONN 128 0 *:514 *:*
# ↑ 接收缓冲区中待读取的数据量
# 查看 UDP 缓冲区默认大小
cat /proc/sys/net/core/rmem_default
# 212992 (208KB)
cat /proc/sys/net/core/rmem_max
# 212992 (208KB)
# 调整 UDP 接收缓冲区(高流量 DNS 服务器可能需要)
sudo sysctl -w net.core.rmem_max=8388608 # 8MB
sudo sysctl -w net.core.rmem_default=2097152 # 2MB
# 查看 UDP 统计信息(缓冲区溢出导致的丢包)
cat /proc/net/snmp | grep Udp
# Udp: InDatagrams NoPorts InErrors OutDatagrams RcvbufErrors SndbufErrors
# Udp: 1234567 234 56 987654 12 0
# ↑ 接收缓冲区溢出次数
# 另一个关键指标
netstat -s | grep "buffer errors"
# 12 receive buffer errors
# 0 send buffer errors

RcvbufErrors 是 UDP 接收缓冲区溢出的直接指标。DNS 服务器、Syslog 收集器等高流量 UDP 服务需要特别关注这个值——持续增长说明缓冲区太小或消费速度不够快。

六、UDP与NAT穿透#

6.1 NAT对UDP的影响#

NAT与中间盒 中讲过 NAPT 的工作原理——内部 IP:Port 映射到外部 IP:Port。TCP 有连接状态(SYN→SYN/ACK→ACK),NAT 可以跟踪连接生命周期。UDP 没有连接状态,NAT 只能靠超时来管理映射表。

flowchart LR subgraph 内网["内网 192.168.1.0/24"] CLIENT["客户端<br/>192.168.1.100:52840"] end subgraph NAT["NAT 网关"] MAP["映射表<br/>192.168.1.100:52840<br/>↕<br/>203.0.113.5:20001"] end subgraph 外网["外网"] SERVER["服务器<br/>10.0.2.10:443"] end CLIENT -->|"源 192.168.1.100:52840"| MAP MAP -->|"源 203.0.113.5:20001"| SERVER SERVER -->|"目的 203.0.113.5:20001"| MAP MAP -->|"目的 192.168.1.100:52840"| CLIENT style 内网 fill:#e3f2fd,stroke:#1565c0 style NAT fill:#fff3e0,stroke:#e65100 style 外网 fill:#e8f5e9,stroke:#2e7d32

NAT 对 UDP 的三个关键影响:

  1. 映射超时:UDP 映射的超时通常 30-120 秒(TCP 是建立连接后一直保持)。长时间无流量的 UDP 映射会被 NAT 删除,后续包到达时 NAT 不知道该转发给谁
  2. 入站过滤:NAT 通常只允许”有映射的入站包”通过。外部主机主动发往 NAT 内部的 UDP 包会被丢弃
  3. 映射行为差异:不同 NAT 对”同一内部端口发往不同外部地址”的映射策略不同——这直接影响 P2P 穿透的可能性

6.2 STUN协议#

STUN(Session Traversal Utilities for NAT,RFC 8489)让客户端发现自己的公网 IP 和端口映射,以及 NAT 的类型和行为。

# 使用 stunclient 测试 NAT 类型
sudo apt install stun-client
stun stun.l.google.com:19302
# 典型输出:
# MappedAddress = 203.0.113.5:20001
# ChangedAddress = 209.85.217.190:19302
# NAT Type = Port Restricted NAT
# 用 Python 实现简单的 STUN 绑定请求
python3 -c "
import socket, struct
# STUN Binding Request (RFC 5389)
# Type: 0x0001 (Binding Request), Length: 0, Magic Cookie: 0x2112A442
req = struct.pack('!HHI12s', 0x0001, 0, 0x2112A442, b'\x00' * 12)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(3)
sock.sendto(req, ('stun.l.google.com', 19302))
data, addr = sock.recvfrom(1024)
# 解析 XOR-MAPPED-ADDRESS (Type 0x0020)
msg_type, msg_len, magic, txn_id = struct.unpack('!HHI12s', data[:20])
# 找到 XOR-MAPPED-ADDRESS 属性
offset = 20
while offset < len(data):
attr_type, attr_len = struct.unpack('!HH', data[offset:offset+4])
if attr_type == 0x0020:
family = data[offset+5]
x_port = struct.unpack('!H', data[offset+6:offset+8])[0]
port = x_port ^ (0x2112A442 >> 16)
if family == 0x01: # IPv4
x_ip = struct.unpack('!I', data[offset+8:offset+12])[0]
ip = socket.inet_ntoa(struct.pack('!I', x_ip ^ 0x2112A442))
print(f'公网映射: {ip}:{port}')
break
offset += 4 + attr_len
"

STUN 的 NAT 类型检测流程通过改变源 IP 和源端口来观察 NAT 的映射行为,将 NAT 分为四类:Full Cone、Restricted Cone、Port Restricted Cone、Symmetric。前三种可以通过”打洞”实现 P2P 通信,Symmetric NAT 则不行。

6.3 TURN中继#

当两端都在 Symmetric NAT 后面时,STUN 打洞不可行。TURN(Traversal Using Relays around NAT,RFC 8656)的方案是:两端都和 TURN 服务器建立 UDP 连接,通过 TURN 服务器中继数据。

TURN 的代价是增加延迟和带宽成本——所有数据都经过 TURN 服务器转发。但它是 NAT 穿透的最后保障:只要两端都能和 TURN 服务器建立 UDP 连接,通信就能成功。

6.4 ICE框架#

ICE(Interactive Connectivity Establishment,RFC 8445)将 STUN 和 TURN 组合成一个完整的 NAT 穿透框架,是 WebRTC 的核心组件:

sequenceDiagram participant A as 客户端 A participant STUN as STUN 服务器 participant TURN as TURN 服务器 participant SIG as 信令服务器 participant B as 客户端 B A->>STUN: Binding Request (获取公网映射) STUN-->>A: Mapped Address 203.0.113.5:20001 A->>TURN: Allocate Request (获取中继地址) TURN-->>A: Relayed Address 198.51.100.1:50000 B->>STUN: Binding Request STUN-->>B: Mapped Address 198.51.100.2:30001 B->>TURN: Allocate Request TURN-->>B: Relayed Address 198.51.100.1:50001 A->>SIG: 候选地址列表 (本地+映射+中继) SIG->>B: 转发 A 的候选地址 B->>SIG: 候选地址列表 SIG->>A: 转发 B 的候选地址 Note over A,B: 连通性检查阶段 A->>B: STUN Binding (尝试直连) B->>A: STUN Binding (尝试直连) A->>TURN: 数据 (中继路径) TURN->>B: 转发数据 Note over A,B: 选择最优路径:直连 > 中继

ICE 的候选地址有三类:

  1. 主机候选(Host Candidate):本地网卡的 IP 地址
  2. 服务器反射候选(Server Reflexive Candidate):STUN 返回的公网映射地址
  3. 中继候选(Relay Candidate):TURN 分配的中继地址

ICE 按优先级排序候选地址(主机 > 反射 > 中继),逐一进行连通性检查,选择优先级最高的可用路径。大多数情况下,反射候选(打洞直连)就能成功,只有 Symmetric NAT 才需要中继。

七、动手实践:UDP编程与抓包#

7.1 Python UDP服务器/客户端#

#!/usr/bin/env python3
"""UDP 回声服务器"""
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server.bind(('0.0.0.0', 9999))
print("UDP 服务器监听 0.0.0.0:9999")
while True:
data, addr = server.recvfrom(4096)
print(f"收到来自 {addr} 的数据: {data!r} ({len(data)} 字节)")
server.sendto(data, addr) # 回声
# --------------------------------------------------
#!/usr/bin/env python3
"""UDP 客户端"""
import socket
import time
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
client.settimeout(2.0)
server_addr = ('127.0.0.1', 9999)
for i in range(5):
msg = f"Hello UDP #{i}".encode()
start = time.time()
client.sendto(msg, server_addr)
try:
data, addr = client.recvfrom(4096)
rtt = (time.time() - start) * 1000
print(f"收到回声: {data!r}, RTT: {rtt:.1f}ms")
except socket.timeout:
print(f"请求 #{i} 超时(UDP 不保证交付)")
client.close()

7.2 UDP丢包测试#

# 启动 iperf3 服务端
iperf3 -s
# 客户端 UDP 带宽测试(默认 1Mbps)
iperf3 -c 127.0.0.1 -u
# 典型输出:
# [ ID] Interval Transfer Bitrate Jitter Lost/Total
# [ 5] 0.00-1.00 sec 128 KBytes 1.05 Mbits/sec 0.023 ms 0/89 (0%)
# [ 5] 1.00-2.00 sec 128 KBytes 1.05 Mbits/sec 0.019 ms 0/89 (0%)
# 加大带宽到 100Mbps,观察丢包
iperf3 -c 127.0.0.1 -u -b 100M
# 典型输出(高带宽下丢包明显):
# [ 5] 0.00-1.00 sec 11.2 MBytes 94.0 Mbits/sec 0.156 ms 12/8200 (0.15%)
# [ 5] 1.00-2.00 sec 11.0 MBytes 92.3 Mbits/sec 0.201 ms 45/8100 (0.56%)
# 测试不同包大小的影响
iperf3 -c 127.0.0.1 -u -b 50M -l 64 # 64 字节小包
iperf3 -c 127.0.0.1 -u -b 50M -l 1400 # 1400 字节大包(接近 MTU)
iperf3 -c 127.0.0.1 -u -b 50M -l 9000 # 9000 字节巨帧(需要 Jumbo Frame 支持)
# 反向测试(服务端发送到客户端)
iperf3 -c 127.0.0.1 -u -b 50M -R

7.3 Wireshark分析UDP流#

# 抓取 UDP 流量并保存
sudo tshark -i eth0 -f "udp" -w udp_capture.pcap -a duration:30
# 分析 UDP 统计
tshark -r udp_capture.pcap -z conv,udp
# 典型输出:
# UDP Conversations
# Filter:udp
# <--> A B A<->B
# 192.168.1.100:52840 <-> 8.8.8.8:53 15 15 630 890
# 192.168.1.100:123 <-> 162.159.200.1:123 5 5 250 250
# 过滤 DNS over UDP
tshark -r udp_capture.pcap -Y "udp.port == 53" -T fields \
-e frame.time -e ip.src -e ip.dst -e dns.qry.name -e dns.flags.rcode
# 检查 UDP 校验和错误
tshark -r udp_capture.pcap -Y "udp.checksum_bad.expert"
# 导出 UDP 数据
tshark -r udp_capture.pcap -Y "udp.port == 9999" \
-T fields -e data | xxd -r -p > udp_payload.bin

八、本章小结#

概念要点
传输层角色从”主机到主机”(IP)到”进程到进程”(端口),多路复用/分用是核心功能
UDP 报文格式8 字节头部:源端口 + 目的端口 + 长度 + 校验和,极简设计
校验和覆盖伪头部 + UDP 头部 + 数据,端到端完整性保障,IPv6 必填
端口号熟知端口(0-1023)、注册端口(1024-49151)、动态端口(49152-65535)
多路复用/分用发送端多进程共享 IP 层,接收端根据目的端口分发到对应套接字
UDP 应用场景DNS(小包快查)、音视频(延迟敏感)、游戏/IoT(低延迟小包)、QUIC(中间盒友好)
UDP 局限无可靠性、无拥塞控制、无流量控制——需要应用层自行实现
可靠 UDPQUIC/KCP/UDT/SCTP 在 UDP 之上实现可靠性,各有侧重
UDP 拥塞控制RFC 8085 要求 UDP 应用实现拥塞控制,避免饿死 TCP 流
NAT 穿透STUN 检测映射、TURN 中继兜底、ICE 组合选路——WebRTC 的基础

参考#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

UDP与传输基础:最简传输协议
https://blog.souloss.com/posts/internet-architecture/udp-and-transport-basics/
作者
Souloss
发布于
2022-06-05
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

相关文章 智能推荐
1
QUIC与HTTP/3:传输层的性能革命
互联网运作 TCP的队头阻塞、握手延迟和连接迁移失败催生了QUIC——一个基于UDP的加密传输协议。0-RTT握手、流级多路复用、连接ID迁移,加上HTTP/3的QPACK头部压缩,传输层迎来了真正的性能革命。
2
TCP连接管理:三次握手与四次挥手的工程细节
互联网运作 TCP如何建立和释放连接?三次握手为什么不能是两次?四次挥手为什么不能是三次?TIME_WAIT到底在等什么?从报文格式到状态机,从SYN Flood防御到连接异常处理,用抓包实验观察TCP连接的完整生命周期。
3
TCP流控与拥塞控制:从滑动窗口到BBR
互联网运作 TCP连接建立之后,数据如何安全高效地传输?滑动窗口保护接收方不被淹没,拥塞控制保护网络不被压垮——从1986年拥塞崩溃到BBR模型驱动,理解TCP流量调控的完整演进。
4
系列导读
互联网运作 本系列从物理介质到应用层、从数据包离开网卡到抵达对端,用真实的包级追踪讲述互联网如何运作——物理编码、以太网交换、ARP首跳、IP寻址、NAT中间盒、域内域间路由、BGP安全、运营商骨干网、IXP互联、TCP/UDP/QUIC传输、DNS/TLS安全、HTTP演进、CDN分发、数据中心SDN、无线异构接入,每章配有Wireshark抓包与GNS3/FRR实验,让你从「会上网」进阶到「理解互联网」。
5
为什么 DNS 使用 UDP 协议
技术科普 深入解析 DNS 协议为什么主要使用 UDP,以及什么时候会切换到 TCP,DNS 协议设计的精妙之处。