tcp粘包和拆包的处理方案

随着智能硬件越来越流行,很多后端开发人员都有可能接触到socket编程。而很多情况下,服务器与端上需要保证数据的有序,稳定到达,自然而然就会选择基于tcp/ip协议的socekt开发。开发过程中,经常会遇到tcp粘包,拆包的问题,本文将从产生原因,和解决方案以及workerman是如何处理粘包拆包问题的,这几个层面来说明这个问题。

什么是粘包拆包

对于什么是粘包、拆包问题,我想先举两个简单的应用场景:

  1. 客户端和服务器建立一个连接,客户端发送一条消息,客户端关闭与服务端的连接。

  2. 客户端和服务器简历一个连接,客户端连续发送两条消息,客户端关闭与服务端的连接。

对于第一种情况,服务端的处理流程可以是这样的:当客户端与服务端的连接建立成功之后,服务端不断读取客户端发送过来的数据,当客户端与服务端连接断开之后,服务端知道已经读完了一条消息,然后进行解码和后续处理...。对于第二种情况,如果按照上面相同的处理逻辑来处理,那就有问题了,我们来看看第二种情况下客户端发送的两条消息递交到服务端有可能出现的情况:

第一种情况:

服务端一共读到两个数据包,第一个包包含客户端发出的第一条消息的完整信息,第二个包包含客户端发出的第二条消息,那这种情况比较好处理,服务器只需要简单的从网络缓冲区去读就好了,第一次读到第一条消息的完整信息,消费完再从网络缓冲区将第二条完整消息读出来消费。

tcp粘包和拆包的处理方案_第1张图片

没有发生粘包、拆包示意图

第二种情况:

服务端一共就读到一个数据包,这个数据包包含客户端发出的两条消息的完整信息,这个时候基于之前逻辑实现的服务端就蒙了,因为服务端不知道第一条消息从哪儿结束和第二条消息从哪儿开始,这种情况其实是发生了TCP粘包。

tcp粘包和拆包的处理方案_第2张图片

TCP粘包示意图

第三种情况:

服务端一共收到了两个数据包,第一个数据包只包含了第一条消息的一部分,第一条消息的后半部分和第二条消息都在第二个数据包中,或者是第一个数据包包含了第一条消息的完整信息和第二条消息的一部分信息,第二个数据包包含了第二条消息的剩下部分,这种情况其实是发送了TCP拆,因为发生了一条消息被拆分在两个包里面发送了,同样上面的服务器逻辑对于这种情况是不好处理的。

tcp粘包和拆包的处理方案_第3张图片

TCP拆包示意图

产生tcp粘包和拆包的原因

我们知道tcp是以流动的方式传输数据,传输的最小单位为一个报文段(segment)。tcp Header中有个Options标识位,常见的标识为mss(Maximum Segment Size)指的是,连接层每次传输的数据有个最大限制MTU(Maximum Transmission Unit),一般是1500比特,超过这个量要分成多个报文段,mss则是这个最大限制减去TCP的header,光是要传输的数据的大小,一般为1460比特。换算成字节,也就是180多字节。

tcp为提高性能,发送端会将需要发送的数据发送到缓冲区,等待缓冲区满了之后,再将缓冲中的数据发送到接收方。同理,接收方也有缓冲区这样的机制,来接收数据。

发生TCP粘包、拆包主要是由于下面一些原因:

  1. 应用程序写入的数据大于套接字缓冲区大小,这将会发生拆包。

  2. 应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包。

  3. 进行mss(最大报文长度)大小的TCP分段,当TCP报文长度-TCP头部长度>mss的时候将发生拆包。

  4. 接收方法不及时读取套接字缓冲区数据,这将发生粘包。

  5. ……

如何解决拆包粘包

既然知道了tcp是无界的数据流,且协议本身无法避免粘包,拆包的发生,那我们只能在应用层数据协议上,加以控制。通常在制定传输数据时,可以使用如下方法:

  1. 使用带消息头的协议、消息头存储消息开始标识及消息长度信息,服务端获取消息头的时候解析出消息长度,然后向后读取该长度的内容。

  2. 设置定长消息,服务端每次读取既定长度的内容作为一条完整消息。

  3. 设置消息边界,服务端从网络流中按消息编辑分离出消息内容。

a)先基于第三种方法,假设区分数据边界的标识为换行符"\n"(注意请求数据本身内部不能包含换行符),数据格式为Json,例如下面是一个符合这个规则的请求包。

{"type":"message","content":"hello"}\n

注意上面的请求数据末尾有一个换行字符(在PHP中用双引号字符串"\n"表示),代表一个请求的结束。

b)基于第一种方法,可以制定,首部固定10个字节长度用来保存整个数据包长度,位数不够补0的数据协议

0000000036{"type":"message","content":"hello"}

c)基于第一种方法,可以制定,首部4字节网络字节序unsigned int,标记整个包的长度

****{"type":"message","content":"hello all"}

其中首部四字节*号代表一个网络字节序的unsigned int数据,为不可见字符,紧接着是Json的数据格式的包体数据。

你可能感兴趣的:(网络)