3. UDP数据报边界验证实验
实验目标与核心问题
本实验要验证一个UDP的关键特性:UDP是面向数据报的协议,保留消息边界。客户端连续调用两次 sendto() 发送两条独立消息,服务端必须通过两次 recvfrom() 分别收到,而不会像TCP那样把两条消息合并成一条字节流。
实验设计要点:
- 客户端发送两条内容不同、长度不同的数据报,例如
"ALPHA"(5字节)和"BRAVO-12345"(11字节)。 - 服务端循环接收,每次
recvfrom()打印收到的字节数和内容,观察是否与发送端一一对应。 - 关键判据:服务端第一次收到5字节,第二次收到11字节,而不是一次收到16字节。
实验拓扑与数据报边界关系
客户端实现(Python)
客户端向 127.0.0.1:9003 连续发送两条独立数据报,中间不等待服务端响应,确保两条消息在时间上紧邻,更能体现边界保持特性。
import socket
SERVER_ADDR = ("127.0.0.1", 9003)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
msg1 = b"ALPHA"
msg2 = b"BRAVO-12345"
sock.sendto(msg1, SERVER_ADDR)
print(f"[客户端] 已发送 #1: {msg1!r} ({len(msg1)}字节)")
sock.sendto(msg2, SERVER_ADDR)
print(f"[客户端] 已发送 #2: {msg2!r} ({len(msg2)}字节)")
sock.close()
服务端实现(Python)
服务端绑定同一端口,循环接收两次,每次打印收到的字节数和内容。注意:recvfrom() 每次只返回一个完整数据报,不会把两条消息拼在一起。
import socket
BIND_ADDR = ("0.0.0.0", 9003)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(BIND_ADDR)
print(f"[服务端] 已绑定 {BIND_ADDR},等待数据报...")
for i in range(1, 3):
data, addr = sock.recvfrom(2048)
print(f"[服务端] 第{i}次接收: 来自 {addr}, "
f"长度={len(data)}字节, 内容={data!r}")
sock.close()
运行与结果判读
先启动服务端,再运行客户端。预期输出如下:
[服务端] 已绑定 ('0.0.0.0', 9003),等待数据报...
[服务端] 第1次接收: 来自 ('127.0.0.1', 54321), 长度=5字节, 内容=b'ALPHA'
[服务端] 第2次接收: 来自 ('127.0.0.1', 54321), 长度=11字节, 内容=b'BRAVO-12345'
边界验证判据:
- 第1次
recvfrom()返回长度 5,内容b'ALPHA'。 - 第2次
recvfrom()返回长度 11,内容b'BRAVO-12345'。 - 如果出现一次接收返回16字节、内容为
b'ALPHABRAVO-12345',则说明发生了字节流合并(UDP正常情况下不会出现)。
常见异常与排查:
- 只收到一条16字节消息:检查是否误用了
SOCK_STREAM(TCP)而非SOCK_DGRAM(UDP)。 - 服务端阻塞在第一次 recvfrom:确认客户端已正确发送,且地址端口一致;可用
ss -ulnp | grep 9003检查绑定状态。 - 收到空数据报:UDP允许0字节数据报,若客户端发送空内容,服务端会收到长度0,这同样证明边界保留(空消息也是一个独立数据报)。
扩展验证:发送顺序与边界独立性
为进一步确认边界与顺序无关,可修改客户端发送顺序为 msg2 先发、msg1 后发,服务端将先收到11字节再收到5字节。这说明UDP保留的是每个数据报的边界,而不是固定顺序或固定长度。
| 发送顺序 | 服务端第1次接收 | 服务端第2次接收 | 结论 |
|---|---|---|---|
| ALPHA → BRAVO-12345 | 5字节 ALPHA | 11字节 BRAVO-12345 | 边界保持 |
| BRAVO-12345 → ALPHA | 11字节 BRAVO-12345 | 5字节 ALPHA | 边界保持,顺序随发送 |