非阻塞模式WinSock编程入门

非阻塞模式WinSock编程入门 

非阻塞模式 WinSock 编程入门

 

介绍

WinSock Windows 提供的包含了一系列网络编程接口的套接字程序库。在这篇文章中,我们将介绍如何把它的非阻塞模式引入到应用程序中。文章中所讨论的通信均为面向连接的通信( TCP ),为清晰起见,文章对代码中的一些细枝末节进行了删减,大家可以依照文末的链接下载完整的工程源码来获取这部分内容。

 

阻塞模式 WinSock

         下述伪代码给出了阻塞模式下 WinSock 的使用方式。

  1. //---------------------------------------   
  2. // 服务器   
  3. //----------------------------------------   
  4. // WinSock初始化   
  5. WSAStartup();  
  6.   
  7. // 创建服务器套接字   
  8. SOCKET server = socket();  
  9.   
  10. // 绑定到本机端口   
  11. bind(server);   
  12.   
  13. // 开始监听   
  14. listen(server);   
  15.   
  16. // 接收到客户端连接,分配一个客户端套接字   
  17. SOCKET client = accept(server);   
  18.   
  19. // 使用新分配的客户端套接字进行消息收发   
  20. send(client);   
  21. recv(client);  
  22.   
  23. // 关闭客户端套接字   
  24. closesocket(client);   
  25.   
  26. // 关闭服务器套接字   
  27. closesocket(server);  
  28.   
  29. // 卸载WinSock   
  30. WSACleanup();  
  

  1. //---------------------------------------   
  2. // 客户端   
  3. //---------------------------------------   
  4. WSAStartup();  
  5.   
  6. // 创建客户端套接字   
  7. SOCKET client = socket();  
  8.   
  9. // 绑定本机端口   
  10. bind(client);  
  11.   
  12. // 连接到服务器   
  13. ServerAddress server;  
  14. connect(client, server);  
  15.   
  16. // 确立连接后收发消息   
  17. recv(client);  
  18. send(client);  
  19.   
  20. // 关闭客户端套接字   
  21. closesocket(client);  
  22.   
  23. WSACleanup();  
  

         代码中,服务器端的 accept() ,客户端的 connect() ,以及服务器和客户端中共同的 recv() send() 函数均会产生阻塞。

服务器在调用 accept() 后不会返回,直到接收到客户端的连接请求;

客户端在调用 connect() 后不会返回,直到对服务器连接成功或者失败;

服务器和客户端在调用 recv() 后不会返回,直到接收到并读取完一条消息;

服务器和客户端在调用 send() 后不会返回,直到发送完待发送的消息。

如果这两段代码被放在 Windows 程序的主线程中,你会发现消息循环被阻塞,程序不再响应用户输入及重绘请求。为了解决这个问题,你可能会想到开辟另外一个线程来运行这些代码。这是可行的,但是考虑到每个 SOCKET 都不应该被其他 SOCKET 的操作所阻塞,是不是需要为每个 SOCKET 开辟一个线程?再考虑到同一 SOCKET 的一个读写操作也不应该被另外一个读写操作所阻塞,是不是应该再为每个 SOCKET 的读和写分别开辟一个线程?一般来说,这种自实现的多线程解决方案带来的诸多线程管理方面的问题,是你绝对不会想要遇到的。

 

非阻塞模式 WinSock

         所幸的是, WinSock 同时提供了非阻塞模式,并提出了几种 I/O 模型。最常见的 I/O 模型有 select 模型、 WSAAsyncSelect 模型及 WSAEventSelect 模型,下面选择其中的 WSAAsyncSelect 模型进行介绍。

         使用 WSAAsyncSelect 模型将非阻塞模式引入到应用程序中的过程看起来很简单,事实上你只需要多添加一个函数就够了。

int WSAAsyncSelect(SOCKET s, HWND hWnd, unsigned int wMsg, long lEvent);

该函数会自动将套接字设置为非阻塞模式,并且把发生在该套接字上且是你所感兴趣的事件,以 Windows 消息的形式发送到指定的窗口,你需要做的就是在传统的消息处理函数中处理这些事件。参数 hWnd 表示指定接受消息的窗口句柄;参数 wMsg 表示消息码值(这意味着你需要自定义一个 Windows 消息码);参数 IEvent 表示你希望接受的网络事件的集合,它可以是如下值的任意组合:

FD_READ, FD_WRITE, FD_OOB, FD_ACCEPT, FD_CONNECT, FD_CLOSE

         之后,就可以在我们熟知的 Windows 消息处理函数中处理这些事件。如果在某一套接字 s 上发生了一个已命名的网络事件,应用程序窗口 hWnd 会接收到消息 wMsg 。参数 wParam 即为该事件相关的套接字 s ;参数 lParam 的低字段指明了发生的网络事件, lParam 的高字段则含有一个错误码,事件和错误码可以通过下面的宏从 lParam 中取出:

#define WSAGETSELECTEVENT(lParam) LOWORD(lParam)

#define WSAGETSELECTERROR(lParam) HIWORD(lParam)

 

下面继续使用伪代码来帮助阐述如何将上一节的阻塞模式 WinSock 应用升级到非阻塞模式。

首先自定义一个 Windows 消息码,用于标识我们的网络消息。

  1. #define WM_CUSTOM_NETWORK_MSG (WM_USER + 100)   

 

服务器端,在监听之前,将监听套接字置为非阻塞模式,并且标明其感兴趣的事件为 FD_ACCEPT

  1. …  
  2. WSAAsyncSelect(server, wnd, WM_CUSTOM_NETWORK_MSG, FD_ACCEPT);  
  3.   
  4. // 开始监听   
  5. listen(server);  
  

客户端,在连接之前,将套接字置为非阻塞模式,并标明其感兴趣的事件为 FD_CONNECT

  1. …  
  2. WSAAsyncSelect(client, wnd, WM_CUSTOM_NETWORK_MSG, FD_CONNECT);  
  3.   
  4. // 连接到服务器   
  5. ServerAddress server;  
  6. connect(client, server);  
  

接着,在 Windows 消息处理函数中,我们将处理监听事件、连接事件、及读写事件,方便起见,这里将服务器和客户端的处理代码放在了一起。

  1. LRESULT  CALLBACK WndProc( HWND  hWnd,  UINT  message,  WPARAM  wParam,  LPARAM  lParam)  
  2. {  
  3.     switch  (message)  
  4.     {  
  5.     …  
  6.     case  WM_CUSTOM_NETWORK_MSG:  // 自定义的网络消息码   
  7.         {  
  8.             SOCKET socket = (SOCKET)wParam; // 发生网络事件的套接字   
  9.             long  event = WSAGETSELECTEVENT(lParam);  // 事件   
  10.             int  error = WSAGETSELECTERROR(lParam);  // 错误码   
  11.   
  12.             switch  (event)  
  13.             {  
  14.             case  FD_ACCEPT:  // 服务器收到新客户端的连接请求   
  15.                 {  
  16.                     // 接收到客户端连接,分配一个客户端套接字   
  17.                     SOCKET client = accept(socket);   
  18.                     // 将新分配的客户端套接字置为非阻塞模式,并标明其感兴趣的事件为读、写及关闭   
  19.                     WSAAsyncSelect(client, hWnd, message, FD_READ | FD_WRITE | FD_CLOSE);  
  20.                 }  
  21.                 break ;  
  22.             case  FD_CONNECT:  // 客户端连接到服务器的操作返回结果   
  23.                 {  
  24.                     // 成功连接到服务器,将客户端套接字置为非阻塞模式,并标明其感兴趣的事件为读、写及关闭   
  25.                     WSAAsyncSelect(socket, hWnd, message, FD_READ | FD_WRITE | FD_CLOSE);  
  26.                 }  
  27.                 break ;  
  28.             case  FD_READ:  // 收到网络包,需要读取   
  29.                 {  
  30.                     // 使用套接字读取网络包   
  31.                     recv(socket);  
  32.                 }  
  33.                 break ;  
  34.             case  FD_WRITE:  
  35.                 {  
  36.                     // FD_WRITE的处理后面会具体讨论   
  37.                 }  
  38.                 break ;  
  39.             case  FD_CLOSE:  // 套接字的连接方(而非本地socket)关闭消息   
  40.                 {  
  41.                 }  
  42.                 break ;  
  43.             default :  
  44.                 break ;  
  45.             }  
  46.         }  
  47.         break ;  
  48.     …  
  49.     }  
  50.     …  
  51. }  
  

以上就是非阻塞模式 WinSock 的应用框架, WSAAsyncSelect 模型将套接字和 Windows 消息机制很好地粘合在一起,为用户异步 SOCKET 应用提供了一种较优雅的解决方案。

 

扩展讨论

         WinSock 在系统底层为套接字收发网络数据各提供一个缓冲区,接收到的网络数据会缓存在这里等待应用程序读取,待发送的网络数据也会先写进这里之后通过网络发送。

相关的,针对 FD_READ FD_WRITE 事件的读写处理,因涉及的内容稍微复杂而容易使人困惑,这里需要特别进行讨论。

         FD_READ 事件中,使用 recv() 函数读取网络包数据时,由于事先并不知道完整网络包的大小,所以需要多次读取直到读完整个缓冲区。这就需要类似如下代码的调用:

  1. void * buf = 0;  
  2. int  size = 0;  
  3. while  ( true )  
  4. {  
  5.     char  tmp[128];  
  6.     int  bytes = recv(socket, tmp, 128, 0);  
  7.     if  (bytes <= 0)  
  8.         break ;  
  9.     else   
  10.     {  
  11.         int  new_size = size + bytes;  
  12.         buf = realloc(buf, new_size);  
  13.         memcpy((void *)((( char *)buf) + size), tmp, bytes);  
  14.         size = new_size;  
  15.     }  
  16. }  
  17. // 此时数据已经从缓冲区全部拷贝到buf中,你可以在这里对buf做一些操作   
  18. …  
  19. free(buf);  
  

         这一切看起来都没有什么问题,但是如果程序运行起来,你会收到比预期多出许多的 FD_READ 事件。如 MSDN 所述,正常的情况下,应用程序应当为每一个 FD_READ 消息仅调用一次 recv() 函数。如果一个应用程序需要在一个 FD_READ 事件处理中调用多次 recv() ,那么它将会收到多个 FD_READ 消息,因为每次未读完缓冲区的 recv() 调用,都会重新触发一个 FD_READ 消息。针对这种情况,我们需要在读取网络包前关闭掉 FD_READ 消息通知,读取完这后再进行恢复,关闭 FD_READ 消息的方法很简单,只需要调用 WSAAsyncSelect 时参数 lEvent FD_READ 字段不予设置即可。

  1. // 关闭FD_READ事件通知   
  2. WSAAsyncSelect(socket, hWnd, message, FD_WRITE | FD_CLOSE);  
  3. // 读取网络包   
  4. …  
  5. // 再次打开FD_READ事件通知   
  6. WSAAsyncSelect(socket, hWnd, message, FD_WRITE | FD_CLOSE | FD_READ);  
  

         第二个需要讨论的是 FD_WRITE 事件。这个事件指明缓冲区已经准备就绪,有了多出的空位可以让应用程序写入数据以供发送。该事件仅在两种情况下被触发:

1. 套接字刚建立连接时,表明准备就绪可以立即发送数据。

2. 一次失败的 send() 调用后缓冲区再次可用时。如果系统缓冲区已经被填满,那么此时调用 send() 发送数据,将返回 SOCKET_ERROR ,使用 WSAGetLastError() 会得到错误码 WSAEWOULDBLOCK 表明被阻塞。这种情况下当缓冲区重新整理出可用空间后,会向应用程序发送 FD_WRITE 消息,示意其可以继续发送数据了。

所以说收到 FD_WRITE 消息并不单纯地等同于这是使用 send() 的唯一时机。一般来说,如果需要发送消息,直接调用 send() 发送即可。如果该次调用返回值为 SOCKET_ERROR WSAGetLastError() 得到错误码 WSAEWOULDBLOCK ,这意味着缓冲区已满暂时无法发送,此刻我们需要将待发数据保存起来,等到系统发出 FD_WRITE 消息后尝试重新发送。也就是说,你需要针对 FD_WRITE 构建一套数据重发的机制,文末的工程源码里包含有这套机制以供大家参考,这里不再赘述。

 

结语

         至此,如何在非阻塞模式下使用 WinSock 进行编程介绍完毕,这个框架可以满足大多数网络游戏客户端及部分服务器的通信需求。更多应用层面上的问题(如 TCP 粘包等)这里没有讨论,或许会在以后的文章中给出。

         文章相关工程源码请移步此处下载 http://download.csdn.net/source/2852485 。该源码展示了采用非阻塞模式编程的服务器和客户端,建立连接后,在服务器窗口输入空格会向所有客户端发送一条字符串消息。源码中对网络通信部分做了简单封装,所以代码结构会和文中的伪代码稍有不同。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值