TCP/UDP 网络协议教程 / 09-UDP 协议详解
09 - UDP 协议详解
9.1 UDP 头部结构
UDP 头部只有 8 个字节:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port | Destination Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Length | Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
字段说明:
Source Port (16 bit): 源端口(可选,0 表示不使用)
Destination Port (16 bit): 目的端口
Length (16 bit): UDP 头部 + 数据的总长度(最小 8)
Checksum (16 bit): 校验和(可选但推荐)
9.2 UDP 与 TCP 对比
| 特性 | TCP | UDP |
|---|---|---|
| 头部大小 | 20-60 字节 | 8 字节 |
| 连接 | 面向连接 | 无连接 |
| 可靠性 | 可靠 | 不可靠 |
| 顺序 | 有序 | 无序 |
| 流控 | 有 | 无 |
| 拥塞控制 | 有 | 无 |
| 传输方式 | 字节流 | 数据报 |
| 边界 | 无边界 | 保留边界 |
| 一对多 | 不支持 | 支持 |
import socket
# TCP Socket
tcp_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# UDP Socket
udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
9.3 数据报边界
"""
TCP 是字节流,没有消息边界:
send(b"Hello") + send(b"World")
→ recv() 可能收到 b"HelloWorld"
UDP 保留消息边界:
sendto(b"Hello", addr) + sendto(b"World", addr)
→ recvfrom() 必然收到两次:b"Hello" 和 b"World"
"""
# UDP 消息边界示例
def udp_sender():
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 发送三个独立的数据报
sock.sendto(b"Message 1", ('127.0.0.1', 9999))
sock.sendto(b"Message 2", ('127.0.0.1', 9999))
sock.sendto(b"Message 3", ('127.0.0.1', 9999))
sock.close()
def udp_receiver():
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('0.0.0.0', 9999))
for _ in range(3):
data, addr = sock.recvfrom(1024)
print(f"收到: {data}") # 每次收到一个完整消息
sock.close()
9.4 UDP 校验和
校验和覆盖范围:
┌──────────────────────────────────────┐
│ 伪头部 (12 字节) │
│ ┌────────────────────────────────┐ │
│ │ 源 IP │ 目的 IP │ 0│17│ UDP 长度│ │
│ └────────────────────────────────┘ │
├──────────────────────────────────────┤
│ UDP 头部 (8 字节) │
├──────────────────────────────────────┤
│ UDP 数据 │
└──────────────────────────────────────┘
IPv4 中校验和是可选的(设为 0 表示不计算)
IPv6 中校验和是必须的
9.5 UDP 适用场景
DNS 查询
"""DNS 查询使用 UDP"""
import socket
import struct
def dns_query(domain, dns_server='8.8.8.8'):
"""简单的 DNS 查询"""
# DNS 头部
header = struct.pack('!HHHHHH',
0x1234, # Transaction ID
0x0100, # Flags: 标准查询
1, 0, 0, 0 # Questions, Answers, Authority, Additional
)
# 查询域名
query = b''
for part in domain.split('.'):
query += bytes([len(part)]) + part.encode()
query += b'\x00'
query += struct.pack('!HH', 1, 1) # Type A, Class IN
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(5)
sock.sendto(header + query, (dns_server, 53))
response, _ = sock.recvfrom(512)
sock.close()
return response
# 使用
response = dns_query('example.com')
print(f"DNS 响应: {len(response)} 字节")
实时音视频
"""
实时音视频使用 UDP 的原因:
1. 低延迟:无握手、无确认等待
2. 可丢帧:人耳/眼对偶尔的丢帧不敏感
3. 实时性:重传旧帧没有意义
4. 恒定速率:适合流媒体传输
"""
9.6 广播 (Broadcast)
广播地址:
• 255.255.255.255 - 受限广播(本网络)
• x.x.x.255 - 定向广播(特定子网)
广播只能在局域网内使用,路由器不转发广播包
import socket
def broadcast_sender():
"""广播发送方"""
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
# 发送广播
sock.sendto(b'Hello LAN!', ('255.255.255.255', 9999))
sock.close()
def broadcast_receiver():
"""广播接收方"""
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('0.0.0.0', 9999))
while True:
data, addr = sock.recvfrom(1024)
print(f"收到来自 {addr}: {data.decode()}")
# 常见使用场景:
# - DHCP 发现
# - ARP 请求
# - 局域网游戏发现
# - 设备发现协议
9.7 多播 (Multicast)
多播地址范围:224.0.0.0 - 239.255.255.255
常见多播地址:
• 224.0.0.1 - 所有主机
• 224.0.0.2 - 所有路由器
• 224.0.0.251 - mDNS
• 239.x.x.x - 私有域多播
多播 vs 广播:
• 广播:所有主机都收到
• 多播:只有加入多播组的主机收到
import socket
import struct
def multicast_sender(group='224.1.1.1', port=5007):
"""多播发送方"""
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
# 设置 TTL
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
for i in range(5):
message = f"Multicast message {i}".encode()
sock.sendto(message, (group, port))
sock.close()
def multicast_receiver(group='224.1.1.1', port=5007):
"""多播接收方"""
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('', port))
# 加入多播组
mreq = struct.pack('4s4s',
socket.inet_aton(group),
socket.inet_aton('0.0.0.0'))
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
while True:
data, addr = sock.recvfrom(1024)
print(f"收到来自 {addr}: {data.decode()}")
# 离开多播组
sock.setsockopt(socket.IPPROTO_IP, socket.IP_DROP_MEMBERSHIP, mreq)
sock.close()
9.8 UDP 不可靠性示例
"""演示 UDP 的不可靠性"""
import socket
import time
import random
def unreliable_sender():
"""模拟不可靠网络"""
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for i in range(100):
message = f"Packet {i}".encode()
# 模拟 10% 的丢包率
if random.random() > 0.1:
sock.sendto(message, ('127.0.0.1', 9999))
else:
print(f"丢弃: Packet {i}")
# 模拟随机延迟
time.sleep(random.uniform(0, 0.01))
sock.close()
def receiver_with_stats():
"""接收方统计"""
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('0.0.0.0', 9999))
sock.settimeout(2)
received = 0
expected = 100
try:
while True:
data, addr = sock.recvfrom(1024)
received += 1
except socket.timeout:
pass
loss_rate = (expected - received) / expected * 100
print(f"发送: {expected}, 接收: {received}, 丢包率: {loss_rate}%")
sock.close()
9.9 UDP 大小限制
"""
UDP 数据报大小限制:
理论最大:65535 字节(IP 包限制) - 20(IP头) - 8(UDP头) = 65507 字节
实际建议:
• 以太网 MTU = 1500 字节
• 减去 IP 头 (20) 和 UDP 头 (8)
• 建议 ≤ 1472 字节(避免 IP 分片)
超过 MTU 会触发 IP 分片:
• 一个 UDP 数据报 → 多个 IP 分片
• 任何一个分片丢失 → 整个数据报丢失
• 分片增加延迟和丢包概率
"""
MAX_SAFE_UDP_PAYLOAD = 1472 # 以太网安全上限
def check_udp_size(size):
"""检查 UDP 包大小是否安全"""
if size <= MAX_SAFE_UDP_PAYLOAD:
return "安全(无分片)"
elif size <= 65507:
return "可能触发分片"
else:
return "超出限制"
9.10 UDP 与 NAT
NAT 穿透问题:
• UDP 没有连接状态,NAT 表项超时快
• 需要定期发送保活包维持 NAT 映射
常见方案:
1. STUN:发现公网地址和端口
2. TURN:中继服务器转发
3. ICE:综合多种连接方式
9.11 注意事项
⚠️ 没有流控:发送太快可能淹没接收方,应用层需要自己控制速率
⚠️ 没有拥塞控制:大量 UDP 流量可能挤占 TCP 带宽,导致 TCP 退让
⚠️ 防火墙问题:UDP 可能被防火墙阻止,需要考虑穿透方案
⚠️ 校验和:IPv4 中可选,但强烈建议启用,否则可能收到损坏的数据
9.12 扩展阅读
下一章:10 - Socket API 基础 - Socket 创建、绑定、监听