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。

完善消息节点

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回调函数

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处理一次读写,而客户端则不停的发送和读取就能制造出粘包现象了。下边是提供的客户端的代码。

#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;//构造endpointtcp::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服务器实现了粘包的处理,但是在不同的平台下仍存在收发数据异常的问题,其根本原因就是平台大小端的差异。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/35035.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

正版软件 | Copywhiz 6:革新您的文件复制、备份与管理体验

在数字化时代&#xff0c;文件管理的效率直接影响到我们的生产力。Copywhiz 6 最新版本&#xff0c;带来了前所未有的文件处理能力&#xff0c;让复制、备份和组织文件变得轻而易举。 智能选择&#xff0c;只复制更新内容 Copywhiz 6 的智能选择功能&#xff0c;让您只需几次点…

【PA交易】BackTrader: 讨论下分析器和评测指标

前言 BackTrader的分析器主要使用的是analyzers模块&#xff0c;我们可以从Analyzers - Backtrader找到一个非常简单的示例。这个示例中使用方式很简单&#xff0c;其他分析器也可以通过如此简单封装方式进行装载。如果仅是复制粘贴官方教程&#xff0c;完全是制造互联网垃圾…

Netty学习(一)——基础组件

根据黑马程序员netty视频教程学习所做笔记。 笔记demo&#xff1a;https://gitee.com/jobim/netty_learn_demo.git 参考博客&#xff1a;https://blog.csdn.net/cl939974883/article/details/122550345 一、概述 1.1 什么是Netty Netty is an asynchronous event-driven netw…

Redis-哨兵模式-主机宕机-推选新主机的过程

文章目录 1、为哨兵模式准备配置文件2、启动哨兵3、主机6379宕机3.4、查看sentinel控制台日志3.5、查看6380主从信息 4、复活63794.1、再次查看sentinel控制台日志 1、为哨兵模式准备配置文件 [rootlocalhost redis]# ll 总用量 244 drwxr-xr-x. 2 root root 150 12月 6 2…

label studio数据标注平台的自动化标注使用

&#xff08;作者&#xff1a;陈玓玏&#xff09; 开源项目&#xff0c;欢迎star哦&#xff0c;https://github.com/tencentmusic/cube-studio 做图文音项目过程中&#xff0c;我们通常会需要进行数据标注。label studio是一个比较好上手的标注平台&#xff0c;可以直接搜…

突出显示列,重点内容一目了然!

老师在发布查询时&#xff0c;希望学生家长一眼就能看到重要的信息&#xff0c;应该如何设置&#xff1f; 易查分的新功能&#xff1a;突出显示列&#xff0c;就可以轻松实现&#xff01;老师可以个性化设置突出显示列的样式&#xff0c;包括颜色、字体大小、隐藏标题等&#x…

P2实验室装修标准都有哪些

P2实验室&#xff08;也称为生物安全二级实验室&#xff0c;BSL-2实验室&#xff09;的装修标准需要满足一系列的设计和施工要求&#xff0c;以确保实验室的安全性和功能性。因此&#xff0c;P2实验室装修标准不仅要满足一般实验室的要求&#xff0c;还需符合生物安全的特殊规定…

餐厅点餐系统JAVA全栈开发(SSM框架+MYSQL)

代码仓库 GitHub - JJLi0427/Online_Order_SystemContribute to JJLi0427/Online_Order_System development by creating an account on GitHub.https://github.com/JJLi0427/Online_Order_System 项目介绍 餐厅点餐系统包含用户使用界面和功能实现&#xff0c;后台店员和管…

C++初学者指南-2.输入和输出---文件输入和输出

C初学者指南-2.输入和输出—文件输入和输出 文章目录 C初学者指南-2.输入和输出---文件输入和输出1.写文本文件2.读文本文件3.打开关闭文件4.文件打开的模式 1.写文本文件 使用&#xff1a; std::ofstream&#xff08;输出文件流&#xff09; #include <fstream> // 文…

YOLOv8关键点pose训练自己的数据集

这里写自定义目录标题 YOLOv8关键点pose训练自己的数据集一、项目代码下载二、制作自己的关键点pose数据集2.1 标注(非常重要)2.1.1 标注软件2.1.2 标注注意事项a.多类别检测框b.单类别检测框2.2 格式转换(非常重要)2.3 数据集划分三、YOLOv8-pose训练关键点数据集3.1 训练…

通过frp实现内外网映射

frp介绍和使用方法可以参考官网:安装 | frp 1、准备两台服务器&#xff0c;一台内网服务器A&#xff0c;一台有公网ip的外网服务器B(47.12.13.15) 2、去官方仓库下载frp安装包&#xff1a;Releases fatedier/frp GitHub 下载包根据自己服务系统选择 ​ 3、先在外网服务器…

《昇思25天学习打卡营第1天|onereal》

昇思25天学习打卡营第1天;有点一头雾水的感觉&#xff0c;说是要在jupyter中签到打卡&#xff0c;是不是就是复制粘贴。我以为是要在终端机器中运行代码呢。 如果只是粘贴代码&#xff0c;那未免太简单了。 我还是想运行这个算力机器&#xff0c;但是他们说每次只能2小时。太…

AI播客下载:Eye on AI(AI深度洞察)

"Eye on A.I." 是一档双周播客节目&#xff0c;由长期担任《纽约时报》记者的 Craig S. Smith 主持。在每一集中&#xff0c;Craig 都会与在人工智能领域产生影响的人们交谈。该播客的目的是将渐进的进步置于更广阔的背景中&#xff0c;并考虑发展中的技术的全球影响…

pp 学习一 生产模块主数据

生产成本&#xff1a;原材料是什么&#xff0c;价格多少&#xff0c;人工耗费时间&#xff0c;以及其他的费用 离散制造&#xff1a;有生产订单。工序是分开的&#xff08;可以停&#xff09; 重复制造&#xff1a;没有生产订单&#xff08;可能有客户下达的任务单或者计划订…

一分钟彻底掌握Java多线程生产者与消费者模型

代码 package com.example.KFC; public class Cooker extends Thread { public void run() { while (true) { synchronized (Desk.lock) { if (Desk.maxCount 0) { break; } else { if (!Desk.flag) { System.out.println("Cooker makes a hamburger"); …

unity中使用commandbuffer将自定义画面渲染到主相机上

CommandBuffer 保存渲染命令列表&#xff08;例如设置渲染目标或绘制给定网格&#xff09;。您可以指示 Unity 在内置渲染管线中的各个点安排和执行这些命令&#xff0c;因此&#xff0c;您可以自定义和扩展 Unity 的渲染功能。 这句话意味着你可以通过command buffer让相机渲…

计算机基础知识——面向对象:封装+继承+多态整理

面向对象三大特性&#xff1a;封装、继承、多态。 1.封装 将一系列相关事物的共同的属性和行为提取出来&#xff0c;放到一个类中&#xff0c;同时隐藏对象的属性和实现细节&#xff0c;仅对外提供公共的访问方式。 【JavaBean类就可以看作是封装的完美案例。】 setter和get…

云动态摘要 2024-06-25

给您带来云厂商的最新动态&#xff0c;最新产品资讯和最新优惠更新。 最新产品更新 Web应用防火墙 - 验证码支持微信小程序接入 阿里云 2024-06-25 支持客户从微信小程序场景下接入&#xff0c;提供人机识别的安全防护。 工业数字模型驱动引擎 - iDME控制台换新升级 华为云…

[20] Opencv_CUDA应用之 关键点检测器和描述符

Opencv_CUDA应用之 关键点检测器和描述符 本节中会介绍找到局部特征的各种方法&#xff0c;也被称为关键点检测器关键点(key-point)是表征图像的特征点&#xff0c;可用于准确定义对象 1. 加速段测试特征功能检测器 FAST算法用于检测角点作为图像的关键点&#xff0c;通过对…

轻松掌握:工科生如何高效阅读国际期刊和撰写论文(上)

⭐️我叫忆_恒心&#xff0c;一名喜欢书写博客的研究生&#x1f468;‍&#x1f393;。 如果觉得本文能帮到您&#xff0c;麻烦点个赞&#x1f44d;呗&#xff01; 近期会不断在专栏里进行更新讲解博客~~~ 有什么问题的小伙伴 欢迎留言提问欧&#xff0c;喜欢的小伙伴给个三连支…