TCP粘包拆包问题

TCP协议的特点

TCP是面向字节流的协议,TCP中的“流”指的是流入到程序或从进程流出的字节序列。

面向字节流的含义是:虽然应用程序和TCP的交互式一次一个数据块(大小不等),但是TCP把应用程序交付下来的数据仅仅看成是一串无结构的字节流,TCP并不知道所传送的字节流的含义,对于应用程序来说,他看到的数据之间没有边界,也无法得知一条报文从哪里开始,到哪里结束,每条报文有多少字节。

而UDP是面向消息的协议,每个数据包都是独立的,UDP本身不提供拆包和组装的机制,因此UDP通信不会发生粘包问题。

导致粘包的情况

连续发送较短数据

在发送数据时,TCP会根据 nagle算法 ,将时间间隔较短的数据一次性发给对方,也就是说,如果发送端连续发送了好几个数据包,经过 nagle算法 的优化,这些小的数据包就可能被封装成一个大的数据包,一次性发送给接收端,而TCP是面向字节流的通信,没有消息保护边界,所以就产生了粘包问题。

接收端没有及时接受数据

可能发送端发来了一段数据,但接收端只接收了部分数据,剩下小部分数据还遗留在接受缓冲区。那么在下一次接受时,接受区不但有上一次遗留的数据,还可能有来自其他报文数据,他们作为一个整体被接收端接受了,造成粘包。

四种情况

解决粘包

对于应用层一个简单灵活的二进制协议实现可以分为固定消息头,变长消息头,消息体三部分。
自定义协议结构

编码器伪代码

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
type FixedHeader struct {
version byte
msgType byte
msgLen uint32
varHeadLen uint32
crc32Sum uint32
}

type Message struct {
fixedHeader *FixedHeader
varHeader PBData
msgBodey PBData
}

func (msg *Message) Encoder() []byte {
buf := make([]byte, 14)
buf[0] = msg.fixedHeader.version
buf[1] = msg.fixedHeader.msgType
copy(buf[2:6], uint32ToBytes(msg.fixedHeader.msgLen))
copy(buf[6:10], uint32ToBytes(msg.fixedHeader.varHeadLen))
copy(buf[10:14], uint32ToBytes(msg.fixedHeader.crc32Sum))
buf = append(buf, msg.varHeader.Bytes())
buf = append(buf, msg.msgBodey.Bytes())
return buf
}

func send(data []byte) {
sock.Send(data)
...
}

解码器伪代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func Accept() {
buf := make([]byte, 14)
for conn.Read(buf) {
msg := &Message{}
msg.fixedHeader.version = buf[0]
msg.fixedHeader.msgType = buf[1]
msg.fixedHeader.msgLen = bytesTouint32(buf[2:6])
msg.fixedHeader.varHeadLen = bytesTouint32(buf[6:10])
msg.fixedHeader.crc32Sum = bytesTouint32(buf[10:14])
varHeadBuf := make([]byte, msg.FixedHeader.varHeadLen)
conn.Read(varHeadBuf)
msg.VarHeader = pb.Data(varHeadBuf)
bodyBuf := make([]byte, msg.FixedHeader.msgLen)
conn.Read(bodyBuf)
msg.msgBodey = pb.Data(bodyBuf)
header.Pool(msg)
}
}