• C++ Qt TCP协议,处理粘包、拆包问题,加上数据头来处理


    目录

    前言:

    场景:

    原因:

    解决:

    方案2具体细节:

    纯C++服务端处理如下:

    Qt客户端处理如下:


    前言:

            tcp协议里面,除了心跳检测是关于长连接操作的处理,这个在前一篇已经提到过了,这一篇将会对tcp本身的一个问题,进行处理:那就是做网络通信大概率会遇到的问题,粘包、拆包问题,碰到这类问题对于新手来说都是比较棘手的,需要好好处理一下。

    场景:

            使用tcp协议的时候:

            1、我明明发单个小包,都很正常呀,没啥问题呀,怎么我对单个小包多发几次,频率快一些,就会数据错乱了;

            2、我明明发小包都好着,怎么发打包就不行了,很奇怪呢?

            其实这2个场景你用抓包工具一抓,分析一下封包内容,就会一目了然。

    原因:

            最本质的原因是tcp协议发送数据的时候,是不会告诉对方当前发送的数据包有多大的,这是协议头决定的,如果接收端用一个很大的缓冲区来接收发送端的小包的时候,就有可能一下子接收到多个小包,从而导致粘包现象;如果用一个较小的缓冲区来接收发送端的较大的数据包,也会导致一个包收不完,得分好多次才能接收完,导致拆包现象;

    解决:

            要解决粘包或者拆包问题, 有如下几个方案;

            方案1:可以给数据包前后加上一些特殊标识,用来区分头尾,这个方案的缺陷是必须找好特殊标识,万一数据内容也包含特殊标识,就会导致解包错误;

            方案2:可以给数据包加上一个数据头,在数据头里面加上一个长度信息来表示整个封包的长度,通过长度来进行收包和解包;这个方案的操作是把整个包看成两部分,数据头+数据体;先收数据头,取出长度来开辟指定长度的缓冲区,先把数据头拷贝到缓冲区,接着再把剩余的数据体内容接收到缓冲区剩余区域里即可。

            方案3:可以把方案1、方案2结合起来,不过方案还是过于复杂,但也相对安全。

    下面就以方案2来进行代码演示:

    方案2具体细节:

            1、由于是C++实现的,可以给应用层封装一个私有协议,那就使用结构体来作为私有协议。

    结构体声明如下:

    1. enum TypeInfo
    2. {
    3. HEART_CHECK_REQ, // 心跳检测请求
    4. HEART_CHECK_RES, // 心跳检测响应
    5. LOGIN_REQ, // 登录请求
    6. LOGIN_RES, // 登录响应
    7. UPLOAD_REQ, // 文件上传请求
    8. UPLOAD_RES, // 文本上传响应
    9. };
    10. struct Head
    11. {
    12. int flag;
    13. int type;
    14. int len;
    15. };
    16. struct HeartCheckReq
    17. {
    18. Head head;
    19. HeartCheckReq(){
    20. head.flag = 0; // 0 表示软件客户端,1表示硬件客户端
    21. head.type = HEART_CHECK_REQ;
    22. head.len = sizeof(HeartCheckReq);
    23. }
    24. };
    25. struct HeartCheckRes
    26. {
    27. Head head;
    28. HeartCheckRes() {
    29. head.flag = 0;
    30. head.type = HEART_CHECK_RES;
    31. head.len = sizeof(HeartCheckRes);
    32. }
    33. };

    纯C++服务端处理如下:

    收包线程函数代码:

    1. void ServerSocket::recvAndSendThread(SOCKET client)
    2. {
    3. // 循环收发包,保存长连接通信
    4. while (true)
    5. {
    6. /*char buffer[1024] = { 0 };
    7. int len_recv = recv(client, buffer, sizeof(buffer), 0);
    8. cout << "len_recv:" << len_recv << endl;
    9. if (len_recv <= 0) {
    10. cout << "socket收包异常:" << WSAGetLastError() << endl;
    11. break;
    12. }*/
    13. // 解决粘包或拆包问题
    14. char *head_buffer = new char[sizeof(Head)];
    15. int len_recv = recv(client, head_buffer, sizeof(Head), 0);
    16. int head_rest = sizeof(Head) - len_recv;
    17. while (head_rest > 0) { // 还有没收完的
    18. len_recv = recv(client, head_buffer+ (sizeof(Head) - head_rest),head_rest , 0);
    19. head_rest -= len_recv;
    20. }
    21. // 表示结构体头收完了,可以拿出总长度
    22. int len_total = ((Head*)head_buffer)->len;
    23. char *buffer = new char[len_total];
    24. memcpy(buffer, head_buffer, sizeof(Head)); // 先把数据头拷贝进去
    25. int len_rest = len_total - sizeof(Head); // 算出剩余长度
    26. while (len_rest > 0) {
    27. len_recv = recv(client, buffer + (len_total - len_rest), len_rest, 0);
    28. len_rest -= len_recv;
    29. }
    30. // 正常
    31. m_clientSockets[client] = HEART_CHECK_TIMES; // 重置心跳阈值
    32. cout << "buffer:" << buffer << endl;
    33. int type = ((Head*)buffer)->type;
    34. if (type == 100) {
    35. Data *d = (Data*)buffer;
    36. cout << "收到内容:" << d->data << endl;
    37. }
    38. else if (type == HEART_CHECK_REQ) {
    39. // 收到心跳包
    40. // 回一个响应包
    41. cout << "收到心跳请求包" << endl;
    42. HeartCheckRes res;
    43. send(client, (char*)&res, res.head.len, 0);
    44. }
    45. else if (type == UPLOAD_REQ) {
    46. // 上传版本文件
    47. cout << "收到版本管理上传文件包" << endl;
    48. UploadFileReq *req = (UploadFileReq*)buffer;
    49. cout << req->file_info.file_name << " md5:" << req->file_info.md5 << " size:" << req->file_info.file_size << endl
    50. << " old:" << req->file_info.old_version << endl;
    51. // 服务端的业务:将文件写到指定目录,并且在数据库中记录相应的信息
    52. // 保存了之后,回一个响应包给客户端
    53. UploadFileManager upload;
    54. upload.business(client, req);
    55. }
    56. // 将收到的数据原封不动的回给客户端
    57. // send(client, buffer, len_recv, 0);
    58. // 释放内存,防止内存泄露
    59. delete[] head_buffer;
    60. delete[] buffer;
    61. head_buffer = nullptr;
    62. buffer = nullptr;
    63. }
    64. closesocket(client); // 关闭客户端套接字
    65. }

    Qt客户端处理如下:

    收包槽函数代码:

            这里要注意的是,Qt的网络通信是异步的,不能像纯windows服务端那样,使用recv来阻塞收包,所以采用了一个全局变量来存储数据包,当然也可以考虑使用静态局部变量或者成员变量来处理,本文为了表示得更加直白,选择使用了全局变量 g_allBuffer。

    1. QByteArray g_allBuffer; // 全局缓冲区,用来保存收到的封包内容
    2. void TcpMainWindow::myRead()
    3. {
    4. QByteArray buffer = m_client->readAll();
    5. g_allBuffer.append(buffer);
    6. int len = g_allBuffer.size();
    7. while(len > 0){
    8. if(len < sizeof(Head)) break; // 不满足数据头大小,继续收包
    9. int len_total = ((Head*)(g_allBuffer.data()))->len;
    10. if(len < len_total) break; // 不满足全部大小,继续收包
    11. QByteArray datas = g_allBuffer.left(len_total); // 可能会收到多个,先拿一个出来出来
    12. emit unpackSignal(datas);
    13. g_allBuffer = g_allBuffer.mid(len_total); // 处理完了,将后面的挪到前面来
    14. len = g_allBuffer.size();
    15. }
    16. }

     解包业务槽代码如下:

    1. void TcpMainWindow::unpackSlot(QByteArray buffer)
    2. {
    3. QString buf = buffer;
    4. ui->label->setText(buf);
    5. m_heartCheckTimes = HEART_CHECK_TIMES; // 重置阈值
    6. int type = ((Head*)buffer.data())->type;
    7. if(type == 100){
    8. Data *d = (Data*)buffer.data();
    9. qDebug()<<"收到:"<data;
    10. buf = d->data;
    11. ui->label->setText(buf);
    12. }else if(type == HEART_CHECK_RES){
    13. // 收到心跳响应包
    14. qDebug()<<"收到心跳响应包";
    15. }
    16. }

    最后,以上只提供了核心代码,哪里有不清楚的,可以留言谈论一些细节,也可以关注后私信给回复,可以发完整工程代码。

            

  • 相关阅读:
    2023年下半年软考考试重磅消息
    5. HTML中常用标签
    昨天步行的思考
    PageHelper插件使用Mybatis二级缓存完美解决分页查询慢问题
    迅为RK3568开发板学习之Linux驱动篇第十三期输入子系统
    socket开发步骤及相关API介绍
    关于Kubernetes中资源限制的一些笔记整理
    哨兵1和2号遥感数据请求失败
    轻松批量剪辑:将MP4视频转换为FLV格式
    网络工作面试题库和答案解析
  • 原文地址:https://blog.csdn.net/mars1199/article/details/134504757