浅谈关于http的多线程断点续传下载

来源:互联网 发布:西北师大知行学院怎走 编辑:程序博客网 时间:2024/05/22 13:03

      本来这篇文章一直都想写的,只是前两天发点小烧,身体不适,所以耽搁了。我查了下网上资料,发现大部分都是ftp多线程下载的,关于http还真的不多,所以想写篇文章阐述下,自己才疏学浅,哪里写的不好,还望高手见笑了。

      前两篇文章都是用wininet来编写的,代码貌似没有问题,但是试验中发现没有办法实现真正的多线程下载,下载速度慢得很,而且即使是多线程,也不能同时internetreadfile。后来看网上说要用异步方式来写,看了下介绍,感觉太麻烦了,听说那些下载软件都是直接用socket来做的,所以自己索性改用socket来封装http数据包了。

      我们在启动多线程前先要获得目标文件的content-length,也就是文件大小,根据这个文件的大小来建立文件:

char buf[1024]={0};
 sprintf(buf,"GET %s HTTP/1.1/r/n"
  "Accept:*/*/r/n"
  "User-Agent: sx/r/n"
  "Host: %s/r/n"
  "Connection:close/r/n"
  "Cache-Control: no-cache/r/n"
  "/r/n/r/n",name,host);
 s.send(buf,strlen(buf));
 int count;
 char r[1024]={0};
 count=s.recv((char*)r,1024);
 s.close();
 char *l=strstr(r,"Content-Length:");
 l+=16;
 char len[100]={0};
 int i1=0;
 while(*l!='/r'){
  len[i1++]=*(l++);
 }
 int len1=atoi(len);

HANDLE f1=CreateFile(local,GENERIC_WRITE,FILE_SHARE_READ|FILE_SHARE_WRITE,0,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,0);
 SetFilePointer(f1,len1,0,FILE_BEGIN);
 SetEndOfFile(f1);
 CloseHandle(f1);

       接着就是对多线程进行数据分配了,当然不一定能平均,但是最后一个线程获得的下载长度一定比前一个的小,下面是实现代码:

for(int i=0;i<threads;i++){
  ff=new file;
  if(threads>1){
   ff->len=i<(threads-1)?len1/(threads-1):(len1-len1/(threads-1)*(threads-1));
   ff->start=i*(len1/(threads-1));
  }else{
   ff->len=len1;
   ff->start=0;
  }
  ff->totle=&totle;
  strcpy(ff->localfile,local);
  strcpy(ff->host,host);
  strcpy(ff->name,name);
  ff->cs=&cs;
  ff->run=&(init1->run);
  dh[i]=CreateThread(0,0,t,(LPVOID)ff,0,0);
 }

ff是传递给线程的结构参数,len1就是目标文件的总长度,ff-》len是线程获得的长度,ff-》start是线程开始下载的位置,thread就是线程数了。

       好了,线程获得了下载的起始位置和下载长度,怎么进行下载呢,http header里有个range这个字段,它就代表了下载的起始位置和终止位置,下面看下线程是如何来发送http数据包的:

char buf[1024]={0};
 sprintf(buf,"GET %s HTTP/1.1/r/n"
  "Accept:*/*/r/n"
  "User-Agent: sx/r/n"
  "Host: %s/r/n"
  "Connection:close/r/n"
  "Cache-Control: no-cache/r/n"
  "Range: bytes=%d-%d/r/n/r/n",ff->name,ff->host,ff->start,ff->start+ff->len-1);
 s.send(buf,strlen(buf));

       没什么好说的,应该很明白了吧。

       再接着便是数据的读取了,这里以分配给每个结构体的文件长度为基准,也就是说每个线程只读取分配给自己长度的数据。还是看下代码:

if(lent+1024>ff->len){
  // EnterCriticalSection(ff->cs);
   count=s.recv((char*)r,ff->len-lent);
   while(count<ff->len-lent){
    
    lent+=count;
    
    EnterCriticalSection(ff->cs);
    if(count>0){
     *(ff->totle)=*(ff->totle)+count;
     
     fwrite(r,1,count,f);
    }
    LeaveCriticalSection(ff->cs);
    count=s.recv((char*)r,ff->len-lent);
   }
  // LeaveCriticalSection(ff->cs);
   lent+=count;
   
   EnterCriticalSection(ff->cs);
   if(count>0){
   *(ff->totle)=*(ff->totle)+count;
  
   fwrite(r,1,count,f);
   }
LeaveCriticalSection(ff->cs);
   goto ex;
  }else{
  // EnterCriticalSection(ff->cs);
   count=s.recv((char*)r,1024);
  // LeaveCriticalSection(ff->cs);
   lent+=count;
   EnterCriticalSection(ff->cs);
   if(count>0){
   *(ff->totle)=*(ff->totle)+count;
            fwrite(r,1,count,f);
   }
  LeaveCriticalSection(ff->cs);
  // fflush(f);
  }

     

     注释的部分可以不看,lent是记录当前一共读取的字节数,因为我们初始化规定每次接受1024个字节,所以当剩余需要读取的字节数小于1024时,我们就读取剩余的字节数,避免多读或少读。ff-》totle是一个int指针,统计当前所有线程一共读取的字节数的,所以它在一个临界区内。

     还有一点要注意的是我们虽然在写本地文件的时候把文件指针定位到相应的位置,但是我们是不可以同时写入的,否则很容易出现错误,所以我还是用临界区包装了下,其实更好的办法是专门开辟一个线程来进行文件的写入。这样既保证了高效率,也不会出现错误了。

      接下来便是收尾工作了,主线程WaitForMultipleObjects下,再对申请的数据进行下释放,便大功告成了。

      那么,如何进行续传呢,这就需要一个配置文件了,我们还要定义一个结构体,把这个结构体写入,因为我们每个线程的数据都不一样,所以写入配置文件的数据也不一样:

CString temp=ff->localfile;
   temp=temp+".sx";
   FILE *f1;
   f1=fopen(temp,"ab");
   EnterCriticalSection(ff->cs);
   off=ff->start+lent;
   ff->len=(ff->len+ff->start)-off;
   ff->start=off;
   ff->totle=(int*)ff->len;
   fwrite(ff,sizeof(file),1,f1);
   LeaveCriticalSection(ff->cs);
/*
      CString str;
      str.Format("%d    %d",ff->start,ff->len);
      ::OutputDebugString(str);*/
   
   fclose(f1);

       取得当前下载的位置和还需下载的字节数,然后写入配置文件。当我们需要继续下载的时候读取这个配置文件即可:

int lent=0;
  FILE *f1;
  f1=fopen(temp,"rb");
  for(int i=0;i<threads;i++){
   ff=new file;
   
   fread(ff,sizeof(file),1,f1);
   if(feof(f1)){
    delete ff;
    break;
   }
   ff->cs=&cs;
   ff->run=&(init1->run);
   lent+=(int)ff->totle;
   ff->totle=&totle;
   
   dh[i]=CreateThread(0,0,t,(LPVOID)ff,0,0);
/*
      CString str;
      str.Format("%d    %d",ff->start,ff->len);
      ::OutputDebugString(str);*/
   
  }
  fclose(f1);
  totle=len1-lent;

       循环读取配置文件,然后把数据分配给每个线程,再启动线程即可。

       当然,实际的下载没这么简单,要是下载文件的服务器不支持range字段呢,或者有些http header的返回里就没有content-length字段,再或者下载文件被重定向到另外一个URL了呢,这些还需要大家一起研究。

      完整的代码我会在最近传到csdn的下载区,因为我用mfc写了个GUI方便使用,所以代码比较多,大家可以看看,有什么疑问可以一起交流下。

        本文有什么不足之处,还望大家多多指教。

原创粉丝点击