--- tags: [计算机网络, TCP粘包, TCP拆包, 应用层协议] create time: 2026-05-18 02:10 --- # TCP 粘包与拆包 ## 概述 TCP 是字节流协议(Byte Stream Protocol),没有"消息边界"的概念。这导致发送方多次 write 可能合并成一个段发出(粘包),也可能一个 write 被分成多个段到达(拆包)。**这不是 Bug,这是 TCP 的设计特性**——需要在应用层解决。 > [!QUESTION] 为什么 HTTP/gRPC 不需要处理粘包? > HTTP/1.1 用 `Content-Length` 或 `Chunked Transfer Encoding` 定义边界;gRPC/Protobuf 每个消息带长度前缀;WebSocket 有 Frame Header。它们都在 TCP 之上抽象了一层消息协议,隐式解决了这个问题。 ## 为什么会产生粘包和拆包 ### 粘包的成因 ``` 发送端操作: write("HELLO") → 进入发送缓冲区 write("WORLD") → Nagle 算法延迟合并 read()/close() → 触发 Fin 实际网络上的 TCP 段: "HELLOWORLD" (一次性发出) ``` 触发条件: | 条件 | 说明 | |------|------| | **Nagle 算法** | 小数据合并后统一发送以减少小包数量 | | **接收端读取慢** | 发送端的多个 write 堆积在发送缓冲区 | | **SendBuffer 累积** | 多个连续 write 都未触发 flush | ### 拆包的成因 ``` 发送端操作: write("A very long message that exceeds MTU...") // 超过 MSS(1460B) 实际网络上的 TCP 段: Segment 1: [Bytes 0..1459] ← MSS = 1500 - 20(IP) - 20(TCP) = 1460 Segment 2: [Bytes 1460..end] ← 剩余部分 接收端 read(1024): ← 只读了前一部分 返回: "A very long m..." ← 不完整! ``` 触发条件: | 条件 | 说明 | |------|------| | **数据包 > MSS** | 超过路径 MTU 自动分片 | | **接收端 read 大小太小** | 一次只读了一部分 | | **网络抖动 / RTT 变化** | 数据到达时间不均匀 | ## 四种解决策略 ### 方法一:固定长度 每条消息固定 N 字节,接收端按 N 字节切割: ```go const msgLen = 1024 // 每条消息 1KB func ReadFixedLength(conn net.Conn) ([]byte, error) { buf := make([]byte, msgLen) n, err := io.ReadFull(conn, buf) if err != nil { return nil, err } return buf[:n], nil } ``` | 优点 | 缺点 | |------|------| | 实现简单 | 短消息浪费带宽,长消息不够用 | | 性能最高 | — | 适用场景:**游戏协议、内部 RPC(如 Dubbo 早期版本)** ### 方法二:特殊分隔符 用特定字符(如 `\r\n\r\n`、`\0`)作为消息结束标志: ```go func ReadDelimited(conn net.Conn) ([]byte, error) { var buf bytes.Buffer for { b := make([]byte, 1) _, err := conn.Read(b) if err != nil { return nil, err } buf.Write(b) if bytes.HasSuffix(buf.Bytes(), []byte("\r\n\r\n")) { return buf.Bytes()[:buf.Len()-4], nil } } } ``` | 优点 | 缺点 | |------|------| | 简单直观 | 消息体中不能包含分隔符 | | 类似 HTTP 协议 | — | 适用场景:**HTTP/1.1、SMTP、IMAP** ### 方法三:长度前缀 ⭐(推荐) 在消息开头加上表示长度的字段,最常见的有两种变体: #### 方案 A:4 字节整数 + 消息体 ```go // 最常用格式: [4-byte length][payload...] // Go encoding/binary 处理 func WriteWithLength(conn net.Conn, data []byte) error { length := uint32(len(data)) binary.Write(conn, binary.BigEndian, length) // 大端序保证跨语言兼容 conn.Write(data) return nil } func ReadWithLength(conn net.Conn) ([]byte, error) { var length uint32 binary.Read(conn, binary.BigEndian, &length) buf := make([]byte, length) if length > 0 { _, err := io.ReadFull(conn, buf) if err != nil { return nil, err } } return buf, nil } ``` ``` 内存布局: ┌─────────────┬──────────────────────────┐ │ Length(uint32) │ Payload │ │ 4 bytes │ variable bytes │ │ 100 │ [actual data of 100B] │ └─────────────┴──────────────────────────┘ ``` #### 方案 B:protobuf 风格(L-Type-V) ``` ┌────────┬───────┬──────────────────────────┐ │ Length │ Type │ Value │ │ VarInt │ VarInt │ variable bytes │ │ (1-10B) │(1-10B) │ │ └────────┴───────┴──────────────────────────┘ ``` | 优点 | 缺点 | |------|------| | ✅ 最灵活,支持变长消息 | ❌ 比固定长度略复杂 | | ✅ gRPC/Protobuf/Hessian2 都用此方式 | ❌ 需要处理超大消息 | | ✅ 跨语言天然兼容 | — | | ✅ 无分隔符冲突问题 | — | ### 方法四:两者结合(工业级) ```go // 工业级协议帧结构 // ┌──────┬──────┬──────┬──────────┬──────┬──────┐ │ Magic │Ver │Type │RequestID │Length│Payload │CRC │ │ 4B │1B │1B │ 4B │4B │Var │4B │ │ 0xABCD│ 1.0 │REQ │ 0x0001 │1024 │data...│ CRC32│ │ EF header │ │ Footer │ └──────┴──────┴──────┴──────────┴──────┴────────┘ 总开销: 4+1+1+4+4+4 = 18 bytes overhead ``` ```go type FrameHeader struct { Magic uint32 // 魔数,防解析错误 Version byte // 协议版本 MessageType byte // 请求/响应/心跳等 RequestID uint32 // 关联请求响应 Length uint32 // Payload 长度 Checksum uint32 // CRC32 校验 } ``` ## 各语言的内置支持 | 语言/框架 | 粘包处理 | 说明 | |-----------|---------|------| | **Go** stdlib `net` | ❌ 零拷贝,完全手动 | 但 `bufio.Scanner` 可帮你 | | Go `encoding/binary` | ✅ Binary 编解码 | 配合 length-prefix | | Java Netty | ✅ 内建 | `DelimiterBasedFrameDecoder`、`LengthFieldBasedFrameDecoder` | | Python asyncio | ❌ 需手动 | 或用 `asyncio.Protocol` | | C++ Boost.Asio | ❌ 需手动 | 或用自定义解包器 | | Rust tokio | ❌ 需手动 | `tokio_util::codec` crate 支持 | ## Nagle 算法的影响 Nagle 算法的初衷是减少小包的发送数量——它规定:**如果有一个未被确认的小包,就不要发送新的小包**。 ```go // Go net.TCPConn 中关闭 Nagle conn.(*net.TCPConn).SetNoDelay(true) // true = 禁用 Nagle ``` | 场景 | 建议 | |------|------| | HTTP/Web API | 关闭 (减少首包延迟) | | 实时游戏/金融 | 关闭 (每毫秒都很重要) | | 大数据传输 | 开启 (减少小包开销) | | SSH | 关闭 (交互性要求高) | ## 关联笔记 - [[hhs/NETWORK/TCP段结构与状态机]] — TCP 为何是字节流而非消息流 - [[hhs/NETWORK/HTTP请求响应]] — HTTP 如何用 Content-Length 解决此问题 - [[hhs/NETWORK/WebSocket全双工通信]] — WebSocket Frame Header 自带 Length