VC++使用多线程和Socket实现断点续下
一、断点续下的基本原理:
1.断点续传的理解可以分为两部分:一部分是断点,一部分是续传。断点的由来是在下载过程中,将一个下载文件分成了多个部分,同时进行多个部分一起的下载,当某个时间点,任务被暂停了,此时下载暂停的位置就是断点了。续传就是当一个未完成的下载任务再次开始时,会从上次的断点继续传送。
2.使用多线程断点续传下载的时候,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,多个线程并发可以占用服务器端更多资源,从而加快下载速度。在下载(或上传)过程中,如果网络故障、电量不足等原因导致下载中断,这就需要使用到断点续传功能。下次启动时,可以从记录位置(已经下载的部分)开始,继续下载以后未下载的部分,避免重复部分的下载。断点续传实质就是能记录上一次已下载完成的位置。
注意:要实现HTTP断点续传,Web服务器必须支持HTTP/1.1
3.HTTP请求是有一个Header的,里面有个Range属性是定义下载区域的,它接收的值是一个区间范围,比如:Range:bytes=0-10000。这样我们就可以按照一定的规则,将一个大文件拆分为若干很小的部分,然后分批次的下载,每个小块下载完成之后,再合并到文件中;这样即使下载中断了,重新下载时,也可以通过文件的字节长度来判断下载的起始点,然后重启断点续传的过程,直到最后完成下载过程。
二、FTP实现断点续传
FTP协议也可以支持断点续传下载数据,基本原理是用get命令拿数据的时候在文件名后面加上要获取的起始位置。FTP实现断点续传有三个条件:
①断点续传需要服务器的支持,FTP服务器必须能提供断点续传的功能。传统的FTP Server是不支持断点续传的,因为它不支持REST指令;目前包括IIS和大部分的FTP架设软件都有了这个功能。用Serv-U架设FTP服务器就能支持断点续传。
②支持断点续传的下载工具软件
QQ旋风、迅雷、影音传送带等大多下载软件都支持断点续传;IE浏览器5.0以前的版本默认的自带下载方式不支持断点续传。在手机上,UC浏览器支持断点续传,能够自动存储已下载的部分,重新打开之后可以继续在已下载部分的基础上继续下载。
③FTP服务器上的文件要与下载到硬盘中的文件名相同。
在使用IE下载文件时,遇到网络中断,不需要重新启动机器,也可实现断点续传。前提是,在恢复下载、开始断点续传并提示再次保存文件时,要使用和第一次下载时相同的路径和文件名。
三、断点续传的基本原理包括以下几个步骤:
-
文件分割:下载的文件被分割成多个小块(或称为“分片”、“段”等)。
-
下载记录:客户端在下载每个文件块时,会记录下已经成功下载的块的信息,这通常包括块的序号、大小、校验码等。
-
中断检测:如果下载过程中发生中断,客户端会检测到这一情况。
-
恢复请求:当用户决定重新开始下载时,客户端会根据记录的下载信息,向服务器发送恢复请求,请求从最后一个成功下载的块开始继续下载。
-
服务器响应:服务器接收到恢复请求后,会根据请求中的信息,从指定的块开始发送数据。
-
数据校验:客户端在接收到数据后,会进行数据校验,确保接收的数据块是正确的。
-
合并文件:随着下载的进行,客户端会将下载的块按顺序合并,最终形成完整的文件。
-
完成下载:当所有块都下载并合并完成后,下载任务结束。
下面我们就新建工程来实现断点续下,如下笔者是直接用从网上下载的工程,叫做DownLoadTest,后面这个工程也会上传Gitee。
具体的代码是:
1.首先定义一个线程基类,用来启动下载。头文件为Thread.h
#ifndef _THREAD_SPECIFICAL_H__
#define _THREAD_SPECIFICAL_H__
#define WIN32_LEAN_AND_MEAN //防止windows.h引入winsock.h与winsock2.h冲突
#include <windows.h>
static unsigned int __stdcall threadFunction(void *);
class Thread {
friend unsigned int __stdcall threadFunction(void *);
public:
Thread();
virtual ~Thread();
int start(void * = NULL);//线程启动函数,其输入参数是无类型指针。
void stop();
void* join();//等待当前线程结束
void detach();//不等待当前线程
static void sleep(unsigned int);//让当前线程休眠给定时间,单位为毫秒
protected:
virtual void * run(void *) = 0;//用于实现线程类的线程函数调用
private:
HANDLE threadHandle;
bool started;
bool detached;
void * param;
unsigned int threadID;
};
#endif
Thread.cpp代码如下:
unsigned int __stdcall threadFunction(void * object)
{
Thread * thread = (Thread *) object;
return (unsigned int ) thread->run(thread->param);
}
Thread::Thread()
{
started = false;
detached = false;
}
Thread::~Thread()
{
stop();
}
int Thread::start(void* pra)
{
if (!started)
{
param = pra;
if (threadHandle = (HANDLE)_beginthreadex(NULL, 0, threadFunction, this, 0, &threadID))
{
if (detached)
{
CloseHandle(threadHandle);
}
started = true;
}
}
return started;
}
//wait for current thread to end.
void * Thread::join()
{
DWORD status = (DWORD) NULL;
if (started && !detached)
{
WaitForSingleObject(threadHandle, INFINITE);
GetExitCodeThread(threadHandle, &status);
CloseHandle(threadHandle);
detached = true;
}
return (void *)status;
}
void Thread::detach()
{
if (started && !detached)
{
CloseHandle(threadHandle);
}
detached = true;
}
void Thread::stop()
{
if (started && !detached)
{
TerminateThread(threadHandle, 0);
//Closing a thread handle does not terminate
//the associated thread.
//To remove a thread object, you must terminate the thread,
//then close all handles to the thread.
//The thread object remains in the system until
//the thread has terminated and all handles to it have been
//closed through a call to CloseHandle
CloseHandle(threadHandle);
detached = true;
}
}
void Thread::sleep(unsigned int delay)
{
::Sleep(delay);
}
2.接下来从该类继承一个类DownLoadHelper用来真正的实现下载操作。
#include <vector>
#include <string>
#include <iostream>
using namespace std;
#include "Thread.h"
#include "ChineseCode.h"
//每个任务线程数
#define THREAD_COUNT 3
//重连时间
#define RECONNECT_INTERVAL 10000
class DownloadHelper: public Thread
{
public:
void * run(void *);
bool startDownload();
//url:"http://www.abc.com/123.jpg"
//location: "f:\\download\\123.jpg"
bool addDownloadTask(const char* remoteUrl, const char* localFolder);
DownloadHelper();
virtual ~DownloadHelper();
//传入函数指针,下载完成后调用
void setOnFinish(void (*func)());
private:
//判断下载列表的文件是否已经存在
//传入index是downloadListRemoteURLs的下标
bool exist(int index);
//文件网络url路径
vector<string> downloadListRemoteURLs;
//文件在本地保存的目录
vector<string> downloadListLocalFolders;
//完成后调用的函数
void (*onFinish)();
ChineseCode chineseCode;
};
bool existInVector(vector<string>& array, string& str);
DownLoadHelper.cpp文件实现如下:
#include "stdafx.h"
#include "DownloadHelper.h"
#include "Mydownload.h"
#include <io.h>
//
// Construction/Destruction
//
DownloadHelper::DownloadHelper()
{
onFinish = NULL;
}
DownloadHelper::~DownloadHelper()
{
}
//添加下载任务,以传入的url作为唯一标识符
bool DownloadHelper::addDownloadTask(const char* remoteUrl, const char* localFolder)
{
string remoteUrlString(remoteUrl);
string localFolderString(localFolder);
if(!existInVector(downloadListRemoteURLs,remoteUrlString)){
downloadListRemoteURLs.push_back(remoteUrlString);
downloadListLocalFolders.push_back(localFolderString);
return true;
}else{
return false;
}
}
//每次删除第一个
/* vector<string>::iterator startIterator = downloadListRemoteURLs.begin();
downloadListRemoteURLs.erase( startIterator );
*/
//判断字符串是否已经在vector<string>中出现
bool existInVector(vector<string>& array, string& str){
for(int k = 0;k<array.size();k++){
if(array[k].compare(str)==0)
return true;
}
return false;
}
//开始下载
bool DownloadHelper::startDownload()
{
this->start();
return true;
}
//多线程重构函数
void * DownloadHelper::run(void *)
{
//分配空间,用于跟踪。
unsigned long temp = 0;
unsigned long *downloaded = &temp;
unsigned long totalSize = 1024;
while(downloadListRemoteURLs.size()>0)
{
cout<<downloadListRemoteURLs[0]<<endl;
//默认三线程下载,可以修改,但必须保持不变,因为断点续传需要前后两次线程数一致
while(true){
//阻塞式,直到下载成功或者网络出错才跳出
fnMyDownload(downloadListRemoteURLs[0].data(),
downloadListLocalFolders[0].data(),downloaded,totalSize,"",0,THREAD_COUNT);
if(!exist(0)){
//文件不存在,表示下载中断
cout<<"网络中断,等待重连..."<<endl;
Sleep(RECONNECT_INTERVAL); //10秒后重连
}else{
//下载成功,删除第一个任务
vector<string>::iterator startIterator = downloadListRemoteURLs.begin();
downloadListRemoteURLs.erase( startIterator );
startIterator = downloadListLocalFolders.begin();
downloadListLocalFolders.erase( startIterator );
break;
}
}
}
if(onFinish!=NULL){
onFinish();
}
return NULL;
}
//判断下载列表的文件是否已经存在
//传入index是downloadListRemoteURLs的下标
bool DownloadHelper::exist(int index)
{
string fileName = downloadListRemoteURLs[index].substr(downloadListRemoteURLs[index].find_last_of("/")+1);
string file(downloadListLocalFolders[index].data()); //copy
file.append(fileName);
return (_access(file.data(), 0) == 0);;
}
//传入函数指针,下载完成后调用
void DownloadHelper::setOnFinish(void (*func)()){
onFinish = func;
}
因为这个项目是使用Http进行下载,所以定义一个Http类,用来处理Http下载请求,对于Http协议,笔者也不是很懂,后面还需要学习一下。这里就直接用原来的代码了。
定义一个HttpGet类来处理Http请求:头文件是MyDownLoad.h
#ifndef Mydownload___
#define Mydownload___
#include "stdafx.h"
#define MAX_RECV_LEN 100 // 每次接收最大字符串长度.
#define MAX_PENDING_CONNECTS 4 // 等待队列的长度.
class CHttpSect
{
public:
CString szProxyAddr; // 理服务器地址.
CString szHostAddr; // Host地址.
int nProxyPort; // 代理服务端口号.
int nHostPort; // Host端口号.
CString szHttpAddr; // Http文件地址.
CString szHttpFilename; // Http文件名.
CString szDesFilename; // 下载后的文件名.
DWORD nStart; // 分割的起始位置.
DWORD nEnd; // 分割的起始位置.
DWORD bProxyMode; // 下载模态.
};
class CHttpGet
{
public:
CHttpGet();
virtual ~CHttpGet();
//static unsigned long m_downloaded;
private:
CHttpSect *sectinfo;
static int m_nCount;
static UINT ThreadDownLoad(void* pParam);
public:
static DWORD m_nFileLength;
private:
static SOCKET ConnectHttpProxy(CString strProxyAddr,int nPort);
static SOCKET ConnectHttpNonProxy(CString strHostAddr,int nPort);
static BOOL SendHttpHeader(SOCKET hSocket,CString strHostAddr,
CString strHttpAddr,CString strHttpFilename,DWORD nPos);
static DWORD GetHttpHeader(SOCKET sckDest,char *str);
static DWORD GetFileLength(char *httpHeader);
static BOOL SocketSend(SOCKET sckDest,CString szHttp);
BOOL FileCombine(CHttpSect *pInfo, FILE *fpwrite);
public:
BOOL HttpDownLoadProxy(
CString strProxyAddr,
int nProxyPort,
CString strHostAddr,
CString strHttpAddr,
CString strHttpFileName,
CString strWriteFileName,
int nSectNum,
DWORD &totalSize);
BOOL HttpDownLoadNonProxy(
CString strHostAddr,
CString strHttpAddr,
CString strHttpFileName,
CString strWriteFileName,
int nSectNum,
DWORD &totalSize);
BOOL HttpDownLoad(
CString strProxyAddr,
int nProxyPort,
CString strHostAddr,
int nHostPort,
CString strHttpAddr,
CString strHttpFileName,
CString strWriteFileName,
int nSectNum,
BOOL bProxy);
};
另外我们还定义一个Socket类,使用Socket来连接服务器进行下载操作。
class CDealSocket
{
public:
CDealSocket();
virtual ~CDealSocket();
public:
SOCKET GetConnect(CString host ,int port);
SOCKET Listening(int port);
CString GetResponse(SOCKET hSock);
};
一个文件类CMyFile,用来辅助需要下载的文件。
class CMyFile
{
public:
CMyFile();
virtual ~CMyFile();
public:
BOOL FileExists(LPCTSTR lpszFileName);
FILE* GetFilePointer(LPCTSTR lpszFileName);
DWORD GetFileSizeByName(LPCTSTR lpszFileName);
CString GetShortFileName(LPCSTR lpszFullPathName);
};
MyDownLoad.cpp文件太长了,这里就不贴出来了。直接去项目看下。
在主函数中的测试程序如下:
DownloadHelper downloadHelper;
downloadHelper.addDownloadTask("http://192.168.1.112/com.zip","C:\\Users\\Administrator\\Desktop\\");
downloadHelper.startDownload();
downloadHelper.join();
上面用用到的网址,是自己在本地搭建的Http服务器。另外需要讲一下在Windows上搭建Http服务器的过程。使用IIS搭建Http服务器进行测试时,遇到了如下问题:http error 503.the service is unavailable错误。最后的解决办法遇到相同问题的可以参考这篇文章。# 成功解决http error 503.the service is unavailable错误
小结:这个项目中用到的知识点有:
1.多线程编程。
2.Http协议。
3.Socket编程。
4.如何在本地搭建Http服务器。
参考文章:
1.解读断点续传的基本原理 - duanxz - 博客园 (cnblogs.com)
2.windows环境(本地端以及华为云服务器)搭建HTTP服务器_本地服务器-CSDN博客
- https://developer.aliyun.com/article/1217820问题解决办法。
最后,就为大家介绍到这里了。项目的源码:DownLoadTest