socket通信之——同步选择机制select()与异步选择机制WSAASyncSelect()的探讨

来源:互联网 发布:php前台模板 编辑:程序博客网 时间:2024/05/01 10:17

内容摘要:本文首先介绍了同步选择机制和异步选择机制的基本概念,然后通过讲述消息驱动与socket服务机制的矛盾引入了异步选择机制,接着本文结合一个实际的异步选择通信案例来具体介绍异步选择机制设计中的相关细节和方法,最后对异步选择机制的特点进行总结并和同步选择机制做了一番比较,得出结论。

关键字:异步选择 同步选择 socket服务 

 

AbstractIn this essay, the Synchronized Selection mechanism and Asynchronized Selection mechanism is introduced at first. Then it tells why Asynchronized Selection mechanism is needed via the conflict between event-driven system and socket service mechanism. After that a practical instance for Asynchronized Selection mechanism is given to explain it more clearly concerned with corresponding design methods and details. At last a conclusion for the feature of Asynchronized Selection mechanism and the difference between Synchronized Selection mechanism and it is shown.

Key WordsAsynchronized Selection, Synchronized Selection, socket service

 

 

 

 

 

 

一、同步选择机制与异步选择机制的基本概念

1.同步选择机制简介

同步选择机制的应用是基于多路复用的实际问题模型。为了能够使多条连接上的数据同时通信,select()同步选择机制应运而生。

Select()函数是同步选择机制中最基础最核心的概念,下面对此函数的功能、参数和相关要素做必要的说明:

功能——该函数能检查多个套接字的状态,服务器可以根据这些信息做出相应的处理;

输入——套接字队列;

输出——各待查套接字的当前状态;

函数定义:

    int select ( IN int nfds, IN OUT fd_set * readfds,

             IN OUT fd_set *writefds, IN OUT fd_set *exceptfds,

             IN struct timeval *timeout )

基于select()的程序可以认为是具有事件驱动的特点,而select可以看做事件发生器,对事件进行分发处理。

Select同步选择机制处理事件的基本流程如下图1所示:

 

1 select同步选择机制的事件处理流程

 

 

2.异步选择机制简介

异步选择机制的提出是为了解决窗口程序中所遇到的窗口无响应问题,这一点将在第二部分进行详细的说明。

因为异步选择机制是对同步选择机制的一些修正和改良,所以它不仅能够解决窗口不响应的问题,并且同样能顾实现多路复用。

异步选择机制中最核心的概念是WSAAsyncSelect()函数,这一函数的详细说明也将在第二部分展开。

异步选择机制处理事件的基本流程如下图2所示:

 

异步选择机制的事件处理流程

 

二、选用异步选择机制的背景

1.消息驱动与socket服务机制的矛盾

下图是基于select的消息驱动机制基本框架,可以看到,该框架中有两个循环:一个是基于Windows(窗口)消息的循环,一个是基于select(读事件、写事件等)的循环。

 

基于select的消息驱动机制

 

一个显而易见的事实是:当程序陷入基于select的事件循环后,没有办法跳出,则不能正常响应窗口消息。

矛盾的根源在于:循环等待客户连接和循环等待客户数据。在窗口程序中循环或者阻塞都会影响到主线程对消息的响应。

2.解决矛盾的两种思路

根据上面提出的矛盾,有两种解决问题的思路:第一种办法是为所有需要循环等待的程序段生成线程,第二种办法是不在程序中循环等待并不断接受客户连接。

第一种办法的解决思想是:为需要循环等待客户连接的部分生成子线程,为需要循环等待数据连接的部分生成子线程。则程序框架如下所示:

 

多线程解决方案

 

 第二种办法的解决思想如下图所示:

 

异步响应解决方案

 

窗口程序是基于消息的,如果将客户连接、客户数据到达也映射为消息则通信程序也可以基于消息驱动,不必自己去循环等待,此即异步选择机制。

 

3.异步选择机制中的重要函数WSAAsyncSelect( )

     int WSAAsyncSelect( SOCKET        s,

                        HWND     hWnd,

                         unsigned int wMsg,

                          long        lEvent)

     wMsg:套接字消息,如UM_SOCK,该消息通过以下方式定义

            #define UM_SOCK WM_USER1

 

     lEvent:套接字事件,事件类型有

 

 

     FD_READ:有数据接收

     FD_WRITE:有数据可以发送

     FD_ACCEPT:有客户建立连接

     FD_CONNECT:与服务器建立连接,或连接失败

     FD_CLOSE:连接被关闭

     FD_OOB:带外(紧急)数据到达

    函数功能及使用方法说明:该函数用于将指定的套接字上发生的指定消息

及事件向指定的窗口注册。当套接字上发生指定事件时,系统会通过消息机制通

知指定的窗口函数处理。

    利用WSAAsyncSelect函数向系统注册一个或多个套接字事件,如

WSAASyncSelect(s,hWnd,UM_SOCK,FD_READ|FD_ACCEPT|FD_CLOSE);

当窗口收到套接字消息后,Wndproc函数将被触发,此时传入的参数中messageUM_SOCK,事件用lParam通知,发生事件的套接字标识符由wParam通知。

不同作用的套接字上注册的事件可能不同,它们可能都会注册FD_READ事件,得到数据到达的通知。而主套接字上注册FD_ACCEPT事件,从套接字上注册FD_READ|FD_CLOSE等事件,客户套接字则主要注册FD_CONNECT事件。

值得注意的是,使用了WSAASyncSelect的套接字自动变为非阻塞状态,这一点与select不同。

 

三、同步/异步选择机制应用实例解析

1.设计步骤及关键思路

1.1设计基于select同步选择机制的Windows消息驱动程序

(1)一个窗口程序的基本框架

  WinMain() {

   RegisterClass();   //Initiate

   CreatWindow();    

   ShowWindow();

   UpdataWindow();

   While(GetMessage(&msg)) {

     TranslateMessage();

     DispatchMessage();

   }

  }

 

 

  WndProc(msg) {

   swtich(msg) {

case WM_CREATE:

   Initiate the user program;

   break;

case WM_DESTROY:

   PostQuitMessage();

   break;

case WM_START:

   StartServer();

   break;

default DefWindowProc(); 

   }

  }

 

(2)基于select同步选择机制的准备工作

  struct socket_list {

SOCKET MainSock;

int num;

SOCKET sock_array[SIZE];

  }

  void init_list(socket_list *list);  //初始化管理队列

  void insert_list(SOCKET s,sock_list *list); //管理队列插入函数

  void delete_list(SOCKET s,sock_list *list); //管理队列删除函数

  void make_fdlist(socket_list *list,fd_set *fd_list); //管理队列生成函数

 

(3)通信程序基本框架移植

  void InitServer(HWND hWnd) {

sockaddr_in server;

int retval;

SOCKET sock;

WSAData wsa;

unsigned long arg;

 

WSAStartup(0x101,&wsa);

sock = socket(AF_INET,SOCK_STREAM,0);

if(sock == SOCKET_ERROR){

MessageBox(hWnd,"socket() failed","server",MB_OK);

return;

}

 

server.sin_family = AF_INET;

server.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

server.sin_port = htons(0x1234);

bind(sock,(sockaddr*)&server,sizeof(server));

listen(sock,5);

 

/*set the socket to nonbolck */

arg = 1;

ioctlsocket(sock,FIONBIO,&arg);

init_list(&sock_list);

 

FD_ZERO(&readfds);

FD_ZERO(&writefds);

FD_ZERO(&exceptfds);

sock_list.MainSock = sock;

 

return;

}

 

  void StartServer(HWND hWnd) {

sockaddr_in remote_addr;

int len;

HDC hdc;

int retval;

int i;

SOCKET sock;

timeval timeout;

 

timeout.tv_sec = 1;

timeout.tv_usec = 0;

 

while(1){

make_fdlist(&sock_list,&readfds);

make_fdlist(&sock_list,&writefds);

make_fdlist(&sock_list,&exceptfds);

retval = select(0,&readfds,&writefds,&exceptfds,&timeout);

if(retval == SOCKET_ERROR){

retval = WSAGetLastError();

break;

}

if(FD_ISSET(sock_list.MainSock,&readfds)){

len = sizeof(remote_addr);

sock = accept(sock_list.MainSock,(sockaddr*)&remote_addr,&len);

if(sock == SOCKET_ERROR){

MessageBox(hWnd,"accept() failed","server",MB_OK);

continue;

}

MessageBox(hWnd, "accept a connection","server",MB_OK);

insert_list(sock,&sock_list);

}

for(i = 0;i < 64;i++){

if(sock_list.sock_array[i] == 0)

continue;

sock = sock_list.sock_array[i];

if(FD_ISSET(sock,&readfds)){

retval = recv(sock,buf,128,0);

if(retval == 0){

closesocket(sock);

MessageBox(hWnd, "close a socket","server",MB_OK);

delete_list(sock,&sock_list);

continue;

}else if(retval == -1){

retval = WSAGetLastError();

if(retval == WSAEWOULDBLOCK)

continue;

closesocket(sock);

MessageBox(hWnd,"close a socket of error","server",MB_OK);

delete_list(sock,&sock_list);

continue;

}

//printf buf

hdc = GetDC(hWnd);

TextOut(hdc,0,row,buf,retval);

row += 16;

ReleaseDC(hWnd,hdc);

//send ack

send(sock,"ACK",3,0);

 

}

//if(FD_ISSET(sock,&writefds)){

//}

//if(FD_ISSET(sock,&exceptfds)){

//}

}

FD_ZERO(&readfds);

FD_ZERO(&writefds);

FD_ZERO(&exceptfds);

}

  }

 

 

(4)细节完善

  switch (message)  {

case WM_COMMAND:

wmId    = LOWORD(wParam); 

wmEvent = HIWORD(wParam); 

// Parse the menu selections:

switch (wmId) {

case IDM_ABOUT:

   DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, hWnd, (DLGPROC)About);

   break;

case IDM_EXIT:

   DestroyWindow(hWnd);

   break;

case IDM_START:

StartServer(hWnd);

break;

default:

   return DefWindowProc(hWnd, message, wParam, lParam);

}

break;

case WM_PAINT:

hdc = BeginPaint(hWnd, &ps);

// TODO: Add any drawing code here...

RECT rt;

GetClientRect(hWnd, &rt);

// DrawText(hdc, szHello, strlen(szHello), &rt, DT_CENTER); 

DrawText(hdc, "Helloyou", 8, &rt, DT_CENTER); 

EndPaint(hWnd, &ps);

break;

case WM_DESTROY:

PostQuitMessage(0);

break;

case WM_CREATE:

InitServer(hWnd);

return DefWindowProc(hWnd, message, wParam, lParam);

break;

default:

return DefWindowProc(hWnd, message, wParam, lParam);

  }

   return 0;

  }

 

 

(5)调试与运行


6 Select机制服务器接受客户连接

 

7 Select机制服务器窗口陷入未响应状态

 


服务器实现多路复用,与多客户机正常通信

 


1.2问题确定与改良方案

(1)问题表象

    启动服务器程序,与客户机连接后,能够实现多路复用,但窗口无响应。

(2)问题本质

    陷入了select通信流程事件等待与处理的循环,无法返回窗口主程序。

(3)模块修正方案

    去循环化,引入异步选择机制,使用WSAAsynSelect进行事件注册。

 

1.3设计基于WSAAsynSelect异步选择机制的Windows消息

(1)去循环后的程序结构

主程序框架与同步选择机制相同,但有两处重要改动需十分注意:

第一,InitServer中的函数部分全部都移植到StartServer中,这样在点击菜单Start时,才能够监听连接,这与之前基于select同步机制的服务器一开始运行就监听连接不同;

第二,将有连接请求、有数据到来、有数据发送等事件作为消息处理,以便WSAAsynSelect机制能够更好地发挥作用。并且如此一来也不需要做while循环,只需等待消息通知即可。

 

(2)WSAAsynSelect()的放置位置和时机

第一,在StartServer()函数中监听后立刻使用:

if(listen(sock,5) != 0){

closesocket(sock);

WSACleanup();

MessageBox(hWnd,"listen() failed\n","server",MB_OK);

PostQuitMessage(0);

}

if(WSAAsyncSelect(sock,hWnd,UM_SOCK,FD_ACCEPT) == SOCKET_ERROR){

MessageBox(hWnd,"WSAAsyncSelect failed\n","server",MB_OK);

}

这样的好处是一旦有连接请求出现,WSAAsynSelect()函数就能够注册ACCEPT事件并通知主套接字,使之能够及时处理该事件。

第二,在WndProc()函数中的switch判决内使用:

switch (message) 

{

      ······

case UM_SOCK:

s = (SOCKET)wParam;

wmEvent = LOWORD(lParam);

switch(wmEvent){

case FD_ACCEPT:

  len = sizeof(remote);

  ns= accept(s,&remote,&len);

            WSAAsynSelect(ns,hWnd,UM_SOCK,FD_READ|FD_CLOSE);      break;

case FD_READ:

retval = recv(s,recvbuf,sizeof(recvbuf),0);

if(retval <= 0){

closesocket(s);

break;

}

recvbuf[retval] = 0;

hdc = BeginPaint(hWnd, &ps);

hdc = GetDC(hWnd);

TextOut(hdc,0,row,recvbuf,strlen(recvbuf));

row += 16;

ReleaseDC(hWnd,hdc);

//回送信息

send(s,"ACK",3,0);

break;

case FD_CLOSE:

MessageBox(hWnd,"closesocket\n","server",MB_OK);

closesocket(s);

break;

case FD_WRITE:

MessageBox(hWnd,"write event\n","server",MB_OK);

break;

default:

if(WSAGETSELECTERROR(lParam) != 0){

MessageBox(hWnd,"select report error\n","server",MB_OK);

closesocket(s);

WSACleanup();

}

}

break;

default:

return DefWindowProc(hWnd, message, wParam, lParam);

   }

接着刚才的思路,由于异步选择机制的通知作用,主套接字上会在case FD_ACCPET处做出对应的事件处理:ns= accept(s,&remote,&len)

并且紧接下来就要进行新的事件注册:是否有读事件或是关闭套接字事件发生。如果有新的事件在套接字上发生,则在下一轮处理中做出事件响应。


(3)调试与运行



异步选择机制服务器主界面

 

   

10 连接客户机后服务器仍能响应窗口

 

11 服务器实现多路复用,与多客户机正常通信

 


2.重要结论说明

(1)基于select同步选择机制的通信服务器程序过于依赖循环结构,在于Windows消息驱动机制结合时会存在固有冲突:对窗口的响应和对通信流程的处理。因此,通过select同步机制的Windows程序设计是对异步选择机制的一个很好地引入,也清楚地解释了异步响应为什么成为了设计Windows消息驱动程序的一个很好的解决方案。

 

(2)WSAAsynSelect()函数是一次注册,若事件得到处理,那么之后的事件也会及时通知。然而若通知的事件没有被处理,那么不仅该事件不会重复通知,之后(在该套接字上)到来的所有事件都将得不到通知,除非之前通知的事件得到处理,也叫做事件使能。

 

(3)WSAAsynSelect()函数必须被正确地放置,才能够保证通信流程按照正常的顺序推进,如果函数位置设计不合理,则可能造成通信失败或者窗口无法关闭等。

 

4)无论是同步选择机制还是异步选择机制,对于阻塞的处理都必须格外注意,尤其是在异步选择机制中。即便使用了WSAAsynSelect()函数,因为诸如recv函数等而阻塞也会导致窗口不能响应。

 

四、异步选择机制总结

1.异步选择的错误处理

异步选择机制需要做更多的错误处理,原因是:基于消息驱动的程序机制,一般来说有多个入口,而程序设计者并不知道使用者将在何时选择哪个入口,而这种随机性可能对程序流造成意想不到的破坏。

欲防止灾难性的错误发生,在程序设计时可采用两种方案:第一,迫使用户按照程序设计者规定的流程来操作,在每个阶段可将不需要的操作选项失效;第二,进行更多的错误判断,是进入每一个入口的条件更加严格。

 

2.异步选择与多路复用的联系

   Windows异步选择机制中,在通知用户的消息中,也同时产生了事件的套接字描述符,所以有以下结论:

第一,异步选择机制支持多路复用,即可以对多个套接字同时进行时间注册并同时处理;

第二,不同于select的严格的管理队列设计,自行管理套接字队列的功能不是异步选择机制所必需的;

第三,Winsock下的多路复用由系统支持,但同时也受到系统限制;

第四,对于大型复杂服务器程序,仍需考虑对套接字队列的管理,甚至要慎重考虑是否需建立在Windows窗口机制下。

 

 

3.异步选择与同步选择的对比

异步选择机制与同步选择机制的相同点与不同点列举出如下表所示:

 

 

基于select的同步选择机制

WSAAsyncSelect异步选择机制

相同点

都是事件驱动机制,都支持多路复用

 

 

 

 

 

 

 

 

不同点

·不断查询

 

·使用了Select的套接字是否阻塞及阻塞时间与timeout参数设计有关

 

·程序框架是不断循环

 

·同步性:查询结果是套接字当前状态

 

·需要用户自行管理套接字队列以备查询

 

·不存在事件使能:即如果不处理查询得到的事件,下次查询时事件依然存在

·一次注册

 

·使用WSAAsynSelect的套接字是自动变成非阻塞状态的

 

 

·程序框架中尽量不出现循环

 

·异步性:不保证通知用户事件后用户能及时处理

 

·用户不必管理套接字队列,通知中包含套接字标识符

 

·存在事件使能:即如果不对本次通知的事件进行处理,那么后续事件也将不再通知,直到完成对本事件的处理

同步选择机制与异步选择机制的异同对比

 

 

 

 

 

参考文献

[1]段景山.Win32 Socket异步消息机制WSAAsynSelect()[Z/OL].2003

[2]段景山.窗口程序设计中的事件驱动,Win32程序基本概念[Z/OL].2003

[3]段景山.多路通信与select(),阻塞与非阻塞机制[Z/OL].2003

[4]尹圣雨.TCP/IP网络编程[M].北京:人民邮电出版社,2014

0 0