强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 对比

特性TCPUDP
头部大小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 创建、绑定、监听