开始前,会这些就够了
- 知道 UDP 以报文发送数据,与 TCP 字节流不同。
- 能读懂 Python bytes、socket 与异常处理。
- 允许使用 127.0.0.1 上两个临时 UDP 端口。
- 需要补基础时:先对照 TCP 字节流
- 需要补基础时:读过 TCP 后,再比较结束信号
一条 UDP 消息有十字节,你给 recvfrom 的缓冲区只留四字节。若把它当成文件或 TCP 流,可能打算循环再读剩下六字节。但对 UDP,这个想法可能让你丢掉一条消息还浑然不知。
本实验真的发送 ABCDEFGHIJ。Windows 的第一次读取报告消息太大;随后才发送 NEXT,下一次读取拿到的就是 NEXT。我们再发送空报文和 AFTER,检查空字节也不能被不加区分地当成结束连接。
把例子带到自己的电脑上
下载完整练习包
需要 Python 3.10 或更新版本,无第三方依赖;制作时实测 Windows / Python 3.14.0。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。
下载 ZIP · 5 个文件包里有什么
- demo.py:完整可运行示例
- README.md:运行说明与独立练习
- observed.stdout.txt:本次实际输出
- verification.json:源码和产物验证记录
- SHA256SUMS.txt:内容摘要
在这个文件夹打开 PowerShell,逐行运行
python --version
python -B demo.py解压到独立练习文件夹,在含 demo.py 的目录打开终端运行。Windows本机127.0.0.1 UDP的10040错误、后继报文、重发、空报文与9项检查;Linux行为仅官方手册核对,未在Linux运行
找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
接收的是一份报文,缓冲区不足不会变成自动分片读取
UDP 保留报文边界。一次普通接收处理的是队列中的一条报文,接收缓冲区用于容纳这次交给应用的内容;它不是让应用声明“先读这一段,其他字节下次接着给我”。
Windows Winsock 的 recvfrom 文档说明,报文大于提供的缓冲区时会报告 WSAEMSGSIZE;对 UDP,超出的部分会丢失。本轮 Python 实际抛出的错误码是 10040。Python 这个调用没有正常返回一个“前四字节加剩余句柄”的结果。
Linux 的常见行为是返回能放下的前缀,并可通过相应消息接口观察截断标记。错误表现可以不同,剩余尾部不会成为下一次普通接收可续读的同一消息。本篇将 Linux 作为文档对照,没有声称已经在 Linux 执行。
第二条报文要等第一次接收完成后再发送
代码先发送十字节,再完成四字节缓冲的第一次 recvfrom。捕获预期错误之后,才发送 NEXT,并使用足够大的缓冲区读取。这样第二次得到 NEXT,而不是 EFGHIJ,能够直接观察下一次接收处理的是另一条报文。
我们没有一口气发送许多报文再猜哪条被读到了,也没有把 UDP 在真实网络上的顺序可靠性当成前提。所有数据都从自己的本机发送套接字送到自己的接收套接字,并检查了收到的源地址。
随后重新发送完整的十字节报文,用三十二字节缓冲区接收,成功得到 ABCDEFGHIJ。这里的“重新发送”是一次新的发送操作,不是被系统保存起来的旧尾部。两次操作要在日志中分开描述。
零字节报文可以是合法输入,不能照搬流的 EOF 判断
接着发送 b'',也就是载荷长度为零的 UDP 报文。接收端确实拿到空字节串,再发送 AFTER,仍然正常收到。UDP 没有因为空载荷而自动完成某个 TCP 式的关闭握手。
在 TCP 接收循环里,空字节串常用于判断对端这一方向已结束;在 UDP 接收中,空载荷可以就是一条报文。能否把它解释为你的应用结束指令,应由你定义的报文协议决定,而不是仅复制 if not data: break。
本实验的空报文没有业务含义,只用来检验读取逻辑。真实应用若不允许空载荷,可以明确记录并拒绝这条输入,然后按协议继续或停止;不能把未定义的含义偷偷塞进底层传输判断。
应用接收空间与系统队列容量是两件事
recvfrom(4) 的四字节是这次应用调用提供的内容空间。即使系统允许排队更多报文,也不会让这次调用自动返回超过四字节的完整消息。因此只调大套接字接收队列,不等于修正了单条报文的应用缓冲区。
应用应定义一条消息允许多大,并使用足以容纳该上限的接收方式,同时识别和拒绝截断结果。若协议需要更大内容,应另行设计分片、编号与组装规则,或选用更适合的传输方案;本例不实现这些机制。
UDP 也不承诺送达、顺序或去重。即使这次本机收到所有小报文,也不能据此证明真实网络不会丢包;本文研究的是应用缓冲区不足造成的本地接收边界。
保留 Windows 实测输出,再对照其他平台分支
保存为 demo.py 后执行 python -B demo.py。两个套接字都只绑定 127.0.0.1,端口由系统分配,每次接收带三秒保护;不使用外部服务器或广播。
本次 Python 3.14.0、Windows 实跑完成九项检查。首行应是 message_too_large:10040,后续依次确认 NEXT、完整重发、空载荷与 AFTER。代码也为直接返回前缀的平台留出分支,但下面保存的预期输出明确来自 Windows。
若本机策略阻止回环通信,实验会报告异常而不是悄悄改成模拟成功。任何非预期错误码都不会被本例当作正常截断吞掉;两个套接字无论成功失败都会关闭。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
"""Real loopback UDP. A short receive buffer does not defer a datagram's tail."""
from contextlib import closing
import errno
import os
import socket
def main():
checks = 0
def check(condition, message):
nonlocal checks
if not condition:
raise AssertionError(message)
checks += 1
with closing(socket.socket(socket.AF_INET, socket.SOCK_DGRAM)) as receiver:
with closing(socket.socket(socket.AF_INET, socket.SOCK_DGRAM)) as sender:
receiver.bind(("127.0.0.1", 0))
sender.bind(("127.0.0.1", 0))
receiver.settimeout(3)
sender.settimeout(3)
address = receiver.getsockname()
payload = b"ABCDEFGHIJ"
check(sender.sendto(payload, address) == len(payload), "first datagram accepted by sendto")
try:
first, peer = receiver.recvfrom(4)
except OSError as error:
code = getattr(error, "winerror", None) or error.errno
check(code in (10040, errno.EMSGSIZE), "only message-too-large is expected")
first_result = f"message_too_large:{code}"
else:
check(first == b"ABCD", "truncated-return platforms expose only the prefix")
first_result = "truncated_prefix:ABCD"
if os.name == "nt":
check(first_result == "message_too_large:10040", "Windows recvfrom reports WSAEMSGSIZE")
else:
check(first_result == "truncated_prefix:ABCD", "POSIX run must show the observed prefix behavior")
# Send NEXT only after the first receive has completed.
sender.sendto(b"NEXT", address)
following, peer = receiver.recvfrom(32)
check(following == b"NEXT", "tail EFGHIJ must not appear in a later receive")
check(peer == sender.getsockname(), "received datagram must come from our sender")
sender.sendto(payload, address)
complete, _ = receiver.recvfrom(32)
check(complete == payload, "a sufficient buffer retains the whole datagram")
check(sender.sendto(b"", address) == 0, "zero-length UDP payload is valid")
empty, _ = receiver.recvfrom(32)
check(empty == b"", "empty payload arrives as a datagram")
sender.sendto(b"AFTER", address)
after, _ = receiver.recvfrom(32)
check(after == b"AFTER", "empty datagram is not a stream EOF")
print(f"short_buffer={first_result}")
print(f"next_datagram={following!r}; complete_datagram={complete!r}")
print(f"empty_datagram={empty!r}; after_empty={after!r}")
print(f"PASS checks={checks}; sockets_closed=True")
if __name__ == "__main__":
main()
怎样运行:在独立练习文件夹中执行 python -B demo.py。2026-10-17 已在 Windows、Python 3.14.0 运行此份源码并通过自带检查。Windows本机127.0.0.1 UDP的10040错误、后继报文、重发、空报文与9项检查;Linux行为仅官方手册核对,未在Linux运行
运行后对照结果
short_buffer=message_too_large:10040
next_datagram=b'NEXT'; complete_datagram=b'ABCDEFGHIJ'
empty_datagram=b''; after_empty=b'AFTER'
PASS checks=9; sockets_closed=True排查报文问题时,先记下传输类型和接收容量
发现消息缺一截时,应记录原报文编号与长度、应用提供的缓冲区长度、平台返回值或错误,以及下一条接收到的报文身份。只看到下一次读取有数据,不能证明它补齐了上一条。
实现通用接收组件时,也应把 TCP 与 UDP 的返回值解释分别测试。空值、超时、截断和连接结束并不是一个共享的“没有数据”分支,它们后续要采取的动作并不相同。
记住这三点
- UDP 应用缓冲区太短,剩余尾部不能按 TCP 字节流方式续读。
- Windows 的 10040 与 Linux 常见截断前缀是不同的接口表现,要按平台验证。
- 空 UDP 载荷是一条可能合法的报文,不自动表示 TCP 式 EOF。
别把例子用过头
- 把多次 UDP recvfrom 当成可以顺序拼回一条任意长消息。
- 只调系统接收队列大小,不检查每次调用提供的空间。
- 把空报文统一当作连接结束。
- 本机实验成功就承诺真实网络会有序可靠送达。
自己试一小步
给每条报文加一个编号和声明载荷长度,怎样区分“这条完整”“这条截断”与“下一条来了”?
- 先设计一个固定大小的教学报头,只在自己生成的小报文中使用。
- 分别发送小于、等于和大于应用接收上限的报文。
- 为截断后接收下一条消息保存编号和平台结果。
- 加入空载荷合法与不合法两种应用契约,观察同一底层返回应如何解释。
做到这些,就算完成
- 不会把下一条报文拼到上一条残缺内容后面。
- 平台差异和本机实际结果分开记录。
- 声明长度不能替代实际收到字节数量的检查。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- Microsoft Winsock:recvfrom 的报文截断与空载荷 (新标签页)↗
核对 WSAEMSGSIZE、UDP 超出部分丢失和空报文返回零。
- Linux man-pages:udp(7) (新标签页)↗
对照 Linux 的一报文接收、截断和
MSG_TRUNC说明。 - RFC 768:UDP 的报文与长度 (新标签页)↗
支持 UDP 报文接口、长度字段及不保证可靠有序送达的边界。
- Python 3.14:socket.recvfrom 与平台边界 (新标签页)↗
核对 recvfrom 返回数据与地址、系统错误和超时封装。