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

03 解包与处理

在理解了前面介绍的数据包的三种格式后,我们来介绍一下针对上述三种格式的数据包技术上应该如何处理。其处理流程都是一样的,这里我们以包头 + 包体这种格式的数据包来说明。处理流程如下:

假设我们的包头格式如下:

//强制一字节对齐 #pragma pack(push, 1) //协议头 struct msg_header { int32_t bodysize; //包体大小 }; #pragma pack(pop)

那么上面的流程实现代码如下:

//包最大字节数限制为10M #define MAX_PACKAGE_SIZE 10 * 1024 * 1024 void ChatSession::OnRead(const std::shared_ptr<TcpConnection>& conn, Buffer* pBuffer, Timestamp receivTime) { while (true) { //不够一个包头大小 if (pBuffer->readableBytes() < (size_t)sizeof(msg_header)) { //LOGI << "buffer is not enough for a package header, pBuffer->readableBytes()=" << pBuffer->readableBytes() << ", sizeof(msg_header)=" << sizeof(msg_header); return; } //取包头信息 msg_header header; memcpy(&header, pBuffer->peek(), sizeof(msg_header)); //包头有错误,立即关闭连接 if (header.bodysize <= 0 || header.bodysize > MAX_PACKAGE_SIZE) { //客户端发非法数据包,服务器主动关闭之 LOGE("Illegal package, bodysize: %lld, close TcpConnection, client: %s", header.bodysize, conn->peerAddress().toIpPort().c_str()); conn->forceClose(); return; } //收到的数据不够一个完整的包 if (pBuffer->readableBytes() < (size_t)header.bodysize + sizeof(msg_header)) return; pBuffer->retrieve(sizeof(msg_header)); //inbuf用来存放当前要处理的包 std::string inbuf; inbuf.append(pBuffer->peek(), header.bodysize); pBuffer->retrieve(header.bodysize); //解包和业务处理 if (!Process(conn, inbuf.c_str(), inbuf.length())) { //客户端发非法数据包,服务器主动关闭之 LOGE("Process package error, close TcpConnection, client: %s", conn->peerAddress().toIpPort().c_str()); conn->forceClose(); return; } }// end while-loop }

上述流程代码的处理过程和流程图中是一致的,pBuffer 这里是一个自定义的接收缓冲区,这里的代码,已经将收到的数据放入了这个缓冲区,所以判断当前已收取的字节数目只需要使用这个对象的相应方法即可。上述代码有些细节我需要强调一下:

  • 取包头时,你应该拷贝一份数据包头大小的数据出来,而不是从缓冲区 pBuffer 中直接将数据取出来(即取出来的数据从 pBuffer 中移除),这是因为倘若接下来根据包头中的字段得到包体大小时,如果剩余数据不够一个包体大小,你又得把这个包头数据放回缓冲区。为了避免这种不必要的操作,只有缓冲区数据大小够整个包的大小(代码中:header.bodysize + sizeof(msg))你才需要把整个包大小的数据从缓冲区移除,这也是这里的 pBuffer->peek() 方法 peek 单词的含义(中文可以翻译成”瞟一眼“或者”偷窥“)。
  • 通过包头得到包体大小时,你一定要对 bodysize 的数值进行校验,我这里要求 bodysize 必须大于 0 且不大于 10 * 1024 * 1024(即 10 M)。当然,实际开发中,你可以根据你自己的需求要决定 bodysize 的上下限(包体大小是 0 字节的包在某些业务场景下是允许的)。记住,一定要判断这个上下限,因为假设这是一个非法的客户端发来的数据,其 bodysize 设置了一个比较大的数值,例如 1 * 1024 * 1024 * 1024(即 1 G),你的逻辑会让你一直缓存该客户端发来的数据,那么很快你的服务器内存将会被耗尽,操作系统在检测到你的进程占用内存达到一定阈值时会杀死你的进程,导致服务不能再正常对外服务。如果你检测了 bodysize 字段的是否满足你设置的上下限,对于非法的 bodysize,直接关闭这路连接即可。这也是服务的一种自我保护措施,避免因为非法数据包带来的损失。还有另外一种情况下 bodysize 也可能不是预期的合理值,即因为网络环境差或者某次数据解析逻辑错误,导致后续的数据错位,把不该当包头数据的数据当成了包头,这个时候解析出来的 bodysize 也可能不是合理值,同样,这种情形下也会被这段检验逻辑检测到,最终关闭连接。
  • 不知道你有没有注意到整个判断包头、包体以及处理包的逻辑放在一个 while 循环里面,这是必要的。如果没有这个 while 循环,当你一次性收到多个包时,你只会处理一个,下次接着处理就需要等到新一批数据来临时再次触发这个逻辑。这样造成的结果就是,对端给你发送了多个请求,你最多只能应答一个,后面的应答得等到对端再次给你发送数据时。这就是对粘包逻辑的正确处理。

以上逻辑和代码是最基本的粘包和半包处理机制,也就是所谓的技术上的解包处理逻辑(业务上的解包处理逻辑后面章节再介绍)。希望读者能理解他们,在理解了他们的基础之上,我们可以给解包拓展很多功能,例如,我们再给我们的协议包增加一个支持压缩的功能,我们的包头变成下面这个样子:

#pragma pack(push, 1) //协议头 struct msg_header { char compressflag; //压缩标志,如果为1,则启用压缩,反之不启用压缩 int32_t originsize; //包体压缩前大小 int32_t compresssize; //包体压缩后大小 char reserved[16]; //保留字段,用于将来拓展 }; #pragma pack(pop)

修改后的代码如下:

//包最大字节数限制为10M #define MAX_PACKAGE_SIZE 10 * 1024 * 1024 void ChatSession::OnRead(const std::shared_ptr<TcpConnection>& conn, Buffer* pBuffer, Timestamp receivTime) { while (true) { //不够一个包头大小 if (pBuffer->readableBytes() < (size_t)sizeof(msg_header)) { //LOGI << "buffer is not enough for a package header, pBuffer->readableBytes()=" << pBuffer->readableBytes() << ", sizeof(msg_header)=" << sizeof(msg_header); return; } //取包头信息 msg_header header; memcpy(&header, pBuffer->peek(), sizeof(msg_header)); //数据包压缩过 if (header.compressflag == PACKAGE_COMPRESSED) { //包头有错误,立即关闭连接 if (header.compresssize <= 0 || header.compresssize > MAX_PACKAGE_SIZE || header.originsize <= 0 || header.originsize > MAX_PACKAGE_SIZE) { //客户端发非法数据包,服务器主动关闭之 LOGE("Illegal package, compresssize: %lld, originsize: %lld, close TcpConnection, client: %s", header.compresssize, header.originsize, conn->peerAddress().toIpPort().c_str()); conn->forceClose(); return; } //收到的数据不够一个完整的包 if (pBuffer->readableBytes() < (size_t)header.compresssize + sizeof(msg_header)) return; pBuffer->retrieve(sizeof(msg_header)); std::string inbuf; inbuf.append(pBuffer->peek(), header.compresssize); pBuffer->retrieve(header.compresssize); std::string destbuf; if (!ZlibUtil::UncompressBuf(inbuf, destbuf, header.originsize)) { LOGE("uncompress error, client: %s", conn->peerAddress().toIpPort().c_str()); conn->forceClose(); return; } //业务逻辑处理 if (!Process(conn, destbuf.c_str(), destbuf.length())) { //客户端发非法数据包,服务器主动关闭之 LOGE("Process error, close TcpConnection, client: %s", conn->peerAddress().toIpPort().c_str()); conn->forceClose(); return; } } //数据包未压缩 else { //包头有错误,立即关闭连接 if (header.originsize <= 0 || header.originsize > MAX_PACKAGE_SIZE) { //客户端发非法数据包,服务器主动关闭之 LOGE("Illegal package, compresssize: %lld, originsize: %lld, close TcpConnection, client: %s", header.compresssize, header.originsize, conn->peerAddress().toIpPort().c_str()); conn->forceClose(); return; } //收到的数据不够一个完整的包 if (pBuffer->readableBytes() < (size_t)header.originsize + sizeof(msg_header)) return; pBuffer->retrieve(sizeof(msg_header)); std::string inbuf; inbuf.append(pBuffer->peek(), header.originsize); pBuffer->retrieve(header.originsize); //业务逻辑处理 if (!Process(conn, inbuf.c_str(), inbuf.length())) { //客户端发非法数据包,服务器主动关闭之 LOGE("Process error, close TcpConnection, client: %s", conn->peerAddress().toIpPort().c_str()); conn->forceClose(); return; } }// end else }// end while-loop }

这段代码先根据包头的压缩标志字段判断包体是否有压缩,如果有压缩,则取出包体大小去解压,解压后的数据才是真正的业务数据。整个程序执行流程图如下:

代码中有一个接收缓冲区变量 pBuffer,关于接收缓冲区如何设计,我们将在后面的章节中详细介绍。

http://www.jsqmd.com/news/1091552/

相关文章:

  • Django可观测性基建:集成 Sentry/Middleware 构建全链路追踪与异常监控体系
  • 看完就会:2026年闭眼可入的专业一键生成论文工具
  • 重构V4L2流程(解决传统read/write,采用内存映射mmap)
  • 2026论文必藏降AIGC平台大曝光:一键把AIGC率降至安全线!
  • 揭秘CPUDoc:一款重新定义CPU性能优化的开源智能调度工具
  • Python自动化集成JIRA Xray:从API调用到CI/CD的测试管理实践
  • 如何用trackerslist项目彻底解决BT下载慢的问题:终极完整指南
  • 05_Verilog基础入门
  • 程序员开启24小时值班时代?Codex杀入移动端,OpenAI内部99.8%Token消耗来自Codex
  • 2028年AI造AI倒计时启动!三大世界级信号亮起,人类准备好了吗?
  • 唐山路北区本地开锁行业基础流程与常见收费标准科普解读
  • ChatGPT提示词工程实战手册(企业级Prompt设计标准V3.2):覆盖87种高频场景的可复用模板库
  • 3步解决老旧电视盒子卡顿问题:创维E900V22C变身专业4K播放器实战指南
  • AI 原生研发流水线(下):跑通 demo 后,才知道真正难的是门禁
  • 深度解析m4s-converter:高效解决B站视频格式转换难题
  • 如何3步完成黑苹果配置:OpCore-Simplify终极自动化工具指南
  • 面试总说不出亮点?双非应届生秋招求职,请提前准备一个企业级AI应用案例
  • TI TLK105L/106L以太网PHY寄存器编程实战:低功耗、中断与诊断
  • AI 辅助存储排障实战:从日志挖掘到根因定位的自动化流水线
  • 远程IO市场主流品牌有哪些?四大标杆品牌性能、场景、选型全解析
  • [特殊字符]别把PLM当摆设!汽配/芯片厂选对系统,研发周期砍半!-全星研发项目管理APQP软件系统PLM系统 #汽车零部件 #芯片电子 #新能源研发 #数字化转型
  • 降AIGC工具红黑榜:亲测3款热门工具,揭露降AI真实效果与隐藏坑点,文末附攻略
  • DeepSeek-V4 DSpark加速模块
  • ChatGPT Plus 支付失败后,为什么不建议连续重试?
  • 现在学习SEO还来得及吗?
  • ChatGPT翻译翻车真相:为什么你写的提示词总被AI“意译”?3步诊断法+5个必改语法陷阱
  • 3步掌握Blender参数化设计:CAD_Sketcher终极入门指南
  • C# CAD多段线等距分割技巧
  • Ubuntu安装中文输入法教程
  • Pixelle-Video:模块化AI视频生成引擎的技术架构与工程实践