TCP-粘包

TCP 协议是面向连接的、可靠的、基于字节流的传输层通信协议,应用层交给 TCP 协议的数据并不会以消息为单位向目的主机传输,这些数据在某些情况下会被组合成一个数据段发送给目标的主机。

问题原因

基于字节流的传输协议,会有控制算法 拆分,组合应用层协议的数据

应用层协议 没有定义 消息的边界 导致 接收方 无法 拼接数据

同时 UDP / 网络层IP协议 等不存在粘包问题的原因是:
UDP 对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。这就涉及到包大小确定:
UDP包允许发送最大值 64K,理想大小(1500 / 548 内外网)需要应用层来手动处理数据问题,但是TCP不存在包大小限制。

IP协议 基于 数据包 拆分为 IP报文的传输与接收是有边界协议的;并且数据在TCP传输层就会考虑到 IP报文的大小限制,主动分段处理。
IP分片一般用在UDP

TCP下的延迟发包算法:Nagle

减少发包的方式提高TCP传输性能的算法,但是现在服务器配置一般默认关闭;

  • TCP_NODELAY 延迟发小包,等待小包重组小包发送
  • TCP_CORK 小包延迟200ms发送

解决方式

设置消息边界即可;有两种方案 基于长度的封包/解包,基于终结符的组包

HTTP 协议的消默认息边界就是基于长度实现的:在 header头中添加了 Content-Length 字段。当应用层协议解析到足够的字节数后,就能从中分离出完整的 HTTP 消息,无论发送方如何处理对应的数据包,我们都可以遵循这一规则完成 HTTP 消息的重组

HTTP 协议除了使用基于长度的方式实现边界,也会使用基于终结符的策略,当 HTTP 使用块传输(Chunked Transfer)机制时,HTTP 头中就不再包含 Content-Length 了,它会使用负载大小为 0 的 HTTP 消息作为终结符表示消息的边界。

自定义消息边界实现

基于消息长度的,应用层添加封包/解包逻辑

  • 添加固定字节的头信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

// tcp 粘包现象:主要原因就是tcp数据传递模式是流模式,在保持长连接的时候可以进行多次的收和发。
// “粘包”可发生在发送端也可发生在接收端:
// 1. 由Nagle算法造成的发送端的粘包
// 2. 接收端接收不及时造成的接收端粘包

// 粘包解决:
// 出现”粘包”的关键在于接收方不确定将要传输的数据包的大小,因此我们可以对数据包进行封包和拆包的操作。
// 封包:封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(过滤非法包时封包会加入”包尾”内容)。包头部分的长度是固定的,并且它存储了包体的长度,根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包。

// 我们可以自己定义一个协议,比如数据包的前4个字节为包头,里面存储的是发送的数据的长度。

func EncodePack(message string) ([]byte, error) {
// 读取消息的长度,转换成int32类型(占4个字节)
var length = int32(len(message))
var pkg = new(bytes.Buffer)
// 写入消息头
err := binary.Write(pkg, binary.LittleEndian, length)
if err != nil {
return nil, err
}
// 写入消息实体
err = binary.Write(pkg, binary.LittleEndian, []byte(message))
if err != nil {
return nil, err
}
return pkg.Bytes(), nil
}

func DecodePack(reader *bufio.Reader) (string, error) {
// 读取消息长度
legthByte, _ := reader.Peek(4)
legthBuff := bytes.NewBuffer(legthByte)
var length int32
err := binary.Read(legthBuff, binary.LittleEndian, &length)
if err != nil {
return "", err
}
// Buffered返回缓冲中现有的可读取的字节数。
if int32(reader.Buffered()) < length+4 {
return "", err
}

// 读取真正的消息数据
pack := make([]byte, int(4+length))
_, err = reader.Read(pack)
if err != nil {
return "", err
}
return string(pack[4:]), nil
}
  • 大端/小端写入:网络字节序,一般是大端,CPU写入内存主流是小端