# 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字节)+文件名的结构,当其为文件内容标识时,数据域为文件内容,当其为文件中止标识时,数据域为空,此类型的数据包在发送方取消发送时发出。
##### 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是日志类,用于记录日志文件。
#### 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地址的快速填写。
3.发送文件
用户在完成参数设置的前提下,通过发送界面的”文件选择“按钮选择待发送的文件,点击”开始“按钮即可。发送过程中点击”停止“按钮可终止发送。界面可显示文件大小、已传输数据的大小、传输速率以及软件关键运行信息,通过进度条显示传输进度。
4.接收文件
用户在完成参数设置的前提下,当收到信任用户传输的文件时,开始文件接收。界面可显示文件大小、已传输数据的大小、传输速率、发送方昵称、发送方IP地址以及软件关键运行信息,通过进度条显示传输进度。
5.查看日志
用户在点击菜单中的”日志“后,即可调用本地文本查看工具查看软件运行日志。
### 五、统计与测试
1. 发送文件速度:软件当前采用单线程发送文件,发送速度约1MByte/s,在设置更大的数据包后会出现Socket发送出错的提示;
2. 内存占用:通过在两台机器上测试1G文件的发送,发送端和接收端内存占用不超过100M字节;
3. 代码统计:代码量约2250行(含头文件);
4. 软件在windows下的release版本约73MB,其中包含了所需的动态链接库。