c++实现文件传输解析
实现:枚举磁盘,枚举目录,获取文件信息
上传文件,下载文件,执行文件,创建目录,删除目录等
传输控制结构
要实现客户端与服务端的通信,设计一个合理的传输控制结构,会使后面的工作轻松很多,为了使代码易读
首先对要使用的命令进行预定义其各个命令定义如下
#define GetDriver 0x01 //磁盘信息
#define GetDirInfo 0x02 //目录信息
#define ExecFile 0x03 //执行文件
#define GetFile 0x04 //下载文件
#define PutFile 0x05 //上传文件
#define DelFile 0x06 //删除文件
#define DelDir 0x07 //删除目录
#define CreateDir 0x08 //创建目录
#define FileInfo 0x09 //文件信息
#define GetScreen 0x10 //查看屏幕
在程序的网络通信中主要有 操作命令 ,命令对像,和具体数据三部分,对于命令的传输定义如下结构
typedef struct
{
int ID; //操作命令
BYTE lparam[BUF_LEN*2]; //命令对像
}COMMAND;
因为在程序中打交道最多的就是文件,对文件的详细属性定义如下结构
typedef struct
{
char FileName[MAX_PATH]; //文件名称
int FileLen; //文件长度
char Time[50]; //时间信息
BOOL IsDir; //为目录否
BOOL Error; //错误信息
HICON hIcon; //图标句柄
}FILEINFO;
服务端结构
服务端还是比较简单的其整体思路如下
1.服务端循环接受连接,并把连接交给线程处理
2.线程接受"命令数据",并跟据命令ID将命令对像和SOCKET句柄传给处理函数
3.函数执行指定功能,并返回执行结果
对整体结构的描述,我们用伪代码表述如下
main()
{ /*初示化设置......*/
while(true)
{
if(client=accept(server,(sockaddr *)&clientaddr,&len))//循环接受连接
{
CreateThread(NULL,NULL,SLisen,(LPVOID)client,NULL,NULL);//传递线程处理
}
}
/*清理释放资源......*/
WSACleanup();
}
服务端程序运行后循环接受连接,如果有新的连接就传递给新的线程处理,线程代码如下
DWORD WINAPI SLisen(LPVOID lparam)
{
SOCKET client=(SOCKET)lparam;
COMMAND command;
while(1)
{
if(recv(client,(char*)&command,sizeof(command),0)==SOCKET_ERROR)//接受命令数据
{
cout<<"The Clinet Socket is Closed\n";
break;
}else
{
switch(command.ID)//判断命令ID
{
case GetDriver://将命令对像和SOCKET句柄传递给处理函数
GetDriverProc (command,client);
break;
case DelFile:
DelFileProc (command,client);
break;
/*其它命令......*/
}
}
}
}
线程式的功能是接受客户端的"命令数据",并跟跟据命令ID 将命令对像传递给处理函数,由函数完成指定的功能
以删除文件命令为例其函数格式如下
DWORD DelFileProc (COMMAND command,SOCKET client)
{
if(DeleteFile((char*)command.lparam)==0)//command.lparam为命令对像,这里为要删除的文件路径
{
send(client,"删除失败...");
}
else
{
send(client,"删除成功...");
}
}
很容易看出,处理函数接受"命令对像"和客户端SOCKET句柄,执行后会把结果传递回去....
客户端结构
客户端结构的实现思路如下
1.跟服务端建立连接
2.发送用户命令
3.启动一个线程,用于接受服务端的返回信息
对整体结构的描述,我们用伪代码表述如下
void CMyDlg::OnConnect()
{
if(connect(server,(SOCKADDR*)&serveraddr,sizeof(serveraddr))<0)//连接....
{
return ;
}
CreateThread(NULL,NULL,CLisen,this,NULL,NULL);//创建线程用于接受SERVER返回信息
}
对于用户发送的命令我们仍以删除文件为例说明其代码如下
void CMyDlg::OnMenuDelFile()
{
HTREEITEM CurrentNode = m_tree.GetSelectedItem(); //取得选择的节点
CString FullPath =GetFullPath(CurrentNode); //取得节点全目录
COMMAND command;
command.ID=DelFile; //设置命令为删除文件 //删除文件
command.lparam=FullPath.LockBuffer()); //将路径加入命令对像
send(server,command);
}
用于接受SERVER返回信息的线程,和服务端接受命令线程相似,这里就不再说明了,有兴趣可以看下源代码
到这里程序的流程框架就介绍完了,下面我们再看一下程序的界面设置.
界面实现
程序的主界面如上图所示,主程序是一个对话框,主要包括一个树控件m_tree和列表控件m_list分别
用于显示磁盘目录和文件,在对话框初示化时用以下代码设置树控件的属性
DWORD dwStyle = GetWindowLong(m_tree.m_hWnd,GWL_STYLE);
dwStyle |=TVS_HASBUTTONS | TVS_HASLINES | TVS_LINESATROOT;
SetWindowLong(m_tree.m_hWnd,GWL_STYLE,dwStyle);
对于列表框控件则没有太多要求,要留意的是,如果显示图标应该把Styles显示属性设置为ICON
VC的做出的界面,常常让人有种摔键盘的冲动。其实稍微留意一下其设置,也可以让它漂亮一些
比如上图所示的界面就是经过简单设置得到的,而没有用其它类库,有点兴趣?其设置方法为:
1.在对话框属性中设置Styles 的Border属性为Thin
2.选重More Styles "可见" 属性
3.选重Extended Styles的"静态边"属性
各个功能的实现:
1.获取磁盘信息
2.获取目录信息
3.获取文件信息
4.运行指定文件
5.删除指定文件
6.删除指定目录
7.创建指定目录
8.上传下载文件
9.获取远程文件图标
获取磁盘信息
磁盘信息可以用API GetDriveType来实现,它以路径名作为参数(如C:\)返回磁盘类型,其实例代码如下
DWORD GetDriverProc(COMMAND command,SOCKET client)
{
for(char i='A';i<='Z';i++)
{
char x[20]={i,':'};
UINT Type=GetDriveType(x);
if(Type==DRIVE_FIXED||Type==DRIVE_REMOVABLE||Type==DRIVE_CDROM)
{
/*返回处理结果...*/
}
}
return 0;
}
GetDriveType可能返回的结果如下
#define DRIVE_UNKNOWN 0 // 无效路径名
#define DRIVE_NO_ROOT_DIR 1 // 无效路经,如无法找到的卷标
#define DRIVE_REMOVABLE 2 // 可移动驱动器
#define DRIVE_FIXED 3 // 固定的驱动器
#define DRIVE_REMOTE 4 // 网络驱动器
#define DRIVE_CDROM 5 // CD-ROM
#define DRIVE_RAMDISK 6 // 随机存取(RAM)磁盘
在上面的实例代码中我们只取,硬盘,光驱和移动磁盘
获取目录信息
这里只要枚举用户指定的目录就可以了,其实例代码如下:
DWORD GetDirInfoProc(COMMAND command,SOCKET client)
{
/*command为要枚举的路径如(C:\)client为返回结果的SOCKET句柄*/
FILEINFO fi;
memset((char*)&fi,0,sizeof(fi));
strcat((char*)command.lparam,"*.*");//枚举所有文件
CFileFind file;
BOOL bContinue = file.FindFile((char*)command.lparam);
while(bContinue)
{
memset((char*)&fi,0,sizeof(fi));
bContinue = file.FindNextFile();
if(file.IsDirectory()) //为目录
{
fi.IsDir=true;
}
strcpy(fi.FileName,file.GetFileName().LockBuffer()); //保存文件名称
if(send(client,(char*)&fi,sizeof(cmd),0)==SOCKET_ERROR)
{
cout << "Send Dir is Error\n";
}
}
return 0;
}
获取文件信息
以下实例代码用来获取 文件的名称,路径,时间,属性等信息
DWORD FileInfoProc (COMMAND command,SOCKET client)
{
/*command为要查看的文件如(C:\TEST.EXE)client为返回结果的SOCKET句柄*/
FILEINFO fi;
HANDLE hFile;
WIN32_FIND_DATA WFD;
memset((char*)&WFD,0,sizeof(WFD));
if((hFile=FindFirstFile((char*)command.lparam,&WFD))==INVALID_HANDLE_VALUE) //查看文件属性
{
fi.Error=true;
return 0;
}
//得到文件的相关信息
SHGetFileInfo(WFD.cFileName,
FILE_ATTRIBUTE_NORMAL,
&shfi, sizeof(shfi),
SHGFI_ICON|SHGFI_USEFILEATTRIBUTES|SHGFI_TYPENAME );
strcpy(fi.FileName,(char*)command.lparam); //文件路径
FileLen=(WFD.nFileSizeHigh*MAXDWORD+WFD.nFileSizeLow)/1024; //文件长度
fi.FileLen=FileLen;
//转化格林时间到本地时间
FileTimeToLocalFileTime(&WFD.ftLastWriteTime,&localtime);
FileTimeToSystemTime(&localtime,&systime);
//文件修改时间
sprintf(stime,"%4d-%02d-%02d %02d:%02d:%02d",
systime.wYear,systime.wMonth,systime.wDay,systime.wHour,
systime.wMinute,systime.wSecond);
if(GetFileAttributes((char*)command.lparam)&FILE_ATTRIBUTE_HIDDEN)
{
/*隐藏文件...*/
}else
if(GetFileAttributes((char*)command.lparam)&FILE_ATTRIBUTE_READONLY)
{
/*只读文件...*/
}
send(client,(char*)&fi,sizeof(fi),0);
FindClose(hFile);
return 0;
}
运行指定文件
运行文件 有以下几种方法 1.WinExec 2.ShellExecute 3.CreateProcess
这里使用的是ShellExecute其实例代码如下
DWORD ExecFileProc (COMMAND command,SOCKET client)
{
/*command为要运行的文件路径如(C:\TEST.EXE)client为返回结果的SOCKET句柄*/
COMMAND cmd;
memset((char*)&cmd,0,sizeof(cmd));
cmd.ID=ExecFile;
if(ShellExecute(NULL,"open",(char*)command.lparam,NULL,NULL,SW_HIDE)<(HINSTANCE)32)
{
strcpy((char*)cmd.lparam,"文件执行失败!");
send(client,(char*)&cmd,sizeof(cmd),0);
}
else
{
strcpy((char*)cmd.lparam,"文件执行成功!");
send(client,(char*)&cmd,sizeof(cmd),0);
}
return 0;
}
API函数ShellExecute原形为:
HINSTANCE ShellExecute(
HWND hwnd, //窗口句柄
LPCTSTR lpOperation, //操作类型
LPCTSTR lpFile, //文件指针
LPCTSTR lpParameters, //文件参数
LPCTSTR lpDirectory, //缺省目录
INT nShowCmd //显示方式
);
这是一个相当有意思的函数,在调用此函数时只须指定要执行的文件名,而不必管用什么程序去打开
或执行文件,WINDOWS会自动根据要打开或执行的文件去判断该如何执行文件或用什么程序去打开文件,如果
要求不高的话比CreateProcess要好用的多,如果想做出像NCPH和灰鸽子那样带参数执行的话,其实也不难
只要指定lpParameters为执行参数就可了
删除指定文件
DWORD DelFileProc (COMMAND command,SOCKET client)
{
/*command为要删除的文件路径如(C:\TEST.EXE)client为返回结果的SOCKET句柄*/
COMMAND cmd;
memset((char*)&cmd,0,sizeof(cmd));
cmd.ID=DelFile;
SetFileAttributes((char*)command.lparam,FILE_ATTRIBUTE_NORMAL); //去掉文件的系统和隐藏属性
if(DeleteFile((char*)command.lparam)==0)
{
strcpy((char*)cmd.lparam,"文件删除失败!");
send(client,(char*)&cmd,sizeof(cmd),0);
}
else
{
strcpy((char*)cmd.lparam,"文件删除成功!");
send(client,(char*)&cmd,sizeof(cmd),0);
}
return 0;
}
需要注意的是在 DeleteFile前应该去文件的系统和隐藏属性,否则会删除失败
删除目录
可以用RemoveDirectory函数删除目录,但是RemoveDirectory有个缺点就是只能删除为空的的目录,对于不为空
的目录就无能为力了,想要删除不无空的目录可以使用下面的实例代码
BOOL DeleteDirectory(char *DirName)
{
CFileFind tempFind;
char tempFileFind[200];
sprintf(tempFileFind,"%s*.*",DirName);
BOOL IsFinded=(BOOL)tempFind.FindFile(tempFileFind);
while(IsFinded)
{
IsFinded=(BOOL)tempFind.FindNextFile();
if(!tempFind.IsDots())
{
char foundFileName[200];
strcpy(foundFileName,tempFind.GetFileName().GetBuffer(200));
if(tempFind.IsDirectory())
{
char tempDir[200];
sprintf(tempDir,"%s\\%s",DirName,foundFileName);
DeleteDirectory(tempDir);
}
else
{
char tempFileName[200];
sprintf(tempFileName,"%s\\%s",DirName,foundFileName);
SetFileAttributes(tempFileName,FILE_ATTRIBUTE_NORMAL); //去掉文件的系统和隐藏属性
DeleteFile(tempFileName);
cout <<"now delete "<<tempFileName<<"\n";
}
}
}
tempFind.Close();
if(!RemoveDirectory(DirName))
{
return FALSE;
}
return TRUE;
}
这个函数的代码可以参照上面 枚举目录的代码来看,它的原理就是枚举目录下的所有文件并删除,最后删除
指定目录,成功返回TRUE失败则返回FALSE,这段代码可以直使用,但要小心使用,因为我在传参数时的失误
结果把整个D盘差点清空了..........
创建目录
实例代码如下:
DWORD CreateDirProc (COMMAND command,SOCKET client)
{
/*command为要创建目录的路径如(C:\)client为返回结果的SOCKET句柄*/
COMMAND cmd;
memset((char*)&cmd,0,sizeof(cmd));
cmd.ID=CreateDir;
if(::CreateDirectory((char*)command.lparam,NULL))
{
strcpy((char*)cmd.lparam,"创建目录成功!");
send(client,(char*)&cmd,sizeof(cmd),0);
}
else
{
strcpy((char*)cmd.lparam,"创建目录失败!可能有重名文件或文件夹");
send(client,(char*)&cmd,sizeof(cmd),0);
}
return 0;
}
在创建目录时应该注意几点,首先创始目录的上层目录必须是存在的,比如想创建C:\DIR1\DIR2目录,要求
DIR1是必须存在,用CreateDirectory并不能创建多级目录.再者不可以存在和要创建目录同名的目录和文件
因为在磁盘上目录和文件的存放格式是相同的,惟一不同的是 目录的属性与文件属性不同
(FILE_ATTRIBUTE_DIRECTORY属性),所在即使有同名文件也会创始失败.
上传下载文件
上传下载是是文件管理的重点所在,在这里按文件的大小,分两种情况讨论文件的传输方法
小文件的传输相对比较简单可按以下方法进行
1.首先发送文件长度和名称
2.跟据文件长度建立缓冲区
3.读取整个文件到缓冲区
4.发送缓冲区里的内容
其实现代码如下:
CFile file;
FILEINFO fileinfo;
if(file.Open(path,CFile::modeRead|CFile::typeBinary))
{
fileinfo.FileLen=file.GetLength(); //文件长度
strcpy(fileinfo.FileName,file.GetFileName()); //文件名称
send(client,(char*)&fileinfo,sizeof(fileinfo),0); //发送长度和名称
char *date=new char[fileinfo.FileLen]; //分配和文件长度相同的缓冲区
int nLeft=fileinfo.FileLen;
int idx=0;
file.Read(date,fileinfo.FileLen); //读整个文件到缓冲区
while(nLeft>0)
{
int ret=send(client,&date[idx],nLeft,0); //发送文件
if(ret==SOCKET_ERROR)
{
break;
}
nLeft-=ret;
idx+=ret;
}
file.Close();
delete[] date;
}
跟据上面的实例相信大家可以领悟到文件传输的基本原理和方法,虽然很简单但用它传输小文件还是非常实用的
大文件传输方法
用上面的方法传输小文件还可以,但是大文件呢?比如一个500M的电影.上面的方法就会力不从心了因为
按思路要创建一个跟文件大小相同的缓冲区,显然这是不太现实的,我们就得采用另种方法了,在这里我们使用
分块文件传输,所谓分块是指把大文件分成若干小文件,然后传输,比如设定每块大小为64KB其思路如下
1.取得文件长度和名称
2.跟据长度/64KB计算文件块数
3.分配64KB缓冲区
4.读文件到缓冲区
5.发送缓冲的数据
6.重复4,5两步直到发完所有数据
其实现代码如下:
#define CHUNK_SIZE (64*1024) //分为64K块传输
DWORD GetFileProc (COMMAND command,SOCKET client)
{
/*command为要下载文件的路径如(C:\TEST.EXE)client为发送文件的SOCKET句柄*/
COMMAND cmd;
FILEINFO fi;
memset((char*)&fi,0,sizeof(fi));
memset((char*)&cmd,0,sizeof(cmd));
cmd.ID=GetFile;
CFile file;
int nChunkCount=0; //文件块数
if(file.Open((char*)command.lparam,CFile::modeRead|CFile::typeBinary))//打开文件
{
int FileLen=file.GetLength(); //取文件长度
fi.FileLen=file.GetLength();
strcpy((char*)fi.FileName,file.GetFileName()); //取文件名称
memcpy((char*)&cmd.lparam,(char*)&fi,sizeof(fi));
send(client,(char*)&cmd,sizeof(cmd),0); //发送文件名称和长度
nChunkCount=FileLen/CHUNK_SIZE; //文件块数
if(FileLen%nChunkCount!=0)
nChunkCount++;
char *date=new char[CHUNK_SIZE]; //创建数据缓冲区
for(int i=0;i<nChunkCount;i++) //分为nChunkCount块发送
{
int nLeft;
if(i+1==nChunkCount) //最后一块
nLeft=FileLen-CHUNK_SIZE*(nChunkCount-1);
else
nLeft=CHUNK_SIZE;
int idx=0;
file.Read(date,CHUNK_SIZE); //读取文件
while(nLeft>0)
{
int ret=send(client,&date[idx],nLeft,0);//发送文件
if(ret==SOCKET_ERROR)
{
break;
}
nLeft-=ret;
idx+=ret;
}
}
file.Close();
delete[] date;
}
return 0;
}
这样文件传输部分就完成了,止于客户端的实现于上面代码其本相同,只是由读文件变为写文件,详细请参考源代码
获取远程ICO文件图标
我们在文件列表框中需要显示文件的图标,但远程文件的ICO图标是无法直接得到的
猛若RADMIN 黑洞者也没有到(对于EXE文件只显示可执行程序图示),当然了也不见的决对没有......
我们可以通过如下变通方法得到:就是跟据文件的扩展名,从本地注册表中查找对应的程序图标
不过这也有它的缺点对于EXE文件它只能显示一个可执行文件的图示,而且只能显示注册过的图示比如,如果
本机装有WINRAR那么就可以识别.RAR的文件图示,否则就无法识别...
实现方法
CImageList m_ImageList;
m_ImageList.Create(32,32,ILC_COLOR32,10,30); //创建图示
m_list.SetImageList(&m_ImageList,LVSIL_NORMAL); //与列表控件相关连
SHFILEINFO info;
memset((char*)&info,0,sizeof(info));
SHGetFileInfo(fi->FileName,0,&info,sizeof(&info), SHGFI_ICON|SHGFI_USEFILEATTRIBUTES);//关键所在
int i = m_ImageList.Add(info.hIcon);
m_list.InsertItem(i,fi->FileName,i);
原来我试图在Server端通过上面的代码把info.hIcon句柄保存下来,然后放到Client,在单台电脑上很好使,但
Server在另一台电脑上时就玩完了,因为info.hIcon里保存的句柄是个索引而每台机器上的索引是不相同的所以
直接导致的结果就是:什么也显示不出来.....
断点续传与多线程文件传输的实现.其实这两项功能是下载软件所
必不可少的功能了,现在我们把它加到自己的木马中来感受感受.提到多线程下载,首先向网络蚂蚁的作者
洪以容前辈致敬,正是由于网络蚂蚁而使得多线程下载被关注并流行起来.在这本篇文章中我们将简单的实现
支持断点续传和多线程传输的程序.为了更清晰的说明问题,我们将断点续传与多线程传输分别用两个程序来实现
多线程传输实现
实现原理
将源文件按长度为分为N块文件,然后开辟N个线程,每个线程传输一块,最后合并所有线线程文件.比如
一个文件500M我们按长度可以分5个线程传输.第一线程从0-100M,第二线程从100M-200M......最后合并5个线程文件.
实现流程
1.客户端向服务端请求文件信息(名称,长度)
2.客户端跟据文件长度开辟N个线程连接服务端
3.服务端开辟新的线程与客户端通信并传输文件
4.客户端将每线程数据保存到一个文件
5.合并所有线程文件
编码实现
大体说来就是按以上步骤进行,详细的实现和一些要点,我们跟据以上流程在编码中实现
结构定义
在通信过程中需要传递的信息包括文件名称,文件长度,文件偏移,操作指令等信息,为了方便操作我们定义如下结构
代码:
typedef struct
{
char Name[100]; //文件名称
int FileLen; //文件长度
int CMD; //操作指令
int seek; //线程开始位置
SOCKET sockid;
}FILEINFO;
1.请求文件信息
客户端代码如下
代码:
FILEINFO fi;
memset((char*)&fi,0,sizeof(fi));
fi.CMD=1; //得到文件信息
if(send(client,(char*)&fi,sizeof(fi),0)==SOCKET_ERROR)
{
cout<<"Send Get FileInfo Error\n";
}
服务端代码如下
while(true)
{
SOCKET client;
if(client=accept(server,(sockaddr *)&clientaddr,&len))
{
FILEINFO RecvFileInfo;
memset((char*)&RecvFileInfo,0,sizeof(RecvFileInfo));
if(recv(client,(char*)&RecvFileInfo,sizeof(RecvFileInfo),0)==SOCKET_ERROR)
{
cout<<"The Clinet Socket is Closed\n";
break;
}else
{
EnterCriticalSection(&CS); //进入临界区
memcpy((char*)&TempFileInfo,(char*)&RecvFileInfo,sizeof(RecvFileInfo));
switch(TempFileInfo.CMD)
{
case 1:
GetInfoProc (client);
break;
case 2:
TempFileInfo.sockid=client;
CreateThread(NULL,NULL,GetFileProc,NULL,NULL,NULL);
break;
}
LeaveCriticalSection(&CS); //离开临界区
}
}
}
在这里服务端循环接受连接,并跟据TempFileInfo.CMD来判断客户端的请求类型,1为请求文件信息,2为下载文件
因为在下载文件的请求中,需要开辟新的线程,并传递文件偏移和文件大小等信息,所以需要对线程同步.这里使用临界区
其文件信息函数GetInfoProc代码如下
代码:
DWORD GetInfoProc(SOCKET client)
{
CFile file;
if(file.Open(FileName,CFile::modeRead|CFile::typeBinary))
{
int FileLen=file.GetLength();
if(send(client,(char*)&FileLen,sizeof(FileLen),0)==SOCKET_ERROR)
{
cout<< "Send FileLen Error\n";
}else
{
cout<< "The Filelen is "<<FileLen<<"\n\n";
}
}
return 0;
}
这里主要是向客户端传递文件长度,而客户端收到长度后则开辟线程进行连接传输文件
2.客户端跟据长度开辟线程
其实现代码如下
代码:
FILEINFO FI;
int FileLen=0;
if(recv(client,(char*)&FileLen,sizeof(FileLen),0)==SOCKET_ERROR)//接受文件长度
{
cout<<"Recv FileLen Error\n";
}else
{
cout<<"FileLen is "<<FileLen<<"\n";
int COUNT_SIZE=FileLen/5; //每线程传输大小
for(int i=0;i<5;i++) //分5个线程传输
{
EnterCriticalSection(&CS); //进入临界区
memset((char*)&FI,0,sizeof(FI));
FI.CMD=2; //请求下载文件
FI.seek=i*COUNT_SIZE; //线程文件偏移
if(i+1==5) //最后一线程长度为总长度减前4个线程长度
{
FI.FileLen=FileLen-COUNT_SIZE*i;
}else
{
FI.FileLen=COUNT_SIZE;
}
Thread=CreateThread(NULL,NULL,GetFileThread,&i,NULL,NULL);
Sleep(500);
LeaveCriticalSection(&CS); //离开临界区
}
}
WaitForMultipleObjects(5,Thread,true,INFINITE); //等所有线程结束
这里默认开辟5个线程传输,当然可以改为想要的线程数目,仍然用临界区来实现线程的同步问题
3.服务端开辟线程传输数据
在1.请求文件信息中以说明了服务端的结构,这里主要介绍线程函数的实现,其代码如下
代码:
DWORD WINAPI GetFileProc(LPVOID lparam)
{
EnterCriticalSection(&CS); //进入临界区
int FileLen=TempFileInfo.FileLen;
int Seek=TempFileInfo.seek;
SOCKET client=TempFileInfo.sockid;
LeaveCriticalSection(&CS); //离开临界区
CFile file;
if(file.Open(FileName,CFile::modeRead|CFile::typeBinary))
{
file.Seek(Seek,CFile::begin); //指针移至偏移位置
char *date=new char[FileLen];
int nLeft=FileLen;
int idx=0;
file.Read(date,FileLen);
while(nLeft>0)
{
int ret=send(client,&date[idx],nLeft,0);
if(ret==SOCKET_ERROR)
{
cout<<"Send Date Error \n";
break;
}
nLeft-=ret;
idx+=ret;
}
file.Close();
delete[] date;
}else
{
cout<<"open the file error\n";
}
closesocket(client);
return 0;
}
还是比较简单的,主要是获取线程的文件长度和偏移,并移动文件指针到偏移处,最后读取发送数据,而客户端
接受数据并写入文件.
4.客户端将线程数据保存到文件
GetFileThread的实现代码如下
代码:
DWORD WINAPI GetFileThread(LPVOID lparam)
{
char TempName[MAX_PATH];
sprintf(TempName,"TempFile%d",*(DWORD*)lparam); //每线程的文件名为"TempName"+线程数
SOCKET client;
SOCKADDR_IN serveraddr;
int port=5555;
client=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
serveraddr.sin_family=AF_INET;
serveraddr.sin_port=htons(port);
serveraddr.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
if(connect(client,(SOCKADDR*)&serveraddr,sizeof(serveraddr))==INVALID_SOCKET)
{
cout<<"Connect Server Error\n";
}
EnterCriticalSection(&CS); //进入临界区
if(send(client,(char*)&FI,sizeof(FI),0)==SOCKET_ERROR)
{
cout<<"Send GetFile Error\n";
return 0;
}
CFile file;
int FileLen=FI.FileLen; //文件长度
int Seek=FI.seek; //文件偏移
LeaveCriticalSection(&CS); //离开临界区
if(file.Open(TempName,CFile::modeWrite|CFile::typeBinary|CFile::modeCreate))
{
char *date = new char[FileLen];
int nLeft=FileLen;
int idx=0;
while(nLeft>0)
{
int ret=recv(client,&date[idx],nLeft,0);
if(ret==SOCKET_ERROR)
{
cout<<"Recv Date Error";
break;
}
idx+=ret;
nLeft-=ret;
}
file.Write(date,FileLen);
file.Close();
delete[] date;
}else
{
cout<<"Create File Error\n";
}
return 0;
}
在此线程函数中,将每线程传输的数据存为一个文件,文件名为"TempName"+线程数,只所以存成单独的文件是
因为比较直观且容易理解,但如果文件很大的话这个方法并不好,因为合并文件又会花费很多时间,另一个方法
是 创始一个文件,让每个线程写入文件的不同偏移,这样就可以不必单独合并文件了,但要记得打开文件时
加入CFile::shareDenyNone属性.这样整个过程就完成了.最后一步合并线程文件
5.合并线程文件
代码:
int UniteFile() //合并线程文件
{
cout<<"Now is Unite Fileing...\n";
int len;
char *date;
CFile file;
CFile file0;
/*其它文件......*/
if(file.Open(FileName,CFile::modeCreate|CFile::typeBinary|CFile::modeWrite))//创建文件
{
file0.Open("TempFile0",CFile::modeRead|CFile::typeBinary);//合并第一线程文件
len=file0.GetLength();
date=new char[len];
file0.Read(date,len);
file.SeekToEnd();
file.Write(date,len);
file1.Open("TempFile1",CFile::modeRead|CFile::typeBinary);//合并第二线程文件
len=file1.GetLength();
date=new char[len];
file1.Read(date,len);
file.SeekToEnd();
file.Write(date,len);
/*合并其它线程......*/
file0.Close();
file1.Close();
/*.......*/
delete[] date;
return true;
}else
{
return false;
}
}
这个简单,就是打开一个文件读取到缓冲区,写入文件,再打开第二个......现在多线程传输部分就介绍完了
所谓的断点续传就是指:文件在传输过程式中被中断后,在重新传输时,可以从上次的断点处开始传输,这样就可节省时间,和其它资源.
实现关键在这里有两个关键点,其一是检测本地已经下载的文件长度和断点值,其二是在服务端调整文件指针到断点处
实现方法
我们用一个简单的方法来实现断点续传的功能.在传输文件的时候创建一个临时文件用来存放文件的断点位置
在每次发送接受文件时,先检查有没有临时文件,如果有的话就从临时文件中读取断点值,并把文件指针移动到
断点位置开始传输,这样便可以做到断点续传了
实现流程
首次传输其流程如下
1.服务端向客户端传递文件名称和文件长度
2.跟据文件长度计算文件块数(文件分块传输请参照第二篇文章)
3.客户端将传输的块数写入临时文件(做为断点值)
4.若文件传输成功则删除临时文件
首次传输失败后将按以下流程进行
1.客户端从临时文件读取断点值并发送给服务端
2.服务端与客户端将文件指针移至断点处
3.从断点处传输文件
编码实现
因为程序代码并不复杂,且注释也比较详细,这里就给出完整的实现
其服务端实现代码如下
代码:
int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
{
cout<<"\t\t服务端-断点续传"<<"\t 作者:冷风\n\n"<<"请输入被下载的文件路径 如 C:\\File.rar \n\n"<<"文件路径: ";
cin >>FilePath;
/*这部分为网络参数与设置,详细请参照源代码......*/
while(true)
{
if(client=accept(server,(sockaddr *)&clientaddr,&len))
{
cout<<"have one connect\n";
int nCurrentPos=0;//接受断点值
if(recv(client,(char*)&nCurrentPos,sizeof(nCurrentPos),0)==SOCKET_ERROR)
{
cout<<"The Clinet Socket is Closed\n";
break;
}else
{
cout<<"The Currentpos is The"<<nCurrentPos<<"\n";
GetFileProc (nCurrentPos,client);
}
}
}
closesocket(server);
closesocket(client);
WSACleanup();
return 0;
return 0;
}
DWORD GetFileProc (int nCurrentPos,SOCKET client)
{
cout <<"Get File Proc is ok\n";
CFile file;
int nChunkCount=0; //文件块数
if(file.Open(FilePath,CFile::modeRead|CFile::typeBinary))
{
if(nCurrentPos!=0)
{
file.Seek(nCurrentPos*CHUNK_SIZE,CFile::begin); //文件指针移至断点处
cout<<"file seek is "<<nCurrentPos*CHUNK_SIZE<<"\n";
}
int FileLen=file.GetLength();
nChunkCount=FileLen/CHUNK_SIZE; //文件块数
if(FileLen%nChunkCount!=0)
nChunkCount++;
send(client,(char*)&FileLen,sizeof(FileLen),0); //发送文件长度
char *date=new char[CHUNK_SIZE];
for(int i=nCurrentPos;i<nChunkCount;i++) //从断点处分块发送
{
cout<<"send the count"<<i<<"\n";
int nLeft;
if(i+1==nChunkCount) //最后一块
nLeft=FileLen-CHUNK_SIZE*(nChunkCount-1);
else
nLeft=CHUNK_SIZE;
int idx=0;
file.Read(date,CHUNK_SIZE);
while(nLeft>0)
{
int ret=send(client,&date[idx],nLeft,0);
if(ret==SOCKET_ERROR)
{
cout<<"Send The Date Error \n";
break;
}
nLeft-=ret;
idx+=ret;
}
}
file.Close();
delete[] date;
}else
{
cout<<"open the file error\n";
}
return 0;
}
客户端实现代码如下
代码:
int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
{
cout<<"\t\t客户端-断点续传"<<"\t 作者:冷风\n\n"<<"请输入保存文件的路径 如 C:\\Save.RAR \n\n"<<"文件路径: ";
cin >>FilePath;
/*网络参数初示化,详细请参照源代码......*/
if(connect(client,(SOCKADDR*)&serveraddr,sizeof(serveraddr))==INVALID_SOCKET)
{
cout<<"Connect Server Error";
return 0;
}
int FileLen=0;
int nCurrentPos=0; //断点位置
UINT OpenFlags;
CFile PosFile;
if(PosFile.Open("PosFile.temp",CFile::modeRead|CFile::typeBinary))//如果有临时文件则读取断点
{
PosFile.Read((char*)&nCurrentPos,sizeof(nCurrentPos)); //读取断点位置
cout<<"The File Pos is "<<nCurrentPos<<"\n";
nCurrentPos=nCurrentPos+1; //从断点的下一块开始
PosFile.Close();
send(client,(char*)&nCurrentPos,sizeof(nCurrentPos),0); //发送断点值
OpenFlags=CFile::modeWrite|CFile::typeBinary; //文件为可写
}
else
{
send(client,(char*)&nCurrentPos,sizeof(nCurrentPos),0); //无断点文件nCurrentPos为0
OpenFlags=CFile::modeWrite|CFile::typeBinary|CFile::modeCreate;//创建文件方式
}
if(recv(client,(char*)&FileLen,sizeof(FileLen),0)!=0)//接受文件长度
{
int nChunkCount;
CFile file;
nChunkCount=FileLen/CHUNK_SIZE; //计算文件块数
if(FileLen%nChunkCount!=0)
{
nChunkCount++;
}
if(file.Open(FilePath,OpenFlags))
{
file.Seek(nCurrentPos*CHUNK_SIZE,CFile::begin); //文件指针移至断点处
char *date = new char[CHUNK_SIZE];
for(int i=nCurrentPos;i<nChunkCount;i++) //从断点处开始写入文件
{
cout<<"Recv The Chunk is "<<i<<"\n";
int nLeft;
if(i+1==nChunkCount) //最后一块
nLeft=FileLen-CHUNK_SIZE*(nChunkCount-1);
else
nLeft=CHUNK_SIZE;
int idx=0;
while(nLeft>0)
{
int ret=recv(client,&date[idx],nLeft,0);
if(ret==SOCKET_ERROR)
{
cout<<"Recv Date Error";
return 0;
}
idx+=ret;
nLeft-=ret;
}
file.Write(date,CHUNK_SIZE);
CFile PosFile; //将断点写入PosFile.temp文件
int seekpos=i+1;
if(PosFile.Open("PosFile.temp",CFile::modeWrite|CFile::typeBinary|CFile::modeCreate));
{
PosFile.Write((char*)&seekpos,sizeof(seekpos));
PosFile.Close();
}
}
file.Close();
delete[] date;
}
if(DeleteFile("PosFile.temp")!=0)
{
cout<<"文件传输完成";
}
}
return 0;
}
客户端运行时会试图打开临时文件,如果存在则读取断点值,如果不存在则断点为0,打开文件后将文件指针移至
断点处开始接受数据,每接受一块就把当前块的数值存入临时文件.其实现代码比较简单结合上面的流程介绍
看代码应该没什么难度,所以我也就不画蛇添足了.
本文来自CSDN博客:http://blog.csdn.net/zhengkangchen/archive/2009/02/27/3942265.aspx