突破编程_C++_网络编程(一种高性能处理 TCP 粘包问题的方法)

2024-04-23 17:44

本文主要是介绍突破编程_C++_网络编程(一种高性能处理 TCP 粘包问题的方法),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

1 前言

在“突破编程_C++_网络编程(Windows 套接字(处理 TCP 粘包问题))”一文中,已经讲解了 TCP 粘包问题,并且给出了样例代码。但是该样例代码的核心是使用队列(std::queue)做报文的处理。

std::queue 是 C++ 标准模板库(STL)中的一个容器适配器,它提供了一种先进先出(FIFO)的数据结构。在 STL 中,std::queue 并不直接存储元素,而是依赖于一个底层容器来管理元素的存储,这个底层容器通常是 std::deque(双端队列)或者 std::list(双向链表),具体取决于实现。

默认情况下,std::queue 会使用 std::deque 作为底层容器:这是最常见的实现方式。std::deque 提供了从两端快速插入和删除元素的能力,这与 std::queue 的操作特性非常吻合。在 std::deque 上,std::queue 的 push() 操作对应于 std::deque 的 push_back(),而 pop() 对应于 std::deque 的 pop_front()。

如果使用 std::queue 作为处理 TCP 粘包问题,所有接收到的 TCP 数据会先存入队列中,然后再一个一个拿出来处理。一般情况下,这种处理模式是可行的,但是当报文的字节数很大时,这种模式的效率会比较低,所以本文会实现一种高性能的处理方式。

2 回顾一下 TCP 粘包问题

TCP(传输控制协议)是一种面向流的协议,它不保留数据包边界。在TCP连接中,数据被看作是一连串无结构的字节流。TCP 粘包问题指的是接收方在接收数据时,由于发送方发送的数据包较小,或者接收方处理较慢等原因,导致多个数据包粘在一起,形成一个大的数据块,从而使得接收方无法辨认出各个数据包的边界。

2.1 粘包产生的原因

(1)发送方原因:

  • 发送数据包较小,尤其是小于TCP协议的MSS(最大报文段长度)。
  • 发送数据包的时间间隔太短,导致接收方来不及处理。

(2)接收方原因:

  • 接收处理速度慢,导致多个数据包到达后才开始处理。
  • 接收缓冲区大小设置不当,可能过大或过小。

(3)网络原因:

  • 网络延迟或拥塞,导致数据包传输时间不一致。

2.2 粘包问题的影响

TCP 粘包问题对网络通信的影响是多方面的,它可能会导致数据的不完整、错误解析、性能下降甚至安全问题。以下是 TCP 粘包问题可能带来的一些具体影响:

  • 数据完整性受损:由于接收方无法准确识别数据包边界,可能会导致数据包被错误地合并或拆分,从而造成数据丢失或重复。

  • 错误解析:粘包可能导致接收方错误地解析数据,比如将两个数据包的内容错误地解释为一个数据包,或者将一个数据包的内容错误地解释为两个数据包。

  • 应用逻辑错误:在某些应用中,数据包的内容和顺序是非常重要的。粘包问题可能会导致应用逻辑处理错误,比如在聊天应用中,消息的顺序可能会被打乱。

  • 性能下降:为了处理粘包问题,接收方可能需要额外的缓冲区来暂存数据,并进行边界检测,这会增加CPU和内存的使用,从而降低系统的整体性能。

  • 延迟增加:在某些情况下,为了等待更多的数据以确定数据包的边界,接收方可能会延迟处理已经接收到的数据,这会增加处理延迟。

  • 资源浪费:由于粘包问题,接收方可能需要分配更大的缓冲区来暂存数据,这可能会导致内存资源的浪费。

  • 安全问题:粘包问题可能会被恶意利用,比如通过发送特制的数据包来破坏接收方的缓冲区,从而导致缓冲区溢出等安全问题。

  • 协议复杂性增加:为了解决粘包问题,可能需要在应用层定义额外的协议来标识数据包的边界,这会增加协议的复杂性。

  • 兼容性问题:不同的系统和应用可能采用不同的方法来处理粘包问题,这可能会导致兼容性问题。

  • 调试困难:粘包问题可能会使得网络通信的调试变得更加困难,因为错误可能不容易被发现和定位。

2.3 解决粘包问题的方法

(1)固定长度消息:

每个消息都发送固定长度的数据,接收方按照固定长度进行读取和处理。

**(2)消息头和消息体:

消息分为消息头和消息体,消息头中包含消息体的长度信息,接收方根据消息头中的长度来确定消息体的边界。

**(3)使用特殊字符或字节序列:

在消息体中使用特殊的字符或字节序列作为消息边界的标识。

**(4)应用层协议:

定义应用层协议,明确消息的开始和结束,例如使用 HTTP 协议中的 Content-Length 字段。

**(5)使用缓冲区处理:

接收方使用缓冲区暂存接收到的数据,当满足一个完整消息的条件时再进行处理。

**(6)使用 TCP 的紧急数据或带外数据:

通过发送紧急指针或带外数据来标记消息的结束。

3 一种高性能的 TCP 粘包问题的处理方式

本方式采用使用消息头(Header)+消息体长度(Length)+消息体(Body)的方法来做处理:

  1. 消息头设计:设计一个固定长度的消息头结构,通常包含消息体的长度信息。消息头的长度是固定的,以便于接收方能够快速地识别出消息头并从中读取消息体的长度。

  2. 消息体长度:在消息头中,最关键的信息是消息体的长度,这个长度值告诉接收方接下来需要读取多少字节的数据来构成一个完整的消息体。

  3. 消息体发送:发送方在发送消息体之前,先发送包含消息体长度的消息头。这样,接收方在接收到消息头后,就能够知道接下来需要读取多少字节的数据。

  4. 接收处理

    • 接收方首先读取消息头,解析出消息体的长度。
    • 根据消息头中的长度信息,接收方分配一个足够大的缓冲区来存储整个消息体。
    • 接收方接着从TCP流中读取指定长度的消息体数据,直到读取完整个消息体。
  5. 连续读取:在读取完一个完整消息后,接收方再次读取消息头,以确定下一个消息的边界,继续处理后续的消息。

  6. 缓冲区管理:为了处理可能的粘包情况,接收方可能需要使用一个缓冲区来暂存接收到的数据。当缓冲区中的数据足以构成一个完整的消息时,就从缓冲区中提取出消息并处理,然后将剩余的数据留在缓冲区中,等待构成下一个消息。

  7. 错误处理:在设计协议时,还应考虑错误处理机制,比如如果接收到的消息头中的长度信息不合理(如长度为负数或过大),则需要采取相应的错误处理措施。

通过这种方式,即使在TCP流中数据包被粘在一起,接收方也能够准确地识别出每个消息的边界,从而有效地解决了粘包问题。这种方法简单、高效,且易于实现,因此在处理TCP粘包问题时被广泛采用。

(1)定义数据缓冲区

#include <iostream>  
#include <string>  
#include <sstream>
#include <vector>  
#include <deque>  
#include <tuple>  
#include <memory>  using namespace std;std::deque<tuple<unique_ptr<char[]>, uint64_t>> remainingDatas;

(2)定义工具函数,用来显示报文

string binaryStringToHex(string& binaryStr, string strPre, string strSplit)
{string ret;static const char *hex = "0123456789ABCDEF";uint64_t offset = 0;for (auto c : binaryStr){ret.append(strPre);ret.push_back(hex[(c >> 4) & 0xf]); //取二进制高四位ret.push_back(hex[c & 0xf]);        //取二进制低四位if (offset < binaryStr.length() - 1){ret.append(strSplit);}offset++;}return ret;
}string binaryCharToHex(const char* data, uint64_t len, string strPre, string strSplit)
{string strBinaryVal = string(data, len);return binaryStringToHex(strBinaryVal, strPre, strSplit);
}

(3)TCP 粘包问题处理函数

void revMsg(const char *data, uint64_t len)
{if (0 == len) {return;}unique_ptr<char[]> dataTmp = make_unique<char[]>(len);memcpy(dataTmp.get(), data, len);remainingDatas.push_back(tuple<unique_ptr<char[]>, uint64_t>(move(dataTmp), len));while (remainingDatas.size() > 0) {auto remainingDataPtr = get<0>(remainingDatas[0]).get();if (0x20 != remainingDataPtr[0]) {remainingDatas.clear();return;}if (1 == get<1>(remainingDatas[0]) && remainingDatas.size() <= 1) {return;}uint8_t msgLen = 0;uint64_t totalLen = 0;for (auto& remainingData : remainingDatas) {totalLen += get<1>(remainingData);}if (1 == get<1>(remainingDatas[0])) {remainingDataPtr = get<0>(remainingDatas[1]).get();msgLen = (uint8_t)remainingDataPtr[0];}else {msgLen = (uint8_t)remainingDataPtr[1];}if (msgLen + 2 > totalLen) {return;}unique_ptr<char[]> totalData = make_unique<char[]>(totalLen);uint64_t totalDataOffset = 0;for (auto& remainingData : remainingDatas) {uint64_t lenTmp = get<1>(remainingData);memcpy(&(totalData.get())[totalDataOffset], get<0>(remainingData).get(), lenTmp);totalDataOffset += lenTmp;}remainingDatas.clear();uint64_t offset = 2;uint64_t lastMsgOffset = 0;			// 最后一帧完整报文的最后一个字节偏移while (offset + msgLen <= totalLen) {uint64_t revMsgLen = msgLen + 2;unique_ptr<char[]> revMsg = make_unique<char[]>(revMsgLen);memcpy(revMsg.get(), &(totalData.get())[offset - 2], revMsgLen);// 打印处理粘包后的一帧完整报文auto strMsg = binaryCharToHex(revMsg.get(), revMsgLen, "", " ");printf("[recive] %s\n", strMsg.c_str());offset += msgLen;lastMsgOffset = offset;if (offset + 2 < totalLen) {if (0x20 != totalData[offset]) {return;}offset++;msgLen = (uint8_t)totalData[offset];offset++;}}if (lastMsgOffset < totalLen) {uint64_t lastRemainLen = totalLen - lastMsgOffset;unique_ptr<char[]> lastRemainData = make_unique<char[]>(lastRemainLen);memcpy(lastRemainData.get(), &(totalData.get())[lastMsgOffset], lastRemainLen);remainingDatas.push_back(tuple<unique_ptr<char[]>, uint64_t>(move(lastRemainData), lastRemainLen));}}
}

(4)使用模拟的字节流数据做测试

std::vector<std::string> split(const std::string& str, const char delimiter)
{std::vector<std::string> tokens;std::istringstream tokenStream(str);std::string token;while (std::getline(tokenStream, token, delimiter)){tokens.push_back(token);}return tokens;
}tuple<unique_ptr<char[]>, size_t> getBytesFromStr(string str)
{vector<string> strs = split(str, ' ');if (strs.size() > 0){unique_ptr<char[]> bytes(new char[strs.size()]);for (size_t i = 0; i < strs.size(); i++){int val = strtol(strs[i].c_str(), nullptr, 16);bytes[i] = (char)val;}return tuple<unique_ptr<char[]>, size_t>(move(bytes), strs.size());}else{return tuple<unique_ptr<char[]>, size_t>(unique_ptr<char[]>(nullptr), 0);}
}int main() {// 模拟TCP接收数据  string str = "20";auto res = getBytesFromStr(str);revMsg((get<0>(res)).get(), get<1>(res));str = "04 02 00 00 01 20 04 02 00 00 02 20 04 02 00";res = getBytesFromStr(str);revMsg((get<0>(res)).get(), get<1>(res));str = "00 03 20 04 02 00 00 04 20 04 02 00";res = getBytesFromStr(str);revMsg((get<0>(res)).get(), get<1>(res));return 0;
}

上面代码的输出为:

[recive] 20 04 02 00 00 01
[recive] 20 04 02 00 00 02
[recive] 20 04 02 00 00 03
[recive] 20 04 02 00 00 04

这篇关于突破编程_C++_网络编程(一种高性能处理 TCP 粘包问题的方法)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!


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

相关文章

MySQL启动报错:InnoDB表空间丢失问题及解决方法

《MySQL启动报错:InnoDB表空间丢失问题及解决方法》在启动MySQL时,遇到了InnoDB:Tablespace5975wasnotfound,该错误表明MySQL在启动过程中无法找到指定的s... 目录mysql 启动报错:InnoDB 表空间丢失问题及解决方法错误分析解决方案1. 启用 inno

C++ RabbitMq消息队列组件详解

《C++RabbitMq消息队列组件详解》:本文主要介绍C++RabbitMq消息队列组件的相关知识,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录1. RabbitMq介绍2. 安装RabbitMQ3. 安装 RabbitMQ 的 C++客户端库4. A

Java使用MethodHandle来替代反射,提高性能问题

《Java使用MethodHandle来替代反射,提高性能问题》:本文主要介绍Java使用MethodHandle来替代反射,提高性能问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑... 目录一、认识MethodHandle1、简介2、使用方式3、与反射的区别二、示例1、基本使用2、(重要)

Python函数返回多个值的多种方法小结

《Python函数返回多个值的多种方法小结》在Python中,函数通常用于封装一段代码,使其可以重复调用,有时,我们希望一个函数能够返回多个值,Python提供了几种不同的方法来实现这一点,需要的朋友... 目录一、使用元组(Tuple):二、使用列表(list)三、使用字典(Dictionary)四、 使

Linux查看系统盘和SSD盘的容量、型号及挂载信息的方法

《Linux查看系统盘和SSD盘的容量、型号及挂载信息的方法》在Linux系统中,管理磁盘设备和分区是日常运维工作的重要部分,而lsblk命令是一个强大的工具,它用于列出系统中的块设备(blockde... 目录1. 查看所有磁盘的物理信息方法 1:使用 lsblk(推荐)方法 2:使用 fdisk -l(

python web 开发之Flask中间件与请求处理钩子的最佳实践

《pythonweb开发之Flask中间件与请求处理钩子的最佳实践》Flask作为轻量级Web框架,提供了灵活的请求处理机制,中间件和请求钩子允许开发者在请求处理的不同阶段插入自定义逻辑,实现诸如... 目录Flask中间件与请求处理钩子完全指南1. 引言2. 请求处理生命周期概述3. 请求钩子详解3.1

使用Python获取JS加载的数据的多种实现方法

《使用Python获取JS加载的数据的多种实现方法》在当今的互联网时代,网页数据的动态加载已经成为一种常见的技术手段,许多现代网站通过JavaScript(JS)动态加载内容,这使得传统的静态网页爬取... 目录引言一、动态 网页与js加载数据的原理二、python爬取JS加载数据的方法(一)分析网络请求1

MySQL查看表的最后一个ID的常见方法

《MySQL查看表的最后一个ID的常见方法》在使用MySQL数据库时,我们经常会遇到需要查看表中最后一个id值的场景,无论是为了调试、数据分析还是其他用途,了解如何快速获取最后一个id都是非常实用的技... 目录背景介绍方法一:使用MAX()函数示例代码解释适用场景方法二:按id降序排序并取第一条示例代码解

Python中合并列表(list)的六种方法小结

《Python中合并列表(list)的六种方法小结》本文主要介绍了Python中合并列表(list)的六种方法小结,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋... 目录一、直接用 + 合并列表二、用 extend() js方法三、用 zip() 函数交叉合并四、用

电脑蓝牙连不上怎么办? 5 招教你轻松修复Mac蓝牙连接问题的技巧

《电脑蓝牙连不上怎么办?5招教你轻松修复Mac蓝牙连接问题的技巧》蓝牙连接问题是一些Mac用户经常遇到的常见问题之一,在本文章中,我们将提供一些有用的提示和技巧,帮助您解决可能出现的蓝牙连接问... 蓝牙作为一种流行的无线技术,已经成为我们连接各种设备的重要工具。在 MAC 上,你可以根据自己的需求,轻松地