C++ 网络文件传输

omenon2007 贡献于2012-02-05

作者 wangdong  创建于2002-09-17 09:32:00   修改者luohui  修改于2011-04-08 01:33:00字数20067

文档摘要:建议读者再看实验步骤之前,先阅读一下文章末尾的【注意事项】一节。这将有助于更好的理解本章的实现。
关键词:

建议读者再看实验步骤之前,先阅读一下文章末尾的【注意事项】一节。这将有助于更好的理解本章的实现。 一.单线程文件传输 (I): ·服务器端(负责发送数据)的实现 1. 建立一个基于对话框的工程Server,并在建立的过程中选择支持windows socket。 2. 在对话框上添加“发送”按钮。 3. 为“发送”按钮添加事件BN_CLICKED的响应函数OnSend()。 void CServerDlg::OnSend() { // TODO: Add your control notification handler code here CFileDialog fd(TRUE); // CFileDialog是MFC提供的一个用于选择文件的对话框类 CString filename; char fn[40]; CSocket listenSocket, socketSend; CFile file; long FileLength; char* data; if(IDOK==fd.DoModal()) // 启动用于选择文件的对话框 { //选择了文件 filename=fd.GetFileName(); // 获取用户选择的文件的文件名 if(!file.Open(filename.GetBuffer(0),CFile::modeRead| File::typeBinary)) { AfxMessageBox(" 打开文件错误,取消发送!"); return; } strcpy(fn,filename.GetBuffer(0)); } else return; //按了取消按钮 listenSocket.Create(7000,SOCK_STREAM); listenSocket.Listen(5); listenSocket.Accept(socketSend); FileLength = file.GetLength(); // 获取文件的长度 socketSend.Send(&FileLength, 4); // 把要发送的文件的长度传送给对方 socketSend.Send(fn,40); // 发送要传送的文件的文件名 data = new char[FileLength]; //分配一块和要传输的文件一样大小的内存空间 file.ReadHuge(data, FileLength); //把文件中所有的数据一次性读入data socketSend.Send(data, FileLength); //把data中的数据都发送出去 file.Close(); delete data; socketSend.Close(); } ·客户端(负责接收数据)的实现 1. 建立一个基于对话框的工程,并在建立的过程中选择支持windows socket。(为了能够利用Server端的代码,在程序编写时,可以复制Server的代码到Client目录,并在Server的基础上修改或添加代码) 2. 在对话框上添加“接收”按钮。 3. 为“发送”按钮添加事件BN_CLICKED的响应函数OnReceive ()。 void CServerDlg::OnReceive() { // TODO: Add your control notification handler code here CSocket socketReceive; CFile file; long FileLength; char * data; char fn[40]; socketReceive.Create(); socketReceive.Connect("127.0.0.1", 7000); //这里为了测试的方便,我们使用127.0.0.1本地回路IP socketReceive.Receive(&FileLength, 4); //获取要接受的文件的长度 socketReceive.Receive(fn, 40); //获取要接受的文件的文件名 data = new char[FileLength]; socketReceive.Receive(data, FileLength); //获取要接受的文件内容 file.Open(fn,CFile::modeCreate|CFile::modeWrite | CFile::typeBinary); //在当前目录建立文件 file.WriteHuge(data, FileLength); file.Close(); delete data; socketReceive.Close(); AfxMessageBox("接收文件成功"); } 上面的程序以最简单的方式实现了文件传输功能。但正是因为它的简单,所以忽略了很多重要的东西。读者读到这里的时候可以考虑一下可能存在着些什么问题。在这里给出一些上面程序的不足: 1. 在本书的原理部分曾经提到阻塞式和非阻塞式两种套接字的工作方式。在上面的程序中使用的CSocket类提供了阻塞式的服务,这令编写程序非常方便。然而这却不利于程序的友好性和可用性——服务器端在没有获得客户端连接的时候固执的侦听,一点也不理会用户的命令。对话框不再响应消息,用户只能用强制关闭来结束程序。 2. 希望一次性地动态分配整个文件大小的堆存贮区作为读入数据的内存空间。这个空间一方面受到堆大小的限制,另一方面也受到CFile类成员函数ReadHuge()和WriteHuge()的读写能力限制,还有Receive()和Send()函数一次能够发送的数据也以其参数的最大值为限,所以在遇到大中型文件的时候,系统将不能满足程序提出的动态分配大块内存的要求,这样传输便不得不终止。 3. 在实际的网络传输中,网络情况是十分多变和复杂的。通过CSocket的成员函数们的返回值可以了解传输的状态,然而在上面的程序中却没有使用这些异常控制,带来的直接后果就是当遇到网络拥塞或对方传送的数据暂时未到时,程序就会认为传输结束而出乎意料的终止。 4. 在上面的程序中,程序没有传送文件的属性数据,而只通过套接字传送文件数据。在实际应用中这种假设通常是不存在的,所以应当把文件属性也传给对方,以达到文件的完全复制。 改进方法: 1. 使用CAsyncSocket实现异步套接字,避免阻塞 2. 化整为零,把文件分若干次读入并发送 3. 在程序中加入异常控制语句已应付网络多变的情况 4. 在传送文件数据之前获取文件属性值传给对方 下面将给出基于上述改进方案的前三点而重新编写的程序,对于第四点,有兴趣的读者可以自己实现。 二.单线程文件传输(II): ·服务器端(负责发送数据)的实现 1. 建立一个基于对话框的工程Server,并在建立的过程中选择支持windows socket。 2. 在对话框上添加“侦听”,“发送”按钮。 3. 用Class Wizard添加一个派生于CAsyncSocket的类CMySocket。 4. 添加CMySocket类的全局变量listenSocket用于侦听。 添加CMySocket类全局变量的位置,最宜在MySocket.cpp的头部。如下: // MySocket.cpp : implementation file // #include "stdafx.h" #include "Server.h" #include "MySocket.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif ///////////////////////////////////////////////////////////////////////////// // CMySocket CMySocket listenSocket; CMySocket::CMySocket() { } CMySocket::~CMySocket() { } 本章中以后所有的CMySocket型全局变量都应仿效本段程序添加。 5. 添加CMySocket类的全局变量sendSocket,用于传输数据 6. 为“发送”按钮添加事件BN_CLICKED的响应函数OnSend() #define ReadSize 500 void CServerDlg ::OnSend() { // TODO: Add your control notification handler code here CFile file; char data[ReadSize]; // 用于存放读入的文件数据块 long ByteSended=0, FileLength,count; CFileDialog fd(TRUE); CString filename; char fn[40]; if(IDOK==fd.DoModal()) // 启动用于选择文件的对话框 { //选择了文件 filename=fd.GetFileName(); //获取选择的文件的文件名 if(!file.Open(filename.GetBuffer(0),CFile::modeRead|CFile::typeBinary)) { AfxMessageBox("打开文件错误,取消发送!"); return; } strcpy(fn,filename.GetBuffer(0)); } else return; //按了取消按钮 FileLength=file.GetLength(); sendSocket.Send(&FileLength,sizeof(long)); sendSocket.Send(fn,40); memset(data,0,sizeof(data)); do{ // 从文件读取数据,每次最多读入ReadSize个字节。count表示实际读入的字节数 count=file.ReadHuge(data, ReadSize); // 发送数据 while(SOCKET_ERROR==sendSocket.Send(data,count)) { } // 统计已发送的字节数 ByteSended =ByteSended+count; }while(ByteSended )。控制socket缓冲区的大小可以优化网络程序的传输性能,但同时也为我们带来了一个问题:当一个应用程序把数据送给TCP实体时,TCP根据自己的判断,可能会立刻将其发送出去或将其缓存起来(为了收集较大量的数据,然后发送)。然而,有时候应用程序很想将数据立即发送出去。例如,假设一个用户登录到了远端的机器上,他输入一条命令并按下回车键之后,该命令行应该立刻送往远端机器而不是暂存起来直到用户输入了下一条命令后再发送。为了强制立即发送数据,应用程序可以使用PUSH标志,通知TCP不能耽搁数据的发送。一些早期的应用程序使用PUSH标志作为一种记号来区分出报文的边界。这种方法有时可以奏效,但有时也会失败,因为并非所有的TCP实现都将PUSH标志传送到接收方的应用程序。而且,如果在每个PUSH标志发送前又有PUSH标志输入进来(例如由于输出线路很忙),那么TCP将会随意地将所有带PUSH标志的数据聚集成一个IP数据报。在这些各种各样的数据片之间不再加以区分(其实,UNIX系统中的文件也有这种特性。读取文件时无法分清文件是一次写一个块、一个字节还是一次全部写入。如同对待UNIX文件一样,TCP软件不知道字节表示什么,并且不无疑去弄清楚,字节就是字节。)① Andrew S.Tanenbaum 著《计算机网络》(第三版)P403 为了解决这个问题,有时不得不编写程序,统计收到的字符数,然后,根据再已知的数据段的长度来区分每个数据段。 下面,让来看一看如何在客户端区分已知长度的数据段。(假设已知数据段的长度是2,并且以‘#’作为数据发送结束的标志) 服务器端: char data[100]={"0123456789#"}; CSocket socketListen, socketSend; socketListen.Create(7000); socketListen.Listen(); socketListen.Accept(socketSend); socketSend.Send(data,strlen(data)); // 服务器端一次把所有的数据发送出去 socketListen.Close(); socketSend.Close(); 客户端: char buffer[1]; // 用以储存每次收到的一个字符 char temp[100]; // 用以储存正在合并中的一个数据段 int i=0; CSocket socketReceive; socketReceive.Create(); socketReceive.Connect("127.0.0.1",7000); memset(temp,0,sizeof(temp)); do{ while(SOCKET_ERROR==socketReceive.Receive(buffer,1)); // 一次接收一个字符 temp[i++]=buffer[0]; // 把接收到的字符加入正在形成的数据段的尾部 if(i==2) // 如果收到的字符数等于已知数据段的长 { CString str; str.Format("收到两个字节 %s",temp); AfxMessageBox(str); // 显示这个数据段 memset(temp,0,sizeof(temp)); // 清空这个数据段,为接收下一个数据段作准备 i=0; } }while(buffer[0]!='#'); // 判断接收到的是否是结束标志 socketReceive.Close(); 三.多线程文件传输 如果仔细地统计单线程传输文件的速度,便会发现即使在像湖南大学这种高速的校园网内,传输的速率也不会超过400KB/S。然而,在用下载工具(比如:netants,flashfxp等)下载资料时,时常可以看到大于600KB/S的速度。疑问由此产生——难道这些软件采用了“五鬼搬运术”?其实,当探究过其中的原理之后,便会惊奇的发现原来这些软件真的使用了具有神话色彩的“五鬼搬运术”,而驱使这些“鬼”来搬运数据的正是多线程技术! 多线程技术在网络程序设计方面有着很大的作用,它不仅能提高程序的发送能力,而且极大地方便了程序员设计编写文件服务器程序(同时处理多个连接)。但是,多线程程序设计有一个很大的问题就是要涉及到线程的同步操作,这确实是一件令人头痛的事情,但为了实现高性能的传输程序,也不得不把它弄个清楚(后文将介绍一些线程技术,至少在文件传输方面会涉及到的一些知识)。 在MFC中,启动线程有多种方法:AfxBeginThread(…),CreateThread(…),还有就是process.h中定义的_beginthread()。传输文件时,不仅应使用多个线程传输文件数据,另外,最好启动一个线程监视文件传输的状态(传输进度等),以便作一些必要的操作,比如:接收方在接收完成时,合并每个线程接收的数据文件;又如:即时显示传输进度或计算传输速率。 操作系统课程讲述了线程PV同步原语,锁和条件变量。在MFC中,用CEvent等类来控制线程同步。CEvent和条件变量比较像,它的成员函数SetEvent()用来把这个事件对象设置为有信号的,ResetEvent()函数用来把这个事件对象设置为无信号的,可配套使用的函数WaitForSingleObject用来挂起线程直到事件对象从无信号的变为有信号的。向线程传递数据可以通过下面多种方式实现,以下的两种方式相对简便:1. 以函数值参数方式,在创建线程时传入;这种方式十分可靠,因为每个函数值参数(都会在函数体内保存一个副本,不会受其他线程修改变量的影响),但是只能在创建线程的时候传入,缺乏灵活性;2. 使用全局变量。使用全局变量要用CMutex类的锁机制保护好,否则会导致线程读入数据的错误。 下面的多线程程序将使用第一种方式为每个线程“工人”分配工作编号(idx)。 在发送端程序中,多个线程同时读一个欲发送的文件。特别需要注意的是,如果不在打开文件时作一些特别的设置,虽然第一个程序可以正常的打开文件,但之后的程序将因为第一个程序对文件的“排他读”行为而打开文件失败。因此所有的线程打开文件时都应该设置共享读写属性 CFile::shareDenyNone。设置方法如下: CFile file; File.Open(“abc.txt”,CFile::modeRead|CFile::shareDenyNone); 上文曾把线程比作工人,它们把数据从一台计算机搬到另一台计算机。为了使它们能完成这项任务,必须教会它们同样的搬运技术,也就是说,需要为它们编写几乎相同的工作代码。然而,毕竟程序中的每个线程要负责一个文件中不同的数据段,所以它们还是有一些小小的区别,比如:它们必须知道各自负责搬动的物品放在哪里(源文件的哪一段)和搬到哪里去(存放在接受方的什么文件中)。另外,要管理好这些工人并为它们分配工作也是一件重要的任务,所以派一个工头(也是一个线程)来完成这项任务是必不可少的不同的,但与现实不同的是,下面程序中的每个工头手下只有一个工人。 使用n个线程传输文件时,应当把文件分为大小类似的n块,每个线程负责其中的一块。在客户端,让每个接收线程(工人)把它们收到的数据保存为一个独立的文件,当所有的线程都工作结束时,再把这些文件合并起来。 ·服务器端(负责发送数据)的实现 1. 一个基于对话框的工程Server,并在建立的过程中选择支持windows socket。 2. 在对话框上添加“侦听”和“发送”按钮。 3. 用ClassWizard添加一个派生于CAsyncSocket的类CMySocket 4. 定义CMySocket型的全局变量listenSocket和数组sendSockets[5] //listenSocket用于侦听,sendSockets中的5个套接字分别被五个线程用来收发数据 下面的程序总共启动5个线程用来传输文件数据。每个线程根据自己的编号idx使用套接字数组sendSocket[5]中相应的套接字传送数据。注意,在Create这个套接字数组的时候,完全可以让它们共享一个端口号。使用CAsyncSocket类时,我们无需做更多的设置就可以使用这项技术。 5. 双击“侦听”按钮,为它添加事件BN_CLICKED的响应函数 OnListen() void CServerDlg ::OnListen() { // TODO: Add your control notification handler code here listenSocket.Create(7000); listenSocket.Listen(); } 6. 为CMySocket类添加消息OnAccpect的响应函数OnAccept() void CMySocket::OnAccept(int nErrorCode) { // TODO: Add your specialized code here and/or call the base class static int File_Socket_Accepted=0; if(!listenSocket.Accept(sendSockets[File_Socket_Accepted])) // 接收对方套接字的连接 { AfxMessageBox("接收连接失败!"); return; } sendSockets[File_Socket_Accepted].AsyncSelect(FD_READ|FD_WRITE|FD_CLOSE); File_Socket_Accepted++; // 已接收的连接数加一 if(File_Socket_Accepted==5) // 如果所有的连接已经都接受了 { AfxMessageBox("连接成功"); listenSocket.Close(); } CAsyncSocket::OnAccept(nErrorCode); } 7. 编写为线程分配要传输的数据块的全局函数GetBeginPos() //获取线程负责的文件数据块起始位置及大小,TotalThreads为启动传输线程的总数,ThreadIndex为线程的编号,BgPos为计算得到的数据块相对于文件头的偏移量,BlkSize为数据块的大小 void GetBeginPos(int TotalThreads,int ThreadIndex/*from 1*/,long file_length, long &BgPos, long &BlkSize) { long BlockSize, lastBlockSize, BeginPos; int i; BlockSize=file_length/TotalThreads; lastBlockSize=file_length; BeginPos=0; for(i=1;i<=ThreadIndex-1;i++) { lastBlockSize=lastBlockSize-BlockSize; BeginPos=BeginPos+BlockSize; } if (ThreadIndex==TotalThreads) { BgPos=BeginPos; BlkSize=lastBlockSize; } else { BgPos=BeginPos; BlkSize=BlockSize; } } 8. 添加全局变量CString filename; 和 char fn[40];用以储存被发送的文件的文件名 把它们添加到ServerDlg.cpp文件的头部。如下: // ServerDlg.cpp : implementation file // #include "stdafx.h" #include "Server.h" #include "ServerDlg.h" #include "MySocket.h" #include "process.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif ///////////////////////////////////////////////////////////////////////////// // CAboutDlg dialog used for App About CString filename; char fn[40]; 9. 双击“发送”按钮,为它添加事件BN_CLICKED的响应函数OnSend() 注意:必须在使用_beingthread()和_endthread()函数的.cpp文件的头部加上#include “process.h” void CServerDlg ::OnSend () { CFileDialog fd(TRUE); if(IDOK==fd.DoModal()) // 启动用于选择文件的对话框 { //选择了文件 filename=fd.GetFileName(); //获取选择的文件的文件名 strcpy(fn,filename.GetBuffer(0)); } else return; //按了取消按钮 for(int i=0;i<5;i++) _beginthread(SendThreadFunction,0,(void *)i); //启动工头线程(由工头线程启动工人线程) } 用传递函数参数的方式为每个线程指定序号,是因为这样可以简化程序,避免加入代码处理因使用全局变量而带来的线程同步问题。这些代码的作用是,驱动工头们到它们的岗位上(每个工头管理一个工人——数据传送线程)。 10. 编写SendThreadFunction函数 这个函数起到了工头的作用,通过参数(void*)i为每个工人线程编号和分配任务。 void SendThreadFunction(void * pParam) { int idx=(int)pParam; SendThread(idx+1); // 启动工人线程 _endthread(); } 11. 编写真正实现数据发送的函数SendThread #define ReadSize 500 void SendThread(int idx) // 传输文件数据的函数(搬运工人) { CFile file; char data[ReadSize]; long BeginPos, Size; long FileLength; long ReadOnce, LeftToRead, count; if(!file.Open(fn,CFile::modeRead|CFile::shareDenyNone)) { AfxMessageBox("read file error!"); return; } FileLength=file.GetLength(); sendSockets[idx-1]. Send(&FileLength, 4); sendSockets[idx-1]. Send(fn, 40); // 获取本线程传输任务(传送块的大小和起始位置) GetBeginPos(5 , idx, FileLength ,BeginPos, Size); //其中的5表示总共有5个线程,idx表示本线程编号 file.Seek(BeginPos, CFile::begin); //每个线程函数找到自己任务的起始点 LeftToRead=Size; while(LeftToRead>0) { ReadOnce=(LeftToRead>ReadSize)?ReadSize:LeftToRead; count=file.ReadHuge(data,ReadOnce); while((SOCKET_ERROR== sendSockets[idx-1].Send(data,count))) { } LeftToRead=LeftToRead-count; } file.Close(); } ·客户端(负责接收数据)的实现 1. 一个基于对话框的工程,并在建立的过程中选择支持windows socket。 2. 在对话框上添加“连接”按钮和“接收”按钮。 3. 用ClassWizard添加一个派生于CAsyncSocket的类CMySocket 4. 定义全局句柄数组HANDLE hEvent[5]; 5. 定义CMySocket型的全局变量listenSocket和数组receiveSockets[5] 6. 定义全局变量char fn[40];用以储存接收文件的文件名 7. 为CMySocket类添加消息OnConnect的响应函数OnConnect() void CMySocket::OnConnect(int nErrorCode) { // TODO: Add your specialized code here and/or call the base class static int File_Socket_Connected=0; if(nErrorCode==0) { File_Socket_Connected++; this->AsyncSelect(FD_READ|FD_WRITE|FD_CLOSE); if(File_Socket_Connected==5) { File_Socket_Connected=0; AfxMessageBox("连接成功"); } } CAsyncSocket::OnConnect(nErrorCode); } 8. 为“连接”按钮添加事件BN_CLICKED的响应函数OnConnect() void CServerDlg::OnConnect() { for(int i=0;i<5;i++) { receiveSockets[i].Create(); receiveSockets[i].Connect(“127.0.0.1”,7000); } } 9. 为“接收”按钮添加事件BN_CLICKED的响应函数OnReceive() void CServerDlg::OnReceive () { //以下for循环的作用是初始化事件对象为无信号的,用于监视线程识别文件传输是否结束 for(int i=0;i<5;i++) { if((hEvent[i]=CreateEvent(NULL,false,false,NULL))==NULL) AfxMessageBox("Create hE-Event Handle Failed"); ResetEvent(hEvent[i]); } _beginthread(ReceiveNotifyFunction,0,NULL); for(i=0;i<5;i++) _beginthread(ReceiveThreadFunction,0,(void *)i); } 10. 编写ReceiveThreadFunction函数 void ReceiveThreadFunction(void * pParam) { int idx=(int)pParam; ReceiveThread(idx+1); _endthread(); } 11. 编写真正实现数据发送的函数ReceiveThread #define ReadSize 500 void ReceiveThread (int idx) // 接收文件数据的函数(搬运工人) { CFile file; char data[ReadSize]; long BeginPos, Size; long FileLength; long WriteOnce; char filename[200]; sprintf(filename,"tmpsave-%d.dat",idx); if(!file.Open(filename,CFile::modeCreate|CFile::modeWrite)) { AfxMessageBox("write file error!"); return; } while(SOCKET_ERROR==receiveSockets[idx-1]. Receive(&FileLength, 4)) { } while(SOCKET_ERROR==receiveSockets[idx-1]. Receive(fn, 40)) { } // 获取本线程传输任务(传送块的大小和起始位置) GetBeginPos(5 , idx, FileLength ,BeginPos, Size); //其中的5表示总共有5个线程,idx表示本线程编号 while(Size>0) { if(SOCKET_ERROR==(WriteOnce= receiveSockets[idx-1].Receive(data,ReadSize))) continue; Size=Size-WriteOnce; file.WriteHuge(data,WriteOnce); } file.Close(); SetEvent(hEvent[idx-1]); // 发信号通知监视线程本线程任务完成 } 12. 编写监视文件接收是否结束的函数ReceiveNotifyFunction void ReceiveNotifyFunction(void * pParam) // 监视文件传输的状态的线程 { WaitForMultipleObjects(5,hEvent,true,INFINITE);//当所有工人线程都报告自己完成任务后,准备合并文件 CombineFiles(); // 拼接每个线程暂存数据的文件 _endthread(); } 13. 编写合并文件的函数CombineFiles void CombineFiles() { CFile fileDest, fileSour; char sourname[500]; static char data[10000]; long count; int i; if(!fileDest.Open(fn,CFile::modeCreate|CFile::modeWrite)) // 建立并打开目标文件 { AfxMessageBox("Combine: Make Dest File Error"); return; } for(i=0;i<5;i++) // 一次打开一个线程暂存的临时文件,把其中数据写入目标文件后,删除临时文件 { wsprintf(sourname,"tmpsave-%d.dat",i+1); if(!fileSour.Open(sourname,CFile::modeRead)) { AfxMessageBox("Combine: Open Part File Error"); return; } count=fileSour.ReadHuge(data,10000); while(count>0) { fileDest.WriteHuge(data,count); count=fileSour.ReadHuge(data,10000); } fileSour.Close(); if(!DeleteFile(sourname)) { AfxMessageBox("Combine: File-Delete Error"); return; } } fileDest.Close(); } 上述的多线程传输程序的传输速度已经相当不错了,但是在系统中运行了这个程序后,操作系统对其他程序的响应明显慢了许多。下面我们通过对比CAsyncSocket类网络通信的两种方式来分析发生这种现象的原因。 (1) 在成员函数OnReceive()中调用Receive() 在OnReceive()中使用Receive()接收数据的方式的优点是充分的利用了CAsyncSocket提供的消息机制,保证每次接收都能正常收到数据。因为OnReceive消息的到来意味着可以从端口接受有效数据,而且保证调用Receive()函数有正的返回值,也就是不可能为SOCKET_ERROR。但其缺点是把Receive()的使用限定在这个成员函数中,这样就缺乏了灵活性,不利于使用线程进行接收。 CMySocket::OnReceive() { …… Receive(buffer,100); …… } (2) 在其他地方直接调用Receive() 在这种情况下,由于没有了消息机制的支持,就需要直接使用CMySocket类对象,调用它的公有的Receive()进行轮询(即不断的调用它,并以它的返回值来判断接收情况)。当没有数据可以接收时,Receive()返回SOCKET_ERROR,用GetLastError()可以发现错误类型是WSAEWOULDBLOCK(10035),这说明此时没有可供接收的数据,否则说明已收到数据。SOCKET_ERROR这种返回值并不一定说明传输错误并且非要中断不可,它通常意味着“本套接字暂时无数据可接收”。 使用这种方法的优点是有利于在线程中进行收发操作,但是进行大量轮询而不利用消息机制会大量耗费CPU的时间。这种情况相当于一个人为了等一封信,而经常出门检查信箱,这无疑是非常累人的事(其实,他可以跟邮递员说好:“每次有信到的时候,敲门通知我”。对这个人来说这种方法的效率毫无疑问的高出了许多!)。如此,倘若使用的线程数一多,必然导致系统的速度骤然下降。典型的反映就是对于鼠标消息的响应迟钝。 CMySocket socket; void fun() { while(SOCKET_ERROR==socket.Receive(buffer,100)) { } } 因为将使用多线程传输文件,而线程函数的定义有规定的语法void __cdecl ThreadFunction(void *pParam),也就是说不能使用CAsyncSocket::OnReceive()作线程函数体,所以下面的程序必然的选择了上述第二种表示方式。问题是:能否引入CAsyncSocket提供的Receive和Send消息,从而融合(2)的灵活性和(1)高效性呢?——答案是肯定的。 创建一个全局的事件句柄,并初始化为无信号的(nonsignal)。然后,在OnReceive()中加入SetEvent(hEvent),用WaitForSingleObject函数等待hEvent变为有信号的(即有Send或Receive消息到来)。这样,稍微改动代码,就可以大大下降接收端的CPU时耗。(注:调用WaitForSingleObject函数等待后,若事件对象没有变为有信号的,那么线程将暂时不使用CPU)。 客户端的代码的改变在下面用黑体标出,服务器端的情况类似,在这里不再赘述。 #define ReadSize 500 void ReceiveThread(int idx) // 传输文件数据的函数 { …… while(Size>0) { WaitForSingleObject(hMessageCome[idx-1],2000); // 等待Receive消息 if(SOCKET_ERROR==(WriteOnce= receiveSocket[idx-1].Receive(data,ReadSize))) continue; Size=Size-WriteOnce; file.WriteHuge(data,WriteOnce); } …… } 为了实现的快捷,对于客户端工程,其实是把已建立的服务器端工程的副本作为基础,在其上删除或添加按钮和代码。读者在自行实现的时候可以考虑如何把服务器端和客户端融为一体(即实现双向的数据收发)。 另外,加入全局变量的最佳位置是定义这个变量的类的.cpp文件的头部。而且如果要在其他源文件中使用这个变量,应当用extern声明。否则,将无法访问这一变量。 比如,我希望在ServerDlg.cpp中使用在MySocket.cpp中定义的变量listenSocket,那么我必须在ServerDlg.cpp文件的头部,加入 // ServerDlg.cpp : implementation file // #include "stdafx.h" #include "Server.h" #include "ServerDlg.h" #include "MySocket.h" #include "process.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif ///////////////////////////////////////////////////////////////////////////// // CAboutDlg dialog used for App About char fn[40]; extern CMySocket listenSocket; 使用第一个单线程传输程序的的步骤是:1.启动服务器端程序,按下“发送”按钮,选择好要传送的文件;2.启动客户端程序,按下“接收”按钮。随后双方便开始传输数据了。如果文件接收完毕,程序会弹出对话框显示 “接收文件成功”。注意:单线程传输的第一个程序只适合传输小型文件。 使用后面两个例子程序的步骤是:先启动服务器端程序,按下“侦听”按钮,然后启动客户端程序,按下“连接”按钮。等到两端程序都弹出显示“连接成功”的对话框后,按下服务器端的“发送”按钮,选择要传送的文件,然后按下客户端的“接收”按钮。如果接收完成,程序会弹出对话框显示“接收文件成功”。注意:运行时必须先启动服务器端程序,后开客户端,否则可能导致连接不成功。 本章实验的理论性较强,读者如果有兴趣可以自行改进上述的代码,以得到更好的应用性。 在这里给出一些改进应用性的建议: 1. 在传送文件数据前先发送文件属性数据,以保证文件被丝毫不变的复制。 2. 实现用进度条显示文件传输进度和传送速率 3. 实现文件队列,自动依次传送队列中的所有文件 4. 实现传送文件夹 5. 让接收方的线程同时写一个文件,从而避免接收完文件后合并的时间耗费 ·潜藏的问题——Socket匹配 请看下面的代码: Client端: CMySocket socket[10]; for(int i=0;i<10;i++) { socket[i].Create(0,SOCK_STREAM,FD_CONNECT); socket[i].Connect(“127.0.0.1”,5000); } Server端: CMySocket socket[10]; for(int i=0;i<10;i++) { socket[i].Create(5000,SOCK_STREAM,FD_ACCEPT); socket[i].Listen(5); } void FileSocket::OnAccpet(int nErrorCode) { static int Accepted=0; Accept(socket[Accepted]); socket[Accepted].AsyncSelect(FD_READ|FD_WRITE|FD_CLOSE); Accepted++; } 理想中的情况是:Client端的socket[i]与Server端的socket[i]连接(0<=i<=9)。实际上,大多数时候也确实如此,但是却并不总是这样。也就是说,在Client端先调用Connect函数的socket[m]并不一定先于socket[n]被Server端接受(m

下载文档到电脑,查找使用更方便

文档的实际排版效果,会与网站的显示效果略有不同!!

需要 10 金币 [ 分享文档获得金币 ] 4 人已下载

下载文档