Chinaunix首页 | 论坛 | 博客
  • 博客访问: 9169098
  • 博文数量: 1669
  • 博客积分: 16831
  • 博客等级: 上将
  • 技术积分: 12594
  • 用 户 组: 普通用户
  • 注册时间: 2011-02-25 07:23
个人简介

柔中带刚,刚中带柔,淫荡中富含柔和,刚猛中荡漾风骚,无坚不摧,无孔不入!

文章分类

全部博文(1669)

文章存档

2023年(4)

2022年(1)

2021年(10)

2020年(24)

2019年(4)

2018年(19)

2017年(66)

2016年(60)

2015年(49)

2014年(201)

2013年(221)

2012年(638)

2011年(372)

分类: WINDOWS

2012-09-04 17:29:49

手把手教你玩转网络编程模型之完成例程(Completion Routine)篇(下)
分类: VC网络编程基础 4677人阅读 评论(17) 举报

 续 手把手教你玩转网络编程模型之完成例程(Completion Routine)篇(上)

 

 

四.         完成例程的实现步骤

基础知识方面需要知道的就是这么多,下面我们配合代码,来一步步的讲解如何亲手实现一个完成例程模型(前面几步的步骤和基于事件通知的重叠I/O方法是一样的)。

【第一步】创建一个套接字,开始在指定的端口上监听连接请求

和其他的SOCKET初始化全无二致,直接照搬即可,在此也不多费唇舌了,需要注意的是为了一目了然,我去掉了错误处理,平常可不要这样啊,尽管这里出错的几率比较小。

 

  1. WSADATA wsaData;  
  2. WSAStartup(MAKEWORD(2,2),&wsaData);  
  3.   
  4. ListenSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);  //创建TCP套接字   
  5.   
  6. SOCKADDR_IN ServerAddr;                           //分配端口及协议族并绑定   
  7. ServerAddr.sin_family=AF_INET;                                  
  8. ServerAddr.sin_addr.S_un.S_addr  =htonl(INADDR_ANY);            
  9. ServerAddr.sin_port=htons(11111);        // 在11111端口监听   
  10.                                     // 端口号可以随意更改,但最好不要少于1024   
  11.   
  12. bind(ListenSocket,(LPSOCKADDR)&ServerAddr, sizeof(ServerAddr)); // 绑定套接字   
  13.   
  14. listen(ListenSocket, 5);                                   //开始监听  
WSADATA wsaData; WSAStartup(MAKEWORD(2,2),&wsaData); ListenSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); //创建TCP套接字 SOCKADDR_IN ServerAddr; //分配端口及协议族并绑定 ServerAddr.sin_family=AF_INET; ServerAddr.sin_addr.S_un.S_addr =htonl(INADDR_ANY); ServerAddr.sin_port=htons(11111); // 在11111端口监听 // 端口号可以随意更改,但最好不要少于1024 bind(ListenSocket,(LPSOCKADDR)&ServerAddr, sizeof(ServerAddr)); // 绑定套接字 listen(ListenSocket, 5); //开始监听

【第二步】接受一个入站的连接请求

  一个accept就完了,都是一样一样一样一样的啊~~~~~~~~~~

 至于AcceptEx的使用,在完成端口中我会讲到,这里就先不一次灌输这么多了,不消化啊^_^

 

  1. AcceptSocket = accept (ListenSocket, NULL,NULL) ;   
AcceptSocket = accept (ListenSocket, NULL,NULL) ;

 

当然,这里是我偷懒,如果想要获得连入客户端的信息(记得论坛上也常有人问到),accept的后两个参数就不要用NULL,而是这样

 

 

  1. SOCKADDR_IN ClientAddr;                   // 定义一个客户端得地址结构作为参数   
  2. int addr_length=sizeof(ClientAddr);  
  3. AcceptSocket = accept(ListenSocket,(SOCKADDR*)&ClientAddr, &addr_length);  
  4. // 于是乎,我们就可以轻松得知连入客户端的信息了   
  5. LPCTSTR lpIP =  inet_ntoa(ClientAddr.sin_addr);      // 连入客户端的 IP   
  6. UINT nPort = ClientAddr.sin_port;                      // 连入客户端的Port  
SOCKADDR_IN ClientAddr; // 定义一个客户端得地址结构作为参数 int addr_length=sizeof(ClientAddr); AcceptSocket = accept(ListenSocket,(SOCKADDR*)&ClientAddr, &addr_length); // 于是乎,我们就可以轻松得知连入客户端的信息了 LPCTSTR lpIP = inet_ntoa(ClientAddr.sin_addr); // 连入客户端的 IP UINT nPort = ClientAddr.sin_port; // 连入客户端的Port

【第三步】准备好我们的重叠结构

有新的套接字连入以后,新建立一个WSAOVERLAPPED重叠结构(当然也可以提前建立好),准备绑定到我们的重叠操作上去。这里也可以看到和上一篇中的明显区别,就是不用再为WSAOVERLAPPED结构绑定一个hEvent了。

  1. // 这里只定义一个,实际上是每一个SOCKET的每一个操作都需要绑定一个重叠结构的,所以在实际使用面对多个客户端的时候要定义为数组,详见示例代码;   
  2. WSAOVERLAPPED AcceptOverlapped;   
  3. ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED));      // 置零  
// 这里只定义一个,实际上是每一个SOCKET的每一个操作都需要绑定一个重叠结构的,所以在实际使用面对多个客户端的时候要定义为数组,详见示例代码; WSAOVERLAPPED AcceptOverlapped; ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED)); // 置零

    

【第四步】开始在套接字上投递WSARecv请求,需要将第三步准备的WSAOVERLAPPED结构和我们定义的完成例程函数为参数

各个变量都已经初始化OK以后,我们就可以开始进行具体的Socket通信函数调用了,然后让系统内部的重叠结构来替我们管理I/O请求,我们只用等待网络通信完成后调用咱们的回调函数就OK了。

这个步骤的重点就是 绑定一个Overlapped变量和一个完成例程函数

  1. // 将WSAOVERLAPPED结构指定为一个参数,在套接字上投递一个异步WSARecv()请求   
  2. // 并提供下面的作为完成例程的_CompletionRoutine回调函数(函数名字)   
  3. if(WSARecv(  
  4.     AcceptSocket,  
  5.     &DataBuf,  
  6.     1,  
  7.     &dwRecvBytes,  
  8.     &Flags,  
  9.     &AcceptOverlapped,  
  10.     _CompletionRoutine) == SOCKET_ERROR)  // 注意我们传入的回调函数指针   
  11.     {  
  12.         if(WSAGetLastError() != WSA_IO_PENDING)  
  13.         {  
  14.             ReleaseSocket(nSockIndex);  
  15.             continue;  
  16.             }  
  17.         }  
  18. }  
// 将WSAOVERLAPPED结构指定为一个参数,在套接字上投递一个异步WSARecv()请求 // 并提供下面的作为完成例程的_CompletionRoutine回调函数(函数名字) if(WSARecv( AcceptSocket, &DataBuf, 1, &dwRecvBytes, &Flags, &AcceptOverlapped, _CompletionRoutine) == SOCKET_ERROR) // 注意我们传入的回调函数指针 { if(WSAGetLastError() != WSA_IO_PENDING) { ReleaseSocket(nSockIndex); continue; } } }

  

【第五步】 调用WSAWaitForMultipleEvents函数或者SleepEx函数等待重叠操作返回的结果

  我们在前面提到过,投递完WSARecv操作,并绑定了Overlapped结构和完成例程函数之后,我们基本就是完事大吉了,等了系统自己去完成网络通信,并在接收到数据的时候,会自动调用我们的完成例程函数。

  而我们在主线程中需要做的事情只有:做别的事情,并且等待系统完成了完成例程调用后的返回结果。

就是说在WSARecv调用发起完毕之后,我们不得不在后面再紧跟上一些等待完成结果的代码。有两种办法可以实现:

1)    和上一篇重叠I/O中讲到的一样,我们可以使用WSAWaitForMultipleEvent来等待重叠操作的事件通知, 方法如下:

 

  1. // 因为WSAWaitForMultipleEvents() API要求   
  2. // 在一个或多个事件对象上等待, 但是这个事件数组已经不是和SOCKET相关联的了   
  3. // 因此不得不创建一个伪事件对象.    
  4. WSAEVENT EventArray[1];       
  5. EventArray[0] = WSACreateEvent();                        // 建立一个事件   
  6.         ////////////////////////////////////////////////////////////////////////////////   
  7. // 然后就等待重叠请求完成就可以了,注意保存返回值,这个很重要   
  8. DWORD dwIndex = WSAWaitForMultipleEvents(1,EventArray,FALSE,WSA_INFINITE,TRUE);  
// 因为WSAWaitForMultipleEvents() API要求 // 在一个或多个事件对象上等待, 但是这个事件数组已经不是和SOCKET相关联的了 // 因此不得不创建一个伪事件对象. WSAEVENT EventArray[1]; EventArray[0] = WSACreateEvent(); // 建立一个事件 //////////////////////////////////////////////////////////////////////////////// // 然后就等待重叠请求完成就可以了,注意保存返回值,这个很重要 DWORD dwIndex = WSAWaitForMultipleEvents(1,EventArray,FALSE,WSA_INFINITE,TRUE);

这里参数的含义我就不细说了,MSDN上一看就明白,调用这个函数以后,线程就会置于一个警觉的等待状态,注意 fAlertable 参数一定要设置为 TRUE

2)    可以直接使用SleepEx函数来完成等待,效果都是一样的。

SleepEx函数调用起来就简单得多,它的函数原型定义是这样的

    

  1. DWORD SleepEx(  
  2.              DWORD dwMilliseconds,  // 等待的超时时间,如果设置为INFINITE就会一直等待下去   
  3.              BOOL   bAlertable   // 是否置于警觉状态,如果为FALSE,则一定要等待超时时间完毕之后才会返回,这里我们是希望重叠操作一完成就能返回,所以同(1)一样,我们一定要设置为TRUE   
  4.  );  
DWORD SleepEx( DWORD dwMilliseconds, // 等待的超时时间,如果设置为INFINITE就会一直等待下去 BOOL bAlertable // 是否置于警觉状态,如果为FALSE,则一定要等待超时时间完毕之后才会返回,这里我们是希望重叠操作一完成就能返回,所以同(1)一样,我们一定要设置为TRUE );

    调用这个函数的时候,同样注意用一个DWORD类型变量来保存它的返回值,后面会派上用场。

【第六步】通过等待函数的返回值取得重叠操作的完成结果

这是我们最关心的事情,费了那么大劲投递的这个重叠操作究竟是个什么结果呢?就是通过上一步中我们调用的等待函数的DWORD类型的返回值,正常情况下,在操作完成之后,应该是返回WAIT_IO_COMPLETION,如果返回的是 WAIT_TIMEOUT,则表示等待设置的超时时间到了,但是重叠操作依旧没有完成,应该通过循环再继续等待。如果是其他返回值,那就坏事了,说明网络通信出现了其他异常,程序就可以报错退出了……

判断返回值的代码大致如下:

  1. ///////////////////////////////////////////////////////////////////////////////////   
  2. // 返回WAIT_IO_COMPLETION表示一个重叠请求完成例程代码的结束。继续为更多的完成例程服务   
  3. if(dwIndex == WAIT_IO_COMPLETION)  
  4. {  
  5. TRACE("重叠操作完成.../n");  
  6. }  
  7. else if( dwIndex==WAIT_TIMEOUT )  
  8. {  
  9.      TRACE(“超时了,继续调用等待函数”);  
  10. }  
  11. else  
  12. {  
  13.     TRACE(“废了…”);  
  14. }  
/////////////////////////////////////////////////////////////////////////////////// // 返回WAIT_IO_COMPLETION表示一个重叠请求完成例程代码的结束。继续为更多的完成例程服务 if(dwIndex == WAIT_IO_COMPLETION) { TRACE("重叠操作完成.../n"); } else if( dwIndex==WAIT_TIMEOUT ) { TRACE(“超时了,继续调用等待函数”); } else { TRACE(“废了…”); }

 

操作完成了之后,就说明我们上一个操作已经成功了,成功了之后做什么?当然是继续投递下一个重叠操作了啊…..继续上面的循环。

 

【第七步】继续回到第四步,在套接字上继续投递WSARecv请求,重复步骤4-7

 大家可以参考我的代码,在这里就先不写了,因为各位都一定比我smart,领悟了关键所在以后,稍作思考就可以灵活变通了。

 

【第八步】“享受”接收到的数据

朋友们看到这里一定会问,我忙活了这么久,那客户端传来的数据在哪里接收啊?怎么一点都没有提到呢……

这个问题问得好,我们写了这么多代码图个什么呢?

其实想要读取客户端的数据很简单,因为我们在WSARecv调用的时候,是传递了一个WSABUF的变量的,用于保存网络数据,而在我们写的完成例程回调函数里面,就可以取到客户端传送来的网络数据了。

因为系统在调用我们完成例程函数的时候,其实网络操作已经完成了,WSABUF里面已经有我们需要的数据了,只是通过完成例程来进行后期的处理。具体可以参考示例代码。 DataBuf.buf就是一个char*字符串指针,听凭你的处理吧,我就不多说了。

 

一.         实际应用中应该完善的地方

其实我一直都很想把我以前做的工程中的代码贴出来给大家分享,但是代码实在是太繁杂了,仅仅把网络通信的部分剥离出来,不经过测试的话,肯定还会有其他的很多问题,反而误导了初学者,不过我的计划是在写下一个“完成端口”部分的时候,直接把项目中的一部分代码拿出来试试看吧……

总之网络服务器端程序,在实际应用的时候,关键的几点就是:

1)    要考虑到客户端很多、通信量很大的时候,如何去处理,如何尽可能的减小开销,提高效率;

2)    多个线程之间共用一些变量的时候,一定要注意到同步问题;

3)    作为一个网络程序,出现异常是家常便饭,一定要把代码写得尽可能的健壮,要尽量全面的考虑处理各种各样的错误;

4)    尽量不要出现各种字符缓冲区的问题,写安全的代码,防止被黑客利用……(这点似乎扯远了,但是确实是一个很现实的问题)

     其他的问题,还希望各位这方面的网络专家使劲批评指正,因为代码是很多年前的了,一定存在着很多的问题。

 

                                                                                                                                           --- Finished at DLUT

                                                                                                                                           --- 2009-02-16

 

 

 

 

 

 

查看评论
15楼 nieanan3602 2012-01-13 17:12发表 [回复] 谢谢楼主 14楼 xx2088 2011-01-20 12:55发表 [回复] 谢谢楼主,的耐心 13楼 pizidi 2010-09-29 10:47发表 [回复] 还有:
1:停止监听后,重新开启监听后,没有将全局变量nSockTotal初始化,带来隐患,有可能重新监听后不能将工作线程唤醒。
该问题,通过在停止监听函数中加入初始化nSockTotal的代码就可以解决。
2:绑定重叠结构的缓冲区DATABUF的时候会出错,有可能出现将客户端a发来的数据当做客户端b发来的数据。是因为你用了局部变量char buffer[DATA_BUFSIZE],通过该为动态申请就可以了。
这两个问题很严重,哥们 Re: PiggyXP 2010-12-14 18:55发表 [回复] 非常感谢你的指正,代码我都好多年没有更新了,非常高兴你能够指出这些问题来,希望大家都能看到 12楼 pizidi 2010-09-28 14:44发表 [回复] 有bug的,你回调函数里,不能根据Overlapped的的Internal值来判断,因为它不是唯一的,有两个结构的Internal相同。
该为直接比较Overlapped就可以了。
int GetCurrentSocketIndex(LPWSAOVERLAPPED Overlapped)
{
for(int i=0;i<nSockTotal;i++)
{

if(&AcceptOverlapped == Overlapped)
return i;
}

return -1;
}

谢谢你的代码,省的我写了,哥们直接拿来用了。额。。拿来主义 11楼 wangrui007 2010-08-27 15:52发表 [回复] 收获不小。
楼主的这种分享的精神值得敬佩,中国it有希望了!! 10楼 gjw116 2010-06-02 12:29发表 [回复] 写的真好,加油,等待你的完成端口 9楼 icycode 2009-11-26 17:24发表 [回复] 写的真好,收获很大 8楼 hailong1988 2009-11-26 17:16发表 [回复] 完成端口的文章在哪啊 7楼 sougoliu 2009-10-15 16:25发表 [回复] 我为什么下不了这个源程序,在空间中点不到下载的内容。求救,我的邮箱是:sougoliu@126.com 6楼 damndarn 2009-07-23 17:16发表 [回复] 楼主写的非常好,站在初学者的立场上考虑问题。值得尊敬。
可惜第5节之后没看得太懂。 5楼 babelan 2009-05-25 15:02发表 [回复] 我准备应用完成例程模型,把工控上位机作为监控客户端,通过串口服务器采集同一IP下多个端口提供的现场数据。
博文的内容写得很好!博主对于技术分享的观点和作为值得尊敬! 4楼 LxzLLL 2009-05-16 21:45发表 [回复] 支持楼主 3楼 pasharp 2009-04-16 21:05发表 [回复] 太好�?刚好学到这里 !“身为一个初学者,时常能体味到初学者入门的艰辛,所以总是想抽空作点什么来尽我所能的帮助那些需要帮助的人。我也希望大家能把自己的所学和他人一起分享,不要去鄙视别人索取时的贪婪,因为最应该被鄙视的是不肯付出时的吝啬。”狂热的喜欢和支持你的这种观念!什么时候把完成端口也写一撒哈�?d=0.6373392057509257 2楼 chinazjf 2009-03-04 09:27发表 [回复] 楼猪很有爱心,谢谢 1楼 PiggyXP 2009-02-19 17:41发表 [回复] 排版排了半天,还是排得这么个烂样子,算了,也不弄了,大家基本能凑合看就行了,实在抱歉......
阅读(797) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~