Qt网络编程-书接上文,浅谈TCP文件收发,以及心跳包
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文件收发,以及心跳包的更多相关文章
- Qt网络编程QTcpServer和QTcpSocket的理解
前一段时间通过调试Qt源码,大致了解了Qt的事件机制.信号槽机制.毕竟能力和时间有限.有些地方理解的并不是很清楚. 开发环境:Linux((fedora 17),Qt版本(qt-everywhere- ...
- 网游中的网络编程系列1:UDP vs. TCP
原文:UDP vs. TCP,作者是Glenn Fiedler,专注于游戏网络编程相关工作多年. 目录 网游中的网络编程系列1:UDP vs. TCP 网游中的网络编程2:发送和接收数据包 网游中的网 ...
- 转 Android网络编程之使用HttpClient批量上传文件 MultipartEntityBuilder
请尊重他人的劳动成果,转载请注明出处:Android网络编程之使用HttpClient批量上传文件 http://www.tuicool.com/articles/Y7reYb 我曾在<Andr ...
- 浅谈TCP IP协议栈(三)路由器简介
读完这个系列的第一篇浅谈TCP/IP协议栈(一)入门知识和第二篇浅谈TCP/IP协议栈(二)IP地址,在第一篇中,可能我对协议栈中这个栈的解释有问题,栈在数据结构中是一种先进后出的常见结构,而在整个T ...
- 浅谈头文件(.h)和源文件(.cpp)的区别
浅谈头文件(.h)和源文件(.cpp)的区别 本人原来在大一写C的时候,都是所有代码写在一个文件里一锅乱煮.经过自己开始写程序之后,发现一个工程只有一定是由多个不同功能.分门别类展开的文件构成的.一锅 ...
- Python网络编程03 /缓存区、基于TCP的socket循环通信、执行远程命令、socketserver通信
Python网络编程03 /缓存区.基于TCP的socket循环通信.执行远程命令.socketserver通信 目录 Python网络编程03 /缓存区.基于TCP的socket循环通信.执行远程命 ...
- 浅谈TCP/IP网络编程中socket的行为
我认为,想要熟练掌握Linux下的TCP/IP网络编程,至少有三个层面的知识需要熟悉: 1. TCP/IP协议(如连接的建立和终止.重传和确认.滑动窗口和拥塞控制等等) 2. Socket I/O系统 ...
- 【游戏开发】网络编程之浅谈TCP粘包、拆包问题及其解决方案
引子 现如今手游开发中网络编程是必不可少的重要一环,如果使用的是TCP协议的话,那么不可避免的就会遇见TCP粘包和拆包的问题,马三觉得haifeiWu博主的 TCP 粘包问题浅析及其解决方案 这篇博客 ...
- QT网络编程Tcp下C/S架构的即时通信
先写一个客户端,实现简单的,能加入聊天,以及加入服务器的界面. #ifndef TCPCLIENT_H #define TCPCLIENT_H #include <QDialog> #in ...
- UNIX网络编程之旅-配置unp.h头文件环境
最近在学习Unix网络编程(UNP),书中steven在处理网络编程时只用了一个#include “unp.h” 相当有个性并且也很便捷 于是我把第三版的源代码编译实现了这个过程,算是一种个性化的开 ...
随机推荐
- 使用ConfigMap配置您的应用程序
转载自:https://kuboard.cn/learning/k8s-intermediate/config/config-map.html ConfigMap 作为 Kubernetes API ...
- Docker MySql 查看版本的三种方法
目录 Docker MySql 查看版本的三种方法 1.mysql -V命令查看版本 2.status命令查看版本 3.version命令查看版本 Docker MySql 查看版本的三种方法 1.m ...
- 洛谷P2216 HAOI2007 理想的正方形 (单调队列)
题目就是要求在n*m的矩形中找出一个k*k的正方形(理想正方形),使得这个正方形内最值之差最小(就是要维护最大值和最小值),显然我们可以用单调队列维护. 但是二维平面上单调队列怎么用? 我们先对行处理 ...
- SQL基础语句入门
SQL语句入门 起因 学校开设数据库相关的课程了,打算总结一篇关于基础SQL语句的文章. SQL介绍 SQL最早版本是由IBM开发的,一直发展到至今. SQL语言有如下几个部分: 数据定义语言DDL: ...
- JavaWeb完整案例详细步骤
JavaWeb完整案例详细步骤 废话少说,展示完整案例 代码的业务逻辑图 主要实现功能 基本的CURD.分页查询.条件查询.批量删除 所使用的技术 前端:Vue+Ajax+Elememt-ui 后端: ...
- springboot+mybatis+shiro项目中使用shiro实现登录用户的权限验证。权限表、角色表、用户表。从不同的表中收集用户的权限、
要实现的目的:根据登录用户.查询出当前用户具有的所有权限.然后登录系统后.根据查询到的权限信息进行不同的操作. 以下的代码是在搭好的框架之下进行的编码. 文章目录 核心实现部分. 第一种是将用户表和角 ...
- 分享个好东西 - 两行前端代码搞定bilibili链接转视频
只需要在您的要解析B站视频的页面的</body>前面加上下面两行代码即可,脚本会在客户端浏览器里解析container所匹配到的容器里的B站超链接 (如果不是外围有a标签的超链接只是纯粹的 ...
- golang中的变量阴影
索引:https://waterflow.link/articles/1666019023270 在 Go 中,在块中声明的变量名可以在内部块中重新声明. 这种称为变量阴影的原理很容易出现常见错误. ...
- Element基本组件
Element按钮组件: <el-row> <el-button>默认按钮</el-button> <el-button type="primary ...
- 详解AQS中的condition源码原理
摘要:condition用于显式的等待通知,等待过程可以挂起并释放锁,唤醒后重新拿到锁. 本文分享自华为云社区<AQS中的condition源码原理详细分析>,作者:breakDawn. ...