如何点对点实现多线程断点续传

来源:互联网 发布:手机淘宝不能删除评价 编辑:程序博客网 时间:2024/04/29 16:55
在如今的网络应用中,文件的传送是重要的功能之一,也是共享的基础。一些重要的协议像HTTP,FTP等都支持文件的传送。尤其是FTP,它的全称就是“文件传送协议”,当初的工程师设计这一协议就是为了解决网络间的文件传送问题,而且以其稳定,高速,简单而一直保持着很大的生命力。作为一个程序员,使用这些现有的协议传送文件相当简单,不过,它们只适用于服务器模式中。这样,当我们想在点与点之间传送文件就不适用了或相当麻烦,有一种大刀小用的意味。笔者一直想寻求一种简单有效,且具备多线程断点续传的方法来实现点与点之间的文件传送问题,经过大量的翻阅资料与测试,终于实现了,现把它共享出来,与大家分享。
  (我写了一个以此为基础的实用程序(网络传圣,包含源代码),可用了基于IP/TCP的电脑上,供大家学习。下载地址:
  http://h2osky.126.com)
  实现方法(VC++,基于TCP/IP协议)如下:
  仍釆用服务器与客户模式,需分别对其设计与编程。
  服务器端较简单,主要就是加入侍传文件,监听客户,和传送文件。而那些断点续传的功能,以及文件的管理都放在客户端上。
  首先介络服务器端:
  最开始我们要定义一个简单的协议,也就是定义一个服务器端与客户端听得懂的语言。而为了把问题简化,我就让服务器只要听懂两句话,一就是客户说“我要读文件信息”,二就是“我准备好了,可以传文件了”。
  由于要实现多线程,必须把功能独立出来,且包装成线程,首先建一个监听线程,主要负责接入客户,并启动另一个客户线程。我用VC++实现如下:
  
  DWORD WINAPI listenthread(LPVOID lpparam)
  {
  //由主函数传来的套接字
  SOCKET pthis=(SOCKET)lpparam;
  //开始监听
   int rc=listen(pthis,30);
  //如果错就显示信息
  if(rc<0){
   CString aaa;
   aaa="listen错误/n";
   AfxGetMainWnd()->SendMessageToDescendants(WM_AGE1,(LPARAM)aaa.GetBuffer(0),1);
   aaa.ReleaseBuffer();
   return 0;
   }
  //进入循环,并接收到来的套接字
   while(1){
  //新建一个套接字,用于客户端http://h2osky.126.com
   SOCKET s1;
   s1=accept(pthis,NULL,NULL);
  
  //给主函数发有人联入消息
  CString aa;
  aa="一人联入!/n";
   AfxGetMainWnd()->SendMessageToDescendants(WM_AGE1,(LPARAM)aa.GetBuffer(0),1);
   aa.ReleaseBuffer();
   DWORD dwthread;
  //建立用户线程
   ::CreateThread(NULL,0,clientthread,(LPVOID)s1,0,&dwthread);
   }
   return 0;
  }
  接着我们来看用户线程:
  先看文件消息类定义:
  struct fileinfo
  {
  int fileno;//文件号
   int type;//客户端想说什么(前面那两句话,用1,2表示)
   long len;//文件长度
   int seek;//文件开始位置,用于多线程
  char name[100];//文件名http://h2osky.126.com
  };
  //用户线程函数:
  DWORD WINAPI clientthread(LPVOID lpparam)
  {
  //文件消息
   fileinfo* fiinfo;
  //接收缓存
  char* m_buf;
  m_buf=new char[100];
  //监听函数传来的用户套接字
   SOCKET pthis=(SOCKET)lpparam;
  //读传来的信息
   int aa=readn(pthis,m_buf,100);
  //如果有错就返回
   if(aa<0){
   closesocket (pthis);
   return -1;
   }
  //把传来的信息转为定义的文件信息
  fiinfo=(fileinfo*)m_buf;
  
   CString aaa;
  //检验客户想说什么
  switch(fiinfo->type)
  {
  //我要读文件信息
  case 0:
  //读文件
  aa=sendn(pthis,(char*)zmfile,1080);
  //有错
  if(aa<0){
   closesocket (pthis);
   return -1;
  }
  //发消息给主函数
  aaa="收到LIST命令/n";
   AfxGetMainWnd()->SendMessageToDescendants(WM_AGE1,(LPARAM)aaa.GetBuffer(0),1);
   break;
  //我准备好了,可以传文件了
  case 2:
  //发文件消息给主函数
   aaa.Format("%s 文件被请求!%s/n",zmfile[fiinfo->fileno].name,nameph[fiinfo->fileno]);
   AfxGetMainWnd()->SendMessageToDescendants(WM_AGE1,(LPARAM)aaa.GetBuffer(0),1);
  //读文件,并传送
  readfile(pthis,fiinfo->seek,fiinfo->len,fiinfo->fileno);
  //听不懂你说什么
  default:
   aaa="接收协议错误!/n";
   AfxGetMainWnd()->SendMessageToDescendants(WM_AGE1,(LPARAM)aaa.GetBuffer(0),1);
   break;
  }
   return 0;
  }
  /////////////http://h2osky.126.com
  //读文件函数
  void readfile(SOCKET so,int seek,int len,int fino)
  
   {
  
  //文件名
   CString myname;
    
   myname.Format("%s",nameph[fino]);
   CFile myFile;
  
  //打开文件
   myFile.Open(myname, CFile::modeRead | CFile::typeBinary|CFile::shareDenyNone);
   //传到指定位置 
   myFile.Seek(seek,CFile::begin);
   char m_buf[SIZE];
   int len2;
   int len1;
  len1=len;
  //开始接收,直到发完整个文件
   while(len1>0){
  len2=len>SIZE?SIZE:len;
  myFile.Read(m_buf, len2);
  int aa=sendn(so,m_buf,len2);
  if(aa<0){
   closesocket (so);
   break;
  }
  len1=len1-aa;
  len=len-aa;
  
   }
   myFile.Close();
   }
  服务器端最要的功能各技术就是这些,下面介绍客户端:
  客户端最重要,也最复杂,它负责线程的管理,进度的记录等工作。
  大概流程如下:
  先连接服务器,接着发送命令1(给我文件信息),其中包括文件长度,名字等,然后根据长度决定分几个线程下载,并初使化下载进程,接着发送命令2(可以给我传文件了),并记录文件进程。最后,收尾。
  这其中有一个十分重要的类,就是cdownload类,定义如下:
  class cdownload
  {
  public:
  
   void createthread();//开线程http://h2osky.126.com
   DWORD finish1();//完成线程
  int sendlist();//发命令1
   downinfo doinfo;//文件信息(与服务器定义一样)
  int startask(int n);开始传文件n
  long m_index;
  BOOL good[BLACK];
   int filerange[100];
   CString fname;
   CString fnametwo;
   UINT threadfunc(long index);//下载进程
   int sendrequest(int n);//发文件信息
   cdownload(int thno1);
   virtual ~cdownload();
  };
  
  
  
  下面先介绍sendrequest(int n),在开始前,
  向服务器发获得文件消息命令,以便让客户端知道有哪些文件可传
  int cdownload::sendrequest(int n)
  {
  //建套接字
   sockaddr_in local;
   SOCKET m_socket;
   int rc=0;
  //初使化服务器地址
   local.sin_family=AF_INET;
   local.sin_port=htons(1028);
   local.sin_addr.S_un.S_addr=inet_addr(ip);
   m_socket=socket(AF_INET,SOCK_STREAM,0);
  
  int ret;
  //联接服务器
  ret=connect(m_socket,(LPSOCKADDR)&local,sizeof(local));
  //有错的话
  if(ret<0){
   AfxMessageBox("联接错误");
  closesocket(m_socket);
  return -1;
  }
  //初使化命令
  fileinfo fileinfo1;
   fileinfo1.len=n;
   fileinfo1.seek=50;
   fileinfo1.type=1;
  //发送命令
   int aa=sendn(m_socket,(char*)&fileinfo1,100);
   if(aa<0){ closesocket(m_socket);
   return -1;}
  //接收服务器传来的信息
   aa=readn(m_socket,(char*)&fileinfo1,100);
   if(aa<0) {closesocket(m_socket);
   return -1;}
  //关闭
  shutdown(m_socket,2);
  closesocket(m_socket);
  return 1;
  }
  有了文件消息后我们就可以下载文件了
  在主函数中,用法如下:
  //下载第clno个文件,并为它建一个新cdownload类
  down[clno]=new cdownload(clno);
  //开始下载,并初使化
  type=down[clno]->startask(clno);
  //建立各线程
  createthread(clno);
  介绍开始方法
  //开始方法
  int cdownload::startask(int n)
  {
  //读入文件长度
  doinfo.filelen=zmfile[n].length;
  
  //读入名字
  fname=zmfile[n].name;
  CString tmep;
  //初使化文件名
  tmep.Format("//temp//%s",fname);
  
  //给主函数发消息
  CString aaa;
  
   aaa="正在读取 "+fname+" 信息,马上开始下载。。。/n";
   AfxGetMainWnd()->SendMessageToDescendants(WM_AGE1,(LPARAM)aaa.GetBuffer(0),1);
   aaa.ReleaseBuffer();
  //如果文件长度小于0就返回
   if(doinfo.filelen<=0)return -1;
  //建一个以.down结尾的文件记录文件信息
   CString m_temp;
   m_temp=fname+".dwon";
  
   doinfo.name=m_temp;
   FILE* fp=NULL;
   CFile myfile;
  //如果是第一次下载文件,初使化各记录文件
   if((fp=fopen(m_temp,"r"))==NULL){
  
   filerange[0]=0;
  //文件分块
   for(int i=0;i<BLACK;i++)
   {
   if(i>0)
  filerange[i*2]=i*(doinfo.filelen/BLACK+1);
  
  filerange[i*2+1]=doinfo.filelen/BLACK+1;
  
   }
   filerange[BLACK*2-1]=doinfo.filelen-filerange[BLACK*2-2];
   myfile.Open(m_temp,CFile::modeCreate|CFile::modeWrite | CFile::typeBinary);
  
  //写入文件长度
  myfile.Write(&doinfo.filelen,sizeof(int));
  myfile.Close();
  
   CString temp;
   for(int ii=0;ii<BLACK;ii++){
  //初使化各进程记录文件信息(以.downN结尾)
  temp.Format(".down%d",ii);
   m_temp=fname+temp;
   myfile.Open(m_temp,CFile::modeCreate|CFile::modeWrite | CFile::typeBinary);
  //写入各进程文件信息
   myfile.Write(&filerange[ii*2],sizeof(int));
   myfile.Write(&filerange[ii*2+1],sizeof(int));
   myfile.Close();
  
   }
  ((CMainFrame*)::AfxGetMainWnd())->m_work.m_ListCtrl->AddItemtwo(n,2,0,0,0,doinfo.threadno);
  
   }
  //如果文件已存在,说明是续传,读上次信息
   else
   {
  
  //打开文件
  fread(&doinfo.filelen,sizeof(int),1,fp);
  fclose(fp);
   CString temp;
  
   m_temp=fname+".down0";
   if((fp=fopen(m_temp,"r"))==NULL)
  
   return 1;
   else fclose(fp);
  int bb;
  bb=0;
  //读各进程记录的信息
  for(int ii=0;ii<BLACK;ii++)
  {
   temp.Format(".down%d",ii);
   m_temp=fname+temp;
  
   myfile.Open(m_temp,CFile::modeRead | CFile::typeBinary);
   myfile.Read(&filerange[ii*2],sizeof(int));
   myfile.Read(&filerange[ii*2+1],sizeof(int));
   myfile.Close();
  
   bb= bb+filerange[ii*2+1];
   CString temp;
  
  }
  if(bb==0)return 1;
  doinfo.totle=doinfo.filelen-bb;
  
  
  
  
  
  ((CMainFrame*)::AfxGetMainWnd())->m_work.m_ListCtrl->AddItemtwo(n,2,doinfo.totle,1,0,doinfo.threadno);
   }
  //建立下载结束进程timethread,以管现各进程结束时间。
   DWORD dwthread;
   ::CreateThread(NULL,0,timethread,(LPVOID)this,0,&dwthread);
   return 0;
  }
  //下面介绍建立各进程函数,很简单:http://h2osky.126.com
  void CMainFrame::createthread(int threadno)
  {
   DWORD dwthread;
  //建立BLACK个进程
   for(int i=0;i<BLACK;i++)
   {
   m_thread[threadno][i]= ::CreateThread(NULL,0,downthread,(LPVOID)down[threadno],0,&dwthread);
  
    
  
   }
  }
  //downthread进程函数
  DWORD WINAPI downthread(LPVOID lpparam)
  {
   cdownload* pthis=(cdownload*)lpparam;
  //进程引索+1
   InterlockedIncrement(&pthis->m_index);
  //执行下载进程
   pthis->threadfunc(pthis->m_index-1);
   return 1;
  }
  下面介绍下载进程函数,最最核心的东西了
  UINT cdownload::threadfunc(long index)
  {
  //初使化联接
   sockaddr_in local;
   SOCKET m_socket;
   int rc=0;
  
   local.sin_family=AF_INET;
   local.sin_port=htons(1028);
   local.sin_addr.S_un.S_addr=inet_addr(ip);
   m_socket=socket(AF_INET,SOCK_STREAM,0);
  
  int ret;
  //读入缓存
   char* m_buf=new char[SIZE];
   int re,len2;
   fileinfo fileinfo1;
  //联接
  ret=connect(m_socket,(LPSOCKADDR)&local,sizeof(local));
  //读入各进程的下载信息
  fileinfo1.len=filerange[index*2+1];
   fileinfo1.seek=filerange[index*2];
   fileinfo1.type=2;
   fileinfo1.fileno=doinfo.threadno;
  
   re=fileinfo1.len;
  
  //打开文件 
  CFile destFile;
  FILE* fp=NULL;
  //是第一次传的话
  if((fp=fopen(fname,"r"))==NULL)
  destFile.Open(fname, CFile::modeCreate|CFile::modeWrite | CFile::typeBinary|CFile::shareDenyNone);
  else
  //如果文件存在,是续传
  destFile.Open(fname,CFile::modeWrite | CFile::typeBinary|CFile::shareDenyNone);
  //文件指针移到指定位置
  destFile.Seek(filerange[index*2],CFile::begin);
  //发消息给服务器,可以传文件了
  sendn(m_socket,(char*)&fileinfo1,100);
  CFile myfile;
  //初使化各进程进度信息http://h2osky.126.com
   CString temp;
  temp.Format(".down%d",index);
   m_temp=fname+temp;
  //当各段长度还不为0时
   while(re>0){
  
  
  len2=re>SIZE?SIZE:re;
  
   //读各段内容
   int len1=readn(m_socket,m_buf,len2);
  //有错的话
   if(len1<0){
   closesocket(m_socket);
   break;
   }
  
  //写入文件
  destFile.Write(m_buf, len1);
  //更改记录进度信息
  filerange[index*2+1]-=len1;
  filerange[index*2]+=len1;
  //移动记录文件指针到头
  myfile.Seek(0,CFile::begin);
  //写入记录进度
   myfile.Write(&filerange[index*2],sizeof(int));
   myfile.Write(&filerange[index*2+1],sizeof(int));
  //减去这次读的长度
  re=re-len1;
  //加文件长度
   doinfo.totle=doinfo.totle+len1;
  
  
  
  
  };
  //这块下载完成,收尾
  
  myfile.Close();
  destFile.Close();
  delete [] m_buf;
  shutdown(m_socket,2);
  
  
  if(re<=0)good[index]=TRUE;
  return 1;
  }
  到这客户端的主要模块和机制已基本介绍完。希望好好体会一下这种多线程断点续传的方法。
  我写了一个以此为基础的实用程序(网络传圣,包含源代码),可用了基于IP/TCP的电脑上,供大家学习。下载地址:
  http://h2osky.126.com
  赵明
  email: papaya_zm@sina.com;zmpapaya@hotmail.com
  web: http://h2osky.126.com
原创粉丝点击