网络编程 -------- 5、IO多路复用

avatar
作者
猴君
阅读量:0


1、IO模型 


    所谓IO模型其实就是研究的就是读写一个文件描述符的几种不同的方式,大致可以分为:

        1)阻塞IO


            读:
                如果有数据(即使小于你要读取的字节数),直接读取数据
                如果没有数据,则阻塞等待,直到有数据或者出错
            写:
                如果有空间(即使小于你要写入的字节数),直接写入数据
                如果没有空间,则阻塞等待,直到有空间或者出错

        2)非阻塞IO 


            读:
                如果有数据,则立即读取数据
                如果没有数据,则立即返回,不等待
            写:
                如果有空间,则立即写入数据
                如果没有空间,则立即返回,不等待

        3)IO多路复用 


            允许同时对多个IO进行控制,同时监听多个文件描述符是否就绪(是否可读/可写/出错)
            允许一个进程同时等待多个文件描述符,直到某个描述符就绪(可读/可写/出错),再进行IO操作。

                例子: 
                    一个服务器,可能同时会有多个客户端与之发生通信 
                        可以在服务器使用多线程技术 与 每一个客户端进行通信 
                        缺点:并不知道何时与客户端进行通信

                ==》IO多路复用 
                    通过一定手段 监听每个客户端的读写状态 以及 服务器的连接状态


2、IO多路复用 


        select / poll / epoll 都是IO多路复用的机制,都是用来监视多个文件描述符,等待IO事件的发生。 


    1)select 

        具体实现为:

        (1)将 服务器 和 连接到服务器的客户端 的套接字描述符 都添加到一个 fd_set 集合中 
        (2)将 fd_set 集合中 所有的文件描述符 都拷贝到内核中 
        (3)内核会自动注册一个函数 pollwait()函数 用来轮询所有的文件描述符的读写状态
        (4)一旦 有一个或者多个文件描述的状态发生了改变,select函数就会返回 
     
 (5)将 fd_set 集合 从内核中 拷贝到 用户空间 ,此时 状态发生改变的文件描述符就已经被                 标记了 

          注意:用类型 fd_set 来表示一个文件描述符集合 
                    可能要监听: 
                        是否可读 
                        是否可写 
                        是否出错
                    需要用到3个fd_set集合 来表示要监听可读的文件描述符的集合 
                       可写的文件描述符集合 
                       出错的文件描述符集合 
                                                

            NAME
                select, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - synchronous I/O multiplexing

            SYNOPSIS
                /* 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);
                    功能:IO多路复用
                    参数: 
                        nfds:所有要监听的文件描述符的最大值 +1 
                        readfds:可读的文件描述符集合 
                                select函数返回时,把整个集合返回,其中 可读的已经被标记了
                        writefds:可写的文件描述符集合 
                                select函数返回时,把整个集合返回,其中 可写的已经被标记了
                        exceptfds:出错的文件描述符集合 
                                select函数返回时,把整个集合返回,其中 出错的已经被标记了
                        timeout:超时时间 
                                    struct timeval 
                                    {
                                        long    tv_sec;         /* 秒 seconds */
                                        long    tv_usec;        /* 微秒 microseconds */
                                    };
                                注意: 
                                    在调用之前,调用者传入的参数 是指“超时时间”
                                    在函数返回之后,这个timeout就表示“剩余时间”
                    返回值: 
                        >0  表示已经就绪的文件描述符的个数
                        ==0 表示超时 
                        ==-1 出错,同时errno被设置 


                void FD_CLR(int fd, fd_set *set);
                    功能:把fd 从set集合中移除 

                int  FD_ISSET(int fd, fd_set *set);
                    功能:判断fd是否在set集合中 (判断是否就绪)
                    返回值:1 存在 0 不存在 

                void FD_SET(int fd, fd_set *set);
                    功能:把fd加入到set集合中 

                void FD_ZERO(fd_set *set);
                    功能:把set集合清空 

           

例子:                  1)select延时效果                       struct timeval  timeout;                     timeout.tv_sec = 10;                     timeout.tv_usec = 0;                      select( 100, NULL, NULL, NULL, &timeout );                                   2)IO多路复用                      利用select 实现基于TCP的一对多的通信                       select_server.c   /  select_client.c                         int main( int argc, char * argv[] )                      {                         //1.创建套接字 socket                          int server_fd = socket( AF_INET, SOCK_STREAM, 0 );                         if( server_fd == -1 )                         {                             perror("socket server error ");                             return -1;                         }                         printf("server_fd = %d\n", server_fd );                                                  //2.绑定服务器的ip和端口 bind  (服务器的ip和端口)                         struct sockaddr_in   server_addr;                          server_addr.sin_family =  AF_INET;                  //协议族                          server_addr.sin_port = htons( atoi(argv[2]) );      //端口号 ( 网络字节序 )                         inet_aton( argv[1] , &server_addr.sin_addr );       //ip地址                           //设置端口号重用                          int n = 1;                         setsockopt( server_fd, SOL_SOCKET, SO_REUSEPORT, &n, sizeof(n) );                          int re = bind( server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr) );                         if( re == -1 )                         {                             perror("bind server error ");                             close( server_fd );                             return -1;                         }                         printf("bind success! \n");                                                    //3.监听  listen                           re = listen( server_fd, 5 );                         if( re == -1 )                         {                             perror("listen server error ");                             close( server_fd );                             return -1;                         }                         printf("listen success! \n");                                                    //========== select()  IO多路复用  ========================                          int client_fd[256] = {0};   //存储已经连接到服务器的客户端的套接字描述符                         int num = 0;                //记录客户端的个数                         int i;                          int max_fd = server_fd;      //文件描述符中的最大值,初始时 最大值一定是服务器的套接字描述符                           //超时时间                          struct timeval timeout;                          //可读的集合                         fd_set read_fds;                          while( 1 )                         {                             timeout.tv_sec = 2;                             timeout.tv_usec = 0;                              //将 read_fds 清空                              FD_ZERO( &read_fds );                              //将 服务器的套接字 加入到 read_fds 集合中                             FD_SET( server_fd, &read_fds );                              //将 连接到服务器的客户端 的套接字 加入到 read_fds 集合中                              for( i=0; i<num; i++ )                              {                                 FD_SET( client_fd[i], &read_fds );                             }                              //select 监听                              re = select( max_fd+1, &read_fds, NULL, NULL, &timeout );                             if( re > 0 )                             {                                 //判断read_fds 集合中 是否有文件描述符就绪                                     //服务器就绪 :  新的客户端来连接                                     //客户端就绪 : 表示客户端上有数据可读                                  if( FD_ISSET( server_fd, &read_fds ) )    //服务器就绪 --> 去接受客户端的连接请求                                  {                                     //接受连接请求 accept                                     struct sockaddr_in  client_addr;                                     socklen_t len = sizeof(client_addr);                                      int new_fd = accept( server_fd, (struct sockaddr *)&client_addr, &len );                                     if( new_fd == -1 )                                     {                                         perror("accept error ");                                         break;                                     }                                     printf("accept success \n");                                     printf("client ip = %s\n", inet_ntoa(client_addr.sin_addr) );                                      //把新的客户端的套接字保存                                      client_fd[num] = new_fd;                                     num++;                                      if( new_fd > max_fd )  //更新文件描述符中的最大值                                      {                                         max_fd = new_fd;                                     }                                 }                                  //客户端就绪 ---> 读取数据                                  for( i=0; i<num; i++ )                                  {                                     //判断客户端的套接字描述符是否可读                                     if( FD_ISSET( client_fd[i], &read_fds ) )                                     {                                         //读取数据                                          //在实际工作在,对于客户端的处理,一般采用线程来处理                                         char buf[128] = {0};                                         int r = recv( client_fd[i], buf, sizeof(buf), 0 );                                         if( r > 0 )                                         {                                             printf("recv : %s\n", buf );                                         }                                          if( buf[0] == '#' )                                         {                                             close( server_fd );                                             return -1;                                         }                                     }                                 }                             }                             else if( re == 0 )                             {                                 //超时                                  printf( "超时 \n" );                             }                             else                             {                                 //出错                                  perror( "select error " );                                 break;                             }                         }                          //关闭套接字                         close( server_fd );                     }

    2)poll 

        poll的作用和实现原理 和 select 是类似的,select监听的文件描述符 在内核中监听的时候存储在数组中而poll存储在链表上。并且poll的功能和select类似,select “监听”多个文件描述符是否就绪 ,而 poll 是用一个结构体 struct pollfd{} 来描述监听请求 。

                    struct pollfd 
                    {
                        int   fd;         /* 指定要监听的文件描述符 file descriptor */
                        short events;     /* 监听事件 requested events */
                                            在Linux内核中 事件是用 bit-fields 位域实现的
                                                POLLIN    可读事件
                                                POLLOUT   可写事件
                                                POLLERR   出错事件
                                                ... 
                                            例子: 
                                                可读可写事件  POLLIN | POLLOUT 

                        short revents;    /* 返回已经就绪的事件 returned events */
                                            在调用poll函数后,revents 字段会被填充相应的事件  
                                                例子: 
                                                    //判断是否已经可读 
                                                    struct pollfd  pfd;
                                                    if( pfd.revents & POLLIN )
                                                    {
                                                        //可读 
                                                    }

                    };


            NAME
                poll - wait for some event on a file descriptor
            SYNOPSIS
                #include <poll.h>

                int poll(struct pollfd *fds, nfds_t nfds, int timeout);
                    功能: 
                    参数: 
                        fds:结构体指针,指向要监听的文件描述符数组
                        nfds:结构体数组的元素个数
                        timeout:超时时间 单位:ms 
                    返回值: 
                        >0  表示已经就绪的文件描述符的个数
                        ==0 表示超时 
                        ==-1 出错,同时errno被设置 

           

练习:                  利用poll 来实现基于TCP的一对多的通信                       poll_server.c   /  poll_client.c                             #define MAX_NUM 256                           //1.创建套接字                         //2.设置服务器的ip和端口                         //3.绑定                          //4.监听                           //================= poll()  IO多路复用  ===================                          //定义一个 struct pollfd 结构体数组,来保存要监听的文件描述符                         struct pollfd  fds[MAX_NUM];                          int client_fd[MAX_NUM] = {0};   //存储已经连接到服务器的客户端的套接字描述符                         int num = 0;                //记录客户端的个数                         int i;                          struct sockaddr_in  client_addr[MAX_NUM];                                                  while( 1 )                         {                             //把服务器的文件描述符 加入到 fds结构体数组中 fds[0]                             fds[0].fd = server_fd;                             fds[0].events = POLLIN;     //要监听的事件:可读                             fds[0].revents = 0;                              //把客户端的文件描述符 加入到 fds结构体数组中                              for( i=0; i<num; i++ )                              {                                 fds[i+1].fd = client_fd[i];                                 fds[i+1].events = POLLIN;     //要监听的事件:可读                                 fds[i+1].revents = 0;                             }                                                           //poll() 监听                              re = poll( fds, MAX_NUM, 2000 );                             if( re > 0 )                             {                                 //服务器就绪 ---> 接受客户端的连接请求                                  if( fds[0].revents & POLLIN )                                 {                                     socklen_t len = sizeof(client_addr[num]);                                      //接受连接请求 accept                                     int new_fd = accept( server_fd, (struct sockaddr *)&client_addr[num], &len );                                     if( new_fd == -1 )                                     {                                         perror("accept error ");                                         break;                                     }                                     printf("accept success \n");                                     printf("client ip = %s\n", inet_ntoa(client_addr[num].sin_addr) );                                      //把新的客户端的套接字保存                                      client_fd[num] = new_fd;                                     num++;                                                                      }                                 else    //客户端就绪 ---> 读取数据                                  {                                     for( i=0; i<num; i++ )                                      {                                         if( fds[i+1].revents & POLLIN )                                         {                                             //读取数据                                              char buf[128] = {0};                                             int r = recv( client_fd[i], buf, sizeof(buf), 0 );                                             if( r > 0 )                                             {                                                 printf("%s : %s\n", inet_ntoa(client_addr[i].sin_addr), buf );                                             }                                         }                                      }                                  }                             }                             else if( re == 0 )                             {                                 //超时                                  printf( "超时 \n" );                             }                             else                              {                                 //出错                                  perror( "poll error " );                                 break;                             }                          }

    3)epoll    ---    epoll_create / epoll_ctl / epoll_wait 

        (3.1) epoll_create() 

                NAME
                    epoll_create - open an epoll file descriptor
                SYNOPSIS
                    #include <sys/epoll.h>

                    int epoll_create(int size);
                        功能: 
创建一个epoll实例 用来监听其他的文件描述符的状态
                        参数: 
                            size:该参数已经被忽略了,现在只需要给一个大于0数即可 
                        返回值: 
                            成功,返回epoll实例对象,就是一个文件描述符
                            失败,返回-1,同时errno被设置 

                创建一个epoll实例 用来监听其他的文件描述符的状态 ,则要被监听的文件描述符 就必须加入到epoll实例中 。

        (3.2) epoll_ctl() 

                NAME
                    epoll_ctl - control interface for an epoll file descriptor
                SYNOPSIS
                    #include <sys/epoll.h>

                    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
                        功能: 控制监听的文件描述符的状态 
                        参数: 
                            epfd:指定要操作的epoll实例   
                            op: 选项 
                                    EPOLL_CTL_ADD 添加  把一个要监听的文件描述符添加到epoll实例中 
                                    EPOLL_CTL_MOD 修改  修改一个已经在epoll实例中的文件描述符的监听事件 
                                    EPOLL_CTL_DEL 删除  从epoll实例中删除一个文件描述符  
                            fd:指定要操作的文件描述符 
                            event:指定要监听的事件 结构体 
                                    struct epoll_event 
                                    {
                                        uint32_t     events;      /* 要监听的事件 Epoll events */
                                                                        EPOLLIN      可读事件
                                                                        EPOLLOUT     可写事件
                                                                        EPOLLERR     出错事件
                                                                        EPOLLET      边缘触发模式
                                                                        ...

                                                                            LT 级别触发 Level-triggered
                                                                                只要有数据,就会不停地往上报告事件
                                                                                默认:LT

                                                                            ET 边缘触发 Edge-triggered
                                                                                只要数据的变化(数量),才报告事件

                                        epoll_data_t data;        /* 用来存储用户的数据 User data variable */
                                    };
                                        typedef union epoll_data 
                                        {
                                            void        *ptr;   //用户的指针数据 
                                            int          fd;    //文件描述符
                                            uint32_t     u32;
                                            uint64_t     u64;
                                        } epoll_data_t;
                        返回值: 
                            成功,返回0
                            失败,返回-1,同时errno被设置 


        (3.3) epoll_wait() 
               

                NAME
                    epoll_wait -  wait  for  an  I/O  event on an epoll file descriptor
                SYNOPSIS
                    #include <sys/epoll.h>

                    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
                        功能: 用来等待监听事件的发生
                        参数: 
                            epfd:指定要操作的epoll实例   
                            events: 结构体事件数组,用来存储已经就绪的事件的信息 
                            maxevents:结构体数组中,最大可以保存多少个事件 
                            timeout:超时时间 单位:ms 
                        返回值: 
                            >0  表示已经就绪的文件描述符的个数
                            ==0 表示超时 
                            ==-1 出错,同时errno被设置 

       

             利用epoll 实现UDP的简单通信                   epoll_udp_server.c   /  epoll_udp_client.c                         //1.创建套接字 UDP                      //2.设置服务器的ip和端口                     //3.绑定                       //================= epoll()  IO多路复用  ===================                       //(1)创建一个epoll实例,用来监听其他的文件描述符的状态                      int epfd = epoll_create( 10 );                     if( epfd == -1 )                     {                         perror("epoll_create error ");                         close( server_fd );                         return -1;                     }                     printf("epoll_create success! \n");                      //(2)把要监听的文件描述符 加入到epoll实例中                      struct epoll_event  ev;                     ev.events = EPOLLIN | EPOLLET;   //要监听的事件:可读 | 边缘触发模式                     ev.data.fd = server_fd;                       re = epoll_ctl( epfd, EPOLL_CTL_ADD, server_fd, &ev );                     if( re == -1 )                     {                         perror("epoll_ctl error ");                         close( server_fd );                         return -1;                     }                     printf("epoll_ctl success! \n");                      struct sockaddr_in  client_addr;                     socklen_t len = sizeof(client_addr);                      while( 1 )                     {                         struct epoll_event  ee[10];   //保存已经就绪的事件信息的数组                          int i;                          //(3)等待监听事件的发生                          int num = epoll_wait( epfd, ee, 10, 2000 );                         if( num > 0 )                         {                             //判断是否已经可读                             for( i=0; i<num; i++ )                             {                                 if( ee[i].events & EPOLLIN )                                 {                                     //接收数据                                     char buf[128] = {0};                                      re = recvfrom( server_fd, buf, sizeof(buf), 0, (struct sockaddr *)&client_addr, &len );                                     if( re > 0 )                                     {                                            printf("recv : %s\n", buf );                                     }                                     else                                      {                                         perror("recvfrom error ");                                         break;                                     }                                 }                             }                          }                         else if( num == 0 )                         {                             //超时                              printf( "超时 \n" );                         }                          else                          {                             //出错                              perror( "epoll_wait error " );                             break;                         }                      }


3、 select、poll、epoll 的区别 

    select、poll、epoll 都是IO多路复用技术,都是用来监听多个文件描述符的状态变化(是否可读/可写/出错)的

    他们的区别:
    1) select  
        时间复杂度  O(n)    无差别轮询 
        缺点: 
            监听文件描述符的数量上有限制,基于数组来存储的
            轮询,效率低 
            文件描述符需要 维护在一个比较大的数组中,在用户空间和内核空间中传递时 开销比较大

    2) poll  
        时间复杂度  O(n)    轮询 
        缺点: 
            轮询,效率低
            文件描述符需要 维护在一个比较大的空间中,在用户空间和内核空间中传递时 开销比较大
        优点:
            没有最大连接数量的限制,基于链表来存储的 

    3) epoll 
        时间复杂度  O(1)   
        优点: 
            没有最大连接数量的限制
            效率提升,不是轮询方式,不会随着fd数量的增加 而效率下降
            内存拷贝开销比较小,利用内存映射

    总结: 
        综上所述,在选择select、poll、epoll时,
            要根据具体的应用场景、系统资源、连接数等因素 以及 三者自身的特点 进行选择。

        (1)表面上看epoll的性能好,但是在连接数量比较少且都比较活跃的情况下 
            select和poll的性能 可能比epoll好,毕竟epoll的通知机制需要很多函数的回调 

        (2)select低效是因为每次都需要去轮询,但是低效也是相对的,视情况而定,
            也可以通过良好的设计改善

    广告一刻

    为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!