點對點(P2P)多線程斷點續傳的實現

http://epan.cnblogs.com/epan/articles/98274.html

 

 

作者:趙明  日期:2004-6-21
出處:P2P中國(PPcn.net)

P2P中國下載源代碼:
upload/2004_06/04062118573143.zip

在如今的網絡應用中,文件的傳送是重要的功能之一,也是共享的基礎。一些重要的協議像HTTP,FTP等都支持文件的傳送。尤其是FTP,它的全稱就是“文件傳送協議”,當 初的工程師設計這一協議就是爲了解決網絡間的文件傳送問題,而且以其穩定,高速,簡單而一直保持着很大的生命力。作爲一個程序員,使用這些現有的協議傳送文件相當簡單,不過,它們只適用於服務器模式中。這樣,當我們想在點與點之間傳送文件就不適用了或相當麻煩,有一種大刀小用的意味。筆者一直想尋求一種簡單有效,且具備多線程斷點續傳的方法來實現點與點之間的文件傳送問題,經過大量的翻閱資料與測試,終於實現了,現把它共享出來,與大家分享。
我寫了一個以此爲基礎的實用程序(網絡傳聖,包含源代碼),可用了基於TCP/IP的電腦上,供大家學習。
upload/2004_06/04062118541204.gif

(本文源代碼運行效果圖)


實現方法(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){
    
//新建一個套接字,用於客戶端
 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];//文件名
}
;

用戶線程函數:

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;
}

 

讀文件函數

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();//開線程
 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<=0return -1;
 
//建一個以.down結尾的文件記錄文件信息
 CString m_temp;
 m_temp
=fname+".down";
 
 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{
 
//如果文件已存在,說明是續傳,讀上次信息
 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==0return 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;
}

下面介紹建立各進程函數,很簡單:

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;
 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;
}

到這客戶端的主要模塊和機制已基本介紹完。希望好好體會一下這種多線程斷點續傳的方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章