避坑-5-TCP粘包
一 TCP粘包优雅处理
1.1 粘包产生的原因
在使用TCP协议分高发送数据时,为了提高发送效率,需要对封包进行拼包处理,将小包凑成大包,在TCP层可以节约包头的大小损耗,I/O层的调用损耗也可以有所降低。但此时也容易形成粘包,也就是没有按照预期的大小得到数据,数据包不完整。
![] (tcpread-01.png)
在接收TCP封包时,接收缓冲区的大小与发送过来的TCP传输单元大小不等,这时候会造成两种情况:
- 接收的数据大于等于接收缓冲区大小时,此时需要将数据复制到用户缓冲,接着 读取后面的封包。
- 接收的数据小于接收缓冲区大小时,此时需要继续等待后续的 TCP 封包。
在go语言的io包中有个函数ReadAtLeast()用来处理封装
func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error) {
if len(buf) < min {
return 0, ErrShortBuffer
}
for n < min && err == nil {
var nn int
nn, err = r.Read(buf[n:])
n += nn
}
if n >= min {
err = nil
} else if n > 0 && err == EOF {
err = ErrUnexpectedEOF
}
return
}
在else判断中,读取目标已经完结,但是己经读取一些数据,也就是说,没 法完成读取任务,发生了不可期望的终结错误。
ReadAtLeast还有个更好的封装:
func ReadFull(r Reader, buf []byte, min int) (n int, err error) {
return ReadAtLeast(r, buf, len(buf))
}
这个函数只需要提供buf接收缓冲区切片,就可以将这个己经分配的buf填充满 。简单地说就是: 给多大空间,填充多少字节,直到填满空间。
使用ReadFull可以优雅地完成对TCP粘包的处理。
1.2 封包发送
封包格式:
- Size:大小,代表Size后面包体的大小
- Body:消息的二进制数据
在发送封包时,需要将封包的Size字段从无符号十六位整型转 换为字节数组(利用binary包的Write函数):
// 二进制封包格式
type Packet struct {
Size uint16 // 包体大小
Body []byte // 包体数据
}
// 将数据写入dataWriter
func writePacket(dataWriter io.Writer, data []byte) error {
// 准备一个字节数组缓冲
var buf bytes.Buffer
// 将Size写入缓冲
err := binary.Write(&buf, binary.LittleEndian, uint16(len(data)))
if err != nil {
return err
}
// 写入包体数据
_, err = buf.Write(data)
if err != nil {
return err
}
// 获取写入的完整数据
out := buf.Bytes()
// 写入完整数据
_, err = dataWriter.Write(out)
if err != nil {
return err
}
return nil
}