Chinaunix首页 | 论坛 | 博客
  • 博客访问: 563578
  • 博文数量: 104
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 1559
  • 用 户 组: 普通用户
  • 注册时间: 2014-08-21 00:58
个人简介

锻炼精神,首先要锻炼肉体

文章分类

全部博文(104)

文章存档

2018年(1)

2016年(1)

2015年(101)

2014年(1)

我的朋友

分类: C/C++

2015-03-30 16:32:40

select 函数某种程度上来说,算是高级的网络编程函数,当然这是相对于 socket, bind ,connect.... 而言的,
比它更加高效的函数还有 epoll 这个函数后面会学习到

对于一对一的连接通信双方,函数select 并没有太多的优势而言,但是当 server 与 client 之间是一对多的时候,
select 的优势就是很明显的。

在学习 select 函数之前,必须要知道的便是,阻塞和非阻塞的通信和 server 与 client 之间一对多关系的时候,
对 server 端会为每个向它发送的连接请求存放到缓存队列中,每次接收一个连接便会通过 accept 方法为其
生成一个新的套接字描述符。

举例来说 服务器 server 的缓冲队列中 存放有来自 client 1 client 3 client 8 ;这三个来自客户端的连接请求,
首先接收 client1 进行连接请求,随之会生成一个新的套接字描述符 fd_c1;
然后,接收来自缓冲队列中的 client3 的连接请求, 又生成一个套接字描述符 fd_c3 ;
随后又接收 client8 的连接请求,随之又生成一个新的套接字描述符 fd_c8 ;

在这里请注意,每次接收一个连接请求之后,为其提供服务之后并不代表该 client 端就没有后续的数据向 server发送了,
所以 server 端为了保持与 client 端的连接是必须要保存它们的套接字描述符的,即: fd_c1,fd_c3, fd_c8
在这里需要讨论阻塞和非阻塞了,其中非阻塞的实现是借助于 select 函数的

如果使用的是阻塞的方式,那么每次 server 端便会执行一个循环---> 轮询,循环 fd_c1 --> fd_c8 看看那个套接字描述符
所对应的缓冲空间中是否有新的数据到达(待发送到网络上、待接收到进程内存中)
在循环的时候便是阻塞的开始, 首先便是阻塞到 fd_c1 上面一直到 client1 向server 发送数据,或者是 server 中有数据要
发往 client1 (只要是 fd_c1 套接字描述符标定的缓冲区中的数据有变化)才会执行相应的操作,否则便一直卡在这个地方,
即便是这个时候 client3 client8 中有数据到达,server 也不予以理会,而是死等则 fd_c1 所指向的空间中有数据发生变化。

是不是很低效?这就是所说的阻塞式轮询:就是 server 进程消耗着宝贵的 CPU 资源来死等一个不知道何时会发生的事件

如果使用的是 select 函数,便可以将 server 进程从主动轮询变为了被动的被提醒

while ( ( ret = select (///)) < 0 )
{
    ////
}

通过上面的这个代码便可以实现,只要是 fd_c1 , fd_c3, fd_c8 中对应的缓冲空间中有数据变化的话,
就会停止循环,然后在通过不同的方法 FD_* 来循环访问以下 fd_c* 便可以知道是哪个client 发送数据,
或是有数据要发送到哪个client 上面了。

只有当有变化的时候才会将信号发送给 server , 而不需要 server 依次询问



select 函数 API 
#include
#include
#include
#include
int select ( int n , fd_set *readfds , fd_set *writefds , fd_set *exceptfds,
                                struct timeval* timeout ) ;
参数:
1.n :           
这个是用来监听文件描述符(套接字描述符)数值的最大值+1 ,
因为在同一个系统中,系统为描述符的分配是以递增的方式进行的。
2.readfds :       
这个是用来存放需要 select 监听的可读文件(缓冲区)的文件描述符(套接字描述符)的集合

3.writefds :      
用来存放要 select 监听的可写文件(缓冲区)的文件描述符(套接字描述符)的集合

4.exception :    
用来存放需要 select 监听的出现异常文件(缓冲区)的文件描述符(如果是网络通信,则是套接字描述符)的集合

5. timeout  :    
这个参数是用来设定时间段的, 在设定的时间段内,如果有select 说监听的某个描述符所对应的缓冲空间中有数据变动的话,
 则会另 select 函数立即返回发生变动的缓冲空间的个数,如果在设定的时间段内,没有任何描述符对应的缓冲空间中的数据发生变动
 则 select 函数便会返回数值 0 
                   


       select 方法是通过参数 timeout 来设定select 的工作方式的:
          1. 完全阻塞
                   timeout 如果设定为空 NULL, 那么select 的工作方式便是完全阻塞的,即程序执行到 select 语句处的时候,
                    如果没有任何一个描述符指向的空间中的数据发生变化,那么便会卡到(阻塞)到这个地方,
                   直到有描述符指向的空间中数据有变化为止,select 方法才会返回对应有多少个描述符指向的空间中的数据发生变化,
                   并将发生变化的缓冲空间个数作为返回值进行返回
          2. 在一定时间段内阻塞
                将timeout 设定一个时间段,比如说 1 分钟,那么当程序走到 select 语句的时候, 会有两种选择
                 {   
                         2.1
从程序开始运行的 1分钟期间内,缓冲空间中的数据有变动,---->
                                 select 函数立即返回,并返回有数据变动的空间的个数
                                 ( 缓冲空间所指的是由描述符<文件描述符、套接字描述符>所指定的系统为它分配的缓冲空间
                                      有变动: 变动1 :有数据从进程内存写入到缓冲区中 变动2 :有数据从网络中被读入到缓冲区中
                         2.2 从程序开始运行的 1分钟期间内,select 所监听的{读操作描述符集合,写操作描述符集合,异常描述符集合}
                                 描述符集合中
的描述符所指定的缓冲空间中没有任何数据的而变动,那么select 将会立即返回 0 
                   }
           3. (完全)非阻塞
                          设定 timeout = NULL ---> 当程序执行到 select 语句处,在这一个时间点上,
                            如果监听的描述符集合中有描述符标定的内存空间中有数据的发生变动,
                            返回发生变动的缓冲空间个数。如果没有发生变动的缓冲区,不会停留(阻塞)而是立即返回 0 ;
                            告知调用者: 当前没有任何缓冲空间中数据发生变化。



示例程序:
下面的这个程序是服务器端在开启监听之后,使用 select 函数来多路复用多个来自 client 的连接的标准程序框架,
个人认为十分的经典,其中 Server 端的主要过程是:
1、 创建监听套接字描述符
2.   绑定套接字描述符到指定的网络地址上面
3,调用 listen 方法开启 server 端的监听服务
4.   开启服务端的主循环 
     for ( ; ; )
      {
                1. 首先调用 select 方法来查看这段时间内,有多少个套接字描述符标定的缓冲区中数据发生变化
                 
                2.优先判断监听套接字描述符,如果来了新的连接,将其加入到存放连接套接字描述符的数组中
                    在这里需要知道的值套接字描述符可以分为{监听套接字描述符,连接请求套接字描述符}
                    监听套接字描述符:是当server端接收到一个新的client发来的连接请求的,它标定的缓冲空间是用来存放来自各个不同的
                        client 连接请求的缓冲队列
                       连接请求套接字描述符:是当 server 决定为 client 提供服务的时候,系统为server 与该 client 进行通信开辟的缓冲区,
                       该缓冲区用来存放来自 client 的请求数据和信息,以及 server 为该client 回复的数据和信息
                       这两个描述符对应标定的缓冲空间类型是不同的                
               3. 遍历连接套接字描述符数组,取出数组中的元素,读取来自该描述符标定的缓冲区中的数据到进程内存中    
  }

运行顺序:
首先运行 服务器程序,从程序中的 select (1,2,3,4,NULL)第五个参数是 NULL 可以知道的是该服务器端的 select 工作模式
是完全阻塞式进行的。 所以运行起来之后,程序将会卡在 select 语句处不动
接下来新打开一个 terminal 运行客户端的程序,然后根据客户端程序的提示信息输入要发送给 Server 端的信息,Client 端的运行
方式是这样的,创建了 3 个客户,以循环顺序的方式向 server 端发送信息,每个 client 发送 2 次,每次一条信息。

等到每个 client 发送 2 次信息结束之后,便会再次通过循环的方式 依次关闭各个 client 的连接,
关闭连接之后,在一定时间阈值之内没有任何数据发往对端的 Server, 那么server 接收到的信息长度变为 0 
服务器在探测到这一信号之后,便会认为 client 端关闭,所以也会关闭与之通信的套接字描述符,
通过关闭描述符,来释放描述符标定的用于存放二者交互信息的缓冲空间

服务器端代码

点击(此处)折叠或打开

  1. // selectTest.cpp

  2. #include <stdio.h>    // perror
  3. #include <string.h>     // memset

  4. #include <sys/types.h>    // AF_INET , SOCK_STREAM , INADDR_ANY
  5. #include <sys/socket.h> // socket , bind , listen , accept
  6. #include <netinet/in.h>
  7. #include <arpa/inet.h>
  8. #include <sys/select.h>
  9. #include <unistd.h> // close

  10. #define MAXSIZE        1024
  11. #define SERVER_PORT    1027
  12. #define LISTEN_QL    128
  13. //#define FD_SETSIZE 64

  14. int main ( int argc , char ** argv )
  15. {
  16.    int i , maxi , maxfd , nready, client_fds[FD_SETSIZE];
  17.    int listenfd , connfd , sockfd ;

  18.    char buf[MAXSIZE] ;
  19.    ssize_t n ; // buffer length

  20.    socklen_t client_len ;
  21.    struct sockaddr_in client_addr , server_addr ;
  22.    
  23.    fd_set temp_set ,all_set ;

  24.   // first get server listen sock fd
  25.    listenfd = socket ( AF_INET , SOCK_STREAM, 0 ) ;
  26.   
  27.  // initialize server_addr
  28.    memset ( &server_addr , 0 , sizeof( struct sockaddr_in ) ) ;
  29.    server_addr.sin_family = AF_INET ; // IP
  30.    server_addr.sin_port = htons (SERVER_PORT) ;
  31.    server_addr.sin_addr.s_addr = htonl ( INADDR_ANY) ;
  32.  
  33. // bind server addr to listen fd
  34.    bind ( listenfd , (struct sockaddr*)&server_addr , sizeof ( struct sockaddr_in) ) ;

  35. // listen
  36.    listen ( listenfd , LISTEN_QL ) ;
  37.    
  38. // initialize maxfd
  39.    maxfd = listenfd ;

  40. // initialize all_set empty
  41.    FD_ZERO ( &all_set ) ;

  42. // put listenfd into all_set
  43.    FD_SET ( listenfd , &all_set ) ;

  44. // initialize all fds in client_fds
  45.    maxi = -1 ; // used as the index of client_fds array
  46.    for ( i = 0 ; i < FD_SETSIZE ; i++ )
  47.     client_fds[i] = -1 ;
  48.   
  49.   // server's main cycle
  50.   for ( ; ; )
  51.   {
  52.     temp_set = all_set ;
  53.       
  54.         // call select method to get how many sockfds are modified
  55.         nready = select ( maxfd+1 , &temp_set , NULL, NULL , NULL ) ;

  56.     if ( nready != 0 )
  57.     //printf ("%d get ready \n", nready) ;

  58.     // we only care about read
  59.     
  60.     if ( FD_ISSET ( listenfd , &temp_set ) ) // true: means new connections coming
  61.     {
  62.         // initialize client_addr and client_len
  63.         bzero ( &client_addr , sizeof( struct sockaddr_in ) ) ;
  64.         client_len = sizeof ( client_addr ) ;
  65.     
  66.         connfd = accept (listenfd , (struct sockaddr *) &client_addr , &client_len ) ;
  67.         
  68.         printf ("comes new connection %d \n", connfd ) ;
  69.         // put connfd into client_fds
  70.     
  71.         for ( i = 0 ; i < FD_SETSIZE ; i++)
  72.             if ( client_fds[i] < 0 )
  73.             {
  74.                 client_fds[i] = connfd ;
  75.                 break ;
  76.             }
  77.         
  78.         // inner for end
  79.     
  80.         // is i out of limit
  81.         if ( i == FD_SETSIZE )
  82.         {
  83.             perror ("too many clients' connections") ;
  84.             goto error ; // server shutdown : bad solution
  85.         }        


  86.      // is index increased ?
  87.         if ( maxi < i )
  88.          maxi = i ; // increased update the max index: maxi
  89.               
  90.      // is maxfd increased ?

  91.         if ( maxfd < connfd )
  92.             maxfd = connfd ; // increased update the max fd

  93.      // add new descriptor connfd into all_set
  94.         FD_SET(connfd, &all_set ) ;
  95.     
  96.      if ( --nready <= 0 )
  97.         continue ; // continue the main loop
  98.     } // end if
  99.     
  100.     for ( i = 0 ; i <= maxfd ; i++ ) // traverse the client_fds
  101.     {
  102.         if ( (sockfd = client_fds[i] ) < 0 )
  103.             continue ;
  104.         if ( FD_ISSET ( sockfd , &temp_set ) )
  105.         {
  106.          // modifications on descritpor sockfd (memset (buf , '\0'  , MAX_LEN) ;
  107.          if ( ( n = read ( sockfd , buf , MAXSIZE)) == 0 )
  108.          {
  109.             // read return 0 , means client close connectin
  110.             printf ("no more data from client %d \n close connection \n" , sockfd) ;
  111.             
  112.             close ( sockfd ) ; // server close the connection too
  113.          FD_CLR (sockfd, &all_set) ;
  114.             // server close connection , release space , sockfd no use , clear it in fd_set
  115.             client_fds[i] = -1 ; // clear the sockfd in client_fds
  116.          }
  117.          else
  118.          {
  119.          printf ("receive %s from client %d \n", buf ,sockfd) ;
  120.          write ( sockfd , buf , n ) ;
  121.          }


  122.         if ( --nready <= 0 )
  123.          break ; // break for the inner for
  124.             } // if
  125.     
  126.       } // inner for
  127.     
  128.     
  129.   }// outer for
  130.       

  131. error :
  132.     perror ( "main error") ;
  133.     goto success ;
  134. success:
  135.     return 0 ;
  136. }
客户端代码

点击(此处)折叠或打开

  1. //selectMultiClient.cpp

  2. #include <stdio.h>     // perror
  3. #include <string.h>     // memset

  4. #include <sys/types.h> // AF_INET , SOCK_STRAM
  5. #include <sys/socket.h> // socket , connect
  6. #include <arpa/inet.h> // inet_aton
  7. #include <netinet/in.h>
  8. #include <unistd.h>

  9. #define SERVER_PORT 1027
  10. #define SERVER_IP "10.0.2.15"
  11. #define MSG_LEN 1024

  12. int main ( int c , char **v )
  13. {
  14.   char buf[MSG_LEN] ;
  15.   ssize_t n ; // message length
  16.   struct sockaddr_in server_addr ;
  17.   int connfd[3] ;
  18.   int client_num = 3 ;
  19.   
  20.   
  21.   for ( int i = 0 ;i < client_num ; i++)
  22.   {
  23.       connfd[i] = socket ( AF_INET , SOCK_STREAM , 0 ) ;
  24.       memset ( &server_addr , 0 , sizeof ( struct sockaddr_in ) ) ;
  25.       server_addr.sin_family = AF_INET ;
  26.       server_addr.sin_port = htons(SERVER_PORT) ;
  27.       inet_aton ( SERVER_IP , &server_addr.sin_addr ) ;

  28.       connect ( connfd[i] , (struct sockaddr*)&server_addr , sizeof(struct sockaddr_in )) ;
  29.  }
  30.  for ( int k = 0 ; k < 2 ; k++ )
  31.  for ( int i = 0 ; i < client_num ; i++ )
  32. {
  33.       printf ("input message send to server \n") ;
  34.       scanf ("%s" ,buf ) ;
  35.  
  36.       write ( connfd[i] , buf , strlen ( buf ) ) ;
  37. }

  38. // finally close all connections
  39. for ( int i =0 ; i < client_num ; i++ )
  40.     close (connfd[i]) ;

  41.   return 0 ;
  42. }

程序执行结果:
首先,运行服务器程序
不显示任何消息

然后,新打开一个 terminal 运行 客户端程序:
input message send to server 
inuyasha
input message send to server 
kikiyou
input message send to server 
kagome
input message send to server 
naruto 
input message send to server 
sasiki
input message send to server 
youki

随后,在服务器端运行 terminal 中看到如下信息:
1 get ready 
comes new connection 4 
comes new connection 5 
comes new connection 6 
receive inuyasha from client 4 
receive kikiyoua from client 5 
receive kagomeua from client 6 
receive narutoua from client 4 
receive sasikiua from client 5 
receive sasikiua from client 4 
receive sasikiua from client 5 
receive youkiiua from client 6 
3 get ready 
no more data from client 4 
 close connection 
no more data from client 5 
 close connection 
no more data from client 6 
 close connection 
(ctrl + c 结束 Server 进程)
不尽人意的地方就是,每次在接受数据的时候应该将 server 端用来存放信息数据的缓冲区置为空,
不然就会显示出此次数据信息没有覆盖的、残留的上次的数据信息
比如说 inuyasha --> kikiyou ----->kikiyou(a)
在 server 代码中的 123 ,124 处添加上注释中的代码 (memset (buf , '\0'  , MAX_LEN)) 即可
阅读(2102) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~