KernelRanch
返回文章列表

TCP 一次 recv 读到半条消息:用长度前缀恢复消息边界

2026-11-13 #network#tcp#python

TCP 应用接收到的是字节流。一次 recv() 可能只给出消息的一部分,也可能读到多条消息的字节。协议需要约定边界。这里用 4 字节无符号大端长度加正文,读取方先拼齐头部,再按声明的长度读取正文。

1. 本机复现

保存为 frame_lab.py,运行 python frame_lab.py。脚本使用本机套接字对,并让每次读取最多取两个字节,稳定覆盖多次读取才能拼齐的分支。

from socket import socketpair
from struct import pack, unpack

MAX_FRAME = 1024

def read_exact(sock, size, *, allow_eof=False):
    data = bytearray()
    while len(data) < size:
        chunk = sock.recv(min(2, size - len(data)))
        if not chunk:
            if allow_eof and not data:
                return None
            raise EOFError(f"expected {size} bytes, got {len(data)}")
        data.extend(chunk)
    return bytes(data)

def read_frame(sock):
    header = read_exact(sock, 4, allow_eof=True)
    if header is None:
        return None
    (size,) = unpack("!I", header)
    if size > MAX_FRAME:
        raise ValueError(f"frame too large: {size}")
    return read_exact(sock, size)

def encode(body):
    return pack("!I", len(body)) + body

def run(wire):
    sender, receiver = socketpair()
    with sender, receiver:
        sender.settimeout(2)
        receiver.settimeout(2)
        sender.sendall(wire)
        sender.close()
        frames = []
        while (body := read_frame(receiver)) is not None:
            frames.append(body)
        return frames

frames = run(encode(b"hello") + encode(b"") + encode(b"bye"))
assert frames == [b"hello", b"", b"bye"]
print("frames:", frames)

for wire, expected in (
    (b"\x00\x00", "expected 4 bytes, got 2"),
    (pack("!I", 5) + b"ab", "expected 5 bytes, got 2"),
    (pack("!I", MAX_FRAME + 1), "frame too large: 1025"),
):
    try:
        run(wire)
    except (EOFError, ValueError) as error:
        assert str(error) == expected
        print(error)
    else:
        raise AssertionError("accepted an invalid frame")

本地实测输出如下。

frames: [b'hello', b'', b'bye']
expected 4 bytes, got 2
expected 5 bytes, got 2
frame too large: 1025

2. 区分正常关闭、消息截断和长度错误

读取状态处理
还没读到下一个头的任何字节,就收到 EOF流正常结束
头或正文已经开始读取,随后收到 EOF当前消息截断,不能交给业务处理
长度超过协议上限在分配正文缓冲区前拒绝,并关闭连接
长度为零按协议处理空正文。本例允许空消息

示例里的两字节读取上限是为了覆盖分支。生产中可根据剩余字节数和缓冲区容量决定读取量。如果需要限制慢速连接,给整条消息设置截止时间。仅给每次 recv() 单独设置超时,无法限制持续少量发送的对端把消息拖多久。

小结

为字节流定义明确的长度格式和上限。仅将读取完整的帧交给业务,并区分帧边界上的 EOF 和帧内截断。

数据来源与检索时间

检索时间 2026-11-13。

来源URL对应内容
RFC 9293 第 2.2 节https://www.rfc-editor.org/rfc/rfc9293.html#section-2.2TCP 提供可靠、有序的字节流
Python Socket Programming HOWTOhttps://docs.python.org/3.11/howto/sockets.html#using-a-socket读写循环、EOF 与消息分帧

核验说明

  • Windows、Python 3.11.9 用 socketpair 实测连续三帧、空消息、头和正文截断及超长声明。读取上限由示例控制,没有测真实网络的分段和吞吐。