突破编程_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

相关文章

SpringBoot分段处理List集合多线程批量插入数据方式

《SpringBoot分段处理List集合多线程批量插入数据方式》文章介绍如何处理大数据量List批量插入数据库的优化方案:通过拆分List并分配独立线程处理,结合Spring线程池与异步方法提升效率... 目录项目场景解决方案1.实体类2.Mapper3.spring容器注入线程池bejsan对象4.创建

线上Java OOM问题定位与解决方案超详细解析

《线上JavaOOM问题定位与解决方案超详细解析》OOM是JVM抛出的错误,表示内存分配失败,:本文主要介绍线上JavaOOM问题定位与解决方案的相关资料,文中通过代码介绍的非常详细,需要的朋... 目录一、OOM问题核心认知1.1 OOM定义与技术定位1.2 OOM常见类型及技术特征二、OOM问题定位工具

PHP轻松处理千万行数据的方法详解

《PHP轻松处理千万行数据的方法详解》说到处理大数据集,PHP通常不是第一个想到的语言,但如果你曾经需要处理数百万行数据而不让服务器崩溃或内存耗尽,你就会知道PHP用对了工具有多强大,下面小编就... 目录问题的本质php 中的数据流处理:为什么必不可少生成器:内存高效的迭代方式流量控制:避免系统过载一次性

C++右移运算符的一个小坑及解决

《C++右移运算符的一个小坑及解决》文章指出右移运算符处理负数时左侧补1导致死循环,与除法行为不同,强调需注意补码机制以正确统计二进制1的个数... 目录我遇到了这么一个www.chinasem.cn函数由此可以看到也很好理解总结我遇到了这么一个函数template<typename T>unsigned

MySQL的JDBC编程详解

《MySQL的JDBC编程详解》:本文主要介绍MySQL的JDBC编程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录前言一、前置知识1. 引入依赖2. 认识 url二、JDBC 操作流程1. JDBC 的写操作2. JDBC 的读操作总结前言本文介绍了mysq

python获取指定名字的程序的文件路径的两种方法

《python获取指定名字的程序的文件路径的两种方法》本文主要介绍了python获取指定名字的程序的文件路径的两种方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要... 最近在做项目,需要用到给定一个程序名字就可以自动获取到这个程序在Windows系统下的绝对路径,以下

JavaScript中的高级调试方法全攻略指南

《JavaScript中的高级调试方法全攻略指南》什么是高级JavaScript调试技巧,它比console.log有何优势,如何使用断点调试定位问题,通过本文,我们将深入解答这些问题,带您从理论到实... 目录观点与案例结合观点1观点2观点3观点4观点5高级调试技巧详解实战案例断点调试:定位变量错误性能分

Python实现批量CSV转Excel的高性能处理方案

《Python实现批量CSV转Excel的高性能处理方案》在日常办公中,我们经常需要将CSV格式的数据转换为Excel文件,本文将介绍一个基于Python的高性能解决方案,感兴趣的小伙伴可以跟随小编一... 目录一、场景需求二、技术方案三、核心代码四、批量处理方案五、性能优化六、使用示例完整代码七、小结一、

Python中 try / except / else / finally 异常处理方法详解

《Python中try/except/else/finally异常处理方法详解》:本文主要介绍Python中try/except/else/finally异常处理方法的相关资料,涵... 目录1. 基本结构2. 各部分的作用tryexceptelsefinally3. 执行流程总结4. 常见用法(1)多个e

C++统计函数执行时间的最佳实践

《C++统计函数执行时间的最佳实践》在软件开发过程中,性能分析是优化程序的重要环节,了解函数的执行时间分布对于识别性能瓶颈至关重要,本文将分享一个C++函数执行时间统计工具,希望对大家有所帮助... 目录前言工具特性核心设计1. 数据结构设计2. 单例模式管理器3. RAII自动计时使用方法基本用法高级用法