分类: C/C++
2008-04-23 22:06:41
基于select I/O模型的远程目录浏览与多线程文件下载
作者:
摘要:
服务器端基于select I/O模型。为防止程序界面阻塞, 有一个子线程用于不断接收socket并select其中的处理。客户端只有一个线程函数, 不过其利用率很高, 可用于远程目录交换, 请求文件大小, 创建若干线程来下载文件。文件传输有上传和下载,还有对等传输, 这个项目中, 传输文件具体指下载。
正文:
一 数据及数据结构
1 传送包, 客户端主线程给子线程传递的结构体
typedef struct{
char packetType; // 请求类型 r:request rootdriver d:directory f:file D::data
unsigned int length; //用于传送int类型
char content[2000]; //传送的内容
}DATA_PACKET;
请求类型:'r':服务器逻辑盘符, d; 盘符和文件夹下的文件夹和文件名, f':文件大小, D:文件内容。
在客户端请求下载文件, length末两位用于记录下载的线程总数, 前面的用于记录当前的线程序号。在服务器端这个反运算很容易实现。
在请求文件大小和请求下载时, content都用于保存请求文件的绝对路径.这个在服务器端做反运算要用自定义函数处理。
2 全局变量(用于线程间的通信)
char* pDrives; // 数据缓冲 CString arrFiles[200]; //文件目录字符串数组 CString savePath; //文件保存路径 long fSize=0l, ,recvFSize=0l; //文件大小,已经接收的文件大小 CString strIP; //IP bool thrFlag=false; //下载线程创建置位
理论上讲, 在程序中应尽量避免使用全局变量,因为破坏程序结构, 君不见Java和C#完全面向类。但为了方便, 所以就用了全局变量, 且是应用程序级的, 这样线程间的通讯很方便。值得一提的是, 这里thrFlag很重要, 它涉及到确保线程创建一定正确的问题, 在后面“要注意的几点问题”中会更详细的讨论。
3 目录树类CExtnTreeCtrl::CTreeCtrl
该类继承于CTreeCtrl类, 主要扩展获得树的某个项目在整棵树的完整路径, 以便把这个路径规格化后能向服务器请求文件。在这里还实现了将一个完整路径转为文件名的函数。
对于如何构造这棵树, 有一个很方便高效的办法:双击树,把双击的项作为根.如果为盘符或文件夹, 就发送请求给服务器, 待服务器返回, 自动填充在这个项下。如果为文件, 则该项名有扩展名, 将请求下载。这个办法操作起来方便, 而且还提高了系统性能, 至少在局域网是这样。如果就一次从服务器中请求整个文件系统的目录内容, 肯定会慢很多。
4.参数设置对话框类 CSetParam
这个类用于设置下载的线程数目, 和默认的保存文件夹路径。这个类会写一个“Setting.ini”文件在C盘以保存参数。具体来说,如果用户一直都没有设置这些参数的话, 那么GetPrivateProfileString(...)试图读取"C:\Setting.ini"文件会返回默认的线程数0, 和一个不是表示路径的字条串“defaultpath”, 这时, 下载程序会自动设置参数, 分别为3, “C:\”。
二 几个要注意的问题
1 MFC与Windows API
就多线程编写网络程序而言, 如果使用MFC的CAsyncSocket或CSocket, 主线程给子线程传参量是一个非常头痛的问题, 如果用Windows
API实现就灵活多了。在文件操作方面, CFile还不错。尽管如此, 我还是用了Windows API函数, 为了设计满足要求。
2 socket传送字符串或字符串数组
理论上, send是底层函数, 只要指定缓冲区首地址指针和缓冲区大小, 不管什么狗屎垃圾它都会帮你把这片内存的内容send 出去。然而, 如果是字符串数组甚至字符串, 或者包含这两者其中之一的封包, 内容是被
send 走了, 接收端缓冲区也显示接收到了, 不过不是你想要的内容, 再看字符串(数组)还是空空然。
山重水复, 得找出路才有柳暗花明。 转转思维, 用另一种做法吧。在Windows中, 文件名是不能含有 "|,<,>,%...."
等特殊字符的.由于恰好要传的内容为目录字符串数组, 所以把字条串数组转为字符数组, 每个字符串用一个特殊字符隔开就OK了。
3. 确保连续创建线程正确性
用循环语句创建线程, 如果没有一定的保护机制, 肯定会出事。程序如:
int i = paramSettingDlg.thrTotal //abtain download threads total for( int j=1;j<=i;j ) { dataPacket.length=j*100 i; // download thread information. ::CreateThread(NULL, 0, ThreadDownload, (LPVOID)&dataPacket, 0, 0); }
for( int j=1;j<=i;j )
{
dataPacket.length=j*100 i; // download thread information. while( ::CreateThread(NULL, 0, ThreadDownload, (LPVOID)&dataPacket, 0, 0) ==0 )
Sleep(30);
}
4 打开关闭文件的控制
多线程读/写文件是一件很混乱的事, 有点像一堆乞丐抢饭吃那样.所以要有条理就要维护秩序, 要维护秩序, 自然要牺牲性能.每次读/写文件都要定位到该包内容对应原文件的位置.这个应用程序是锁定读/写一个包的时间.如果锁定整个线程, 即等一个线程读/写该线程应该读/写的部分再解锁, 系统性能必会急剧下降, 具体操作可以看"程序流程图"。
另外, 线程写入的数据要及时刷新文件流。否则, 程序等到缓冲区满时才真正写入文件的, 结果就是文件乱七八糟了。
5 双循环程序
这个问题是在这个项目编程遇到的.双循环的程序如下:
CString strArray[6];
char* pChar = "abc1|abc2|abc3|ac";
int len = strlen( "abc1|abc2|abc3|ac" );
int i=0,j=0;
while( i<= len && j<=6 )
{
int k = 0;
while( pChar[i]!='|' )
{
strArray[j].Insert( k, pChar[i] );
i ;
k ;
}
i ;
j ;
}
while( pChar[i]!='|' && i<=len )
三 重要程序说明:
1 select I/O模型框架
select I/O模型对于大访问量的网络特别有效。 在服务器端, 可以建立多个线程, 每个线程创建可同时创建一个读写fd_set。 fd_set的默认大小为64, 即在默认的情况下一个Set最多可以接受64个socket的连接。
DWORD WINAPI CFileTransSvrView::ThreadSelect(LPVOID lpParameter) { // 初始化fd_set SOCKET sListen=(SOCKET)lpParameter; fd_set fdSocket; FD_ZERO(&fdSocket); FD_SET(sListen,&fdSocket); // 不断循环遍历fd_set, 如果某项置位, 则表示该socket可用, 如果状态为正在侦听, // 则建立连接, 并把它加入到fd_set中, 否则就等待接收数据。select有自动机制把不可 //用的socket从fd_set中删除。 while(TRUE) { fd_set fdRead=fdSocket; int nRet=::select(0, &fdRead, NULL, NULL, NULL); if(nRet>0) { for(int i=0;i<(int)fdSocket.fd_count;i ) { if(FD_ISSET(fdSocket.fd_array[i],&fdRead)) { if(fdSocket.fd_array[i] == sListen) { sockaddr_in addrRemote; int nAddrLen=sizeof(addrRemote); SOCKET sNew=::accept(sListen,(sockaddr*)&addrRemote,&nAddrLen); FD_SET(sNew,&fdSocket); } else { DATA_PACKET recvPacket; int nRecev=::recv(fdSocket.fd_array[i], (char*)&recvPacket, sizeof(recvPacket), 0)。 if(nRecev>0) { // respond request } else { ::closesocket(fdSocket.fd_array[i]); FD_CLR(fdSocket.fd_array[i], &fdSocket); } } } } } } return 0; }
void CFileTransCltView::FillInFile(SOCKET socket, int thrInfo, int packIndex, char* pPackCont) { // thrInfo末两位为下载线程总数,再上两位为当前线程序号 int thrIndex = thrInfo/100; // 当前线程序号 int thrTotal = thrInfo-thrIndex*100; // 下载线程的总数 int thrLength = fSize/thrTotal; // 当前线程要下载的文件长度 int bPoint = (thrIndex-1)*thrLength; // 当前线程要下载的文件起点 if( thrIndex == thrTotal ) thrLength = fSize-bPoint; // 当前线程等于线程总数,则重新赋予要下载的文 //件长度 FILE *file; if( (file = fopen( savePath, "ab" )) == NULL ) AfxMessageBox( "Open file occur an error\n" ); // 每个线程都会试图打开文件, 如果已打开, 返回文件流指针。 为什么这样?因为 //在TCP下载文件时, 线程的包是按顺序来的, 但线程却不一定会按前后顺序执行。 int packTotal = thrLength/2000 1; int subTotal = packTotal; DATA_PACKET dPacket; while( subTotal>0 ) { critSection.Lock(5000); fseek( file,bPoint (packIndex-1)*2000,SEEK_SET ); if( packIndex == packTotal ) //the last packet in thread,data maybe less than 2000B { int fLength = thrLength-(packTotal-1)*2000; int errorcode=fwrite( pPackCont,1,fLength,file); fflush(file); recvFSize =fLength; } else { fwrite( pPackCont,sizeof(char),2000,file ); fflush(file); recvFSize =2000; } critSection.Unlock(); subTotal--; if( subTotal>0 ) { if( recv( socket, (char*)&dPacket, sizeof(dPacket), 0 ) != 0 ) { packIndex = dPacket.length; Memcpy( pPackCont, dPacket.content, sizeof(dPacket.content) ); } } else { // 该线程下载任务完成, 判断是否已经完全下载完文件, 如果是则关闭 if( recvFSize == fSize ) fclose(file); return; } } }
DWORD WINAPI CFileTransCltView::ThreadRequest(LPVOID lpParameter) { DATA_PACKET requestPacket=*(DATA_PACKET*)lpParameter; SOCKET socket=::socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); sockaddr_in sin; sin.sin_addr.S_un.S_addr=inet_addr(strIP); sin.sin_family=AF_INET; sin.sin_port=htons(1032); if(::connect(socket,(sockaddr*)&sin,sizeof(sin))!=0) { AfxMessageBox("Request failure!"); return 0; } int thrInfo; if( requestPacket.packetType=='D' ) //save download thead infomation for function // FillInFile() { thrInfo=requestPacket.length; thrFlag=true; } ::send( socket,(char*)&requestPacket,sizeof(requestPacket),0 ); memset( requestPacket.content,0,sizeof(requestPacket.content) ); if( ::recv(socket,(char*)&requestPacket,sizeof(requestPacket),0) ) { if( requestPacket.packetType=='r' ) { int szBuffer=requestPacket.length; pDrives=new char[2000]; //Make sure that allocate memery! memcpy(pDrives,requestPacket.content,szBuffer); //Make sure that use memcpy() } if(requestPacket.packetType=='d' ) { pDrives=new char[2000]; // 再次为pDrives分配空间才不会出错 Memcpy( pDrives,requestPacket.content,sizeof(requestPacket.content) ); /* 2000 will take an error ?????*/ //char's pointer to CString Array int i=0,j=0; while( i四 程序结构图
看别人的程序绝对不是一件好事, 要看个明白就更难了, 特别是没有算法描述甚至一句注释都没有的程序。所以把程序流程图画了出来,好明白一些。
1. 服务器端
服务器端的大致流程就是这样, 线程里面就是select(...)。不过有些判断和出错处理未画出。从商业程序角度看来,有两点是很重要的, 一是捕获异常和出错处理, 二是简洁高效。我们编程应往这方面靠拢。
图1
2. 客户端
图2
3. 客户端线程
图3
4. 双击树项目:
图4
5. 写文件FillInFile
图5
技术支持: QQ:358996566