qt网络编程-书接上文,浅谈文件收发

上文Qt网络编程-从0到多线程编程中谈到 在qt中的qtcpsocket通讯的用法,接下来浅谈一下关于tcp通讯的实际应用,当然了由于是浅谈,也不能保证其功能的完整性,所以在此不能保证每一个技术细节都正确。

写在前面,只有tcp是适合于发送文件的,而udp不适合,原因也很简单,因为udp发送的信号是断断续续的,并不能保证客户端一定能收到服务器的所有包,有可能要陷入到无尽的等待中,当然可能较小的文件是可以的,但是光是在脑海中想想这个损耗应该就是不可接受的,故在此不谈udp文件收发的应用,当我想到了,可能又会写一篇。

首先我们来说套接字,也就是socket通讯的这个socket。其实我个人是很不喜欢套接字这个说法的,所谓的socket翻译成套接字这个,其实非常的难以理解,我觉得数据包这个翻译还可以,但是意思还是差点,不过既然如此我们就以数据包的形式来理解tcp就好了。

我们在通信中,可以理解tcp通信为一个数据包,然后直接从服务端发送给客户端,这样就完成了数据的传输。

是这么回事吗?

显然不是的,要考虑的问题很多

第一个,你怎么能保证一次传输数据的完整性?你发出一个包,中途可能遇到很多情况,比如断网,内存出错,或者数组超载等,如果是这样的话,那么接收到的数据就不完整了,那么组装起来的数据就会七扭八歪,失去其原有的含义。

第二个,数据在传输的过程中有没有可能错位?前面的数据传到后面去,或者后面的数据传到前面去?这些都是有可能的

第三个,如果传的是文件,又要怎么保证文件的完整性?文件可以看成是一个很长很长的二进制代码,长到需要用Mb乃至Gb来形容其文本大小

第四个,如果传的是文件,但是实际上传输的文件中是不会带有该文件的信息的,比如名字、后缀、所有者等信息,而是一个二进制的文本,这个文本的信息我们该如何获取呢?

以上这些都是可能在tcp协议中遇到的问题,就以上这些问题,也就构成了tcp实际应用中的一些常见基本构造,封包、心跳包、tcp头等等,接下来就简单聊聊该怎么实现这些功能。

1.封包

实际上在早期的cpp程序里面,数组的大小还不像现在现在用的qt那样直接动态的申请长度,而是用多少申请多少,比如我现在这个数组我需要1024个字节,那就只能一次申请1024个字节,少了还好说,剩下的大不了空着,但是如果多了,那就不好意思,直接内存溢出让整个程序崩溃。为了解决这个问题,我们在tcp发送的信息里面会适当地添加上一些指定的信号,以此来指挥接收端。

所以一个tcpsocket的长消息,我们要分为三个部分,分别是头 身 尾

一般头部就存放一些属性,比如包的长度,当前包个数,包的总个数,也可能是长度信息。身体就是内容。尾一般是一个0字符,就像制止符,单纯告诉你这条消息到此为止的。

当然,如果是自己用的包,就可以不用规定这么多,只需要在里面定义一个当前文件的总长度即可,至于多少个包,就可以自己内部规定,我就在内部规定的一个包就是1024个字节。另外因为是本地阻塞地发送tcpsoket(需要写锁实现),而且又是局域网内传输,所以不考虑沾包丢包的情况,当然了,如果是安全保密性要求高的链接,或者是网络通信,那么肯定是要求有沾包丢包的处理的,因为我还没实践,所以不讨论这种问题。

现在我们知道了发送端怎么处理,那么接收端呢?

接收端在创建一个实例来接受封包的时候,需要建立一个结构来存放期望接受的大小和以及实际接受到的大小,每轮接受固定长度,当实际接收到的数据大于等于期望大小时,这样就可以说明我们的一次t cp消息接受完成了,这大概就是tcp通讯的一个流程。

2.如何传输文件

解决完前面几个问题,现在来解决后续的问题,如何传输文件的信息呢?

其实也就和传输tcp信息一样,在已经实现了tcp通讯的前提下,我们在总的数据链上,再写一个自己定义的头,来存放一些信息,比如可以用|符分割,第一段存放xxx,第二段存放xxx,第三段存放xxx。。。以此类推,这样就可以组成一个完整的信息,一个包含了所有需要数据的信息,通过分割的方式来组成一个完整的文件。

这个就是比较简单的理论介绍,那么现在来看看实际上我们的工程该怎么写这样的文件

首先我们准备一个QtTcpBaseHandler类,这个类是我们做封包收发的基础,也是我们做客户端或者服务端的基础,它不需要有什么别的功能,只需要有基础的收发功能就行,也就是能定义头身尾,发送TCP和接收TCP消息

首先我们来定义一个头

struct MsgHeader
{
/*
头部12字节
[0] = 4;
[1] = 'H';
[2] = 'E';
[3] = 'A';
[4] = 'D';
[5] = 0;
[6] = 0;
[7] = 0;
[8] = 0;
[9] = 0;
[10] = 0;
[11] = 0;
*/
char gcBaseHeader[12] = { 0 }; /*
4字节,长度信息:内容长度+1
*/
int nMsgBodyLen = 0; /*
预留字节
*/
char gcReserved[12] = { 0 };
};

头部的话比较简单,就是首先你要定义头的前几个字符,以此为标准,然后后续预留几个字节用来发送别的信息,这里存储的信息是5-10字节合起来代表了一个字节长度信息,最后预留第十二字节是0字符结尾,用以代表头部结束。下面来看发送信息

void SendTcp(QTcpSocket * pTcpSocket, const QByteArray &bytes)
{
/* 消息结构:头 + 内容 + 结尾符0 */
MsgHeader header;
header.nMsgBodyLen = bytes.length() + 1; QByteArray bytesHeaderInfo((char*)&header, sizeof(MsgHeader));
pTcpSocket->write(bytesHeaderInfo); char *pData = new char[header.nMsgBodyLen];
memset(pData, 0, header.nMsgBodyLen);
memcpy_s(pData, header.nMsgBodyLen, bytes.data(), bytes.length());
for (int i = 0; i < header.nMsgBodyLen; i += 1024)
{
pTcpSocket->write(pData + i, qMin(1024, header.nMsgBodyLen - i));
} delete[] pData;
}

发送信息的话,就是把我们要发送的消息和头部信息组装起来,先做一个pData,这个是我们用来发送数组的一个缓冲区,先把所有的数据读到这个缓冲区内,

//缓冲区
memset(pData, 0, header.nMsgBodyLen);
memcpy_s(pData, header.nMsgBodyLen, bytes.data(), bytes.length());

再由QTcpsocket 转手发送出去,每次转发固定长度,像我们这里是固定了它的长度为1024长

//每次发送1024个字节,当然了如果没有1024个字节了就发剩下的部分就行了。
for (int i = 0; i < header.nMsgBodyLen; i += 1024)
{
pTcpSocket->write(pData + i, qMin(1024, header.nMsgBodyLen - i));
}

那这边就是发送端,是一个比较简单的结构,那么来看接收端。接收端的话,因为qt的tcpsocket通信是异步的操作,所以非常有可能导致接收包的动作会因为QThread::sleep 或者 调试阻塞等行为导致一些无法预料的异常,从而导致接收到的包发生占包,丢包,错位等情况。之前也说过了,当前这个tcp通信类只是在本地实现的,所以在头部只有一个信息,就是消息总长度。那么在接收端就需要写一个锁,当我们没有读完上一条tcp消息时,下一条消息到来之前需要阻塞(时间非常短,因为一条消息只有1024个字节,对于现在的局域网来说传输起来没有什么苦困难)

首先我们需要一个接收tcp消息的结构体如下:

typedef struct tagTCPRecvData
{
int nExpectSize = 0; // 期望大小
int nRecvedLen = 0; // 已收大小
QByteArray bytes; void Clear()
{
nExpectSize = 0;
nRecvedLen = 0;
bytes.clear();
}
}TCPRecvData;

我们通过这个结构体来接收文件,记录当前期望大小和已接受大小,当已收大小大于等于期望大小时,接收行为结束

为了保证每次申请的结构都能智能释放和不重复读包、读包正确,这里使用了智能指针和哈希表

using SPTCPRecvData = std::shared_ptr<TCPRecvData>;
QHash<QTcpSocket*, SPTCPRecvData> g_qhsSock2RecvData;

一把同步互斥锁:

QMutex g_mtForQhsSock2RecvData;

整个读取tcp消息的函数如下:

void ReadTCPMsg(QTcpSocket * pTcpSocket, qint64 &nCountRecvedMsg, fun_Notice funNotice)
{
QMutexLocker am(&g_mtForQhsSock2RecvData); if (!g_qhsSock2RecvData.contains(pTcpSocket))
{
SPTCPRecvData spTcpRecvData = std::make_shared<TCPRecvData>();
g_qhsSock2RecvData.insert(pTcpSocket, spTcpRecvData);
}
SPTCPRecvData spTcpRecvData = g_qhsSock2RecvData[pTcpSocket]; while (pTcpSocket->bytesAvailable() > 0)
{
if (0 == spTcpRecvData->nRecvedLen)
{
if (pTcpSocket->bytesAvailable() < sizeof(MsgHeader)) return; QByteArray bytesHeader = pTcpSocket->read(sizeof(MsgHeader)); MsgHeader *pHeader = (MsgHeader*)(bytesHeader.data());
spTcpRecvData->nExpectSize = pHeader->nMsgBodyLen;
} int nBytesAvailable = pTcpSocket->bytesAvailable();
if (nBytesAvailable > 0)
{
int nThisRecv = qMin(nBytesAvailable, spTcpRecvData->nExpectSize - spTcpRecvData->nRecvedLen);
spTcpRecvData->bytes += pTcpSocket->read(nThisRecv);
spTcpRecvData->nRecvedLen += nThisRecv; // 其实就是等于,加大于号为了保险...
if (spTcpRecvData->nRecvedLen >= spTcpRecvData->nExpectSize)
{
// 通知
if (nullptr != funNotice) funNotice(spTcpRecvData->bytes); // 重置
spTcpRecvData->Clear();
}
}
} nCountRecvedMsg = spTcpRecvData->nRecvedLen;
}

一条锁 QMutexLocker am(&g_mtForQhsSock2RecvData);锁定当前函数,这个 QMutexLocker将保证在销毁前不重复执行当前函数。

后续就是处理具体数据的部分了,就是拆开来慢慢读写的问题,其实既然都能保证数据按队列输入了,接下来也就是考虑意外的问题,这里就没什么好说的了。

那以上这个类就是tcp通讯的收发基础,接下来就是写服务端和客户端两端,也就是应用层。

Qt网络编程-书接上文,浅谈TCP文件收发,以及心跳包的更多相关文章

  1. Qt网络编程QTcpServer和QTcpSocket的理解

    前一段时间通过调试Qt源码,大致了解了Qt的事件机制.信号槽机制.毕竟能力和时间有限.有些地方理解的并不是很清楚. 开发环境:Linux((fedora 17),Qt版本(qt-everywhere- ...

  2. 网游中的网络编程系列1:UDP vs. TCP

    原文:UDP vs. TCP,作者是Glenn Fiedler,专注于游戏网络编程相关工作多年. 目录 网游中的网络编程系列1:UDP vs. TCP 网游中的网络编程2:发送和接收数据包 网游中的网 ...

  3. 转 Android网络编程之使用HttpClient批量上传文件 MultipartEntityBuilder

    请尊重他人的劳动成果,转载请注明出处:Android网络编程之使用HttpClient批量上传文件 http://www.tuicool.com/articles/Y7reYb 我曾在<Andr ...

  4. 浅谈TCP IP协议栈(三)路由器简介

    读完这个系列的第一篇浅谈TCP/IP协议栈(一)入门知识和第二篇浅谈TCP/IP协议栈(二)IP地址,在第一篇中,可能我对协议栈中这个栈的解释有问题,栈在数据结构中是一种先进后出的常见结构,而在整个T ...

  5. 浅谈头文件(.h)和源文件(.cpp)的区别

    浅谈头文件(.h)和源文件(.cpp)的区别 本人原来在大一写C的时候,都是所有代码写在一个文件里一锅乱煮.经过自己开始写程序之后,发现一个工程只有一定是由多个不同功能.分门别类展开的文件构成的.一锅 ...

  6. Python网络编程03 /缓存区、基于TCP的socket循环通信、执行远程命令、socketserver通信

    Python网络编程03 /缓存区.基于TCP的socket循环通信.执行远程命令.socketserver通信 目录 Python网络编程03 /缓存区.基于TCP的socket循环通信.执行远程命 ...

  7. 浅谈TCP/IP网络编程中socket的行为

    我认为,想要熟练掌握Linux下的TCP/IP网络编程,至少有三个层面的知识需要熟悉: 1. TCP/IP协议(如连接的建立和终止.重传和确认.滑动窗口和拥塞控制等等) 2. Socket I/O系统 ...

  8. 【游戏开发】网络编程之浅谈TCP粘包、拆包问题及其解决方案

    引子 现如今手游开发中网络编程是必不可少的重要一环,如果使用的是TCP协议的话,那么不可避免的就会遇见TCP粘包和拆包的问题,马三觉得haifeiWu博主的 TCP 粘包问题浅析及其解决方案 这篇博客 ...

  9. QT网络编程Tcp下C/S架构的即时通信

    先写一个客户端,实现简单的,能加入聊天,以及加入服务器的界面. #ifndef TCPCLIENT_H #define TCPCLIENT_H #include <QDialog> #in ...

  10. UNIX网络编程之旅-配置unp.h头文件环境

    最近在学习Unix网络编程(UNP),书中steven在处理网络编程时只用了一个#include “unp.h”  相当有个性并且也很便捷 于是我把第三版的源代码编译实现了这个过程,算是一种个性化的开 ...

随机推荐

  1. 使用ConfigMap配置您的应用程序

    转载自:https://kuboard.cn/learning/k8s-intermediate/config/config-map.html ConfigMap 作为 Kubernetes API ...

  2. Docker MySql 查看版本的三种方法

    目录 Docker MySql 查看版本的三种方法 1.mysql -V命令查看版本 2.status命令查看版本 3.version命令查看版本 Docker MySql 查看版本的三种方法 1.m ...

  3. 洛谷P2216 HAOI2007 理想的正方形 (单调队列)

    题目就是要求在n*m的矩形中找出一个k*k的正方形(理想正方形),使得这个正方形内最值之差最小(就是要维护最大值和最小值),显然我们可以用单调队列维护. 但是二维平面上单调队列怎么用? 我们先对行处理 ...

  4. SQL基础语句入门

    SQL语句入门 起因 学校开设数据库相关的课程了,打算总结一篇关于基础SQL语句的文章. SQL介绍 SQL最早版本是由IBM开发的,一直发展到至今. SQL语言有如下几个部分: 数据定义语言DDL: ...

  5. JavaWeb完整案例详细步骤

    JavaWeb完整案例详细步骤 废话少说,展示完整案例 代码的业务逻辑图 主要实现功能 基本的CURD.分页查询.条件查询.批量删除 所使用的技术 前端:Vue+Ajax+Elememt-ui 后端: ...

  6. springboot+mybatis+shiro项目中使用shiro实现登录用户的权限验证。权限表、角色表、用户表。从不同的表中收集用户的权限、

    要实现的目的:根据登录用户.查询出当前用户具有的所有权限.然后登录系统后.根据查询到的权限信息进行不同的操作. 以下的代码是在搭好的框架之下进行的编码. 文章目录 核心实现部分. 第一种是将用户表和角 ...

  7. 分享个好东西 - 两行前端代码搞定bilibili链接转视频

    只需要在您的要解析B站视频的页面的</body>前面加上下面两行代码即可,脚本会在客户端浏览器里解析container所匹配到的容器里的B站超链接 (如果不是外围有a标签的超链接只是纯粹的 ...

  8. golang中的变量阴影

    索引:https://waterflow.link/articles/1666019023270 在 Go 中,在块中声明的变量名可以在内部块中重新声明. 这种称为变量阴影的原理很容易出现常见错误. ...

  9. Element基本组件

    Element按钮组件: <el-row> <el-button>默认按钮</el-button> <el-button type="primary ...

  10. 详解AQS中的condition源码原理

    摘要:condition用于显式的等待通知,等待过程可以挂起并释放锁,唤醒后重新拿到锁. 本文分享自华为云社区<AQS中的condition源码原理详细分析>,作者:breakDawn. ...