# fileTrans **Repository Path**: weianxiange/fileTrans ## Basic Information - **Project Name**: fileTrans - **Description**: 通过Udp协议实现大文件可靠传输 - **Primary Language**: C++ - **License**: LGPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 4 - **Forks**: 0 - **Created**: 2023-10-14 - **Last Updated**: 2025-05-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## FileTrans-UDP协议实现的大文件传输工具 ### 一、项目介绍 ​ 该软件是利用UDP协议实现的大文件传输工具,可利用其实现端到端的大文件传输;利用用户层的协议实现了丢包数据的重传,保证了数据传输的可靠性;非一次性读入文件到内存,因此可用于大文件传输;使用Qt编程框架开发,具备跨平台编译运行的能力;具备良好的人机交互界面,用户操作方便,数据传输过程可视化效果好;具备信任管理机制,软件只接收信任的IP地址传送的文件。 ### 二、软件设计 #### 2.1 总体设计 ​ 软件为单一客户端,同时具备发送和接收数据的能力,并可实现全双工收发文件;软件需实现网络数据发送与接收、文件读写、数据校验与重传、传输过程可视化、人机交互界面等功能;软件采用Qt编程框架使用C++编程语言。 #### 2.2 用户层可靠协议设计 ​ 由于UDP协议是无连接的网络层协议,直接用于传输文件将会导致因数据丢包导致的文件错误,因此为保证文件的完整可用,需设计用户层的可靠传输协议。 ##### 2.2.1 数据包结构 ​ 数据包包含包头、数据域和包尾3部分。包头包含了信息类别标识(4字节)、包序号(8字节),包尾为CRC校验码(1字节)。数据包结构如图所示。信息类别标识包含文件信息标识、文件内容标识、文件中止标识。当信息标识为文件信息标识时,数据域部分为文件长度(8字节)+文件名的结构,当其为文件内容标识时,数据域为文件内容,当其为文件中止标识时,数据域为空,此类型的数据包在发送方取消发送时发出。 packageStructure ##### 2.2.2 选择重传机制 ​ 选择重传(Selective Repeat)是一种数据链路层的协议,通常用于可靠地传输数据。在网络通信中,当发生数据包丢失或损坏时,选择重传协议允许发送方只重传丢失或损坏的数据包,而不是整个数据流。该协议中,发送方发送数据包并等待确认(ACK)。接收方接收数据包,并发送确认信息。如果发送方在规定的时间内未收到确认信息,它会重新发送该数据包。选择重传协议允许接收方缓存接收到的数据包,并在需要时重发丢失的数据包。 ​ 为了实现文件传输的高可靠性,本软件使用类似的机制。发送方在发送数据包后对数据先进行缓存,并对每一个缓存的数据包开启一个定时器,当收到接收方发来的Ack确认后再清理缓存的数据,如果在定时器超时后仍没有收到确认将对该数据包进行重新发送;接收方收到数据后先进行缓存,在包序号部分有序的情况下,将有序的部分写入硬盘,并清理对应的缓存。 ##### 2.2.3 数据校验 ​ 由于 UDP 协议是无连接服务,在使用 UDP 协议进行传输的时候会存在多种潜在错误,如帧丢失、帧失序、帧错误等。对于帧丢失和帧失序,软件通过帧序号进行判断,对于帧错误,软件通过 CRC 校验实现,使用 CRC-CCITT (Kermit)标准按字节计算校验值。发送前,计算得到校验值并在数据包末尾加入校验值;接收时,取出数据包末尾的校验值,计算前部的校验值并与其进行比对,若校验正确,则对接收数据包作进一步的处理,否则抛弃该数据包。 #### 2.3 多线程设计 ​ 软件为全双工设计,即单一客户端可同时发送和接收文件,因此软件需使用多线程保证数据的同时收发,同时为了保证用户的操作流畅执行,避免用户界面卡顿,用户界面显示需使用单独的线程。本软件将用户界面作为主线程,设计了发送线程和接收线程,此外,为了实现在发送时对文件高效的读取,设计了读线程用于读取磁盘文件。读线程和发送线程通过共享缓冲区进行数据传递,实现了本地文件的高速读取。 ### 三 、具体实现 #### 3.1 软件类图 ​ 软件具体实现的类图如图所示,其中MainWindows、AddTrustAddrDialog、SetDialog是窗口类,分别用于显示主界面、设置信任用户和软件参数设置;ReceiveThreadObj是接收线程对象类,在使用QThread的moveToThread方法启动线程时使用,实现接收文件和将写入本地的功能;SendThreadObj是发送线程对象类;ReadFileThreadObj是读文件线程对象类;CCycleBuffer是环形缓冲区类,供SendThreadObj和ReadFileThreadObj共享使用;package类是数据包管理类,提供了数据包解包和组包的功能;Helper并不是一个类,在类图中承载了用于添加校验码和检验校验码的全局函数;SaveLog是日志类,用于记录日志文件。 Image description #### 3.2 主要功能代码摘录 1.线程的启动和信号槽的绑定(以读线程为例)。 `` ```c++ m_readThread=new QThread; m_readThreadObj=new ReadFileThreadObj(nullptr,&m_shareBuffer); m_readThreadObj->moveToThread(m_readThread); connect(m_readThreadObj,&ReadFileThreadObj::message,this,&MainWindow::widgetMessage); connect(this,&MainWindow::read,m_readThreadObj,&ReadFileThreadObj::startRead); connect(this,&MainWindow::finishRead,m_readThreadObj,&ReadFileThreadObj::closeFile); connect(m_readThreadObj,&ReadFileThreadObj::endOfFile, m_senderThreadObj,&SendThreadObj::getFileState); m_readThread->start(); ``` 2.读文件到缓冲区。 `` ``` bool ReadFileThreadObj::readFile() { if(m_file==nullptr){ return false; } if(m_fileShift>=m_fileLength){ qDebug()<<"All file is loaded"; emit message("文件读取完毕"); emit endOfFile(); closeFile(); return false; } char *data= new char[FILE_BLOCK_SIZE]; m_file->seek(m_fileShift); qint64 readLength=m_file->read(data,FILE_BLOCK_SIZE); if(m_shareBuffer->GetEmptyLength()>=readLength){ m_fileShift+=readLength; m_shareBuffer->WriteBuffer(data,readLength); } return true; } ``` 3.组包并发送数据,对发送过的数据先缓存并设置超时重发定时器。 `` ``` void SendThreadObj::send(QByteArray sendData, quint32 sendType) { package a(this); if(!a.combine(sendType,sendData,sequenceNumber,true)){ return; } qint64 bytesSent = sendSocket->writeDatagram(a.all, m_destAddr, m_destPort); if (bytesSent !=a.allLength) { // 发送失败处理 emit sendFailed(); emit message(QString("发送数据出错,错误代码:%1,待发送数据大小%2").arg(sendSocket->error()).arg(sendData.size())); qDebug()<<"发送数据出错,udpsocket未能完全发送整包数据"; return; } // 设置定时器,关联序列号与定时器 int timerId=startTimer(timeout); timerMap[sequenceNumber] = timerId;//key是序列号,value是timerID // 存储数 // sentDataBuffer[sequenceNumber] = sendData; sentDataBuffer.push_back(a); // 增加序列号 sequenceNumber++; } ``` 4.数据包组包,实现了包头、数据域和校验码的组合。 ``` bool package::combine(quint32 typ, QByteArray cont, quint64 num=0,bool crcCheck=true) { if(typ==packageType::ack||typ==packageType::breakSend||typ==packageType::fileInfo||typ==packageType::fileContent){ type=typ; }else{ return false; } number=num; all.resize(12); memcpy(all.data(),&typ,4);//type memcpy(all.data()+4,&num,8);//number all.append(cont);//加内容 if(crcCheck){ //对整个包做校验 uint8_t temp; uint16_t crc=0; crc = crc16_CCITT(all.data(),all.size()); temp = crc >> 8; all.append(temp); temp = crc & 0xff; all.append(temp); } allLength=all.size(); return true; } ``` 5.数据包解包,包含了类型判定、检查校验码和数据包拆分。 ``` bool package::decompose(QByteArray full, bool crcCheck=true) { if(full.size()==0){ return false; } all=full.data(); allLength=full.size(); memcpy(&type,full.data(),4); if(type==packageType::ack||type==packageType::breakSend||type==packageType::fileInfo||type==packageType::fileContent){ }else{ return false; } memcpy(&number,full.data()+4,8); if(!crcCheck){ int contentLength=full.size()-12; // number=num; content .resize(contentLength); memcpy(content.data(),full.data()+12,contentLength); }else{ if(!check_CCITT(full.data(),full.size())){ return false; } int contentLength=full.size()-14; content.resize(contentLength); memcpy(content.data(),full.data()+12,contentLength); } return true; } ``` 6.接收线程接收数据并对每个数据包进行确认 ``` void ReceiveThreadObj::readPendingDatagrams() { while (m_receiveSocket->hasPendingDatagrams()) { QByteArray datagram; datagram.resize(m_receiveSocket->pendingDatagramSize()); quint16 m_senderPort; int code=m_receiveSocket->readDatagram(datagram.data(), datagram.size(), &m_senderAddr,&m_senderPort); if(code==-1) { qDebug()<<"接收线程接收帧失败"; return; } if (m_senderAddr.protocol() == QAbstractSocket::IPv4Protocol) { if(!m_trustAddrs.contains(m_senderAddr.toString())){ qDebug()<start(500); m_startTime=QDateTime::currentSecsSinceEpoch(); } }else return; }else if(m_workState==receiverState::receiving){ if(temp.type==packageType::breakSend){ emit message("文件接收失败:发送方取消了发送!"); qDebug()<<"文件接收失败:发送方取消了发送!"; emit receiveAll(); enterEmptyState(); }else if(temp.type==packageType::fileContent){ // if(m_dataBuffer.find(temp.number)==m_dataBuffer.end()){ // m_dataBuffer[temp.number]=temp.content; // } for(int i=0;itemp.number&&i-1>=0){ if(m_dataBuffer[i-1].number 2.参数设置 ​ 用户可根据需要设置发送和接收相关参数,具体如图所示。值得说明的是快速选择功能,用户可从信任名单中选择用户实现IP地址的快速填写。 Image description 3.发送文件 ​ 用户在完成参数设置的前提下,通过发送界面的”文件选择“按钮选择待发送的文件,点击”开始“按钮即可。发送过程中点击”停止“按钮可终止发送。界面可显示文件大小、已传输数据的大小、传输速率以及软件关键运行信息,通过进度条显示传输进度。 Image description 4.接收文件 ​ 用户在完成参数设置的前提下,当收到信任用户传输的文件时,开始文件接收。界面可显示文件大小、已传输数据的大小、传输速率、发送方昵称、发送方IP地址以及软件关键运行信息,通过进度条显示传输进度。 Image description 5.查看日志 ​ 用户在点击菜单中的”日志“后,即可调用本地文本查看工具查看软件运行日志。 ### 五、统计与测试 1. 发送文件速度:软件当前采用单线程发送文件,发送速度约1MByte/s,在设置更大的数据包后会出现Socket发送出错的提示; 2. 内存占用:通过在两台机器上测试1G文件的发送,发送端和接收端内存占用不超过100M字节; 3. 代码统计:代码量约2250行(含头文件); 4. 软件在windows下的release版本约73MB,其中包含了所需的动态链接库。