非阻塞套接字及select模型案例

两种socket I/O模式
    *阻塞模式:   执行I/O操作完成前会一直进行等待,不会将控制权交给程序。套接字默认为阻塞模式。可以通过多线程技术进行处理。
    *非阻塞模式:执行I/O操作时,Winsock函数会返回并交出控制权。这种模式使用起来比较复杂,因为函数在没有运行完成就进行返回,会不断地返回 WSAEWOULDBLOCK错误,但功能强大。为了解决这个问题,提出了进行I/O操作的一些非阻塞I/O模型,

下面将介绍最常见的三种:


select模型:
  通过调用select函数可以确定一个或多个套接字的状态,判断套接字上是否有数据,或者能否向一个套接字写入数据。

      int select(
        int nfds,                           
        fd_set FAR *readfds,               
        fd_set FAR *writefds,              
        fd_set FAR *exceptfds,             
        const struct timeval FAR *timeout  
      );
    
    nfds 
        [in] Ignored. The nfds parameter is included only for compatibility with Berkeley sockets. 
    readfds 
        [in, out] Optional pointer to a set of sockets to be checked for readability. 
    writefds 
        [in, out] Optional pointer to a set of sockets to be checked for writability 
    exceptfds 
        [in, out] Optional pointer to a set of sockets to be checked for errors. 
    timeout 
        [in] Maximum time for select to wait, provided in the form of a TIMEVAL structure. Set the timeout parameter to NULL for blocking operation. 


readfds: 
    If listen has been called and a connection is pending, accept will succeed. 
    Data is available for reading (includes OOB data if SO_OOBINLINE is enabled). 
    Connection has been closed/reset/terminated. 
    
writefds: 
    If processing a connect call (nonblocking), connection has succeeded. 
    Data can be sent. 
    
exceptfds: 
    If processing a connect call (nonblocking), connection attempt failed. 
    OOB data is available for reading (only if SO_OOBINLINE is disabled). 


The select function returns the total number of socket handles that are ready and contained in the fd_set structures,
zero if the time limit expired, or SOCKET_ERROR if an error occurred.

The fd_set structure is used by various Windows Sockets functions and service providers, such as the select function,
to place sockets into a "set" for various purposes, such as testing a given socket for readability using the readfds parameter of the select function.

typedef struct fd_set {
  u_int    fd_count;                 // how many are SET? 
  SOCKET   fd_array[FD_SETSIZE];     // an array of SOCKETs 
} fd_set;

Members:
    fd_count 
        Number of sockets in the set. 
    fd_array 
        Array of sockets that are in the set. 

操作下面一些宏检查 fd_set 中的内容: 
FD_CLR(s, *set) 
    Removes the descriptor s from set. 
FD_ISSET(s, *set) 
    Nonzero if s is a member of the set. Otherwise, zero. 
FD_SET(s, *set) 
    Adds descriptor s to set. 
FD_ZERO(*set) 
    Initializes the set to the NULL set. 

readfds、writefds、exceptfds三个变量至少有一个不为空,同时这个不为空的套接字组种至少有一个socket,道理很简单,否则要select干什么呢。

举例:

测试一个套接字是否可读:
fd_set fdread;

FD_ZERO(&fdread);
FD_SET(s,&fdread); //加入套接字

if(select(0,&fdread,NULL,NULL,NULL)>0
{
        //成功
        if(FD_ISSET(s,&fread) //是否存在fread中,详细定义请看winsock2.h
        {
                //是可读的
        }
}

控制I/O模型套接字的函数:主要用于获取与套接字相关的操作参数,如将套接字设置为 非阻塞模式。

The ioctlsocket function controls the I/O mode of a socket.

intioctlsocket(
  SOCKET s,         
  long cmd,         
  u_long FAR *argp  
);

s为I/O操作的套接字。
cmd为对套接字的操作命令。
argp为命令所带参数的指针。

The ioctlsocket function can be used on any socket in any state. It is used to set or retrieve operating parameters associated with the socket
,independent of the protocol and communications subsystem. Here are the supported commands to use in the cmd parameter and their semantics: 

FIONBIO 
    Use with a nonzero argp parameter to enable the nonblocking mode of socket s. 
    The argp parameter is zero if nonblocking is to be disabled. 
    When a socket is created, it operates in blocking mode by default (nonblocking mode is disabled). 
    
    To set the socket back to blocking mode, an application must first disable WSAAsyncSelect by calling WSAAsyncSelect with the lEvent parameter equal to zero, 
    or disable WSAEventSelect by calling WSAEventSelect with the lNetworkEvents parameter equal to zero. 
    
FIONREAD 
    Use to determine the amount of data pending in the network's input buffer that can be read from socket s.
    The argp parameter points to an unsigned long value in which ioctlsocket stores the result.
    FIONREAD returns the amount of data that can be read in a single call to the recv function,
    which may not be the same as the total amount of data queued on the socket.
    
    If s is message oriented (for example, type SOCK_DGRAM), FIONREAD still returns the amount of pending data in the network buffer
    , however, the amount that can actually be read in a single call to the recv function is limited to the data size written in the send or sendto function call. 

SIOCATMARK 

    Use to determine whether or not all OOB data has been read. (See section Windows Sockets 1.1 Blocking Routines and EINPROGRESS for a discussion on out of band (OOB) data.)
    This applies only to a stream oriented socket (for example, type SOCK_STREAM) that has been configured for in-line reception of any OOB data (SO_OOBINLINE). If no OOB data is waiting to be read, the operation returns TRUE. Otherwise, it returns FALSE, and the next recv or recvfrom performed on the socket will retrieve some or all of the data preceding the mark. The application should use the SIOCATMARK operation to determine whether any data remains. If there is any normal data preceding the urgent (out of band) data, it will be received in order. (A recv or recvfrom will never mix OOB and normal data in the same call.)
    The argp parameter points to an unsigned long value in which ioctlsocket stores the Boolean result. 



WSAAsynSelect模型:
WSAAsynSelect模型也是一个常用的异步I/O模型

应用程序可以在一个套接字上接收以WINDOWS消息为基础的网络事件通知。该模型的实现方法是通过调用WSAAsyncSelect函自动将套接字设置为非阻塞模式,并向WINDOWS注册一个或多个网络事件,并提供一个通知时使用的窗口句柄

当注册的事件发生时,对应的窗口将收到一个基于消息的通知。

int  WSAAsyncSelect( SOCKET s, HWND hWnd, u_int wMsg, long lEvent);       
作用:

自动将套接字设置为非阻塞模式,并向windows注册一个或多个应用程序感兴趣的网络事件,并提供一个通知时使用的窗口句柄。

当这些事件发生时,应用程序相应的窗口函数将收到一个消息。

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);

 

参数说明:
s为需要事件通知的套接字
hWnd为接收消息的窗口句柄
wMsg为要接收的消息
lEvent为掩码,指定应用程序感兴趣的网络事件组合,主要如下:

#define FD_READ_BIT 0
#define FD_READ (1 << FD_READ_BIT)
#define FD_WRITE_BIT 1
#define FD_WRITE (1 << FD_WRITE_BIT)
#define FD_OOB_BIT 2
#define FD_OOB (1 << FD_OOB_BIT)
#define FD_ACCEPT_BIT 3
#define FD_ACCEPT (1 << FD_ACCEPT_BIT)
#define FD_CONNECT_BIT 4
#define FD_CONNECT (1 << FD_CONNECT_BIT)
#define FD_CLOSE_BIT 5
#define FD_CLOSE (1 << FD_CLOSE_BIT)

用法:

要接收读写通知:
int nResult= WSAAsyncSelect(s,hWnd,wMsg,FD_READ|FD_WRITE);
if(nResult==SOCKET_ERROR)
{
        //错误处理
}

取消通知:
      int nResult= WSAAsyncSelect(s,hWnd,0,0); 

当应用程序窗口hWnd收到消息时,wMsg.wParam参数标识了套接字,lParam的低字标明了网络事件,高字则包含错误代码。

int wmEvent  = LOWORD(wParam);
int  wmId = HIWORD(wParam);

 

 


WSAEventSelect模型
WSAEventSelect模型类似WSAAsynSelect模型,但最主要的区别网络事件发生时会被发送到一个事件对象句柄,而不是发送到一个窗口。

使用步骤如下:
1)、 创建事件对象来接收网络事件:
#define WSAEVENT HANDLE
#define LPWSAEVENT LPHANDLE
WSAEVENT WSACreateEvent( void );

该函数的返回值为一个事件对象句柄,它具有两种工作状态:已传信(signaled)和未传信(nonsignaled)以及两种工作模式:人工重设(manual reset)和自动重设(auto reset)。默认未未传信的工作状态和人工重设模式。

2)、将事件对象与套接字关联,同时注册事件,使事件对象的工作状态从未传信转变未已传信。

int  WSAEventSelect( SOCKET s,WSAEVENT hEventObject,long lNetworkEvents );  

s为套接字
hEventObject为刚才创建的事件对象句柄
lNetworkEvents为掩码,定义如上面所述

3)、I/O处理后,设置事件对象为未传信
BOOL WSAResetEvent( WSAEVENT hEvent );

Hevent为事件对象

成功返回TRUE,失败返回FALSE。

4)、等待网络事件来触发事件句柄的工作状态:
DWORD WSAWaitForMultipleEvents( DWORD cEvents,
const WSAEVENT FAR * lphEvents, BOOL fWaitAll,
DWORD dwTimeout, BOOL fAlertable );

lpEvent为事件句柄数组的指针
cEvent为为事件句柄的数目,其最大值为WSA_MAXIMUM_WAIT_EVENTS 
fWaitAll指定等待类型:TRUE:当lphEvent数组重所有事件对象同时有信号时返回;
FALSE:任一事件有信号就返回。
dwTimeout为等待超时(毫秒)
fAlertable为指定函数返回时是否执行完成例程

对事件数组中的事件进行引用时,应该用WSAWaitForMultipleEvents的返回值,减去预声明值WSA_WAIT_EVENT_0,得到具体的引用值。例如:
nIndex=WSAWaitForMultipleEvents(…);
MyEvent=EventArray[Index- WSA_WAIT_EVENT_0];

5)、判断网络事件类型:
int WSAEnumNetworkEvents( SOCKET s,
WSAEVENT hEventObject, LPWSANETWORKEVENTS lpNetworkEvents );

s为套接字
hEventObject为需要重设的事件对象
lpNetworkEvents为记录网络事件和错误代码,其结构定义如下:

typedef struct _WSANETWORKEVENTS {
        long lNetworkEvents;
        int iErrorCode[FD_MAX_EVENTS];
} WSANETWORKEVENTS, FAR * LPWSANETWORKEVENTS;

6)、关闭事件对象句柄:
BOOL WSACloseEvent(WSAEVENT hEvent);

调用成功返回TRUE,否则返回FALSE


 

对比:
使用阻塞套接字,可能会对应用程序的性能造成影响,在阻塞套接字时,程序必须等待SOCKET处理完毕或者出错后才返回,这段时间CPU就无聊的等待;

而对于非阻塞可以在这段时间做一些数据准备工作,一旦SOCKET可以用就立即发送数据,并行性更好。

非阻塞在单socket的情况下操作起来比阻塞要复杂点,但是对于多个socket并且要求实时性比较高的场合下基本上阻塞就不能用。

譬如,socket A先进行了一个阻塞操作,那么不超时/不结束调用的话,代码的控制权不会返回,那么B socket上面就算有数据需要被接受并且处理,由于已经阻塞在A上面了,此时没有办法去处理B上面的数据。而非阻塞的时候调用立即返回,B就可以被处理。



非阻塞的服务器程序:  
  #include   <winsock2.h>  
  #include   <stdio.h>  
   
   
  #define   MYPORT   5550         //   定义默认通信端口  
   
  void   main(void)  
  {  
        WSADATA                             wsaData;  
        SOCKET                               ListeningSocket;     //   服务器套接字  
        SOCKET                               NewConnection;       //   客户机套接字  
        SOCKADDR_IN                     ServerAddr;      
        SOCKADDR_IN                     ClientAddr;      
        int                                     Ret,   ClientAddrLen;  
        char                                   DataBuffer[1024];     //   接收数据的缓冲区  
        fd_set                               readfds;                 //   等待可读性检查的套接口结构体  
        unsigned   long                 ul   =   1;                
        struct   timeval               timeout;               //   最多等待时间  
   
        printf("\n------------------非阻塞模式的套接字(服务器)------------------\n\n");  
   
        //   初始化   Winsock   2.2   版本  
        if   ((Ret   =   WSAStartup(MAKEWORD(2,2),   &wsaData))   !=   0)  
        {  
              printf("WSAStartup   failed   with   error   %d\n",   Ret);  
              return;  
        }  
     
        //   创建一个新套接字来监听客户机的连接  
        if   ((ListeningSocket   =   socket(AF_INET,   SOCK_STREAM,   IPPROTO_TCP))   ==   INVALID_SOCKET)  
        {  
              printf("socket   failed   with   error   %d\n",   WSAGetLastError());  
              WSACleanup();  
              return;  
        }    
   
        //   初始化一个   SOCKADDR_IN   结构  
        ServerAddr.sin_family   =   AF_INET;       //   使用IP地址族  
        ServerAddr.sin_port   =   htons(MYPORT);     //   通信端口(5550)      
        ServerAddr.sin_addr.s_addr   =   htonl(INADDR_ANY);     //   使用自己的IP地址,实际默认为0  
   
        //   将这个地址信息和套接字关联起来  
        if   (bind(ListeningSocket,   (SOCKADDR   *)&ServerAddr,   sizeof(ServerAddr))   ==   SOCKET_ERROR) 
        {  
              printf("bind   failed   with   error   %d\n",   WSAGetLastError());  
              closesocket(ListeningSocket);  
              WSACleanup();  
              return;  
        }  
   
        //   将套接字转入监听模式,监听客户机的连接,指定最大队列长度为5  
        if   (listen(ListeningSocket,   5)   ==   SOCKET_ERROR)  
        {  
              printf("listen   failed   with   error   %d\n",   WSAGetLastError());  
              closesocket(ListeningSocket);  
              WSACleanup();  
              return;  
        }    
   
        //   设置套接字为非阻塞模式  
        Ret   =   ioctlsocket(ListeningSocket,   FIONBIO,   (unsigned   long   *)   &ul);  
   
        if   (Ret   ==   SOCKET_ERROR)  
        {  
        printf("ioctlsocket   failed   with   error   %d\n",   WSAGetLastError());  
        }  
        else  
        {  
        printf("set   nonblock   mode   successed,   return   value   %d\n",   Ret);  
        }  
   
        printf("waiting   for   connection   on   port   %d....\n",   MYPORT);  
   
        //   定义Select()   的最多等待时间  
        timeout.tv_sec   =   0;  
        timeout.tv_usec   =   500;  
   
        while   (1)  
        {  
        FD_ZERO(&readfds);       //   对结构体进行初始化  
        FD_SET(ListeningSocket,   &readfds);       //   把套接字加入集合  
   
        //   查询套接口的可读性  
        Ret   =   select(0,   &readfds,   NULL,   NULL,   &timeout);  
         
        if   (Ret   >   0)  
        {  
        if   (FD_ISSET(ListeningSocket,   &readfds))  
        {        
        ClientAddrLen   =   sizeof(ClientAddr);  
         
        //   当有连接请求到达时,接受一个新连接  
        if   ((NewConnection   =   accept(ListeningSocket,   (SOCKADDR   *)   &ClientAddr,  
        &ClientAddrLen))   ==   INVALID_SOCKET)  
        {  
        printf("accept   failed   with   error   %d\n",   WSAGetLastError());  
        closesocket(ListeningSocket);  
        WSACleanup();  
        exit(1);  
        }  
         
        printf("got   a   connection   from   %s   .\n",   inet_ntoa(ClientAddr.sin_addr));  
         
        //   循环  
        while(1)  
        {  
        //       FD_ZERO(&readfds);  
        FD_SET(NewConnection,   &readfds);  
   
        //   第二次检查套接口的可读性,等待时间设为空,否则会出现超时错误  
        Ret   =   select(0,   &readfds,   NULL,   NULL,   NULL);  
         
        if   (FD_ISSET(NewConnection,   &readfds))  
        {        
        //   接收数据  
        Ret   =   recv(NewConnection,   DataBuffer,   sizeof(DataBuffer)-1,   0);    
         
        if   (Ret   ==   SOCKET_ERROR   ||   Ret   ==   0)    
        {  
        //   接收错误  
        printf("recv   failed   with   error   %d\n",   WSAGetLastError());  
        closesocket(NewConnection);  
        WSACleanup();  
        return   ;  
        }    
        else  
        {  
            DataBuffer[Ret]   =   '\0';  
        printf("received   %d   byte:   %s",   (Ret-1),   DataBuffer);        
        }  
         
        }  
        }  
        }  
        }  
        }  
         
        //   关闭套接字  
        closesocket(NewConnection);  
         
        //   释放由   winsock   分配的资源    
        WSACleanup();  
  }
  
  
  
  
  非阻塞的客户机程序:  
    #include   <winsock2.h>  
    #include   <stdio.h>  
     
    #define   MYPORT   5550       //   定义默认通信端口  
    #define   LINELEN       128       //   定义一行数据的最大长度  
     
    void   main(int   argc,   char   **argv)  
    {  
          WSADATA                     wsaData;  
          SOCKET                       s;  
          SOCKADDR_IN             ServerAddr;  
          int                             Ret,   length;  
          char                           buf[LINELEN];        
          fd_set                       writefds;             //   等待可写性检查的套接口结构体  
          unsigned   long         ul   =   1;  
          struct   timeval       timeout;               //   最多等待时间  
     
          //   对主函数的参数进行处理  
          switch(argc)  
          {  
          case   1:  
          argv[1]   =   "127.0.0.1";     //   定义一个默认的IP地址  
          break;  
     
          case   2:  
          argv[1]  
          argv[2]  
          break;  
     
          default:  
          printf("argument   error!\n");  
          exit(1);  
          }  
     
          printf("\n------------------非阻塞模式的套接字(客户机)------------------\n\n");  
     
          //   初始化   Winsock   2.2   版本  
          if   ((Ret   =   WSAStartup(MAKEWORD(2,2),   &wsaData))   !=   0)  
          {  
                 
                printf("WSAStartup   failed   with   error   %d\n",   Ret);  
                return;  
          }  
           
          //   创建一个新套接字来建立客户机连接  
          if   ((s   =   socket(AF_INET,   SOCK_STREAM,   IPPROTO_TCP))   ==   INVALID_SOCKET)  
          {  
                printf("socket   failed   with   error   %d\n",   WSAGetLastError());  
                WSACleanup();  
                return;  
          }  
       
          //   设置套接字为非阻塞模式  
          Ret   =   ioctlsocket(s,   FIONBIO,   (unsigned   long   *)   &ul);  
     
          if   (Ret   ==   SOCKET_ERROR)  
          {  
          printf("ioctlsocket   failed   with   error   %d\n",   WSAGetLastError());  
          }  
          else  
          {  
          printf("set   nonblock   mode   successed,   return   value   %d\n",   Ret);  
          }  
     
          //   初始化一个   SOCKADDR_IN   结构  
          ServerAddr.sin_family   =   AF_INET;  
          ServerAddr.sin_port   =   htons(MYPORT);          
          ServerAddr.sin_addr.s_addr   =   inet_addr(argv[1]);     //   定义服务器地址    
     
          //   用套接字   s   来创建一个到服务器的连接  
          Ret   =   connect(s,   (SOCKADDR   *)   &ServerAddr,   sizeof(ServerAddr));  
          if   (WSAGetLastError()   !=   WSAEWOULDBLOCK)  
          {  
          printf("connect   failed   with   error   %d\n",   WSAGetLastError());  
          closesocket(s);  
          WSACleanup();  
          return;  
          }    
          else  
          {  
          //   和服务器的连接成功  
          printf("connect   to   %s   on   %d   succeeded.\n",   argv[1],   MYPORT);  
          printf("please   input   send   data....\n");  
     
          //   定义Select()   的最多等待时间  
          timeout.tv_sec   =   0;  
          timeout.tv_usec   =   500;  
           
          while   (1)  
          {  
          FD_ZERO(&writefds);  
          FD_SET(s,   &writefds);  
     
          //   查询套接口的可写性  
          Ret   =   select(   0,   NULL,   &writefds,   NULL,   &timeout);  
           
          if   (Ret   >   0)  
          {  
          if   (FD_ISSET(s,   &writefds))  
          {  
          //   循环:从输入流中取数据  
          while   (fgets(buf,   sizeof(buf),   stdin))  
          {  
          buf[LINELEN]   =   '\0';         //   在字符串最后加终止符    
          length   =   strlen(buf);       //   实际发送字节数    
           
          //   如果输入是回车,则结束程序  
          if   (buf[0]   ==   '\n')  
          {  
          closesocket(s);  
          WSACleanup();  
          return;  
          }  
           
          //   发送数据  
          if   ((Ret   =   send(s,   buf,   length,   0))   ==   SOCKET_ERROR)  
          {  
          printf("send   failed   with   error   %d\n",   WSAGetLastError());  
          closesocket(s);  
          WSACleanup();  
          return;  
          }  
          }  
          }  
          }  
          }  
          }  
          //   关闭套接字  
          closesocket(s);  
           
          //   释放由   winsock   分配的资源  
          WSACleanup();  

  }

转载:点击打开链接

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值