一种实现简单网络断点续传的方法介绍
一、思路分析
断点续传的思路其实也简单。
- 实现发送方的数据接续发送。
- 实现数据接收方的数据断点接收。
二、重难点分析
为了实现这两点,思路也很简单,关键点在于:
- 在文件发送之前,应该先发送一个先行报文,告诉数据接收方文件名,文件大小,中断前已发送的文件包数。
- 需要有一个全局变量,记录下每一个发送文件的发送进度,以便在一次发送中断时,在下次能找到断点。
- 实现从中断处开始发送数据。
- 判断文件是否是新接收还是之前已接受过。新接收则创建再接收,已接受过则判断是否已接收完毕,避免重复接收。
- 需要有一个全局变量,记录下每一个文件的接收进度,以便在一次接收中断时,能保存下来断点。
- 实现从中断处开始接收并写入数据。
- 在断点处,对已发送数据和已接收数据的大小进行比较,确保之前发送的已经确定接收,避免数据丢失。
- 使用追加写入的方式写入文件,避免覆写。
三、实现方式
为解决上述的重难点,提出以下解决思路。
(一)发送方
1 找到并指向断点位置
创建一个全局函数,记录发送进度,每次启动发送时,利用QFile的seek()函数移动指针到已发送的位置。
头文件全局函数
qint64 Process = 0;
cpp文件
//打开文件
QFile SrcFile(send_file);
if(!SrcFile.open(QIODevice::ReadOnly))
{
LogSend(QString("%1 文件打开失败.停止发送.\n").arg(send_file));
return;
}
//得到文件大小信息
File_TotalBytes=SrcFile.size();
Send_TotalBytes = iter.value().Process;
//指针指向断点,如为新发送文件,指向0
SrcFile.seek(Process);
2 先行发送文件信息报文
QString str;
QFileInfo send_file_info(send_file);
//提前发送的的一个报文,告诉数据接收端将要发送的文件的文件名和文件大小,以及已发送的文件大小(因为可能是断点续传)。
str=QString("#%1#%2#%3").arg(send_file_info.fileName()).arg(send_file_info.size()).arg(Send_TotalBytes);
LogSend(str);
qDebug()<<"str.toStdString().c_str():"<<str.toStdString().c_str()<<",len:"<<str.size();
if (UDT::ERROR == (ss = UDT::send(client, str.toStdString().c_str(),strlen(str.toStdString().c_str()), 0)))
{
LogSend("send_error:" + QString(UDT::getlasterror().getErrorMessage()));
return;
}
3 发送并记录断点
if (UDT::ERROR == (send_len = UDT::send(client,byte.data(),byte.size(), 0)))
{
LogSend("send_error:" + QString(UDT::getlasterror().getErrorMessage()));
break;
}
//记录发送的长度
Process+=send_len;
(二)接收方
1 获取发送文件的信息并判断
拆解第一包,获取将接收的文件的信息,文件名,大小,已接收的大小,已经判断是否为续传,新接收则创建,不是则判断是否之前已经接收完毕,是否需要续传,如果需要续传,则从全局函数获取断点信息,并判断已接收文件数据和发送者已发送数据是否一致。
qint64 s_file_size=0; //文件大小
qint64 s_already_recv_size=0; //文件已接收大小,续传
QString s_file_name;//包含路径
QString fileName;//仅文件名
std::string std_str_data=(char*)data;
QString Rx_str_data=QString("%1").fromStdString(std_str_data);
LogSend("接收的原始第一包数据:"+Rx_str_data);
int addr=Rx_str_data.indexOf(QByteArray("#"));
if(addr!=-1) //查找成功
{
QStringList Strs = Rx_str_data.split("#");
fileName = Strs.at(0);//取出文件名
QString str_size=Strs.at(1); //取出文件字节大小字符串
QString str_already_recv_size = Strs.at(2);
s_file_name=QString("%1/%2").arg(recv_save_path).arg(fileName);
s_file_size=str_size.toLongLong(); //得到文件字节大小
s_already_recv_size = str_already_recv_size.toLongLong(); //得到之前已经传送的文件大小
//判断文件是否已经存在
recv_file_p=new QFile(s_file_name);
if(recv_file_p->exists())
{
//如果原来存在,则打开
if(recv_file_p->open(QIODevice::ReadWrite))
{
//如果已经接收完毕,或者已经接收的文件大小和已经发送的文件大小不一致,则退出
if(recv_file_p->size()==s_file_size || s_already_recv_size!=Map_recv_fInfos[fileName].Process)
{
//大小相同,则认为已经接收完毕,否则认为是续传。
recv_file_p->close();
delete recv_file_p;
recv_file_p=nullptr;
if(recv_file_p->size()==s_file_size)
{
LogSend(QString("线程退出:%1:%2:%3。因为文件已经接收完毕\n").arg(s_file_name).arg(s_file_size).arg(file_len));
}else if(s_already_recv_size!=Map_recv_fInfos[fileName].Process){
LogSend(QString("线程退出:%1:%2:%3。已接收文件和已发送文件大小不一致,续传失败!请删除已接收文件,然后重传!\n").arg(s_file_name).arg(s_file_size).arg(file_len));
}
emit recv_stop_signal();
delete [] data;
UDT::close(recver);
#ifndef WIN32
return NULL;
#else
return 0;
#endif
}
//设置断点
file_len = Map_recv_fInfos[fileName].Process;
LogSend(QString("%1 文件打开成功,准备续传.\n").arg(s_file_name));
}
else
{
recv_file_p=nullptr;
LogSend(QString("%1 文件打开失败.\n").arg(s_file_name));
}
}else{
if(recv_file_p->open(QIODevice::ReadWrite))
{
fInfos newfInfos;
newfInfos.filePath = recv_save_path;
newfInfos.size = s_file_size;
newfInfos.Process = 0;
newfInfos.isRecvOk = false;
newfInfos.speed=0.0;
Map_recv_fInfos.insert(fileName,newfInfos);
LogSend(QString("%1 文件创建成功.\n").arg(s_file_name));
}
else
{
recv_file_p=nullptr;
LogSend(QString("%1 文件创建失败.\n").arg(s_file_name));
}
}
2 记录接收文件的断点
边接收边记录已接收文件内容的大小。
if (UDT::ERROR == (rs = UDT::recv(recver,(char*)data,size,0)))
{
LogSend("recv:" +QString(UDT::getlasterror().getErrorMessage()));
LogSend("客户端断开连接");
break;
}
file_len+=rs;
Process=file_len;
3 追加的方式写入文件
QTextStream out(recv_file_p); //输入流
out<<(char*)data; //写入内容