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


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低效是因为每次都需要去轮询,但是低效也是相对的,视情况而定,
            也可以通过良好的设计改善

  • 18
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值