随着智能硬件愈来愈流行,不少后端开发人员都有可能接触到socket编程。而不少状况下,服务器与端上须要保证数据的有序,稳定到达,天然而然就会选择基于tcp/ip协议的socekt开发。开发过程当中,常常会遇到tcp粘包,拆包的问题,本文将从产生缘由,和解决方案以及workerman是如何处理粘包拆包问题的,这几个层面来讲明这个问题。php
什么是粘包拆包编程
对于什么是粘包、拆包问题,我想先举两个简单的应用场景:json
客户端和服务器创建一个链接,客户端发送一条消息,客户端关闭与服务端的链接。后端
客户端和服务器简历一个链接,客户端连续发送两条消息,客户端关闭与服务端的链接。数组
对于第一种状况,服务端的处理流程能够是这样的:当客户端与服务端的链接创建成功以后,服务端不断读取客户端发送过来的数据,当客户端与服务端链接断开以后,服务端知道已经读完了一条消息,而后进行解码和后续处理...。对于第二种状况,若是按照上面相同的处理逻辑来处理,那就有问题了,咱们来看看第二种状况下客户端发送的两条消息递交到服务端有可能出现的状况:服务器
第一种状况:网络
服务端一共读到两个数据包,第一个包包含客户端发出的第一条消息的完整信息,第二个包包含客户端发出的第二条消息,那这种状况比较好处理,服务器只须要简单的从网络缓冲区去读就行了,第一次读到第一条消息的完整信息,消费完再从网络缓冲区将第二条完整消息读出来消费。app
没有发生粘包、拆包示意图框架
第二种状况:socket
服务端一共就读到一个数据包,这个数据包包含客户端发出的两条消息的完整信息,这个时候基于以前逻辑实现的服务端就蒙了,由于服务端不知道第一条消息从哪儿结束和第二条消息从哪儿开始,这种状况实际上是发生了TCP粘包。
TCP粘包示意图
第三种状况:
服务端一共收到了两个数据包,第一个数据包只包含了第一条消息的一部分,第一条消息的后半部分和第二条消息都在第二个数据包中,或者是第一个数据包包含了第一条消息的完整信息和第二条消息的一部分信息,第二个数据包包含了第二条消息的剩下部分,这种状况实际上是发送了TCP拆,由于发生了一条消息被拆分在两个包里面发送了,一样上面的服务器逻辑对于这种状况是很差处理的。
TCP拆包示意图
产生tcp粘包和拆包的缘由
咱们知道tcp是以流动的方式传输数据,传输的最小单位为一个报文段(segment)。tcp Header中有个Options标识位,常见的标识为mss(Maximum Segment Size)指的是,链接层每次传输的数据有个最大限制MTU(Maximum Transmission Unit),通常是1500比特,超过这个量要分红多个报文段,mss则是这个最大限制减去TCP的header,光是要传输的数据的大小,通常为1460比特。换算成字节,也就是180多字节。
tcp为提升性能,发送端会将须要发送的数据发送到缓冲区,等待缓冲区满了以后,再将缓冲中的数据发送到接收方。同理,接收方也有缓冲区这样的机制,来接收数据。
发生TCP粘包、拆包主要是因为下面一些缘由:
应用程序写入的数据大于套接字缓冲区大小,这将会发生拆包。
应用程序写入数据小于套接字缓冲区大小,网卡将应用屡次写入的数据发送到网络上,这将会发生粘包。
进行mss(最大报文长度)大小的TCP分段,当TCP报文长度-TCP头部长度>mss的时候将发生拆包。
接收方法不及时读取套接字缓冲区数据,这将发生粘包。
……
如何解决拆包粘包
既然知道了tcp是无界的数据流,且协议自己没法避免粘包,拆包的发生,那咱们只能在应用层数据协议上,加以控制。一般在制定传输数据时,可使用以下方法:
使用带消息头的协议、消息头存储消息开始标识及消息长度信息,服务端获取消息头的时候解析出消息长度,而后向后读取该长度的内容。
设置定长消息,服务端每次读取既定长度的内容做为一条完整消息。
设置消息边界,服务端从网络流中按消息编辑分离出消息内容。
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的数据格式的包体数据。
基于workerman的解决方案
制定了数据协议,那咱们下面来经过代码具体分析一下,php中workerman,是如何解决上述问题的。为了便于理解,能够看下下面的流程图
workerman是基于策略模式来设计处理tcp粘包,拆包问题的。具体数据协议的制定在应用目录Applications/YourApp/Protocols目录下,实现则是在框架目录Workerman/Connection/TcpConnection.php中。这样的好处就是用户能够随意定制本身的数据协议格式,而框架代码都能处理。
咱们如今Applications/YourApp/Protocols目录下,建一个jsonNL.php,来实现本身制定本身定义的数据协议。
JsonNL.php的实现
再看下TcpConnection.php中,接收数据时,如何处理。
上面的代码比较多,不须要细读,几个关键的地方能够看出处理的思路,先把接收的数据包追加到_recvBuffer变量中,而后调用用户本身定义的数据协议中的input方法。input方法则会判断数据中是否包含边界符,若是不包含则返回0,包含则返回当前数据包的大小。框架中接收到input的返回值后,若是接收值为0,则跳出循环不作处理,若是接收值不为0,则将截取的数据包赋值给one_request_buffer,而且重置_recvBuffer
最后:tcp虽然是个强大的协议,能保证数据的稳定性,一致性,但在实际开发中,咱们还须要根据实际的数据协议,来控制每次获取的包是客户端发过来的一个完整的能够解析的包。