数据包穿越了 NAT与中间盒 的地址翻译,经过 域内路由 和 BGP与域间路由 的逐跳转发,跨过运营商骨干网和 IXP与互联网交换,终于到达了目的主机的 IP 层。但一台主机上同时跑着浏览器、SSH、DNS 客户端、视频会议——这个数据包该交给谁?
IP 协议只管把数据包送到主机,不管主机上哪个进程来收。从”到主机”到”到进程”这一步跨越,就是传输层的职责。传输层是网络层与应用层之间的桥梁——网络层提供主机到主机的交付,传输层提供进程到进程的交付。
本章从传输层的角色出发,先看最简单的传输层协议 UDP 的报文格式与校验和机制,再深入端口号与多路复用的实现,最后分析 UDP 的应用场景、局限与增强方案,以及 UDP 在 NAT 穿透中的关键角色。
一、传输层的角色
1.1 从网络层到传输层
IP 协议的核心抽象是主机到主机的交付——目的 IP 地址标识一台主机,不标识主机上的进程。但实际通信发生在进程之间:浏览器和 Web 服务器通信,SSH 客户端和 SSH 守护进程通信。传输层要解决的就是”数据包到了主机之后,怎么交给正确的进程”。
传输层用端口号区分同一主机上的不同进程。IP 地址 + 端口号的组合就是套接字(socket),唯一标识一个通信端点。一对套接字 (源IP:源端口, 目的IP:目的端口) 唯一标识一条传输层连接。
1.2 传输层的功能
传输层提供四个核心功能:
- 多路复用与分用(Multiplexing/Demultiplexing):多个进程共享同一个网络层接口发送数据(复用),接收端根据端口号将数据分发给正确的进程(分用)
- 可靠性(Reliability):确认、重传、排序——保证数据完整有序地到达。UDP 不提供,TCP 提供
- 流量控制(Flow Control):防止发送方淹没接收方。UDP 不提供,TCP 用滑动窗口实现
- 拥塞控制(Congestion Control):防止发送方淹没网络。UDP 不提供,TCP 用慢启动/拥塞避免实现
UDP 只实现了第 1 项功能——多路复用与分用,外加一个可选的校验和。TCP 实现了全部四项。这不是 UDP 的缺陷,而是设计选择——有些应用不需要可靠性,强加可靠性反而增加延迟。
1.3 传输层协议对比
| 维度 | UDP | TCP | SCTP | QUIC |
|---|---|---|---|---|
| RFC | 768 | 9293 | 4960 | 9000 |
| 连接建立 | 无 | 三次握手 | 四次握手 | 1-RTT/0-RTT |
| 可靠性 | 无 | 确认+重传 | 确认+重传 | 确认+重传 |
| 有序交付 | 无 | 是 | 部分有序 | 流内有序 |
| 流量控制 | 无 | 滑动窗口 | 滑动窗口 | 基于信用 |
| 拥塞控制 | 无 | Cubic/BBR | 类TCP | Cubic/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 structimport 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 = 52840dst_port = 53payload = 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)——否则说明数据在传输中被篡改。
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
# 过滤特定端口的 UDPsudo 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/21 | FTP | TCP | 文件传输 |
| 22 | SSH | TCP | 远程登录 |
| 25 | SMTP | TCP | 邮件发送 |
| 53 | DNS | UDP/TCP | 域名解析 |
| 67/68 | DHCP | UDP | 地址分配 |
| 80 | HTTP | TCP | Web 服务 |
| 123 | NTP | UDP | 时间同步 |
| 161/162 | SNMP | UDP | 网络管理 |
| 443 | HTTPS | TCP | 安全 Web |
| 514 | Syslog | UDP | 系统日志 |
| 1194 | OpenVPN | UDP | VPN 隧道 |
| 5060 | SIP | UDP/TCP | VoIP 信令 |
注意 DNS 同时使用 UDP 和 TCP——通常查询用 UDP(快),区域传送和超长响应用 TCP(可靠)。NTP、SNMP、Syslog 这类”小包、允许丢”的协议只用 UDP。
3.2 多路复用与分用
多路复用是发送端的行为:多个进程通过不同的源端口共享同一个 IP 层发送数据。分用是接收端的行为:操作系统根据目的端口将收到的 UDP 数据报分发给对应的进程。
分用的具体过程:操作系统维护一个端口号 → 套接字的映射表。收到 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 udpIANA 维护的正式注册表在 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.1UDP 端口扫描比 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 Asudo 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)承载质量反馈。
音视频选择 UDP 的原因:
- 延迟敏感:一个视频帧迟到 200ms 就没用了——播放器要么跳过,要么卡顿。TCP 的重传机制会让后续帧等待丢失帧的重传,造成更大的延迟
- 允许丢包:丢一帧视频,播放器插值或跳过,人眼几乎察觉不到。丢一个音频包,播放器用前一个包的样本来填补
- 速率可控:编码器可以根据网络状况调整码率,不需要 TCP 的拥塞窗口来限速
4.3 游戏与IoT
| 应用类型 | 包大小 | 发送频率 | 延迟要求 | 丢包容忍 | 典型协议 |
|---|---|---|---|---|---|
| FPS 游戏 | 50-200B | 20-60Hz | <50ms | 高(预测补偿) | 自定义 UDP |
| MOBA 游戏 | 100-500B | 10-30Hz | <100ms | 中 | 自定义 UDP |
| IoT 传感器 | 10-100B | 0.1-1Hz | 秒级 | 低 | CoAP/UDP |
| IoT 执行器 | 10-50B | 事件驱动 | <500ms | 极低 | CoAP/DTLS |
| 流媒体推送 | 1-4KB | 25-60fps | <200ms | 高 | RTP/UDP |
| VPN 隧道 | 变长 | 持续 | 中 | 低 | OpenVPN/UDP |
游戏选择 UDP 的核心原因:TCP 的队头阻塞不可接受。一个 TCP 包丢失,后续所有包都在内核缓冲区等待重传——即使后续包已经到达。游戏需要最新的状态,旧状态可以直接丢弃。IoT 选择 UDP 则是因为开销小——一个 10 字节的传感器读数,TCP 的 20 字节头部 + 三次握手完全不值得。
4.4 QUIC的UDP基础
QUIC(RFC 9000)选择在 UDP 之上而非直接在 IP 之上构建,原因有三个:
- 中间盒友好:NAT、防火墙、流量整形设备通常只识别 TCP 和 UDP。一个新的 IP 协议号会被大量中间盒丢弃。UDP 是”通行证”——几乎所有网络设备都放行 UDP 流量
- 内核无需修改:TCP 在操作系统内核中实现,修改 TCP 协议栈需要升级内核。QUIC 在用户态实现,部署和迭代不需要改操作系统
- 避免 TCP 的历史包袱:TCP 的拥塞控制、重传机制、连接状态都在内核中,应用无法定制。QUIC 可以在用户态实现更灵活的拥塞控制算法
QUIC 本质上是”在 UDP 之上重新实现了 TCP 的可靠性 + 增加了多流和加密”。理解 UDP 是理解 QUIC 的前提——QUIC 的连接 ID、流多路复用、0-RTT 握手等特性,都是在 UDP 的”无连接”基础上构建的。详见 QUIC与HTTP/3。
五、UDP的局限与增强
5.1 UDP的不可靠性
UDP 不提供任何可靠性保证——数据报可能丢失、重复、乱序到达。这不是缺陷,而是设计选择:可靠性需要确认和重传机制,这些机制引入延迟和状态,与 UDP 的”简单快速”目标矛盾。
UDP 不可靠的三种表现:
- 丢包:网络拥塞时路由器丢包,UDP 不重传。接收方毫无感知
- 乱序:数据报经过不同路径到达,UDP 不排序。应用收到的顺序可能和发送顺序不同
- 重复:极少数情况下(如链路层重传),同一个数据报可能到达两次。UDP 不去重
对于 DNS 查询这种”发一个、收一个”的场景,不可靠性不是问题——超时重发即可。但对于文件传输、消息队列等场景,不可靠性不可接受。
5.2 可靠UDP方案
既然 UDP 不提供可靠性,而 TCP 的可靠性又有性能代价(队头阻塞、内核态实现),很多应用选择在 UDP 之上自己实现可靠性:
| 方案 | 可靠性 | 有序性 | 拥塞控制 | 多流 | 典型应用 |
|---|---|---|---|---|---|
| QUIC | ARQ | 流内有序 | Cubic/BBR | 是 | HTTP/3 |
| KCP | ARQ | 是 | 可选 | 否 | 游戏、加速器 |
| UDT | ARQ | 是 | UDT 专用 | 否 | 高带宽传输 |
| SCTP | ARQ | 部分有序 | 类TCP | 是 | 电信信令 |
| WebRTC DataChannel | SCTP | 部分有序 | 类TCP | 是 | 浏览器 P2P |
| ENET | ARQ | 可选 | 否 | 否 | 游戏 |
KCP 是中国开发者林伟(skywind3000)创建的可靠 UDP 协议,核心思想是”用 10%-20% 的带宽代价换取比 TCP 低 30%-40% 的延迟”。KCP 通过更激进的重传策略(不等超时就重传)和更灵活的拥塞控制(可以关闭拥塞控制,由应用层决定发送速率)来降低延迟。
5.3 UDP拥塞控制
RFC 8085 明确指出:使用 UDP 的应用应该实现拥塞控制。没有拥塞控制的 UDP 流在丢包时会持续以全速发送,挤占 TCP 流的带宽——TCP 遇到丢包就降速,UDP 不降速,最终 UDP 流”饿死”TCP 流。
UDP 拥塞控制的实现方式:
- 应用层实现:QUIC 在用户态实现了 Cubic/BBR 等拥塞控制算法
- TFRC(RFC 5348):TCP 友好速率控制,根据丢包率计算允许的发送速率,公式为
X = s / (R * sqrt(2p/3) + t_RTO * (3 * sqrt(3p/8) * p * (1+32p^2))),其中s是包大小,R是 RTT,p是丢包率 - DCCP(RFC 4340):提供拥塞控制但不提供可靠性的传输协议,但部署量极小
不实现拥塞控制的 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 # 8MBsudo 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 errorsRcvbufErrors 是 UDP 接收缓冲区溢出的直接指标。DNS 服务器、Syslog 收集器等高流量 UDP 服务需要特别关注这个值——持续增长说明缓冲区太小或消费速度不够快。
六、UDP与NAT穿透
6.1 NAT对UDP的影响
NAT与中间盒 中讲过 NAPT 的工作原理——内部 IP:Port 映射到外部 IP:Port。TCP 有连接状态(SYN→SYN/ACK→ACK),NAT 可以跟踪连接生命周期。UDP 没有连接状态,NAT 只能靠超时来管理映射表。
NAT 对 UDP 的三个关键影响:
- 映射超时:UDP 映射的超时通常 30-120 秒(TCP 是建立连接后一直保持)。长时间无流量的 UDP 映射会被 NAT 删除,后续包到达时 NAT 不知道该转发给谁
- 入站过滤:NAT 通常只允许”有映射的入站包”通过。外部主机主动发往 NAT 内部的 UDP 包会被丢弃
- 映射行为差异:不同 NAT 对”同一内部端口发往不同外部地址”的映射策略不同——这直接影响 P2P 穿透的可能性
6.2 STUN协议
STUN(Session Traversal Utilities for NAT,RFC 8489)让客户端发现自己的公网 IP 和端口映射,以及 NAT 的类型和行为。
# 使用 stunclient 测试 NAT 类型sudo apt install stun-clientstun 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: 0x2112A442req = 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 = 20while 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 的核心组件:
ICE 的候选地址有三类:
- 主机候选(Host Candidate):本地网卡的 IP 地址
- 服务器反射候选(Server Reflexive Candidate):STUN 返回的公网映射地址
- 中继候选(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 socketimport 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 -R7.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 UDPtshark -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 局限 | 无可靠性、无拥塞控制、无流量控制——需要应用层自行实现 |
| 可靠 UDP | QUIC/KCP/UDT/SCTP 在 UDP 之上实现可靠性,各有侧重 |
| UDP 拥塞控制 | RFC 8085 要求 UDP 应用实现拥塞控制,避免饿死 TCP 流 |
| NAT 穿透 | STUN 检测映射、TURN 中继兜底、ICE 组合选路——WebRTC 的基础 |
参考
- RFC 3550 — RTP: A Transport Protocol for Real-Time Applications
- RFC 4340 — Datagram Congestion Control Protocol (DCCP)
- RFC 5348 — RTP Control Protocol (RTCP) Extended Report (XR)
- RFC 5389 — STUN - Simple Traversal of UDP through NAT
- RFC 8085 — UDP Usage Guidelines
- RFC 8445 — Session Traversal Utilities for NAT (STUN)
- RFC 8489 — Traversal Using Relays around NAT (TURN)
- RFC 8656 — TURN Extension for IPv6
- RFC 9000 — RTP Control Protocol (RTCP) Feedback
- IANA Service Name and Transport Protocol Port Number Registry
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






