您的位置:首页 > 其它

编写断点续传和多线程下载模块

2008-05-01 12:17 459 查看

  在当今

网络时代,下载软件是使用最为频繁

软件之一。几年来,下载技术也在不停地发展。最原始

下载功能仅仅是个“下载”过程,即从WEB服务器上连续地读取文件。其最大

问题是,由于网络

不稳定性,一旦连接断开使得下载过程中断,就不得不全部从头再来一次。

字串9

  随后,“断点续传”

概念就出来了,顾名思义,就是如果下载中断,在重新建立连接后,跳过已经下载

部分,而只下载还没有下载

部分。

字串7

  无论“多线程下载”技术是否洪以容先生

发明,洪以容使得这项技术得到前所未有

关注是不争

事实。在“网络蚂蚁”软件流行开后,许多下载软件也都纷纷效仿,是否具?quot;多线程下载"技术、甚至能支持多少个下载线程都成了人们评测下载软件

要素。"多线程下载"

基础是WEB服务器支持远程

随机读取,也即支持"断点续传"。这样,在下载时可以把文件分成若干部分,每一部分创建一个下载线程进行下载。

字串5

  现在,不要说编写专门

下载软件,在自己编写

软件中,加入下载功能有时也非常必要。如让自己

软件支持自动在线升级,或者在软件中自动下载新

数据进行数据更新,这都是很有用、而且很实用

功能。本文

主题即怎样编写一个支持"断点续传"和"多线程"

下载模块。当然,下载

过程非常复杂,在一篇文章中难以全部阐明,所以,与下载过程关系不直接

部分基本上都忽略了,如异常处理和网络错误处理等,敬请各位读者注意。我使用

开发环境是C Builder 5.0,使用其他开发环境或者编程语言

朋友请自行作适当修改。 字串7

  HTTP协议简介 字串3

  下载文件是电脑与WEB服务器交互

过程,它们交互

"语言"

专业名称是协议。传送文件

协议有多种,最常用

是HTTP(超文本传输协议)和FTP(文件传送协议),我采用

是HTTP。

字串9

  HTTP协议最基本

命令只有三条:Get、Post和Head。Get从WEB服务器请求一个特定

对象,比如HTML页面或者一个文件,WEB服务器通过一个Socket连接发送此对象作为响应;Head命令使服务器给出此对象

基本描述,比如对象

类型、大小和更新时间。Post命令用于向WEB服务器发送数据,通常使把信息发送给一个单独

应用程序,经处理生成动态

结果返回给浏览器。下载即是通过Get命令实现。

字串2

  基本

下载过程

字串8

  编写下载程序,可以直接使用Socket函数,但是这要求开发人员理解、熟悉TCP/IP协议。为了简化Internet客户端软件

开发,Windows提供了一套WinInet API,对常用

网络协议进行了封装,把开发Internet软件

门槛大大降低了。我们需要使用

WinInet API函数如图1所示,调用顺序基本上是从上到下,其具体

函数原型请参考MSDN。 字串6

字串1



  图1

字串1

  在使用这些函数时,必须严格区分它们使用

句柄。这些句柄

类型是一样

,都是HINTERNET,但是作用不同,这一点非常让人迷惑。按照这些句柄

产生顺序和调用关系,可以分为三个级别,下一级

句柄由上一级

句柄得到。

字串8

  InternetOpen是最先调用

函数,它返回

HINTERNET句柄级别最高,我习惯定义为hSession,即会话句柄。

字串5

  InternetConnect使用hSession句柄,返回

是http连接句柄,我把它定义为hConnect。

字串3

  HttpOpenRequest使用hConnect句柄,返回

句柄是http请求句柄,定义为hRequest。 字串2

  HttpSendRequest、HttpQueryInfo、InternetSetFilePointer和InternetReadFile都使用HttpOpenRequest返回

句柄,即hRequest。

字串8

  当这几个句柄不再使用是,应该用函数InternetCloseHandle把它关闭,以释放其占用

资源。

字串3

  首先建立一个名为THttpGetThread、创建后自动挂起

线程模块,我希望线程在完成后自动销毁,所以在构造函数中设置: 字串1

  FreeOnTerminate = True; // 自动删除

字串4

  并增加以下成员变量: 字串2

  
char Buffer[HTTPGET_BUFFER_MAX 4]; // 数据缓冲区
AnsiString FURL; // 下载对象

URL
AnsiString FOutFileName; // 保存

路径和名称
HINTERNET FhSession; // 会话句柄
HINTERNET FhConnect; // http连接句柄
HINTERNET FhRequest; // http请求句柄
bool FSuccess; // 下载是否成功
int iFileHandle; // 输出文件

句柄


字串1

  1、建⒘?lt;/p>

字串6

  按照功能划分,下载过程可以分为4部分,即建立连接、读取待下载文件

信息并分析、下载文件和释放占用

资源。建立连接

函数如下,其中ParseURL

作用是从下载URL地址中取得主机名称和下载

文件

WEB路径,DoOnStatusText用于输出当前

状态: 字串6

  
//初始化下载环境
void THttpGetThread::StartHttpGet(void)
{
  AnsiString HostName,FileName;
  ParseURL(HostName, FileName);
  try
  {
    // 1.建立会话
    FhSession = InternetOpen("http-get-demo",
       INTERNET_OPEN_TYPE_PRECONFIG,
       NULL,NULL,
       0); // 同步方式
    if( FhSession==NULL)throw(Exception("Error:InterOpen"));
    DoOnStatusText("ok:InterOpen");
    // 2.建立连接
    FhConnect=InternetConnect(FhSession,
       HostName.c_str(),
       INTERNET_DEFAULT_HTTP_PORT,
       NULL,NULL,
       INTERNET_SERVICE_HTTP, 0, 0);
    if(FhConnect==NULL)throw(Exception("Error:InternetConnect"));
    DoOnStatusText("ok:InternetConnect");
    // 3.初始化下载请求
    const char *FAcceptTypes = "*/*";
    FhRequest = HttpOpenRequest(FhConnect,
       "GET", // 从服务器获取数据
       FileName.c_str(), // 想读取

文件

名称 字串4
       "HTTP/1.1", // 使用

协议
       NULL,
       &FAcceptTypes,
       INTERNET_FLAG_RELOAD,
       0);
    if( FhRequest==NULL)throw(Exception("Error:HttpOpenRequest"));
    DoOnStatusText("ok:HttpOpenRequest");
    // 4.发送下载请求
    HttpSendRequest(FhRequest, NULL, 0, NULL, 0);
    DoOnStatusText("ok:HttpSendRequest");
  }catch(Exception &exception)
  {
    EndHttpGet(); // 关闭连接,释放资源
    DoOnStatusText(exception.Message);
  }
}
// 从URL中提取主机名称和下载文件路径
void THttpGetThread::ParseURL(AnsiString &HostName,AnsiString &FileName)
{
  AnsiString URL=FURL;
  int i=URL.Pos("http://");
  if(i>0)
  {
    URL.Delete(1, 7);
  }
  i=URL.Pos("/");
  HostName = URL.SubString(1, i-1);
  FileName = URL.SubString(i, URL.Length());
}


字串9

字串7

  可以看到,程序按照图1中

顺序,依次调用InternetOpen、InternetConnect、HttpOpenRequest函数得到3个相关

句柄,然后通过HttpSendRequest函数把下载

请求发送给WEB服务器。 字串2

  InternetOpen

第一个参数是无关

,最后一个参数如果设置为INTERNET_FLAG_ASYNC,则将建立异步连接,这很有实际意义,考虑到本文

复杂程度,我没有采用。但是对于需要更高下载要求

读者,强烈建议采用异步方式。

字串5

  HttpOpenRequest打开一个请求句柄,命令是"GET",表示下载文件,使用

协议是"HTTP/1.1"。

字串4

  另外一个需要注意

地方是HttpOpenRequest

参数FAcceptTypes,表示可以打开

文件类型,我设置为"*/*"表示可以打开所有文件类型,可以根据实际需要改变它

值。 字串9

  2、读取待下载

文件

信息并分析 字串3

  在发送请求后,可以使用HttpQueryInfo函数获取文件

有关信息,或者取得服务器

信息以及服务器支持

相关操作。对于下载程序,最常用

是传递HTTP_QUERY_CONTENT_LENGTH参数取得文件

大小,即文件包含

字节数。模块如下所示: 字串8

  
// 取得待下载文件

大小
int __fastcall THttpGetThread::GetWEBFileSize(void)
{
  try
  {
    DWORD BufLen=HTTPGET_BUFFER_MAX;
       DWORD dwIndex=0;
       bool RetQueryInfo=HttpQueryInfo(FhRequest,
       HTTP_QUERY_CONTENT_LENGTH,
       Buffer, &BufLen,
       &dwIndex);
    if( RetQueryInfo==false) throw(Exception("Error:HttpQueryInfo"));
    DoOnStatusText("ok:HttpQueryInfo");
    int FileSize=StrToInt(Buffer); // 文件大小
    DoOnGetFileSize(FileSize);
  }catch(Exception &exception)
  {
    DoOnStatusText(exception.Message);
  }
  return FileSize;
}


字串3

  模块中

DoOnGetFileSize是发出取得文件大小

事件。取得文件大小后,对于采用多线程

下载程序,可以按照这个值进行合适

文件分块,确定每个文件块

起点和大小。 字串7

  3、下载文件

模块

字串9

  开始下载前,还应该先安排

怎样保存下载结果。方法很多,我直接采用了C Builder提供

文件函数打开一个文件句柄。当然,也可以采用Windows本身

API,对于小文件,全部缓冲到内存中也可以考虑。 字串8

字串2

  
// 打开输出文件,以保存下载

数据
DWORD THttpGetThread::OpenOutFile(void)
{
  try
  {
  if(FileExists(FOutFileName))
    DeleteFile(FOutFileName);
  iFileHandle=FileCreate(FOutFileName);
  if(iFileHandle==-1) throw(Exception("Error:FileCreate"));
  DoOnStatusText("ok:CreateFile");
  }catch(Exception &exception)
  {
    DoOnStatusText(exception.Message);
  }
  return 0;
}
// 执行下载过程
void THttpGetThread::DoHttpGet(void)
{
  DWORD dwCount=OpenOutFile();
  try
  {
    // 发出开始下载事件
    DoOnStatusText("StartGet:InternetReadFile");
    // 读取数据
    DWORD dwRequest; // 请求下载

字节数
    DWORD dwRead; // 实际读出

字节数
    dwRequest=HTTPGET_BUFFER_MAX;
    while(true)
    {
     Application->ProcessMessages(); 字串2
     bool ReadReturn = InternetReadFile(FhRequest,
        (LPVOID)Buffer,
        dwRequest,
        &dwRead);
     if(!ReadReturn)break;
     if(dwRead==0)break;
     // 保存数据
     Buffer[dwRead]='/0';
     FileWrite(iFileHandle, Buffer, dwRead);
     dwCount = dwCount dwRead;
     // 发出下载进程事件
     DoOnProgress(dwCount);
    }
    Fsuccess=true;
  }catch(Exception &exception)
  {
    Fsuccess=false;
    DoOnStatusText(exception.Message);
  }
  FileClose(iFileHandle);
  DoOnStatusText("End:InternetReadFile");
}
字串9

  下载过程并不复杂,与读取本地文件一样,执行一个简单

循环。当然,如此方便

编程还是得益于微软对网络协议

封装。 字串5

字串1

  4、释放占用

资源 字串6

  这个过程很简单,按照产生各个句柄

相反

顺序调用InternetCloseHandle函数即可。

字串2

  
void THttpGetThread::EndHttpGet(void)
{
  if(FConnected)
  {
    DoOnStatusText("Closing:InternetConnect");
    try
    {
     InternetCloseHandle(FhRequest);
     InternetCloseHandle(FhConnect);
     InternetCloseHandle(FhSession);
    }catch(...){}
    FhSession=NULL;
    FhConnect=NULL;
    FhRequest=NULL;
    FConnected=false;
    DoOnStatusText("Closed:InternetConnect");
  }
}
字串5

  我觉得,在释放句柄后,把变量设置为NULL是一种良



编程习惯。在这个示例中,还出于如果下载失败,重新进行下载时需要再次利用这些句柄变量

考虑。

字串9

  5、功能模块

调用

字串1

  这些模块

调用可以安排在线程对象

Execute方法中,如下所示: 字串9

  
void __fastcall THttpGetThread::Execute()
{
  FrepeatCount=5;
  for(int i=0;i  {
    StartHttpGet();
    GetWEBFileSize();
    DoHttpGet();
    EndHttpGet();
    if(FSuccess)break;
  }
  // 发出下载完成事件
  if(FSuccess)DoOnComplete();
  else DoOnError();
}
字串9

  这里执行了一个循环,即如果产生了错误自动重新进行下载,实际编程中,重复次数可以作为参数自行设置。 字串7

  实现断点续传功能

字串1

  在基本下载

代码上实现断点续传功能并不是很复杂,主要

问题有两点:

字串7

  1、 检查本地

下载信息,确定已经下载

字节数。所以应该对打开输出文件

函数作适当修改。我们可以建立一个辅助文件保存下载

信息,如已经下载

字节数等。我处理得较为简单,先检查输出文件是否存在,如果存在,再得到其大小,并以此作为已经下载

部分。由于Windows没有直接取得文件大小

API,我编写了GetFileSize函数用于取得文件大小。注意,与前面相同

代码被省略了。 字串7

  
DWORD THttpGetThread::OpenOutFile(void)
{
  ……
  if(FileExists(FOutFileName))
  {
    DWORD dwCount=GetFileSize(FOutFileName);
    if(dwCount>0)
    {
     iFileHandle=FileOpen(FOutFileName,fmOpenWrite);
     FileSeek(iFileHandle,0,2); // 移动文件指针到末尾
     if(iFileHandle==-1) throw(Exception("Error:FileCreate"));
     DoOnStatusText("ok:OpenFile");
     return dwCount;
    }
    DeleteFile(FOutFileName);
  }
  ……
}
字串1

字串1

  2、 在开始下载文件(即执行InternetReadFile函数)之前,先调整WEB上

文件指针。这就要求WEB服务器支持随机读取文件

操作,有些服务器对此作了限制,所以应该判断这种可能性。对DoHttpGet模块

修改如下,同样省略了相同

代码:

字串4

  
void THttpGetThread::DoHttpGet(void)
{
  DWORD dwCount=OpenOutFile();
  if(dwCount>0) // 调整文件指针
  {
    dwStart = dwStart dwCount;
    if(!SetFilePointer()) // 服务器不支持操作
    {
     // 清除输出文件
     FileSeek(iFileHandle,0,0); // 移动文件指针到头部
    }
  }
  ……
}
字串6

  多线程下载 字串9

  要实现多线程下载,最主要

问题是下载线程

创建和管理,已经下载完成后文件

各个部分

准确合并,同时,下载线程也要作必要

修改。

字串6

  1、下载线程

修改

字串6

  为了适应多线程程序,我在下载线程加入如下成员变量:

字串2

  int FIndex; // 在线程数组中

索引

字串1

  DWORD dwStart; // 下载开始

位置 字串1

  DWORD dwTotal; // 需要下载

字节数

字串1

  DWORD FGetBytes; // 下载

总字节数 字串8

  并加入如下属性值: 字串5

  
__property AnsiString URL = { read=FURL, write=FURL };
__property AnsiString OutFileName = { read=FOutFileName, write=FOutFileName};
__property bool Successed = { read=FSuccess};
__property int Index = { read=FIndex, write=FIndex};
__property DWORD StartPostion = { read=dwStart, write=dwStart};
__property DWORD GetBytes = { read=dwTotal, write=dwTotal};
__property TOnHttpCompelete OnComplete = { read=FOnComplete, write=FOnComplete };
字串4

  同时,在下载过程DoHttpGet中增加如下处理,

字串5

  
void THttpGetThread::DoHttpGet(void)
{
  ……
  try
  {
    ……
    while(true)
    {
     Application->ProcessMessages();
     // 修正需要下载

字节数,使得dwRequest dwCount      if(dwTotal>0) // dwTotal=0表示下载到文件结束
     {
       if(dwRequest dwCount>dwTotal)
       dwRequest=dwTotal-dwCount;
     }
     ……
     if(dwTotal>0) // dwTotal <=0表示下载到文件结束
     {
       if(dwCount>=dwTotal)break;
     }
    }
  }
  ……
  if(dwCount==dwTotal)FSuccess=true;
}


字串8

字串4

  2、建立多线程下载组件 字串2

  我先建立了以TComponent为基类、名为THttpGetEx

组件模块,并增加以下成员变量:

字串7

  // 内部变量

字串4

  THttpGetThread **HttpThreads; // 保存建立

线程 字串6

  AnsiString *OutTmpFiles; // 保存结果文件各个部分

临时文件 字串4

  bool *FSuccesss; // 保存各个线程

下载结果 字串6

  // 以下是属性变量 字串5

  int FHttpThreadCount; // 使用

线程个数 字串9

  AnsiString FURL;

字串7

  AnsiString FOutFileName; 字串8

  各个变量

用途都如代码注释,其中

FSuccess

作用比较特别,下文会再加以详细解释。因为线程

运行具有不可逆性,而组件可能会连续地下载不同

文件,所以下载线程只能动态创建,使用后随即销毁。创建线程

模块如下,其中GetSystemTemp函数取得系统

临时文件夹,OnThreadComplete是线程下载完成后

事件,其代码在其后介绍: 字串1

  
// 分配资源
void THttpGetEx::AssignResource(void)
{
  FSuccesss=new bool[FHttpThreadCount];
  for(int i=0;i    FSuccesss[i]=false;
  OutTmpFiles = new AnsiString[FHttpThreadCount];
  AnsiString ShortName=ExtractFileName(FOutFileName);
  AnsiString Path=GetSystemTemp();
  for(int i=0;i    OutTmpFiles[i]=Path ShortName "-" IntToStr(i) ".hpt";
  HttpThreads = new THttpGetThread *[FHttpThreadCount];
}
// 创建一个下载线程
THttpGetThread * THttpGetEx::CreateHttpThread(void)
{
  THttpGetThread *HttpThread=new THttpGetThread(this);
  HttpThread->URL=FURL;
  …… // 初始化事件
  HttpThread->OnComplete=OnThreadComplete; // 线程下载完成事件
  return HttpThread;
}
// 创建下载线程数组
void THttpGetEx::CreateHttpThreads(void)
{
  AssignResource();
  // 取得文件大小,以决定各个线程下载

起始位置
  THttpGetThread *HttpThread=CreateHttpThread(); 字串8
  HttpThreads[FHttpThreadCount-1]=HttpThread;
  int FileSize=HttpThread->GetWEBFileSize();
  // 把文件分成FHttpThreadCount块
  int AvgSize=FileSize/FHttpThreadCount;
  int *Starts= new int[FHttpThreadCount];
  int *Bytes = new int[FHttpThreadCount];
  for(int i=0;i  {
    Starts[i]=i*AvgSize;
    Bytes[i] =AvgSize;
  }
  // 修正最后一块

大小
  Bytes[FHttpThreadCount-1]=AvgSize (FileSize-AvgSize*FHttpThreadCount);
  // 检查服务器是否支持断点续传
  HttpThread->StartPostion=Starts[FHttpThreadCount-1];
  HttpThread->GetBytes=Bytes[FHttpThreadCount-1];
  bool CanMulti=HttpThread->SetFilePointer();
  if(CanMulti==false) // 不支持,直接下载
  {
    FHttpThreadCount=1;
    HttpThread->StartPostion=0;
    HttpThread->GetBytes=FileSize;
    HttpThread->Index=0;
    HttpThread->OutFileName=OutTmpFiles[0];
  }else
  {

字串5

字串3

  下载文件

下载

函数如下:

字串7

  
void __fastcall THttpGetEx::DownLoadFile(void)
{
  CreateHttpThreads();
  THttpGetThread *HttpThread;
  for(int i=0;i  {
    HttpThread=HttpThreads[i];
    HttpThread->Resume();
  }
}


字串1

  线程下载完成后,会发出OnThreadComplete事件,在这个事件中判断是否所有下载线程都已经完成,如果是,则合并文件

各个部分。应该注意,这里有一个线程同步

问题,否则几个线程同时产生这个事件时,会互相冲突,结果也会混乱。同步

方法很多,我

方法是创建线程互斥对象。

字串3

  
const char *MutexToThread="http-get-thread-mutex";
void __fastcall THttpGetEx::OnThreadComplete(TObject *Sender, int Index)
{
  // 创建互斥对象
  HANDLE hMutex= CreateMutex(NULL,FALSE,MutexToThread);
  DWORD Err=GetLastError();
  if(Err==ERROR_ALREADY_EXISTS) // 已经存在,等待
  {
    WaitForSingleObject(hMutex,INFINITE);//8000L);
    hMutex= CreateMutex(NULL,FALSE,MutexToThread);
  }
  // 当一个线程结束时,检查是否全部认为完成
  FSuccesss[Index]=true;
  bool S=true;
  for(int i=0;i  {
    S = S && FSuccesss[i];
  }
  ReleaseMutex(hMutex);
  if(S)// 下载完成,合并文件

各个部分
  {
    // 1. 复制第一部分
    CopyFile(OutTmpFiles[0].c_str(),FOutFileName.c_str(),false);
    // 添加其他部分
    int hD=FileOpen(FOutFileName,fmOpenWrite);
    FileSeek(hD,0,2); // 移动文件指针到末尾
    if(hD==-1) 字串9
    {
     DoOnError();
     return;
    }
    const int BufSize=1024*4;
    char Buf[BufSize 4];
    int Reads;
    for(int i=1;i    {
     int hS=FileOpen(OutTmpFiles[i],fmOpenRead);
     // 复制数据
     Reads=FileRead(hS,(void *)Buf,BufSize);
     while(Reads>0)
     {
       FileWrite(hD,(void *)Buf,Reads);
       Reads=FileRead(hS,(void *)Buf,BufSize);
     }
     FileClose(hS);
    }
    FileClose(hD);
  }
}
字串3

字串1

  结语

字串6

  到此,多线程下载

关键部分就介绍完了。但是在实际应用时,还有许多应该考虑

因素,如网络速度、断线等等都是必须考虑

。当然还有一些细节上

考虑,但是限于篇幅,就难以一一写明了。如果读者朋友能够参照本文编写出自己满意

下载程序,我也就非常欣慰了。我也非常希望读者能由此与我互相学习,共同进步。

字串4

    HttpThread->OutFileName=OutTmpFiles[FHttpThreadCount-1];
    HttpThread->Index=FHttpThreadCount-1;
    // 支持断点续传,建立多个线程
    for(int i=0;i    {
     HttpThread=CreateHttpThread();
     HttpThread->StartPostion=Starts[i];
     HttpThread->GetBytes=Bytes[i];
     HttpThread->OutFileName=OutTmpFiles[i];
     HttpThread->Index=i;
     HttpThreads[i]=HttpThread;
    }
  }
  // 删除临时变量
  delete Starts;
  delete Bytes;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: