tcp是字节流协议,conn.read()仅返回内核缓冲区当前可读的任意长度字节,无天然消息边界,易导致粘包/拆包;必须由应用层通过定长头、分隔符或tlv等方式定义消息边界,并配合io.readfull()与setreaddeadline()保障可靠读取。

Go 里实现 TCP 私有协议,核心不是“造轮子”,而是控制好粘包、拆包和消息边界——否则收发数据会错位、丢字节、解析崩溃。
为什么 conn.Read() 不能直接当“一条消息”用
TCP 是字节流,conn.Read() 返回的只是当前内核缓冲区里能读到的任意长度字节,可能是一条消息的前半截,也可能是三条消息拼在一起。没有内置消息边界,靠应用层定义。
- 常见错误现象:
json.Unmarshal()报invalid character,或解出乱码、字段缺失 - 典型场景:客户端连续发两个
{"cmd":"login"},服务端一次Read()收到{"cmd":"login"}{"cmd":"ping"},JSON 解析器卡在第二个{上 - 根本原因:没做消息定界,把多条消息当一条处理了
三种主流定界方式怎么选
选哪种取决于协议设计目标:简单性、兼容性、扩展性。别一上来就搞 TLV(Type-Length-Value)——多数内部服务用前两种足够。
-
固定头长 + 消息体长度字段(推荐):头 4 字节存
uint32表示 body 长度,先读够 4 字节,再按长度读 body。Go 里用binary.Read(conn, binary.BigEndian, &length)+ 循环io.ReadFull()最稳 -
特殊分隔符(如
\n或\x00):适合纯文本协议,但必须确保消息体里不出现该字符;用bufio.Scanner可以省事,但注意MaxScanTokenSize默认只有 64KB,超长消息会失败 -
TLV 结构:适合需要多类型混合、未来要加字段的协议,但解析成本高,调试困难;Go 中建议用
encoding/binary手动读 type/length,避免反射开销
io.ReadFull() 和 conn.SetReadDeadline() 必须配对用
只靠 ReadFull() 不够——如果对方断连或卡死,你的 goroutine 会永久阻塞。必须设读超时,否则连接堆积、goroutine 泄露。
- 错误写法:
io.ReadFull(conn, header[:])前没设 deadline,网络异常时 goroutine 挂住 - 正确做法:每次读前调用
conn.SetReadDeadline(time.Now().Add(30 * time.Second)),超时后检查err是否为net.ErrDeadlineExceeded - 性能影响:deadline 是 per-call 的,不影响后续读写;但频繁设时间戳有微小开销,可复用
time.Now()结果
结构体序列化别默认用 json,优先考虑 gob 或自定义二进制编码
JSON 虽易调试,但字段名重复传输、无类型信息、解析慢,在私有协议中是冗余负担。
- 内部服务间通信:用
gob最省事,支持 struct 直接编解码,且 Go 原生保证兼容性(同版本下) - 跨语言或需人眼可读:用 Protocol Buffers,但得加 build 步骤;别手写 JSON 字段映射,容易漏
json:"xxx"tag - 绝对性能敏感场景(如高频行情推送):手写二进制编码,用
binary.Write()写字段顺序,跳过任何中间结构体分配
真正麻烦的从来不是“怎么发”,而是“怎么确认对方收到了、处理成功了、要不要重发”。TCP 只保证送达,不保证业务语义;私有协议里 ACK、心跳、重连策略这些,比编码方式重要得多。











