tcp粘包问题
什么是粘包
TCP粘包问题是指在使用TCP协议进行网络通信时,客户端和服务器之间发送的数据包可能会被TCP协议栈在底层进行合并或者拆分,导致客户端接收到的数据不再是单独、完整的数据包,而是多个数据包的内容被粘在一起或者多个数据包的内容被拆分到不同的接收缓冲区中。
粘包原因
1. 因为TCP是面向字节流的协议
传输的数据是以流的形式,而流数据是没有明确的开始结尾边界,所以 TCP 也没办法判断哪一段流属于一个消息;TCP协议是流式协议;所谓流式协议,即协议的内容是像流水一样的字节流,内容与内容之间没有明确的分界标志,需要认为手动地去给这些协议划分边界。
例如客户端每次发送N个字节给服务端,N取决于当前客户端的发送缓冲区是否有数据,比如发送缓冲区总大小为10个字节,当前有5个字节数据(例如上次要发送的数据‘loveu’)未发送完,那么此时只有5个字节的空闲空间,客户端调用发送接口发送“hello world!”其实就是只能发送“hello”给服务器,那么服务器一次性得到的数据就是“loveuhello”,而剩余的“world!”只能留给下一次发送,下一次服务器收到的就是“world!”
2. 数据发送和接收速率不匹配
如果发送方发送数据的速度比接收方处理数据的速度快,就可能导致多个消息被一次性读取。比如客户端1s内发送了两次“hello world!”,服务器过了2s才接收到数据,那一次性就会读出两个“hello world”
3. tcp底层的安全和效率机制不允许字节数特别少的小包发送频率过高
tcp会在底层累计数据长度到一定大小才一起发送,比如连续发送1字节的数据要累计到多个字节才发送,可以了解下tcp底层的Nagle算法 。
处理粘包
处理粘包的方式主要采用应用层定义收发包格式的方式,这个过程俗称切包处理,常用的协议被称为tlv
协议(消息id+消息长度+消息内容)
为了方便理解,这里先简化发送格式,改成“消息长度+消息内容”的方式
消息节点
1 |
|
Session的改进
为了能够对收到的数据进行切包处理,需要定义一个消息接收节点、一个bool变量表示头部信息是否处理完成,以及将处理好的头部先缓存起来的结构
1 | std::shared_ptr<MsgNode> _recv_msg_node; //收到消息结构 |
完善接收逻辑
1 | void Session::HandleRead( const boost::system::error_code &error, |
copy_len
:已经处理的数据长度,因为存在一次接收多个包的情况,所以copy_len的意义是在于记录已经处理的数据的长度首先判断
_b_head_parse
是否为false
,如果为false
,则表示头部未处理,需要先处理头部。先判断接收的数据是否小于HEAD_LENGTH
,如果小于则需要拷贝数据到_recv_head_node
中,然后再读取剩余的数据。如果受到的数据比头部数据多,可能是多个数据包,需要做切包处理。根据之前保留在
_recv_head_node
中的数据长度,计算出剩余未读取的头部长度,然后取出剩余头部长度保存在_recv_head_node
中。然后通过memcpy
从节点拷贝出数据写入short类型的data_len
,并更新copy_len
,进而得到消息长度, 然后再读取剩余的消息体。先判断接收到数据未处理部分的长度和总共要接收的数据长度大小,如果小于总共要接收的长度,说明消息体还没接收完,则将未处理的部分写入到_recv_msg_node
里,回调读事件。否则说明消息体接收完全将消息体数据接收到
_recv_msg_node
中,接收完全后返回给对端。当然存在多个逻辑包粘连,此时要判断bytes_transferred
是否<=0,如果是则说明只有一个逻辑包,我们处理完了,继续监听读事件,就直接返回即可,否则说明有多个数据包粘连,就继续执行上述操作因为存在
_b_head_parse
为true
,就是包头接收并处理完的情况,但是包体未接收完,则再次出发读事件,此时就要继续进行上述操作
总体流程如下
粘包测试
为了测试粘包,需要制造粘包产生的现象,可以让客户端发送的频率高一些,服务器接收的频率低一些,这样造成前后端收发数据不一致导致多个数据包在服务器tcp缓冲区滞留产生粘包现象。
测试粘包之前,在服务器的Session
中添加打印二进制函数
1 | void Session::PrintRecvData(char *data, int length) { |
然后将这个函数放到HandleRead里,每次收到数据就调用这个函数打印接收到的最原始的数据,然后睡眠2秒再进行收发操作,用来延迟接收对端数据制造粘包,之后的逻辑不变
1 | void Session::HandleRead( const boost::system::error_code &error, |
客户端代码实现收发分离
1 |
|
总结
该服务虽然实现了粘包处理,但是服务器仍存在不足,比如当客户端和服务器处于不同平台时收发数据会出现异常,根本原因是未处理大小端模式的问题。