---
title: "TCP 一次 recv 读到半条消息：用长度前缀恢复消息边界"
date: 2026-11-13
slug: tcp-length-prefixed-frames
canonical: https://kernelranch.com/blog/2026/11/tcp-length-prefixed-frames
markdown_url: https://kernelranch.com/blog/2026/11/tcp-length-prefixed-frames.md
tags: ["network", "tcp", "python"]
---TCP 应用接收到的是字节流。一次 `recv()` 可能只给出消息的一部分，也可能读到多条消息的字节。协议需要约定边界。这里用 4 字节无符号大端长度加正文，读取方先拼齐头部，再按声明的长度读取正文。

## 1. 本机复现

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

```python
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")
```

本地实测输出如下。

```text
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.2 | TCP 提供可靠、有序的字节流 |
| Python Socket Programming HOWTO | https://docs.python.org/3.11/howto/sockets.html#using-a-socket | 读写循环、EOF 与消息分帧 |

## 核验说明

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