UDP协议详解:从原理到Python实践

目录

一、什么是 UDP?

UDP(User Datagram Protocol,用户数据报协议)是互联网协议族中的传输层协议,与 TCP 并列,但设计哲学完全不同:追求速度,放弃可靠性保证。

生活中的类比:

  • UDP 像寄明信片------写好扔进邮箱,对方可能收到也可能收不到,你不会知道结果,也不会补寄
  • TCP 像快递------有签收确认,丢了会补发,但慢一些

二、UDP 报文结构

txt 复制代码
 bit:  0      7 8     15 16    23 24    31
      +--------+--------+--------+--------+
      |   源端口 (16位)  |  目标端口 (16位) |
      +--------+--------+--------+--------+
      |   长度 (16位)    |  校验和 (16位)   |
      +--------+--------+--------+--------+
      |              数据载荷 ...           |
      +--------+--------+--------+--------+

顶部数字是 bit 位编号,每行 32 bit = 4 字节。1 字节 = 8 bit。

报头四个字段各占 16 bit(2字节),合计 8 字节,极为精简。

数据载荷上限的计算:

txt 复制代码
UDP 包总长度上限(16位能表示的最大值 2^16 - 1)  65535 字节
  - UDP 报头                                    -   8 字节
  - IP 报头                                     -  20 字节
                                               ──────────
实际可用载荷上限                                 65507 字节

代码里常见的 recvfrom(65535) 只是"一个足够大的数",实际载荷不会超过 65507 字节。

三、UDP 的核心特性

1. 无连接

TCP 发送数据前需要"三次握手"建立连接,UDP 直接发送,不需要任何预备步骤。

2. 不可靠

  • 数据包可能丢失
  • 数据包可能乱序到达
  • 数据包可能重复

UDP 本身不处理这些问题,由应用层决定如何应对(或者干脆忽略)。

3. 低延迟

没有握手、没有确认、没有重传,延迟极低且稳定。

4. 数据报边界保留

这是 UDP 与 TCP 一个容易忽视的重要区别。

TCP 是字节流,没有消息边界(粘包问题):

txt 复制代码
发送方:send("Hello") + send("World") + send("!")
接收方:recv() 可能得到 "HelloWorld!"(三条合并)
        或者 "Hel" + "loWorld!"(一条被拆开)

接收方必须自己处理粘包,比如用固定长度或在头部写长度字段。

UDP 天然保留边界:

txt 复制代码
发送方:sendto("Hello") + sendto("World") + sendto("!")
接收方:recvfrom() 一定是 "Hello" → "World" → "!"
        一次 sendto 对应一次 recvfrom,永远不会粘包

四、UDP vs TCP

对比项 UDP TCP
连接 无连接,直接发 三次握手建立连接
可靠性 不保证到达 保证到达(重传)
顺序 不保证 保证
消息边界 天然保留 无边界(字节流)
速度 快,延迟低 慢,有重传开销
报头大小 8 字节 20~60 字节
适用场景 实时数据 文件传输、网页

五、Python 示例

示例一:最简单的发送/接收

接收端(先运行):

py 复制代码
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
#                                     ↑ SOCK_DGRAM 指定 UDP 协议
#                    (TCP 对应的是 SOCK_STREAM)
sock.bind(("0.0.0.0", 9000))
print("等待数据...")

while True:
    data, addr = sock.recvfrom(65535)
    print(f"收到来自 {addr} 的数据: {data.decode()}")

发送端(后运行):

py 复制代码
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(b"Hello, UDP!", ("127.0.0.1", 9000))
# ↑ 无需 connect(),直接带目标地址发出,体现"无连接"特性
sock.close()

运行结果:

txt 复制代码
# 接收端输出:
等待数据...
收到来自 ('127.0.0.1', 54321) 的数据: Hello, UDP!

54321 是操作系统随机分配给发送端的临时端口,每次运行不同。发送端无任何输出,执行完直接退出。

UDP 体现在哪里:

  • SOCK_DGRAM --- 指定 UDP 协议
  • 发送端没有 connect(),直接 sendto() --- 无连接
  • 接收端没有 listen() / accept() --- 无握手
  • recvfrom() 每次返回一个完整数据报 --- 边界保留

示例二:验证数据报边界保留

py 复制代码
import socket
import threading
import time

def receiver():
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.bind(("0.0.0.0", 9001))
    sock.settimeout(2.0)
    print("=== 接收端 ===")
    try:
        while True:
            data, _ = sock.recvfrom(65535)
            print(f"recvfrom() 收到: {data.decode()!r}")
    except socket.timeout:
        print("接收完毕")
    sock.close()

def sender():
    time.sleep(0.1)
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    messages = ["Hello", "World", "!"]
    print("=== 发送端 ===")
    for msg in messages:
        sock.sendto(msg.encode(), ("127.0.0.1", 9001))
        print(f"sendto() 发送: {msg!r}")
        time.sleep(0.01)
    sock.close()

threading.Thread(target=receiver).start()
threading.Thread(target=sender).start()

运行结果:

txt 复制代码
=== 接收端 ===
=== 发送端 ===
sendto() 发送: 'Hello'
sendto() 发送: 'World'
sendto() 发送: '!'
recvfrom() 收到: 'Hello'
recvfrom() 收到: 'World'
recvfrom() 收到: '!'
接收完毕

三次 sendto 对应三次 recvfrom,消息边界完整保留,不会粘包。

示例三:带序号的实时数据流(模拟遥操场景)

这是最贴近工程实际的用法------发送端持续发送带序号和时间戳的数据,接收端只保留最新帧,丢弃过期数据。

struct 格式字符串详解

发送和接收都用到了 struct.pack/unpack,格式字符串 "!Id3f" 的含义:

字符 含义 字节数
! 网络字节序(大端序) -
I unsigned int,无符号32位整数 4
d double,64位浮点数 8
3f 3个 float,各32位浮点数 4*3=12

总计:4 + 8 + 12 = 24 字节。pack 和 unpack 的格式字符串必须完全一致,否则解析结果会错乱。

为什么需要 !(网络字节序)?

多字节数值在内存中的存储顺序有两种:

txt 复制代码
数值 0x12345678

大端序(网络字节序): 12 34 56 78  ← 高位在前,符合人类阅读习惯
小端序(x86主机):   78 56 34 12  ← 低位在前

网络传输约定使用大端序,! 确保不同机器之间收发数据时解析结果一致。跨机器通信必须统一字节序,否则同一段字节在两端会被解析成不同的数值。

发送端(100 Hz 持续发送):

py 复制代码
import socket
import struct
import time

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
seq = 0
print("开始发送传感器数据 (100 Hz)...")

while True:
    timestamp = time.time()
    x, y, z = seq * 0.1, seq * 0.2, seq * 0.3
    # 打包:序号(I=uint32) + 时间戳(d=double) + 三轴坐标(3f=3个float)
    # 总计 4 + 8 + 12 = 24 字节
    packet = struct.pack("!Id3f", seq, timestamp, x, y, z)
    sock.sendto(packet, ("127.0.0.1", 9002))
    seq += 1
    time.sleep(1.0 / 100)

接收端(只用最新帧):

py 复制代码
import socket
import struct
import time

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(("0.0.0.0", 9002))
sock.settimeout(1.0)

last_seq = -1
drop_count = 0

while True:
    try:
        data, _ = sock.recvfrom(65535)
    except socket.timeout:
        print("超时,连接可能断开")
        break

    seq, ts, x, y, z = struct.unpack("!Id3f", data)
    # 格式字符串与 pack 完全一致,24字节还原为 5 个数值

    # 丢弃乱序到达的旧包
    if seq <= last_seq:
        drop_count += 1
        continue

    last_seq = seq
    latency = (time.time() - ts) * 1000
    print(f"seq={seq:4d}  pos=({x:.2f}, {y:.2f}, {z:.2f})  "
          f"延迟={latency:.1f}ms  累计丢弃={drop_count}")

运行结果:

txt 复制代码
seq=   0  pos=(0.00, 0.00, 0.00)  延迟=0.8ms  累计丢弃=0
seq=   1  pos=(0.10, 0.20, 0.30)  延迟=0.7ms  累计丢弃=0
seq=   2  pos=(0.20, 0.40, 0.60)  延迟=0.9ms  累计丢弃=0
seq=   3  pos=(0.30, 0.60, 0.90)  延迟=0.8ms  累计丢弃=0
...

六、UDP 适合哪些场景?

适合:

  • 机器人遥操 --- 旧的控制指令没有价值,宁可丢包也不要等重传
  • 实时音视频 --- 丢一帧比卡顿好(Zoom、直播)
  • 在线游戏 --- 需要低延迟,旧状态无意义
  • DNS 查询 --- 单次请求/响应,不需要连接开销
  • 广播/多播 --- UDP 原生支持,TCP 不支持

不适合:

  • 文件传输 --- 不能丢数据
  • 网页浏览 --- 需要完整 HTML/CSS/JS
  • 数据库操作 --- 不能丢 SQL 指令

七、小结

UDP 的设计哲学是把可靠性的决策权交给应用层。它用最简单的 8 字节报头换来了极低的延迟和最大的灵活性。在实时性比完整性更重要的场景下,UDP 是比 TCP 更合适的选择。理解 UDP 的无连接、不可靠、边界保留三个核心特性,才能写出正确处理丢包和乱序的健壮代码。

相关推荐
pengyi8710152 小时前
共享 IP 与独享 IP 怎么选?被封后升级方案避坑
网络·网络协议·tcp/ip
YuanDaima20482 小时前
Linux 进阶运维与 AI 环境实战:进程管理、网络排错与 GPU 监控
linux·运维·服务器·网络·人工智能
凯勒姆4 小时前
网工网络设备原理及配置
网络·智能路由器
上海云盾-小余4 小时前
网站恶意爬虫拦截策略:智能识别与封禁实操方案
网络·爬虫·安全·web安全
xhbh6664 小时前
网关端口映射和路由器端口转发有什么区别?配置要点全解析
运维·服务器·网络·智能路由器·端口映射·映射·无痕网关
半壶清水5 小时前
用P4 Tutorial、BMv2 和 Mininet‌解析网络第一集------模拟环境搭建
运维·服务器·网络·网络协议·tcp/ip
高翔·权衡之境5 小时前
主题10:实时性——硬实时与软实时
服务器·网络·驱动开发·信息与通信·智能硬件
BullSmall5 小时前
Promtheus和Alertmanager 之间是通过管理平面还是业务层面IP交互
网络协议·tcp/ip·平面
黄筱筱筱筱筱筱筱5 小时前
交换综合实验
网络