当前位置: 首页 > article >正文

【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);
	}
}
  1. copy_len 的作用
    copy_len 用于记录已经处理过的数据长度,因为在数据接收过程中可能一次性接收到多个包,因此它用来追踪已处理数据的长度。
  2. 处理包头
      初始时,判断 _b_head_parse 是否为 false,如果为 false,表示包头尚未处理完。此时首先检查接收到的数据是否小于包头的大小。如果数据不足包头大小,则将接收到的数据存入 _recv_head_node 中,并继续监听读取事件等待更多数据。如果数据大于或等于包头大小,则进入下一步处理。
  3. 处理粘包情况
    如果接收到的数据大于包头部分,说明可能存在多个逻辑包。此时,需要进行切包操作:
    • 根据 _recv_head_node 中保存的数据长度,计算出剩余未解析的包头部分,并将其保存回 _recv_head_node
    • 使用 memcpy 将包头数据拷贝到 short 类型的 data_len 中,以获取消息的总长度。
    • 然后处理包体(即消息体)。接下来,判断接收到的数据长度是否小于总的消息体长度。如果接收的数据不足以构成完整的消息体,则将未处理的部分存入 _recv_msg_node,并继续监听读取事件。如果接收到的数据足够,表示消息体接收完毕,进入下一步。
  4. 消息体接收完成
      当包体接收完全后,将剩余的消息体数据存入 _recv_msg_node,并返回给对端。如果此时发现有多个逻辑包粘连在一起,需继续处理:
    • 判断 bytes_transferred 是否小于等于 0。如果小于等于 0,说明只接收了一个完整的逻辑包,处理完成后继续监听读事件。
    • 如果 bytes_transferred 大于 0,表示还有更多数据包粘连,需继续执行上述的处理逻辑。
  5. 处理未完成的包体
       在 _b_head_parsetrue 的情况下,表示包头已经处理完毕,但包体尚未接收完全。此时重新触发 HandleRead 事件继续接收和处理未完成的消息体。其处理逻辑与步骤 3 和 4 相似:先接收未完成的部分,直到整个消息体接收完毕。

http://www.kler.cn/a/429057.html

相关文章:

  • JavaWeb简单开发
  • 彻底理解JVM类加载机制
  • 数字小偷:2025年全面防护指南
  • php-2025面试题准备
  • Vue.js组件开发-如何处理跨域请求
  • C 语言运算符的优先级和结合性
  • dockers网络连接指令:docker network connect
  • 数据结构初阶---链表(2)---双向链表
  • Vue 组件通信全面解析
  • 【text2sql】低资源场景下Text2SQL方法
  • 【CKS最新模拟真题】从ETCD 中读取 Secret的键值
  • mac电脑安装hadoop、hive等大数据组件
  • 计算机病毒的特效及种类【知识点+逐字稿+答辩题】----高中信息技术教资面试
  • 设计模式10:观察者模式(订阅-发布)
  • 朗新科技集团如何用云消息队列 RocketMQ 版“快、准、狠”破解业务难题?
  • 生活大爆炸版石头剪刀布(洛谷P1328)
  • SpringBoot 赋能:精铸超稳会员制医疗预约系统,夯实就医数据根基
  • flume对kafka中数据的导入导出、datax对mysql数据库数据的抽取
  • vscode(二)常用的文件变量
  • 基于卷积神经网络的人脸表情识别系统,resnet50,mobilenet模型【pytorch框架+python源码】
  • C# AES
  • spring中的@Bean和@Component有什么区别?
  • CentOS 9 Stream上安装SQL Server 2022
  • OceanBase数据库使用 INSERT 语句违反唯一约束冲突解决办法及两者差异分析
  • python+docker实现分布式存储的demo
  • git commit -m “Add user login feature“