qinghuan
← 回到知识探索

后端现场 / 可下载小项目

UDP 只读得下半条消息,剩下的能留到下次吗?

在 Windows 本机实际收到 10040 错误后,再发一条带标记的新报文;观察尾部没有续读,并验证空 UDP 报文不等于 TCP 的结束。

展开本文目录
  1. 下载完整练习包
  2. 接收的是一份报文,缓冲区不足不会变成自动分片读取
  3. 第二条报文要等第一次接收完成后再发送
  4. 零字节报文可以是合法输入,不能照搬流的 EOF 判断
  5. 应用接收空间与系统队列容量是两件事
  6. 保留 Windows 实测输出,再对照其他平台分支
  7. 排查报文问题时,先记下传输类型和接收容量
  8. 记住这三点
  9. 别把例子用过头
  10. 自己试一小步
  11. 查原始资料
开始前,会这些就够了
遇到不熟悉的词,先看这 1 个解释
数据报
UDP 一次发送形成的一条有边界的消息。一次普通接收针对一条消息,不会把过长消息剩余的尾巴留成下一次接收的续篇。
打开完整术语小抄 ↗

一条 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。

若本机策略阻止回环通信,实验会报告异常而不是悄悄改成模拟成功。任何非预期错误码都不会被本例当作正常截断吞掉;两个套接字无论成功失败都会关闭。

展开收起代码 · 小实验python · 62 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

小实验 · python
"""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 当成可以顺序拼回一条任意长消息。
  • 只调系统接收队列大小,不检查每次调用提供的空间。
  • 把空报文统一当作连接结束。
  • 本机实验成功就承诺真实网络会有序可靠送达。

自己试一小步

给每条报文加一个编号和声明载荷长度,怎样区分“这条完整”“这条截断”与“下一条来了”?

  1. 先设计一个固定大小的教学报头,只在自己生成的小报文中使用。
  2. 分别发送小于、等于和大于应用接收上限的报文。
  3. 为截断后接收下一条消息保存编号和平台结果。
  4. 加入空载荷合法与不合法两种应用契约,观察同一底层返回应如何解释。

做到这些,就算完成

  • 不会把下一条报文拼到上一条残缺内容后面。
  • 平台差异和本机实际结果分开记录。
  • 声明长度不能替代实际收到字节数量的检查。

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。