首页 > 编程资源分享区 > C/C++源代码共享 > 基于select I/O模型的远程目录浏览与多线程文件下载
2007
02-12

基于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);
}

i为下载线程的总数, j为当前线程序号.主线程循环不管CreateThread创建成功与否还继续执行, 当真正CreateThread在创建线程时, j的值可能已经被修改了.程序修改如下:
	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);
}

在本项目中, 用的是置位机制.线程创建成功, 由线程置位为1, 主线程睡眼等待, 检测到位为1时, 则置位为0并继续创建线程.

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

调试的时候, i>len程序像得了疯牛病一样还是往前跑, 不会停下来。要确保正确性, 内循环应该修改如:
while( pChar[i]!=’|’ && i<=len )

 
6  memcpy预分配空间
在程序中对全局变量已经分配了一次空间char[2000], 在后续的使用中程序不会有问题, 但当整个程序关闭时却冒出一个提示框说程序遇到一个问题需要关闭。把后续使用memcpy对这个变量赋值时, 如果小于1993个char空间就不会出现前面说到的提示框。
后来试着在第二次使用时再次给该变量分配了一次空间, 之后一切正常了。不知何解, 睇来memcpy的确不是安全的东西。


三 重要程序说明:
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;
}


2 文件下载函数
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;
}

}
}


3. 客户端线程函数

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<requestPacket.length )
{
for(int k=0; pDrives[i]!=’|’ && i<requestPacket.length ; k++,i++ )
arrFiles[j].Insert(k,pDrives[i]);

i++;
j++;
}
return 0;
}

if( requestPacket.packetType==’f’ )
fSize=requestPacket.length;

//Download file
if( requestPacket.packetType==’D’ )
{
//memset( pDrives,0,2000 );
Memcpy( pDrives,requestPacket.content,sizeof(requestPacket.content) );
FillInFile( socket,thrInfo,requestPacket.length,pDrives);
}
}

::closesocket(socket);
return 0;
}


四 程序结构图
看别人的程序绝对不是一件好事, 要看个明白就更难了, 特别是没有算法描述甚至一句注释都没有的程序。所以把程序流程图画了出来,好明白一些。
1. 服务器端
服务器端的大致流程就是这样, 线程里面就是select(…)。不过有些判断和出错处理未画出。从商业程序角度看来,有两点是很重要的, 一是捕获异常和出错处理, 二是简洁高效。我们编程应往这方面靠拢。



                       图1


2. 客户端



                                                      图2


3. 客户端线程



                              图3


4. 双击树项目:



                 图4


5. 写文件FillInFile



      图5


技术支持: QQ:358996566 


留下一个回复