IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> 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:给服务器发送文件信息

第一步,读取需要发送文件的大小。

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

第二步,将有效数据组成json结构体字符串数据

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

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

std::string DataSocketParsing::AnalysisFileInforToJson(std::string sfileName, int nfileSize)
{
	Json::Value jsData;
	jsData["fileName"] = sfileName;
	jsData["fileSize"] = nfileSize;
	std::string sSendData = jsData.toStyledString();
	return sSendData;
}

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

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

第三步,给服务器发送信息

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阶段实现完成后,我们需要进行以下操作向服务器发送有效文件内容。

第一步,我们需要将文件指针恢复到头位置

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

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

第二步:定义文件发送时需要的变量

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

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

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

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

第三步:读取文件并发送

读取文件之前我们需要将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"的方法,之前的文章中我也提到过。这里使用方式我不再过多说明,只是提供一个方向。

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

5:服务器接收客户端发送的文件信息

第一步,定义接收数据结构体

首先,我们需要记录文件信息,也就是文件名称、文件大小以及已经接收的文件大小。数据量较多,这里采用的结构体进行存储。

struct ServerDeviceFilesInforSt
{
  int nClientNumber; //客户端编号
  std::string sFileName; //文件名称
  int nTotalFileSize; //文件大小
  int nReceivedSize; //已经接收的大小
  FILE* writeFile; //文件指针
  std::string sSavePath; //记录文件的保存地址
}

客户端与服务器一般都是多对一的关系,我们在服务端这边记录客户端数据时采用了std::vector<ServerDeviceFilesInforSt> m_vetInfo 成员变量。根据实际使用习惯可以定义其他容器比如:map、list

第二步,记录客户端发送的文件信息

首先,从众多的客户端中查询出当前发送消息的客户端

bool bFind  = false;
std::vector<ServerDeviceFilesInforSt>::iterator itFind = m_vetInfo.begin();
for(itFind; itFind != m_vetInfo.end(); itFind++)
{
  if(itFind->nClientNumber == nDeviceNumber)
  {
     bFind = true; //说明查找到了
     break;
  }
}

当我们查询到匹配客户端后,如果当前在结构体中记录的总体文件大小 ==0时,说明是第一次存储文件的基本情况,解析数据并进行存储

if(itFind->nTotalFileSize == 0)
{
  //说明:该编号的客户端文件属于第一次存储
  int nTotalFilesSize = 0;
  std::string sFileName = "";
  /*
  解析客户端发送的内容并获取有效数据,文件名称以及大小
  这里不做说明
  */
  
   //将解析的内容进行存储
   itFind->sFileName = sFileName;
   itFind->nTotalFileSize = nTotalFilesSize;
   itFind->nReceivedSize = 0; //未进行数据接收

   //创建文件并打开
   std::string sSavePath = "这是当前文件的路径,并是已经创建好的";
   itFind->writeFile = fopen(sSavePath, "wb");
   itFind->sSavePath = sSavePath;
}

第三步,记录实际文件信息

当结构体中的文件总体大小不为0时,说明正在接收实际的有效数据

获取接收真实的文件大小

int nJsonSize = (unsigned char)data[24] * 256 * 256 + (unsigned char)data[25] * 256 + (unsigned char)data[26];

将接收的内容存储到文件指针中存储

fwrite(data + 27, sizeof(char), nJsonSize, itFind->writeFile); 

记录到文件指针后记录已经接收的实际长度

itFind->nReceivedSize += nJsonSize;

当服务端记录的文件大小 <= 接收的大小时,说明文件接收完成

if(itFind->nTotalFileSize <= itFind->nReceivedSize)
{
  fclose(itFind->writeFile); //关闭当前文件
  //文件关闭后清空结构体中的数据
  itFind->nTotalFileSize = 0;
  itFind->nReceivedSize = -1;
  itFind->sSavePath = "";
  itFind->sFileName = "";
}

以上便是服务端接收的实际操作。

今天的TCP传送文件操作已经讲解完成。虽然功能很鸡肋,就当自己提升了一下通讯水平。

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2021-08-20 15:27:26  更:2021-08-20 15:27:54 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/25 20:33:21-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码