7.2 KiB
7.2 KiB
tags, create time
| tags | 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 字节切割:
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)作为消息结束标志:
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 字节整数 + 消息体
// 最常用格式: [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 都用此方式 | ❌ 需要处理超大消息 |
| ✅ 跨语言天然兼容 | — |
| ✅ 无分隔符冲突问题 | — |
方法四:两者结合(工业级)
// 工业级协议帧结构
// ┌──────┬──────┬──────┬──────────┬──────┬──────┐
│ 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
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 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