• windows和linux套接字中的select机制浅析


    先来谈谈为什么会出现select函数,也就是select是解决什么问题的?

    平常使用的recv函数时阻塞的,也就是如果没有数据可读,recv就会一直阻塞在那里,这是如果有另外一个连接过来,就得一直等待,这样实时性就不是太好。

    这个问题的几个解决方法:1. 使用ioctlsocket函数,将recv函数设置成非阻塞的,这样不管套接字上有没有数据都会立刻返回,可以重复调用recv函数,这种方式叫做轮询(polling),但是这样效率很是问题,因为,大多数时间实际上是无数据可读的,花费时间不断反复执行read系统调用,这样就比较浪费CPU的时间。并且循环之间的间隔不好确定。2. 使用fork,使用多进程来解决,这里终止会比较复杂(待研究)。 3.使用多线程来解决,这样避免了终止的复杂性,但却要求处理线程之间的同步,在减少复杂性方面这可能会得不偿失。4. 使用异步IO(待研究)。5. 就是本文所使用的I/O多路转接(多路复用)--其实就是在套接字阻塞和非阻塞之间做了一个均衡,我们称之为半阻塞。

    经过对select的初步了解,在windows和linux下的实现小有区别,所以分开来写。这里先写windows下的select机制。

    select的大概思想:将多个套接字放在一个集合里,然后统一检查这些套接字的状态(可读、可写、异常等),调用select后,会更新这些套接字的状态,然后做判断,如果套接字可读,就执行read操作。这样就巧妙地避免了阻塞,达到同时处理多个连接的目的。当然如果没有事件发生,select会一直阻塞,如果不想一直让它等待,想去处理其它事情,可以设置一个最大的等待时间。

    /***********************************************************************************************************/

    下面具体讲讲函数的参数,参见MSDN的解释http://msdn.microsoft.com/en-us/library/windows/desktop/ms740141(v=vs.85).aspx

    [cpp] view plaincopy
     
    1. int select(  
    2.   _In_     int nfds,  
    3.   _Inout_  fd_set *readfds,  
    4.   _Inout_  fd_set *writefds,  
    5.   _Inout_  fd_set *exceptfds,  
    6.   _In_     const struct timeval *timeout  
    7. );  


    函数的返回值,表示准备好的套接字的个数,如果是0,则表示没有一个准备好(超时就是一种情况),如果是-1(SOCKET_ERROR),表示有错误发生,可以使用WSAGetLastError()函数来得到错误代码,从而知道是什么错误。

    函数的参数,第一个是输入参数nfds,表示满足条件的套接字的个数,windows下可以设置为0,因为fd_set结构体中已经包含了这个参数,这个参数已经是多余的了,之所以还存在,只是是为了与FreeBSD兼容。

    第二三四参数都是输入输出参数(值-结果参数,输入和输出会不一样),表示套接字的可读、可写和异常三种状态的集合。调用select之后,如果指定套接字不可读或者不可写,就会从相应队列中清除,这样就可以判断哪些套接字可读或者可写。 

    说明一下,这里的可读性是指:如果有客户的连接请求到达,套接口就是可读的,调用accept能够立即完成,而不发生阻塞;如果套接口接收队列缓冲区中的字节数大于0,调用recv或者recvfrom就不会阻塞。可写性是指,可以向套接字发送数据(套接字创建成功后,就是可写的)。当然不是套接字可写就会去发送数据,就像不是看到电话就去打电话一样,而是由打电话的需求了,才去看电话是否可打;可读就不一样了,电话响了,自然要去接电话(除非,你有事忙或者不想接,一般都是要接的)。可读已经包含了缓冲区中有数据可以读取,可写只是说明了缓冲区有空间让你写,你需不需要写就要看你有没有数据要写了.关于异常,就是指一些意外情况,自己用的比较少,以后用到了,再过来补上。

    第五个参数是等待的最大时间,是一个结构体:struct timeval,它的定义是:

    [cpp] view plaincopy
     
    1. /* 
    2. * Structure used in select() call, taken from the BSD file sys/time.h. 
    3. */  
    4. struct timeval {  
    5.         long    tv_sec;         /* seconds */  
    6.         long    tv_usec;        /* and microseconds */  
    7. };  

    具体到秒和微妙,按照等待的时间长短可以分为不等待、等待一定时间、一直等待。对应的设置分别为,(0,0)是不等待,这是select是非阻塞的,(x,y)最大等待时间x秒y微妙(如果有事件就会提前返回,而不继续等待),NULL表示一直等待,直到有事件发生。这里可以将timeout分别设置成0(不阻塞)或者1微妙(阻塞很短的时间),然后观察CPU的使用率,会发现设置成非阻塞后,CPU的使用率已下载就上升到了50%左右,这样可以看出非阻塞占用CPU很多,但利用率不高。

    /***********************************************************************************************************/

    跟select配合使用的几个宏和fd_set结构体介绍:

    套接字描述符为了方便管理是放在一个集合里的,这个集合是fd_set,它的具体定义是:

    [cpp] view plaincopy
     
    1. typedef struct fd_set {  
    2.         u_int   fd_count;               /* how many are SET? */  
    3.         SOCKET  fd_array[FD_SETSIZE];   /* an array of SOCKETs */  
    4. } fd_set;  

    fd_count是集合中已经设置的套接口描述符的数量。fd_array数组保存已经设置的套接口描述符,其中FD_SETSIZE的定义是:

    [cpp] view plaincopy
     
    1. #ifndef FD_SETSIZE  
    2. #define FD_SETSIZE      64  
    3. #endif /* FD_SETSIZE */  

    这个默认值在一般的程序中已经够用,如果需要,可以将其更改为更大的值。

    集合的管理操作,比如元素的清空、加入、删除以及判断元素是否在集合中都是用宏来完成的。四个宏是:

    [html] view plaincopy
     
    1. FD_ZERO(*set)  
    2. FD_SET(s, *set)  
    3. FD_ISSET(s, *set)  
    4. FD_CLR(s, *set)  

    下面一一介绍这些宏的作用和定义:

    FD_ZERO(*set),是把集合清空(初始化为0,确切的说,是把集合中的元素个数初始化为0,并不修改描述符数组).使用集合前,必须用FD_ZERO初始化,否则集合在栈上作为自动变量分配时,fd_set分配的将是随机值,导致不可预测的问题。它的宏定义如下:

    [cpp] view plaincopy
     
    1. #define FD_ZERO(set) (((fd_set FAR *)(set))->fd_count=0)  

    FD_SET(s,*set),向集合中加入一个套接口描述符(如果该套接口描述符s没在集合中,并且数组中已经设置的个数小于最大个数时,就把该描述符加入到集合中,集合元素个数加1)。这里是将s的值直接放入数组中。它的宏定义如下:

    [cpp] view plaincopy
     
    1. #define FD_SET(fd, set) do {   
    2.     if (((fd_set FAR *)(set))->fd_count < FD_SETSIZE)   
    3.         ((fd_set FAR *)(set))->fd_array[((fd_set FAR *)(set))->fd_count++]=(fd);  
    4. while(0)  

    FD_ISSET(s,*set),检查描述符是否在集合中,如果在集合中返回非0值,否则返回0. 它的宏定义并没有给出具体实现,但实现的思路很简单,就是搜索集合,判断套接字s是否在数组中。它的宏定义是:

    [cpp] view plaincopy
     
    1. #define FD_ISSET(fd, set) __WSAFDIsSet((SOCKET)(fd), (fd_set FAR *)(set))  

    FD_CLR(s,*set),从集合中移出一个套接口描述符(比如一个套接字连接中断后,就应该移除它)。实现思路是,在数组集合中找到对应的描述符,然后把后面的描述依次前移一个位置,最后把描述符的个数减1. 它的宏定义是:

    [cpp] view plaincopy
     
    1. #define FD_CLR(fd, set) do {   
    2.     u_int __i;   
    3.     for (__i = 0; __i < ((fd_set FAR *)(set))->fd_count ; __i++) {   
    4.         if (((fd_set FAR *)(set))->fd_array[__i] == fd) {   
    5.             while (__i < ((fd_set FAR *)(set))->fd_count-1) {   
    6.                 ((fd_set FAR *)(set))->fd_array[__i] =   
    7.                     ((fd_set FAR *)(set))->fd_array[__i+1];   
    8.                 __i++;   
    9.             }   
    10.             ((fd_set FAR *)(set))->fd_count--;   
    11.             break;   
    12.         }   
    13.     }   
    14. while(0)  


    /***********************************************************************************************************/

    至此,一些基础的点基本就讲完了,然后给出大概流程和一个示例:

    1.调用FD_ZERO来初始化套接字状态;

    2.调用FD_SET将感兴趣的套接字描述符加入集合中(每次循环都要重新加入,因为select更新后,会将一些没有满足条件的套接字移除队列);

    3.设置等待时间后,调用select函数--更新套接字的状态;

    4.调用FD_ISSET,来判断套接字是否有相应状态,然后做相应操作,比如,如果套接字可读,就调用recv函数去接收数据。

    关键技术:套接字队列和状态的表示与处理。

    server端得程序如下(套接字管理队列一个很重要的作用就是保存套接字描述符,因为accept得到的套接字描述符会覆盖掉原来的套接字描述符,而readfs中的描述符在select后会删除这些套接字描述符):

    [cpp] view plaincopy
     
    1. // server.cpp :   
    2. //程序中加入了套接字管理队列,这样管理起来更加清晰、方便,当然也可以不用这个东西  
    3.   
    4. #include "winsock.h"  
    5. #include "stdio.h"  
    6. #pragma comment (lib,"wsock32.lib")  
    7. struct socket_list{  
    8.     SOCKET MainSock;  
    9.     int num;  
    10.     SOCKET sock_array[64];  
    11. };  
    12. void init_list(socket_list *list)  
    13. {  
    14.     int i;  
    15.     list->MainSock = 0;  
    16.     list->num = 0;  
    17.     for(i = 0;i < 64;i ++){  
    18.         list->sock_array[i] = 0;  
    19.     }  
    20. }  
    21. void insert_list(SOCKET s,socket_list *list)  
    22. {  
    23.     int i;  
    24.     for(i = 0;i < 64; i++){  
    25.         if(list->sock_array[i] == 0){  
    26.             list->sock_array[i] = s;  
    27.             list->num += 1;  
    28.             break;  
    29.         }  
    30.     }  
    31. }  
    32. void delete_list(SOCKET s,socket_list *list)  
    33. {  
    34.     int i;  
    35.     for(i = 0;i < 64; i++){  
    36.         if(list->sock_array[i] == s){  
    37.             list->sock_array[i] = 0;  
    38.             list->num -= 1;  
    39.             break;  
    40.         }  
    41.     }  
    42. }  
    43. void make_fdlist(socket_list *list,fd_set *fd_list)  
    44. {  
    45.     int i;  
    46.     FD_SET(list->MainSock,fd_list);  
    47.     for(i = 0;i < 64;i++){  
    48.         if(list->sock_array[i] > 0){  
    49.             FD_SET(list->sock_array[i],fd_list);  
    50.         }  
    51.     }  
    52. }  
    53. int main(int argc, char* argv[])  
    54. {  
    55.     SOCKET s,sock;  
    56.     struct sockaddr_in ser_addr,remote_addr;  
    57.     int len;  
    58.     char buf[128];  
    59.     WSAData wsa;  
    60.     int retval;  
    61.     struct socket_list sock_list;  
    62.     fd_set readfds,writefds,exceptfds;  
    63.     timeval timeout;        //select的最多等待时间,防止一直等待  
    64.     int i;  
    65.     unsigned long arg;  
    66.   
    67.     WSAStartup(0x101,&wsa);  
    68.     s = socket(AF_INET,SOCK_STREAM,0);  
    69.     ser_addr.sin_family = AF_INET;  
    70.     ser_addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);  
    71.     ser_addr.sin_port = htons(0x1234);  
    72.     bind(s,(sockaddr*)&ser_addr,sizeof(ser_addr));  
    73.   
    74.     listen(s,5);  
    75.     timeout.tv_sec = 5;     //如果套接字集合中在1s内没有数据,select就会返回,超时select返回0  
    76.     timeout.tv_usec = 0;  
    77.     init_list(&sock_list);  
    78.     FD_ZERO(&readfds);  
    79.     FD_ZERO(&writefds);  
    80.     FD_ZERO(&exceptfds);  
    81.     sock_list.MainSock = s;  
    82.     arg = 1;  
    83.     ioctlsocket(sock_list.MainSock,FIONBIO,&arg);  
    84.     while(1){  
    85.         make_fdlist(&sock_list,&readfds);  
    86.         //make_fdlist(&sock_list,&writefds);  
    87.         //make_fdlist(&sock_list,&exceptfds);  
    88.   
    89.         retval = select(0,&readfds,&writefds,&exceptfds,&timeout);     //超过这个时间,就不阻塞在这里,返回一个0值。  
    90.         if(retval == SOCKET_ERROR){  
    91.             retval = WSAGetLastError();  
    92.             break;  
    93.         }  
    94.         else if(retval == 0) {  
    95.             printf("select() is time-out! There is no data or new-connect coming! ");  
    96.             continue;  
    97.         }  
    98.         if(FD_ISSET(sock_list.MainSock,&readfds)){  
    99.             len = sizeof(remote_addr);  
    100.             sock = accept(sock_list.MainSock,(sockaddr*)&remote_addr,&len);  
    101.             if(sock == SOCKET_ERROR)  
    102.                 continue;  
    103.             printf("accept a connection ");  
    104.             insert_list(sock,&sock_list);  
    105.         }  
    106.         for(i = 0;i < 64;i++){  
    107.             if(sock_list.sock_array[i] == 0)  
    108.                 continue;  
    109.             sock = sock_list.sock_array[i];  
    110.             if(FD_ISSET(sock,&readfds)){  
    111.                 retval = recv(sock,buf,128,0);  
    112.                 if(retval == 0){  
    113.                     closesocket(sock);  
    114.                     printf("close a socket ");  
    115.                     delete_list(sock,&sock_list);  
    116.                     continue;  
    117.                 }else if(retval == -1){  
    118.                     retval = WSAGetLastError();  
    119.                     if(retval == WSAEWOULDBLOCK)  
    120.                         continue;  
    121.                     closesocket(sock);  
    122.                     printf("close a socket ");  
    123.                     delete_list(sock,&sock_list);   //连接断开后,从队列中移除该套接字  
    124.                     continue;  
    125.                 }  
    126.                 buf[retval] = 0;  
    127.                 printf("->%s ",buf);  
    128.                 send(sock,"ACK by server",13,0);  
    129.             }  
    130.             //if(FD_ISSET(sock,&writefds)){  
    131.             //}  
    132.             //if(FD_ISSET(sock,&exceptfds)){  
    133.               
    134.         }  
    135.         FD_ZERO(&readfds);  
    136.         FD_ZERO(&writefds);  
    137.         FD_ZERO(&exceptfds);  
    138.     }  
    139.     closesocket(sock_list.MainSock);  
    140.     WSACleanup();  
    141.     return 0;  
    142. }  

    关于linux下的select跟windows下的区别还有待学习。

    参考书籍:

    《WinSock网络编程经络》第19章

    《UNIX环境高级编程》

  • 相关阅读:
    Java中splite的用法与小技巧
    android asmack调用MultiUserChat.getHostedRooms方法出现空指针的异常解决方案
    android java.lang.ExceptionInInitializerError
    二维码生成与返回客户端
    微信查询所有关注该公众号的用户
    连接数据库报错:句柄无效
    httpclient POST请求(urlencoded)
    小程序——获得用户敏感信息
    作用域浅析
    小程序开发——统一请求方法
  • 原文地址:https://www.cnblogs.com/lidabo/p/3804411.html
Copyright © 2020-2023  润新知