点对点P2P多线程断点续传的实现.docx
《点对点P2P多线程断点续传的实现.docx》由会员分享,可在线阅读,更多相关《点对点P2P多线程断点续传的实现.docx(19页珍藏版)》请在冰豆网上搜索。
点对点P2P多线程断点续传的实现
点对点(P2P)多线程断点续传的实现
在如今的网络应用中,文件的传送是重要的功能之一,也是共享的基础。
一些重要的协议像HTTP,FTP等都支持文件的传送。
尤其是FTP,它的全称就是“文件传送协议”,当初的工程师设计这一协议就是为了解决网络间的文件传送问题,而且以其稳定,高速,简单而一直保持着很大的生命力。
作为一个程序员,使用这些现有的协议传送文件相当简单,不过,它们只适用于服务器模式中。
这样,当我们想在点与点之间传送文件就不适用了或相当麻烦,有一种大刀小用的意味。
笔者一直想寻求一种简单有效,且具备多线程断点续传的方法来实现点与点之间的文件传送问题,经过大量的翻阅资料与测试,终于实现了,现把它共享出来,与大家分享。
我写了一个以此为基础的实用程序(网络传圣,包含源代码),可用了基于TCP/IP的电脑上,供大家学习。
upload/2004_06/04062118541204.gif
(本文源代码运行效果图)
实现方法(VC++,基于TCP/IP协议)如下:
仍釆用服务器与客户模式,需分别对其设计与编程。
服务器端较简单,主要就是加入待传文件,监听客户,和传送文件。
而那些断点续传的功能,以及文件的管理都放在客户端上。
一、服务器端
首先介绍服务器端:
最开始我们要定义一个简单的协议,也就是定义一个服务器端与客户端听得懂的语言。
而为了把问题简化,我就让服务器只要听懂两句话,一就是客户说“我要读文件信息”,二就是“我准备好了,可以传文件了”。
由于要实现多线程,必须把功能独立出来,且包装成线程,首先建一个监听线程,主要负责接入客户,并启动另一个客户线程。
我用VC++实现如下:
DWORDWINAPIlistenthread(LPVOIDlpparam)
{
//由主函数传来的套接字
SOCKETpthis=(SOCKET)lpparam;
//开始监听
intrc=listen(pthis,30);
//如果错就显示信息
if(rc<0){
CStringaaa;
aaa="listen错误\n";
AfxGetMainWnd()->SendMessageToDescendants(WM_AGE1,(LPARAM)aaa.GetBuffer(0),1);
aaa.ReleaseBuffer();
return0;
}
//进入循环,并接收到来的套接字
while
(1){
//新建一个套接字,用于客户端
SOCKETs1;
s1=accept(pthis,NULL,NULL);
//给主函数发有人联入消息
CStringaa;
aa="一人联入!
\n";
AfxGetMainWnd()->SendMessageToDescendants(WM_AGE1,(LPARAM)aa.GetBuff
er(0),1);
aa.ReleaseBuffer();
DWORDdwthread;
//建立用户线程
:
:
CreateThread(NULL,0,clientthread,(LPVOID)s1,0,&dwthread);
}
return0;
}
接着我们来看用户线程:
先看文件消息类定义:
structfileinfo
{
intfileno;//文件号
inttype;//客户端想说什么(前面那两句话,用1,2表示)
longlen;//文件长度
intseek;//文件开始位置,用于多线程
charname[100];//文件名
};
用户线程函数:
DWORDWINAPIclientthread(LPVOIDlpparam)
{
//文件消息
fileinfo*fiinfo;
//接收缓存
char*m_buf;
m_buf=newchar[100];
//监听函数传来的用户套接字
SOCKETpthis=(SOCKET)lpparam;
//读传来的信息
intaa=readn(pthis,m_buf,100);
//如果有错就返回
if(aa<0){
closesocket(pthis);
return-1;
}
//把传来的信息转为定义的文件信息
fiinfo=(fileinfo*)m_buf;
CStringaaa;
//检验客户想说什么
switch(fiinfo->type)
{
//我要读文件信息
case0:
//读文件
aa=sendn(pthis,(char*)zmfile,1080);
//有错
if(aa<0){
closesocket(pthis);
return-1;
}
//发消息给主函数
aaa="收到LIST命令\n";
AfxGetMainWnd()->SendMessageToDescendants(WM_AGE1,(LPARAM)aaa.GetBu
ffer(0),1);
break;
//我准备好了,可以传文件了
case2:
//发文件消息给主函数
aaa.Format("%s文件被请求!
%s\n",zmfile[fiinfo->fileno].name,nameph[fii
nfo->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.GetBu
ffer(0),1);
break;
}
return0;
}
读文件函数
voidreadfile(SOCKETso,intseek,intlen,intfino)
{
//文件名
CStringmyname;
myname.Format("%s",nameph[fino]);
CFilemyFile;
//打开文件
myFile.Open(myname,CFile:
:
modeRead|CFile:
:
typeBinary|CFile:
:
shareDen
yNone);
//传到指定位置
myFile.Seek(seek,CFile:
:
begin);
charm_buf[SIZE];
intlen2;
intlen1;
len1=len;
//开始接收,直到发完整个文件
while(len1>0){
len2=len>SIZE?
SIZE:
len;
myFile.Read(m_buf,len2);
intaa=sendn(so,m_buf,len2);
if(aa<0){
closesocket(so);
break;
}
len1=len1-aa;
len=len-aa;
}
myFile.Close();
}
服务器端最要的功能各技术就是这些,下面介绍客户端。
二、客户端
客户端最重要,也最复杂,它负责线程的管理,进度的记录等工作。
大概流程如下:
先连接服务器,接着发送命令1(给我文件信息),其中包括文件长度,名字等,然后根据长度决定分几个线程下载,并初使化下载进程,接着发送命令2(可以给我传文件了),并记录文件进程。
最后,收尾。
这其中有一个十分重要的类,就是cdownload类,定义如下:
classcdownload
{
public:
voidcreatethread();//开线程
DWORDfinish1();//完成线程
intsendlist();//发命令1
downinfodoinfo;//文件信息(与服务器定义一样)
intstartask(intn);开始传文件n
longm_index;
BOOLgood[BLACK];
intfilerange[100];
CStringfname;
CStringfnametwo;
UINTthreadfunc(longindex);//下载进程
intsendrequest(intn);//发文件信息
cdownload(intthno1);
virtual~cdownload();
};
下面先介绍sendrequest(intn),在开始前,向服务器发获得文件消息命令,以便让客户端知道有哪些文件可传
intcdownload:
:
sendrequest(intn)
{
//建套接字
sockaddr_inlocal;
SOCKETm_socket;
intrc=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);
intret;
//联接服务器
ret=connect(m_socket,(LPSOCKADDR)&local,sizeof(local));
//有错的话
if(ret<0){
AfxMessageBox("联接错误");
closesocket(m_socket);
return-1;
}
//初使化命令
fileinfofileinfo1;
fileinfo1.len=n;
fileinfo1.seek=50;
fileinfo1.type=1;
//发送命令
intaa=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);
return1;
}
有了文件消息后我们就可以下载文件了。
在主函数中,用法如下:
//下载第clno个文件,并为它建一个新cdownload类
down[clno]=newcdownload(clno);
//开始下载,并初使化
type=down[clno]->startask(clno);
//建立各线程
createthread(clno);
下面介绍开始方法:
//开始方法
intcdownload:
:
startask(intn)
{
//读入文件长度
doinfo.filelen=zmfile[n].length;
//读入名字
fname=zmfile[n].name;
CStringtmep;
//初使化文件名
tmep.Format("\\temp\\%s",fname);
//给主函数发消息
CStringaaa;
aaa="正在读取"+fname+"信息,马上开始下载。
。
。
\n";
AfxGetMainWnd()->SendMessageToDescendants(WM_AGE1,(LPARAM)aaa.GetBuffer
(0),1);
aaa.ReleaseBuffer();
//如果文件长度小于0就返回
if(doinfo.filelen<=0)return-1;
//建一个以.down结尾的文件记录文件信息
CStringm_temp;
m_temp=fname+".down";
doinfo.name=m_temp;
FILE*fp=NULL;
CFilemyfile;
//如果是第一次下载文件,初使化各记录文件
if((fp=fopen(m_temp,"r"))==NULL){
filerange[0]=0;
//文件分块
for(inti=0;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:
:
typeBina
ry);
//写入文件长度
myfile.Write(&doinfo.filelen,sizeof(int));
myfile.Close();
CStringtemp;
for(intii=0;ii//初使化各进程记录文件信息(以.downN结尾)
temp.Format(".down%d",ii);
m_temp=fname+temp;
myfile.Open(m_temp,CFile:
:
modeCreate|CFile:
:
modeWrite|CFile:
:
typeBina
ry);
//写入各进程文件信息
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{
//如果文件已存在,说明是续传,读上次信息
CStringtemp;
m_temp=fname+".down0";
if((fp=fopen(m_temp,"r"))==NULL)
return1;
elsefclose(fp);
intbb;
bb=0;
//读各进程记录的信息
for(intii=0;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];
CStringtemp;
}
if(bb==0)return1;
doinfo.totle=doinfo.filelen-bb;
((CMainFrame*):
:
AfxGetMainWnd())->m_work.m_ListCtrl->AddItemtwo(n,2,doi
nfo.totle,1,0,doinfo.threadno);
}
//建立下载结束进程timethread,以管现各进程结束时间。
DWORDdwthread;
:
:
CreateThread(NULL,0,timethread,(LPVOID)this,0,&dwthread);
return0;
}
下面介绍建立各进程函数,很简单:
voidCMainFrame:
:
createthread(intthreadno)
{
DWORDdwthread;
//建立BLACK个进程
for(inti=0;i{
m_thread[threadno][i]=:
:
CreateThread(NULL,0,downthread,(LPVOID)down[t
hreadno],0,&dwthread);
}
}
downthread进程函数
DWORDWINAPIdownthread(LPVOIDlpparam)
{
cdownload*pthis=(cdownload*)lpparam;
//进程引索+1
InterlockedIncrement(&pthis->m_index);
//执行下载进程
pthis->threadfunc(pthis->m_index-1);
return1;
}
下面介绍下载进程函数,最最核心的东西了
UINTcdownload:
:
threadfunc(longindex)
{
//初使化联接
sockaddr_inlocal;
SOCKETm_socket;
intrc=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);
intret;
//读入缓存
char*m_buf=newchar[SIZE];
intre,len2;
fileinfofileinfo1;
//联接
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;
//打开文件
CFiledestFile;
FILE*fp=NULL;
//是第一次传的话
if((fp=fopen(fname,"r"))==NULL)
destFile.Open(fname,CFile:
:
modeCreate|CFile:
:
modeWrite|CFile:
:
typeB
inary|CFile:
:
shareDenyNone);
else
//如果文件存在,是续传
destFile.Open(fname,CFile:
:
modeWrite|CFile:
:
typeBinary|CFile:
:
shareD
enyNone);
//文件指针移到指定位置
destFile.Seek(filerange[index*2],CFile:
:
begin);
//发消息给服务器,可以传文件了
sendn(m_socket,(char*)&fileinfo1,100);
CFilemyfile;
CStringtemp;
temp.Format(".down%d",index);
m_temp=fname+temp;
//当各段长度还不为0时
while(re>0){
len2=re>SIZE?
SIZE:
re;
//读各段内容
intlen1=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;
return1;
}
到这客户端的主要模块和机制已基本介绍完。
希望好好体会一下这种多线程断点续传的方法。