boost asio异步服务器(4)处理粘包问题tlv

粘包的产生

当客户端发送多个数据包给服务器时,服务器底层的tcp接收缓冲区收到的数据为粘连在一起的。这种情况的产生通常是服务器端处理数据的速率不如客户端的发送速率的情况。比如:客户端1s内连续发送了两个hello world!,服务器过了2s才接收数据,那一次性读出两个hello world!

tcp底层的安全和效率机制不允许字节数特别少的小包发送频率过高,tcp会在底层累计数据长度到一定大小才一起发送,比如连续发送1字节的数据要累计到多个字节才发送。

粘包处理

处理粘包的方式主要采用应用层定义收发包格式 的方式,这个过程俗称切包处理,常用的协议被称为tlv协议(消息id+消息长度+消息内容)。

tlv

TLV(Type-Length-Value)是一种通信协议,用于在通信中传输结构化数据。它将数据分为三个部分:类型(Type)、长度(Length)和值(Value),每个部分都以固定的格式进行编码和解码。

但是我下边的格式并不是标准的tlv格式,而是采用的lv模式,即只包含length和value。

完善消息节点

cpp 复制代码
class MsgNode {
public:
    //这里的构造方法主要方便后续调用Send接口构造消息节点
	MsgNode(char* msg, short data_len) : total_len(data_len + HEAD_LENGTH), cur_len(0) {
		_data = new char[total_len + 1];
		memcpy(_data, &data_len, HEAD_LENGTH);
		memcpy(_data + HEAD_LENGTH, msg, data_len);
		_data[total_len] = '\0';
	}
    //这里的构造方法则是用于在进行切包过程中构造处理数据的节点
	MsgNode(short data_len) :total_len(data_len), cur_len(0) {
		_data = new char[total_len + 1];
	}
    //Clear方法是用于清理节点的数据,避免多次构造析构节点
	void Clear() {
		memset(_data, 0, total_len);
		cur_len = 0;
	}
	~MsgNode() {
		delete[] _data;
	}
private:
	friend class Session;
	//表示已经处理的数据长度
	int cur_len;
	//表示处理数据的总长度
	int total_len;
	//表示数据的首地址
	char* _data;
};

完善两个构造函数和添加Clear函数

1、第一个构造方法主要方便后续调用Send接口构造消息节点

2、第二个构造方法则是用于在进行切包过程中构造处理数据的节点

3、Clear方法是用于清理节点的数据,避免多次构造析构节点

session类完善

_recv_msg_node用于存放收到数据包中的数据

_b_head_parse表示头部是否解析完成

_recv_head_node用于存放接收到数据包中的头部信息

完善hand_read回调函数

cpp 复制代码
void Session::handle_read(const boost::system::error_code& ec, size_t bytes_transferred,
	std::shared_ptr<Session> self_shared) {
	if (ec) {
		std::cout << "read error, error code: " << ec.value() <<
			" read message: " << ec.message() << std::endl;
		Close();
		server_->ClearSession(uuid);
	}
	else {
		PrintRecvData(data_, bytes_transferred);
		std::chrono::milliseconds dura(2000);
		std::this_thread::sleep_for(dura);
		
		//已经移动的字节数
		int copy_len = 0;
		while (bytes_transferred) {
			//头部尚未解析完成
			if (!_b_head_parse) {
				//收到的数据不足头部大小,这种情况很少发生
				if (bytes_transferred + _recv_head_node->cur_len < 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);
					sock_.async_read_some(boost::asio::buffer(data_, MAX_LENGTH),
						std::bind(&Session::handle_read, this,
							std::placeholders::_1, std::placeholders::_2, self_shared));
					return;
				}

				//走到这里,说明收到的数据大于头部,可能是一个粘连的数据包,但是首先需要将头部节点两字节读完

				//处理头部剩余未复制的长度
				int head_remain = HEAD_LENGTH - _recv_head_node->cur_len;
				if (head_remain) {
					memcpy(_recv_head_node->_data + _recv_head_node->cur_len, data_ + copy_len, head_remain);
					//更新已处理的数据
					copy_len += head_remain;
					/*
					* 这里不能更新头部节点的cur_len。
					* 因为
					* 1、当一次进来cur_len等于0,处理之后的偏移量copy_len就为2
					* 2、当头部未读取完成,后续读取会修正为正确的偏移量(但是种情况很少发生)
					* 3、之后的读取头部信息都会发生覆盖
					*/
					//_recv_head_node->cur_len += head_remain;
					bytes_transferred -= head_remain;
				}

				//获取头部数据
				short data_len = 0;
				memcpy(&data_len, _recv_head_node->_data, HEAD_LENGTH);
				std::cout << "data_len is " << data_len << std::endl;

				if (data_len > MAX_LENGTH) {
					std::cout << "invalid data length is " << data_len << std::endl;
					server_->ClearSession(uuid);
					return;
				}

				//头部节点处理完成,就可以开始处理数据域的数据节点
				_recv_msg_node = std::make_shared<MsgNode>(data_len);

				//消息长度小于头部规定长度,说明数据未收全,则先将消息放到接收节点中
				if (bytes_transferred < data_len) {
					memcpy(_recv_msg_node->_data + _recv_msg_node->cur_len, data_ + copy_len, bytes_transferred);
					_recv_msg_node->cur_len += bytes_transferred;
					memset(data_, 0, MAX_LENGTH);
					sock_.async_read_some(boost::asio::buffer(data_, MAX_LENGTH),
						std::bind(&Session::handle_read, this,
							std::placeholders::_1, std::placeholders::_2, self_shared));

					//表示头部处理完成,当下次进来的时候,就会直接跳过头部处理环节
					_b_head_parse = true;
					return;
				}

				//走到这里表示消息长度大于头部规定长度,这里可能是一个完整包,也可能是多个粘连的包
				memcpy(_recv_msg_node->_data + _recv_msg_node->cur_len, data_ + copy_len, data_len);
				_recv_msg_node->cur_len += data_len;
				copy_len += data_len;
				bytes_transferred -= data_len;
				_recv_msg_node->_data[_recv_msg_node->total_len] = '\0';
				std::cout << "receive data is: " << _recv_msg_node->_data << std::endl;

				//调用send发送给客户端
				Send(_recv_msg_node->_data, _recv_msg_node->total_len);

				//继续轮询处理下个未处理的数据,重置数据包和头部解析的情况
				_b_head_parse = false;
				_recv_msg_node->Clear();
				//说明这不是一个多个粘连的数据包
				if (bytes_transferred <= 0) {
					memset(data_, 0, MAX_LENGTH);
					sock_.async_read_some(boost::asio::buffer(data_, MAX_LENGTH),
						std::bind(&Session::handle_read, this,
							std::placeholders::_1, std::placeholders::_2, self_shared));
					return;
				}
				//走到这里说明这就是一个多个粘连的数据包
				continue;
			}

			//走到这里就说明头部是已经解析完成的,是处理数据未收全的情况
			int remain_msg = _recv_msg_node->total_len - _recv_msg_node->cur_len;
			//说明收到的数据仍然不足头部规定大小的情况
			if (bytes_transferred < remain_msg) {
				memcpy(_recv_msg_node->_data + _recv_msg_node->cur_len, data_ + copy_len, bytes_transferred);
				_recv_msg_node->cur_len += bytes_transferred;
				memset(data_, 0, MAX_LENGTH);
				sock_.async_read_some(boost::asio::buffer(data_, MAX_LENGTH),
					std::bind(&Session::handle_read, this,
						std::placeholders::_1, std::placeholders::_2, self_shared));
				return;
			}

			//走到这里说明收到的数据是大于等于头部规定大小的,接收到的数据可能是个完整的数据包,也可能多个粘连的数据包
			memcpy(_recv_msg_node->_data + _recv_msg_node->cur_len, data_ + copy_len, remain_msg);
			_recv_msg_node->cur_len += remain_msg;
			bytes_transferred -= remain_msg;
			copy_len += remain_msg;
			_recv_msg_node->_data[_recv_msg_node->total_len] = '\0';
			std::cout << "receive data is: " << _recv_msg_node->_data << std::endl;

			//处理完当前数据包的分割后,调用send接口向客户端发送回去
			Send(_recv_msg_node->_data, _recv_msg_node->total_len);

			//继续轮询处理下个数据包,重置接收数据节点和头部解析情况
			_b_head_parse = false;
			_recv_msg_node->Clear();
			//说明数据包并不是粘连的
			if (bytes_transferred <= 0) {
				memset(data_, 0, MAX_LENGTH);
				sock_.async_read_some(boost::asio::buffer(data_, MAX_LENGTH),
					std::bind(&Session::handle_read, this,
						std::placeholders::_1, std::placeholders::_2, self_shared));
				return;
			}
			//走到这里说明数据包是粘连的
			continue;	
		}
	}
}

这里hand_read函数的完善逻辑代码比较长,其中的注释给的比较详细,需要各位仔细读。但是逻辑可能头一两次读可能还是会有些蒙,多读几遍可能就会好得多。

这里还是得必要得说一下,我们都知道异步读写函数得回调函数中的参数bytes_transferred表示已经读取到的字节数,但是我们在这里还是需要对这些已经读到的数据进行处理。其中定义copy_len表示已经处理的字节数,bytes_transferred则表示为还未处理的数据(尽管已经被读取到了,但是还是尚未被处理,需要好好理解下)。

这里在session类中还定义了两个宏,MAX_LENGTH表示数据包的最大长度,就是1024*2字节。HEAD_LENGTH表示头部长度,就是2字节。

这里我也画了一个逻辑图供大家梳理这里的代码逻辑,希望能对大家理解有帮助。

粘包现象的测试

在session类中写一个打印函数,在每次触发读事件回调的时候调用下这个函数。这里打印的是tcp缓冲区的数据,boost asio从tcp已经是已经做了将tcp缓冲区的数据拿出来的,所以这里打印即可。

为了制造粘包现象,我们可以让服务器端隔2s处理一次读写,而客户端则不停的发送和读取就能制造出粘包现象了。下边是提供的客户端的代码。

cpp 复制代码
#include <iostream>
#include <boost/asio.hpp>
#include <thread>
using namespace std;
using namespace boost::asio::ip;
const int MAX_LENGTH = 1024 * 2;
const int HEAD_LENGTH = 2;
int main()
{
	//测试粘包现象客户端
	try {
		//创建上下文服务
		boost::asio::io_context   ioc;
		//构造endpoint
		tcp::endpoint  remote_ep(address::from_string("127.0.0.1"), 1234);
		tcp::socket  sock(ioc);
		boost::system::error_code   error = boost::asio::error::host_not_found;
		sock.connect(remote_ep, error);
		if (error) {
			cout << "connect failed, code is " << error.value() << " error msg is " << error.message();
			return 0;
		}

		thread send_thread([&sock] {
			for (;;) {
				this_thread::sleep_for(std::chrono::milliseconds(2));
				const char* request = "hello world!";
				size_t request_length = strlen(request);
				char send_data[MAX_LENGTH] = { 0 };
				memcpy(send_data, &request_length, 2);
				memcpy(send_data + 2, request, request_length);
				boost::asio::write(sock, boost::asio::buffer(send_data, request_length + 2));
			}
			});

		thread recv_thread([&sock] {
			for (;;) {
				this_thread::sleep_for(std::chrono::milliseconds(2));
				cout << "begin to receive..." << endl;
				char reply_head[HEAD_LENGTH];
				size_t reply_length = boost::asio::read(sock, boost::asio::buffer(reply_head, HEAD_LENGTH));
				short msglen = 0;
				memcpy(&msglen, reply_head, HEAD_LENGTH);
				char msg[MAX_LENGTH] = { 0 };
				size_t  msg_length = boost::asio::read(sock, boost::asio::buffer(msg, msglen));

				std::cout << "Reply is: ";
				std::cout.write(msg, msglen) << endl;
				std::cout << "Reply len is " << msglen;
				std::cout << "\n";
			}
			});

		send_thread.join();
		recv_thread.join();
	}
	catch (std::exception& e) {
		std::cerr << "Exception: " << e.what() << endl;
	}
	return 0;
}

现象如下图,测试环境Windows visual studio

完整服务端代码:codes-C++: C++学习 - Gitee.com

这里的echo服务器实现了粘包的处理,但是在不同的平台下仍存在收发数据异常的问题,其根本原因就是平台大小端的差异。

相关推荐
小蜗牛慢慢爬行6 分钟前
有关异步场景的 10 大 Spring Boot 面试问题
java·开发语言·网络·spring boot·后端·spring·面试
秋名山小桃子8 分钟前
Kunlun 2280服务器(ARM)Raid卡磁盘盘符漂移问题解决
运维·服务器
与君共勉121389 分钟前
Nginx 负载均衡的实现
运维·服务器·nginx·负载均衡
MARIN_shen11 分钟前
Marin说PCB之POC电路layout设计仿真案例---06
网络·单片机·嵌入式硬件·硬件工程·pcb工艺
努力学习的小廉16 分钟前
深入了解Linux —— make和makefile自动化构建工具
linux·服务器·自动化
MZWeiei20 分钟前
Zookeeper基本命令解析
大数据·linux·运维·服务器·zookeeper
Arenaschi39 分钟前
在Tomcat中部署应用时,如何通过域名访问而不加端口号
运维·服务器
小张认为的测试39 分钟前
Linux性能监控命令_nmon 安装与使用以及生成分析Excel图表
linux·服务器·测试工具·自动化·php·excel·压力测试
waicsdn_haha1 小时前
Java/JDK下载、安装及环境配置超详细教程【Windows10、macOS和Linux图文详解】
java·运维·服务器·开发语言·windows·后端·jdk
m0_748240021 小时前
Chromium 中chrome.webRequest扩展接口定义c++
网络·c++·chrome