【C++boost::asio网络编程】有关处理粘包问题的笔记
粘包问题
粘包问题是服务器在进行基于TCP通信时常见的一种现象:当客户端连续发送多个数据包给服务器时,服务器底层的tcp缓冲区收到的数据很有可能是“粘连”在一起的
如图,当客户端连续发送两次helloworld给服务端,但是服务端第一次接收到的数据中包含了客户端第二次发送的数据,并且服务端第二次接收到的数据又发生了缺失
发生粘包问题的原因主要有
- TCP底层是面向字节流的,并且tcp只保证发送数据的正确性和有序性,但是tcp一次性能接收多少数据是由tcp底层的输入缓冲区剩余空间大小决定的,比如此时客户端发送10个字节大小的数据,但是服务端的输入缓冲区内只有5个字节的空闲空间,所以服务器第一次就只能接收5个字节的数据,客户端剩下5个字节的数据就有可能和下一次发送的数据混在一起发送过来。然后上层接收的时候就会造成"粘连"的现象
- 客户端发送频率大于服务端的接收频率导致客户端多次发送的数据在服务端tcp的接收缓冲区内部造成粘连
- tcp底层在发送时会将要发送的字节数比较小的数据积累到一定大小才会发送出去,这也会导致粘包问题(Negle算法)
处理粘包的方法
处理粘包都是在应用层解决的:在要发送数据的前面加上数据的大小,这样对方会先接收到的你要发送数据的大小,然后根据大小在来决定读多少数据(比如:http协议报头中的Content-Length
这个字段就是用来记录报文的大小的)
const int MAX_LENGTH = 1024;
const int HEAD_LENGTH = 2;
class MsgNode
{
friend class Session;
public:
MsgNode(const char* data, int max_len)//发送时使用
:_cur_len(0)
, _total_len(max_len+HEAD_LENGTH)
{
_data = new char[_total_len + 1];
memcpy(_data, &max_len, HEAD_LENGTH);
memcpy(_data+HEAD_LENGTH, data, max_len);
_data[_total_len] = '\0';
}
MsgNode(int max_len)
:_total_len(max_len)
, _cur_len(0)
{
_data = new char[_total_len + 1];
}
~MsgNode()
{
delete[] _data;
}
void Clear()
{
memset(_data, '\0', _total_len);
_cur_len = 0;
}
private:
int _cur_len;
int _total_len;
char* _data;
};
为了处理粘包问题,事先规定在数据的前面会加上整个数据的大小信息,并且存放这个信息的空间固定为两个字节
Session(boost::asio::io_context& ioc,Server* server)
:_socket(ioc)
,_server(server)
,_b_head_parse(false)
{
boost::uuids::uuid a_uuid = boost::uuids::random_generator()();
_uuid = boost::uuids::to_string(a_uuid);
_recv_head_node = std::make_shared<MsgNode>(HEAD_LENGTH);
}
std::shared_ptr<MsgNode> _recv_msg_node;
bool _b_head_parse;
//收到的头部结构
std::shared_ptr<MsgNode> _recv_head_node;
在Session类中,会在构造函数时就确定_recv_head_node
所要接收数据的大小。
然后需要做的就是完善handle_read
回调函数
void Session::handle_read(const boost::system::error_code& ec, size_t bytes_transferred, std::shared_ptr<Session> self_shared)
{
std::cout << _data << std::endl;
if (!ec)
{
int copy_len = 0;//已经移动的字节数
while (bytes_transferred > 0)
{
if (!_b_head_parse)//如果头部还没有被解析
{
//收到的数据不足头部的大小
if (_recv_head_node->_cur_len + bytes_transferred < HEAD_LENGTH)
{
memcpy(_recv_head_node->_data + _recv_head_node->_cur_len,
_data + copy_len,
bytes_transferred);
_recv_head_node->_cur_len += bytes_transferred;
memset(_data, 0, max_length);
_socket.async_read_some(boost::asio::buffer(_data, max_length),
std::bind(&Session::handle_read, this, std::placeholders::_1, std::placeholders::_2, shared_from_this()));
return;
}
//收到的数据比头部多
int head_remain = HEAD_LENGTH - _recv_head_node->_cur_len;
memcpy(_recv_head_node->_data + _recv_head_node->_cur_len,
_data + copy_len,
head_remain);
copy_len += head_remain;
bytes_transferred -= head_remain;
short data_len = 0;
memcpy(&data_len, _recv_head_node->_data, HEAD_LENGTH);//获取头部长度
//data_len = boost::asio::detail::socket_ops::network_to_host_short(data_len);//将网络字节序转成本地字节序
std::cout << "data len is " << data_len << std::endl;
//Send(_recv_msg_node->_data, _recv_msg_node->_total_len);
if (data_len > MAX_LENGTH)//如果发现了一个非法的长度
{
std::cout << "invalid data length is " << data_len << std::endl;
_server->ClearSession(_uuid);
return;
}
_b_head_parse = true;//头部已经被解析完了
_recv_msg_node = std::make_shared<MsgNode>(data_len);
//消息的长度要小于头部规定的长度,说明数据没有收全
if (bytes_transferred < data_len)
{
memcpy(_recv_head_node->_data + _recv_msg_node->_cur_len,
_data + copy_len,
bytes_transferred);
_recv_msg_node->_cur_len += bytes_transferred;
memset(_data, 0, max_length);
_socket.async_read_some(boost::asio::buffer(_data, max_length),
std::bind(&Session::handle_read, this, std::placeholders::_1, std::placeholders::_2, shared_from_this()));
_b_head_parse = true;
return;
}
//收到的消息是全的
memcpy(_recv_msg_node->_data + _recv_msg_node->_cur_len,
_data + copy_len,
data_len);
copy_len += data_len;
bytes_transferred -= data_len;
_recv_msg_node->_data[data_len] = '\0';
std::cout << "recv data is " << _recv_msg_node->_data << std::endl;
Send(_recv_msg_node->_data, data_len);
std::cout << bytes_transferred << std::endl;
//继续轮询剩余未处理数据
_b_head_parse = false;
_recv_head_node->Clear();
if (bytes_transferred <= 0)
{
memset(_data, 0, max_length);
_socket.async_read_some(boost::asio::buffer(_data, max_length),
std::bind(&Session::handle_read, this, std::placeholders::_1, std::placeholders::_2, shared_from_this()));
return;
}
continue;
}
}
}
else
{
std::cout << "Session::handle_read:error code:" << ec.value() << " error message:" << ec.message() << std::endl;
_server->ClearSession(_uuid);
}
}
copy_len
的作用
copy_len
用于记录已经处理过的数据长度,因为在数据接收过程中可能一次性接收到多个包,因此它用来追踪已处理数据的长度。- 处理包头
初始时,判断_b_head_parse
是否为false
,如果为false
,表示包头尚未处理完。此时首先检查接收到的数据是否小于包头的大小。如果数据不足包头大小,则将接收到的数据存入_recv_head_node
中,并继续监听读取事件等待更多数据。如果数据大于或等于包头大小,则进入下一步处理。 - 处理粘包情况
如果接收到的数据大于包头部分,说明可能存在多个逻辑包。此时,需要进行切包操作:- 根据
_recv_head_node
中保存的数据长度,计算出剩余未解析的包头部分,并将其保存回_recv_head_node
。 - 使用
memcpy
将包头数据拷贝到short
类型的data_len
中,以获取消息的总长度。 - 然后处理包体(即消息体)。接下来,判断接收到的数据长度是否小于总的消息体长度。如果接收的数据不足以构成完整的消息体,则将未处理的部分存入
_recv_msg_node
,并继续监听读取事件。如果接收到的数据足够,表示消息体接收完毕,进入下一步。
- 根据
- 消息体接收完成
当包体接收完全后,将剩余的消息体数据存入_recv_msg_node
,并返回给对端。如果此时发现有多个逻辑包粘连在一起,需继续处理:- 判断
bytes_transferred
是否小于等于 0。如果小于等于 0,说明只接收了一个完整的逻辑包,处理完成后继续监听读事件。 - 如果
bytes_transferred
大于 0,表示还有更多数据包粘连,需继续执行上述的处理逻辑。
- 判断
- 处理未完成的包体
在_b_head_parse
为true
的情况下,表示包头已经处理完毕,但包体尚未接收完全。此时重新触发HandleRead
事件继续接收和处理未完成的消息体。其处理逻辑与步骤 3 和 4 相似:先接收未完成的部分,直到整个消息体接收完毕。