IO多路复用--select

1 IO多路复用的基本概念

IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。IO多路复用适用如下场合:

  (1)当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用。

  (2)当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。

  (3)如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。

  (4)如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。

  (5)如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。

与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。

2 select 函数

/* According to POSIX.1-2001, POSIX.1-2008 */
#include <sys/select.h>

/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

参数描述:

nfds: 集合中所有文件描述符的范围,即所有文件描述符的最大值加1,描述字0、1、2...nfds-1均将被测试;

readfds: 指向fd_set结构的指针,这个集合中包括需要监控读事件的文件描述符,若可读则返回;当然也可以置空(NULL),表示并不关心;

writefds, exceptfds: 同readfds;

timeout: select的超时时间,这个参数至关重要,它可以使select处于三种状态:

  • 若将NULL以形参传入,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止; 
  • 若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;
  • timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。

返回值:

  • >0:就绪描述字的正数目;
  • -1:出错;
  • 0 : 超时;

fd_set

select使用描述字集,典型地是一个整数数组,其中每个整数中的每一位对应一个描述字。假设使用32位整数,那么该数组的第一个元素对应于描述字0~31,第二个元素对应于描述字32~63,依此类推。所有的实现细节都与应用程序无关,它们隐藏在名为fd_set的数据类型和以下四个宏中:
void FD_ZERO (fd_set *fdset); // clear all bits in fdset
void FD_SET (int fd,fd_set *fdset); // turn on the bit for fd in fdset
void FD_CLR (int fd,fd_set *fdset); // turn off the bit for fd in fdset
int FD_ISSET(int fd,fd_set *fdset); // is the bit for fd on in fdset

struct timeval

timeval结构用于指定这段时间的秒数和微秒数,

struct timeval {
    long tv_sec;   //seconds
    long tv_usec;  //microseconds
};
select() may update the timeout argument to indicate how much time was left.  pselect() does not change this argument.

3 select函数的典型应用

下面介绍一个给予select的echo server;client直接通过telnet连接server:telnet host port,主要包括以下几个

基本功能

(1) client 发送quit,则server主动断开与客服端的连接;

(2) clinet发送其他data到server,则server会原样返回;

注意点:

(1) 由于通过telnet与server通信,telnet client发送过来的字符串的结束符为" " ,所以判断client的断开信号时,需判断是否为"quit "

(2) 同样打印客服端的数据时不需要再添加换行符,因为data form telnet client自带的就有;

(3) 由于本文中,每次读数据时都是放在一个公共的缓冲区buffer,所以每次接收数据(read)并处理完(send)后,应重置缓冲区,比如重置受影响的区域(大小size)都为'',memset(buffer, 0, size);
 
/* Handle multiple socket connections with select and fd_set on Linux */
  
#include <stdio.h>
#include <string.h>   //strlen
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>   
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/time.h> //FD_SET, FD_ISSET, FD_ZERO macros
  
#define TRUE   1
#define FALSE  0
#define PORT 8888
#define CLIENT_NUM 2
#define BLOCK_NUM 2
#define BUFFER_SIZE 1024
 
int main(int argc , char *argv[])
{
    int opt = TRUE;
    int master_socket , addrlen , new_socket , client_socket[CLIENT_NUM] , activity, i , valread , sd;
    int max_sd;
    struct sockaddr_in address;
    char buffer[BUFFER_SIZE+1];  //data buffer of 1K
    struct timeval tv;
    char quit[7]="quit
";     //for data from telnet client, the data end with "
"
   
    //set of socket descriptors
    fd_set readfds;
      
    //a message retuan to client when it connect to the server. 
    char *message = "ECHO Daemon v1.0 
";
  
    //initialise all client_socket[] to 0 so not checked
    for (i = 0; i < CLIENT_NUM; i++) 
    {
        client_socket[i] = 0;
    }
    printf("size of buffer:%lu
",sizeof(buffer));
    memset(buffer,0,sizeof(buffer)*sizeof(buffer[0]));
      
    //create a master socket for listening
    if( (master_socket = socket(AF_INET , SOCK_STREAM , 0)) == 0) 
    {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
  
    /*一个端口释放后会等待两分钟之后才能再被使用,SO_REUSEADDR是让端口释放后立即就可以被再次使用。*/
    if( setsockopt(master_socket, SOL_SOCKET, SO_REUSEADDR, (char *)&opt, sizeof(opt)) < 0 )
    {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }
  
    //type of socket created
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons( PORT );
      
    //bind the socket to localhost port 8888
    if (bind(master_socket, (struct sockaddr *)&address, sizeof(address))<0) 
    {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }
    printf("Listener on port %d 
", PORT);
     
    //try to specify maximum of BLOCK_NUM pending connections for the master socket
    if (listen(master_socket, BLOCK_NUM) < 0)
    {
        perror("listen");
        exit(EXIT_FAILURE);
    }
      
    //accept the incoming connection
    addrlen = sizeof(address);
    puts("Waiting for connections ...");
    
    /********************************call select in a infinite loop*******************************/ 
    while(TRUE) 
    {
        /*每次调用select前都要重新设置文件描述符和时间,因为事件发生后,文件描述符和时间都被内核修改啦*/
        FD_ZERO(&readfds);
  
        /*添加监听套接字*/
        FD_SET(master_socket, &readfds);
        max_sd = master_socket;
        
        tv.tv_sec = 30;
        tv.tv_usec = 0;
         
        /*添加客户端套接字*/
        for ( i = 0 ; i < CLIENT_NUM ; i++) 
        {
            //socket descriptor
            sd = client_socket[i];
             
            //if valid socket descriptor then add to read list
            if(sd > 0)
                FD_SET( sd , &readfds);
             
            //highest file descriptor number, need it for the select function
            if(sd > max_sd)
                max_sd = sd;
        }
  
    printf("
select begin, timeout left:%lds-%ldms.
",tv.tv_sec,tv.tv_usec);
        //wait for an activity on one of the sockets, timeout is tv; 
        activity = select( max_sd + 1 , &readfds , NULL , NULL , &tv);
       
        //return value < 0, error happend; 
        if ((activity < 0) && (errno!=EINTR)) 
        {
            printf("select error");
        }
    //return value = 0, timeout;
    else if(activity == 0){
        //no event happend in specific time, timeout;
        printf("select return, activity:%d,  timeout left:%lds-%ldms.
", activity, tv.tv_sec, tv.tv_usec);
        continue;
    }
          
        //If something happened on the master socket , then its an incoming connection
        if (FD_ISSET(master_socket, &readfds)) 
        {
        printf("select return, timeout left:%lds-%ldms.
",tv.tv_sec,tv.tv_usec);
            if ((new_socket = accept(master_socket, (struct sockaddr *)&address, (socklen_t*)&addrlen))<0)
            {
                perror("accept");
                exit(EXIT_FAILURE);
            }
          
            //inform user of socket number - used in send and receive commands
            printf("New connection , socket fd is %d , ip is : %s , port : %d 
" , new_socket , inet_ntoa(address.sin_addr) , ntohs(address.sin_port));
        
            //send new connection greeting message
            if( send(new_socket, message, strlen(message), 0) != strlen(message) ) 
            {
                perror("send");
            }
              
            puts("Welcome message sent successfully");
              
            //add new socket to array of sockets
            for (i = 0; i < CLIENT_NUM; i++) 
            {
                //if position is empty
                if( client_socket[i] == 0 )
                {
                    client_socket[i] = new_socket;
                    printf("Adding to list of sockets as %d
" , i);
                     
                    break;
                }
            }
        }
          
        //else there is some IO operation on some other socket :)
        for (i = 0; i < CLIENT_NUM; i++) 
        {
            sd = client_socket[i];
        printf("check client--%d:fd--%d
",i,sd);
              
            if (FD_ISSET( sd , &readfds)) 
            {
        printf("select return, timeout left:%lds-%ldms.
",tv.tv_sec,tv.tv_usec);
                //Check if it was for closing , and also read the incoming message
                valread = read( sd , buffer, 1024);
        printf("read form client,size:%d
",valread);
                if (valread <= 0)
                {
                    //Somebody disconnected , get his details and print
                    getpeername(sd , (struct sockaddr*)&address , (socklen_t*)&addrlen);
                    printf("Host disconnected , ip %s , port %d 
" , inet_ntoa(address.sin_addr) , ntohs(address.sin_port));
                      
                    //Close the socket and mark as 0 in list for reuse
                    close( sd );
                    client_socket[i] = 0;
                }
                //get message "quit", close the connection;
                else if(valread == strlen(quit) && memcmp(buffer,quit,strlen(quit))==0) {
            printf("client of fd:%d will be closed.
", sd);
            client_socket[i] = 0;
            close(sd);
        }
                //Echo back the message that came in
        else
                {   
                     
                    printf("data come from client:%s",buffer);
                    send(sd , buffer , strlen(buffer) , 0 );
                    memset(buffer,0,valread);
            //printf("sleep 5s
");
            //sleep(5);
                }
            }
        }
    }
      
    return 0;
} 
View Code

 

运行结果:

[root@localhost LibEvent_practice]# ./echo-server-select 
size of buffer:1025
Listener on port 8888 
Waiting for connections ...

select begin, timeout left:30s-0ms.
select return, timeout left:25s-213990ms.                                           
//clinet1 connect to server, select return, the time left to timeout is "timeout left"; New connection , socket fd
is 4 , ip is : 10.204.214.232 , port : 45042 //detail info of client Welcome message sent successfully Adding to list of sockets as 0 check client--0:fd--4 //client fd info saved in client_socket[CLIENT_NUM] check client--1:fd--0 select begin, timeout left:30s-0ms.                                select return, timeout left:20s-751072ms. //client2 connect to server New connection , socket fd is 5 , ip is : 10.204.214.248 , port : 48117 Welcome message sent successfully Adding to list of sockets as 1 check client--0:fd--4 check client--1:fd--5 select begin, timeout left:30s-0ms. check client--0:fd--4 select return, timeout left:25s-940254ms.
//client1 fd可读,读取数据并回传给client read form client,size:
21 data come from client:aaaaaaaaaaaaaaaaaaa check client--1:fd--5 select begin, timeout left:30s-0ms. check client--0:fd--4 check client--1:fd--5 select return, timeout left:23s-593254ms.
//client2 fd可读,读取数据并回传,可以看到dataSize=7,可以看到的数据为bbbbb,实际来自client的数据为"bbbbb ",打印出"bbbbb"然后换行; read form client,size:
7 data come from client:bbbbb select begin, timeout left:30s-0ms. check client--0:fd--4 select return, timeout left:19s-478711ms. read form client,size:6 client of fd:4 will be closed.
//读取到来自client的"quit "字符串,主动关闭client check client
--1:fd--5 select begin, timeout left:30s-0ms. select return, activity:0, timeout left:0s-0ms. select begin, timeout left:30s-0ms. ^C
 参考: 
 
原文地址:https://www.cnblogs.com/harvyxu/p/7441151.html