写点什么

C++|TCP 客户端中发送文件

  • 2022 年 7 月 05 日
  • 本文字数:3393 字

    阅读完需:约 11 分钟

C++|TCP客户端中发送文件

正常情况下,谁会用 TCP 传送一个文件呢?

一开始写我就觉得这个功能特别鸡肋,TCP 传送文件,没办法需要这样的功能,只要硬着头皮上了,如果不是有这个需求,我肯定会骂人,真不是多余嘛!

好了言归正传,直接说在 TCP 通讯中文件发送是如何应用的。

需要的功能:客户端 -->> 服务器 发送文件


开发环境:VS2017 + QT5.14.2

开发语言:C++


实现这个功能,我们会遇到哪些主要难解决的问题呢?


  1. 文件过大怎么办?

  2. 传输中断怎么解决?

  3. 传输过程中,界面卡死怎么处理?

  4. 文件发送过去,服务端如何处理呢?


以上是我在实现发送文件功能时遇到的问题,在后续文章也会对此一一解答。如果还有其他不懂的问题可以留言告诉我,一同探讨解决。


1:定义通讯协议

TCP 通讯中,连接服务器与客户端的桥梁便是通讯协议。定义一个简单明了的通信协议对我们实现功能来说也是事半功倍。

说到了发送文件,那么我们可以假设发送文件的通讯命令字是 A1。

首先,在发送文件内容之前,我们需要将文件的名称、大小发给服务器。

我们可以定义一个 json 结构发送

{    "fileName":"测试图片.png",    "fileSize":100}
复制代码

上述结构体中,fileName 存储了需要发送的文件名称,使用 string 类型表示;fileSize 存储了需要发送文件的大小。

其次,后续直接发送带有命令字 A1 的实际文件内容。为了快速传送,假设一次最多可发送 40960 个真实内容。

以上,我们就将发送文件的通讯指令建立了。下面开始直接使用


2:给服务器发送文件信息

2.1、读取需要发送文件的大小。

读文件的方式有很多种,例如说:QFile、ifstream、或者是 FILE 等等。

不知道大家在写程序的时候是个什么规范,就我个人而言,对于一些通讯类来说,绝大多数会采用纯 C++标准的方式,这样无论是使用 MFC 框架以及 QT 框架,甚至是 WIN32 程序都是可以兼容的。

FILE* readFile = fopen(spath.c_str(), "rb");fseek(readFile, 0, SEEK_END);int nFileSize = ftell(readFile);
复制代码

2.2、将有效数据组成 json 结构体字符串数据

json 数据序列化以及反序列化的功能,大家应该都不陌生,这里我也写出来了。

在使用 json 功能之前,记得要添加 #include "json.h"

写法很简单,不用我再过多说明了。

但是需要注意一点的是:通讯过程中必须要使用 utf8 格式的编号。大家可以用 C++方法也可以用 QT 方法,QT 的方法会更快捷一些。毕竟在这里我用的是 VS+QT 的开发环境。


2.3、给服务器发送信息

std::string sJson = m_pDataParsing.AnalysisFileInforToJson("文件名称","文件大小");int nSendJsonSize = sJson.length();int nTotalSize = 27 + nSendJsonSize + 1;char* chSend = new char[nTotalSize];memset(chSend, 0, nTotalSize);this->InitHeaderData(chSend);chSend[21] = 0x00; //服务端编号chSend[22] = 0x00;chSend[23] = 0xA1; //命令字,占两个字节chSend[24] = nSendJsonSize /256/256;chSend[25] = nSendJsonSize /256;chSend[26] = nSendJsonSize %256;//将json数据字符串拷贝到char*数组中memcpy(chSend+27, sJson.c_str(), nSendJsonSize );chSend[nTotalSize-1] = 0xEF; chSend[nTotalSize] = '\0';//socket通讯发送这里不做说明,就是send发送而已//销毁new出来的内容delete[] chSend;chSend = nullptr;
复制代码

以上代码是将 json 数据转成 char 数据数据进行发送的过程。

InitHeaderData():这里存放的是 20 位预留字节,是在开放过程中防止后续有添加,根据个人需求设置。

24、25、26 三个字节存放文件的大小,为了防止传送较大的文件。

对于我的理解来说,难点在于如何将 json 字符串数据赋值到 char 数组中,其他的都是很简单的。

3:给服务器发送文件内容

这一步骤是比较难理解的,涉及到了页面显示效果。

当我们发送一个很小的文件时,消息很快就发送了,几乎不会造成页面卡顿。但是在发送一个大一些的文件时,就会出现这个问题。

第 2 阶段实现完成后,我们需要进行以下操作向服务器发送有效文件内容。

3.1、我们需要将文件指针恢复到头位置

rewind(readFile);  //指针恢复到头位置
复制代码

原因:是因为在读取整个文件大小时,文件指针的位置已经到了文件的末尾。发送实际内容时需要从开始发送

3.2、定义文件发送时需要的变量

在发送文件时,我们都需要知道哪些呢?

int nTotalSize = nFileSize;char chFileBuffer[g_nMaxSendByte];int nMoveSize = 0; //移动大小bool bOK = true;
复制代码

文件的总体大小 nTotalSize、记录每次读取的文件内容 chFileBuffer、文件指针移动的位置以及是否成功标识帮助我们跳出循环。

第 1 阶段已经说明了我们在每次读取时最多取 40960 个有效内容,这里的 g_nMaxSendByte = 40960;

3.3、读取文件并发送

读取文件之前我们需要将 chFileBuffer 中的旧数据置空,好习惯一定要养成。

memset(chFileBuffer, 0, g_nMaxSendByte];
复制代码

获取读取当前发送的实际大小。

这是一个三目运算符操作,主要是我太懒了,不想写 if、else 语句,哈哈。

意思就是说:

当文件的总大小 < 40960 个长度时,读取的文件内容是文件的总大小,反之就是 40960 个长度

int nSendSize = nTotalSize <= g_nMaxSendByte ? nTotalSize : g_nMaxSendByte;
复制代码

记录需要移动的文件指针大小

nMoveSize += nSendSize;
复制代码

从文件中读取刚刚计算的内容

fread(chFileBuffer, sizeof(char), nSendSize, readFile);
复制代码

将读取的文件内容(chFileBuffer)发送给服务端。

发送方法和发送 json 数据的格式差不多,最主要的差别在于,这里是发送的是 char 数据。那么我就把如何将 char 数组赋值到 char*数据这部分说明下

memcpy(data + 27, senddata, nsendSize); //发送的json串
复制代码

发送成功文件后,记得从总文件大小中减去已经发送的大小。当文件大小为负数时,说明文件已经发送完成

nTotalSize -= nSendSize;if (nTotalSize <= 0){	break; //跳出循环}
复制代码

将文件的指针进行偏移

fseek(readFile, nMoveSize, SEEK_SET); //将指针偏离到头文件 nSendSize个字节处
复制代码

在正常情况下,while 循环发送的主要内容就这些了。

为什么说是正常情况呢?万一文件传输过程中,与服务器断开连接了呢?这时,我们就需要判断 socket 是否有有效数据,如果 socket == INVALID_SOCKET,也需要跳出循环

接下来展示整体的循环发送文件内容

while (true){	Sleep(100);	memset(chFileBuffer, 0, g_nMaxSendByte);	//获取当前发送的大小	int nSendSize = nTotalSize <= g_nMaxSendByte ? nTotalSize : g_nMaxSendByte;	nMoveSize += nSendSize;	fread(chFileBuffer, sizeof(char), nSendSize, readFile);          int nTotalSize = 27 + nSendSize + 1;        char* chSend = new char[nTotalSize];        /*        这里是组装发送数据与发送文件信息类似,不做详细说明        */	if (m_pTcpPanel->m_ClientThreadInfo.serverSocket != INVALID_SOCKET)	{		int nRet = send(serverSocket, chSendData, nsocketSize, 0);		if (nRet == SOCKET_ERROR)		{			//发送失败,停止发送                        //每当发送完一次数据时记得要销毁创建的资源	                	delete[] chSendData;                 	chSendData = nullptr;			bOK = false;			break;		}	}	else	{		bOK = false;                //每当发送完一次数据时记得要销毁创建的资源	        	delete[] chSendData;	        chSendData = nullptr;		break; //socket异常,直接退出	}	//每当发送完一次数据时记得要销毁创建的资源		delete[] chSendData;	chSendData = nullptr;	nTotalSize -= nSendSize;	if (nTotalSize <= 0)	{		break; //跳出循环	}	fseek(readFile, nMoveSize, SEEK_SET); //将指针偏离到头文件 nSendSize个字节处}fclose(readFile); //结束发送后,需要关闭文件
复制代码

4:界面卡死处理

当我们使用上述方式发送一个较大的文档时,肯定会出现界面卡死的现象。这也是 C++在程序处理过程中最头疼的一部分了。

开线程,这是我第一次在写这个功能时第一时间想到的解决方法。

但是,我又想到假设我写的这个程序中有很多加载数据慢的地方,也不一定都是传送文件,可能是其他的功能。我还有线程满天飞吗?

仔细想想我都头疼。后来我采用的是"QtConcurrent::run"的方法,这属于 QT 中快速建立线程的方式,但是使用这种方式的时候,所调用的函数的返回值一定是 True,否则就陷入了死循环中。

在发送文件时,一遍加载等待页面一遍发送文件内容,使用进程的方式处理,比开线程简单多了。


使用 TCP 客户端发送数据就讲解完成了~


我是中国好公民 st,一名 C++开发程序媛~


发布于: 刚刚阅读数: 4
用户头像

还未添加个人签名 2022.07.01 加入

擅长语言:C++ 涉及语言:Python

评论

发布
暂无评论
C++|TCP客户端中发送文件_c++_中国好公民st_InfoQ写作社区