• 网络编程:poll


    原理

    和select类似,只是描述fd集合的方式不同,poll使用pollfd结构而非select的fd_set结构。 管理多个描述符也是进行轮询,根据描述符的状态进行处理,但poll没有最大文件描述符数量的限制。

    select 和poll的区别

    select采用的是位掩码的模型,参考NDK中sysroot/usr/include/sys/select.h,即

    #define FD_SETSIZE 1024
    #define NFDBITS (8 * sizeof(unsigned long))
    #define __FDSET_LONGS (FD_SETSIZE/NFDBITS)
    
    typedef struct {
      unsigned long fds_bits[__FDSET_LONGS];
    } fd_set;
    
    #define __FDELT(fd) ((fd) / NFDBITS)
    #define __FDMASK(fd) (1UL << ((fd) % NFDBITS))
    #define __FDS_BITS(set) (((fd_set*)(set))->fds_bits)
    
    #define FD_ZERO(set) (memset(set, 0, sizeof(*(fd_set*)(set))))
    
    #if defined(__BIONIC_FORTIFY)
    extern void __FD_CLR_chk(int, fd_set*, size_t);
    extern void __FD_SET_chk(int, fd_set*, size_t);
    extern int  __FD_ISSET_chk(int, fd_set*, size_t);
    #define FD_CLR(fd, set) __FD_CLR_chk(fd, set, __bos(set))
    #define FD_SET(fd, set) __FD_SET_chk(fd, set, __bos(set))
    #define FD_ISSET(fd, set) __FD_ISSET_chk(fd, set, __bos(set))
    #else
    #define FD_CLR(fd, set) (__FDS_BITS(set)[__FDELT(fd)] &= ~__FDMASK(fd))
    #define FD_SET(fd, set) (__FDS_BITS(set)[__FDELT(fd)] |= __FDMASK(fd))
    #define FD_ISSET(fd, set) ((__FDS_BITS(set)[__FDELT(fd)] & __FDMASK(fd)) != 0)
    #endif /* defined(__BIONIC_FORTIFY) */
    

    不考虑__BIONIC_FORTIFY,即select是通过申请了一个unsigned long fds_bits[]的数组,数组中的每一位代表一个文件句柄的掩码,即select最大只支持fd < 1024的情况,如果fd >= 1024,就必须重新编译内核了。
    那么区别点:

    • 区别1:select使用的是定长数组,而poll是通过用户自定义数组长度的形式(pollfd[])。
    • 区别2:select只支持最大fd < 1024,如果单个进程的文件句柄数超过1024,select就不能用了。poll在接口上无限制,考虑到每次都要拷贝到内核,一般文件句柄多的情况下建议用epoll。
    • 区别3:select由于使用的是位运算,所以select需要分别设置read/write/error fds的掩码。而poll是通过设置数据结构中fd和event参数来实现read/write,比如读为POLLIN,写为POLLOUT,出错为POLLERR:
    struct pollfd pfd;
    pfd.fd = fd;
    pfd.events = POLLIN;
    pfd.revents = 0;
    
    • 区别4:select中fd_set是被内核和用户共同修改的,所以要么每次FD_CLR再FD_SET,要么备份一份memcpy进去。而poll中用户修改的是events,系统修改的是revents。所以参考muduo的代码,都不需要自己去清除revents,从而使得代码更加简洁。
    • 区别5:select的timeout使用的是struct timeval *timeout,poll的timeout单位是int。
    • 区别6:select使用的是绝对时间,poll使用的是相对时间。
    • 区别7:select的精度是微秒(timeval的分度),poll的精度是毫秒。
    • 区别8:select的timeout为NULL时表示无限等待,否则是指定的超时目标时间;poll的timeout为-1表示无限等待。所以有用select来实现usleep的。
    • 区别9:理论上poll可以监听更多的事件,比如sysroot/usr/include/asm-generic/poll.h中有
    #define POLLIN 0x0001
    #define POLLPRI 0x0002
    /* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */
    #define POLLOUT 0x0004
    #define POLLERR 0x0008
    #define POLLHUP 0x0010
    #define POLLNVAL 0x0020
    /* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */
    #define POLLRDNORM 0x0040
    #define POLLRDBAND 0x0080
    #ifndef POLLWRNORM
    #define POLLWRNORM 0x0100
    /* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */
    #endif
    #ifndef POLLWRBAND
    #define POLLWRBAND 0x0200
    #endif
    /* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */
    #ifndef POLLMSG
    #define POLLMSG 0x0400
    #endif
    #ifndef POLLREMOVE
    /* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */
    #define POLLREMOVE 0x1000
    #endif
    #ifndef POLLRDHUP
    #define POLLRDHUP 0x2000
    /* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */
    #endif
    #define POLLFREE 0x4000
    #define POLL_BUSY_LOOP 0x8000
    struct pollfd {
    /* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */
     int fd;
     short events;
     short revents;
    };
    

    API

    poll函数原型

    int poll(struct pollfd *fds, unsigned long nfds, int timeout);  
    返回值:若有就绪描述符则为其数目,若超时则为0,若出错则为-1
    

    第一个参数是一个 pollfd 的数组

    其中 pollfd 的结构如下:

    struct pollfd {
        int    fd;       /* file descriptor */
        short  events;   /* events to look for */
        short  revents;  /* events returned */
     };
    

    首先是描述符 fd,然后是描述符上待检测的事件类型 events,注意这里的 events 可以表示多个不同的事件,具体的实现可以通过使用二进制掩码位操作来完成,例如,POLLIN 和 POLLOUT 可以表示读和写事件。

    events类型的事件可以分为两大类

    • 第一类是可读事件,有以下几种
    #define POLLIN     0x0001    /* any readable data available */
    #define POLLPRI    0x0002    /* OOB/Urgent readable data */
    #define POLLRDNORM 0x0040    /* non-OOB/URG data available */
    #define POLLRDBAND 0x0080    /* OOB/Urgent readable data */
    
    • 第二类是可写事件,有以下几种:
    #define POLLOUT    0x0004    /* file descriptor is writeable */
    #define POLLWRNORM POLLOUT   /* no write type differentiation */
    #define POLLWRBAND 0x0100    /* OOB/Urgent data can be written */
    

    一般我们在程序里面统一使用 POLLOUT。套接字可写事件和 select 的 writeset 基本一致,是系统内核通知套接字缓冲区已准备好,通过 write 函数执行写操作不会被阻塞。

    其次参数 nfds 描述的是数组 fds 的大小,简单说,就是向 poll 申请的事件检测的个数。
    最后一个参数timeout,描述poll的行为

    如果是一个 <0 的数,表示在有事件发生之前永远等待;如果是 0,表示不阻塞进程,立即返回;如果是一个 >0 的数,表示 poll 调用方等待指定的毫秒数后返回。

    关于返回值,,当有错误发生时,poll 函数的返回值为 -1;如果在指定的时间到达之前没有任何事件发生,则返回 0,否则就返回检测到的事件个数,也就是“returned events”中非 0 的描述符个数。

    实践

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <signal.h>
    #include <errno.h>
    #include <sys/poll.h> 
    #include <sys/socket.h>
    #include <netinet/in.h>
    
    #define SERV_PORT 43211
    #define LISTENQ 1024
    #define INIT_SIZE 128
    #define MAXLINE 1024
    
    
    
    int tcp_server_listen(int port)
    {
        int listen_fd;
        listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    
        struct sockaddr_in server_addr;
        bzero(&server_addr, sizeof(server_addr));
        server_addr.sin_family = AF_INET;
        server_addr.sin_port = htons(port);
        server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    
        int on = 1;
        setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
    
        int rt1 = bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
        if(rt1 < 0)
        {
            perror("bind failed");
            return -1;
        }
    
        int rt2 = listen(listen_fd, LISTENQ);
        if(rt2 < 0)
        {
            perror("listen failed");
            return -1;
        }
        signal(SIGPIPE, SIG_IGN);
    
        return listen_fd;
    
    }
    
    int main(int argc, char *argv[])
    {
        int listen_fd, connected_fd;
        int ready_number;
        ssize_t n;
        char buf[MAXLINE];
        struct sockaddr_in client_addr;
    
        listen_fd = tcp_server_listen(SERV_PORT);
    
        //初始化pollfd数组,这个数组的第一个元素是listen_fd,其余的用来记录将要连接的connect_fd
        //将监听套接字 listen_fd 和对应的 POLLRDNORM 事件加入到 event_set 里,表示我们期望系统内核检测监听套接字上的连接建立完成事件。
        struct pollfd event_set[INIT_SIZE];
        event_set[0].fd = listen_fd;
        event_set[0].events = POLLRDNORM;
    
        //用-1表示这个数组位置还没有被占用
        //对应 pollfd 里的文件描述字 fd 为负数,poll 函数将会忽略这个 pollfd
        int i;
        for(int i = 1; i < INIT_SIZE; i++)
        {
            event_set[i].fd = -1;
        }
    
        for(;;)
        {
            //调用 poll 函数来进行事件检测。poll 函数传入的参数为 event_set 数组,数组大小 INIT_SIZE 和 -1
            //之所以传入 INIT_SIZE,是因为 poll 函数已经能保证可以自动忽略 fd 为 -1 的 pollfd,否则我们每次都需要计算一下 event_size 里真正需要被检测的元素大小;
            //timeout 设置为 -1,表示在 I/O 事件发生之前 poll 调用一直阻塞。
            if((ready_number = poll(event_set, INIT_SIZE, -1)) < 0)
            {
                perror("poll failed");
                return -1;
            }
            // 使用了如 event_set[0].revent 来和对应的事件类型进行位与操作
            // 因为 event 都是通过二进制位来进行记录的,位与操作是和对应的二进制位进行操作,一个文件描述字是可以对应到多个事件类型的。
            if(event_set[0].revents & POLLRDNORM)
            {
                socklen_t client_len = sizeof(client_addr);
                //调用 accept 函数获取了连接描述字
                connected_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_len);
    
                //找到一个可以记录该连接套接字的位置
                for(i = 1; i < INIT_SIZE; i++)
                {
                    if(event_set[i].fd < 0)
                    {
                        event_set[i].fd = connected_fd;
                        event_set[i].events = POLLRDNORM;
                        break;
                    }
                }
                //event_set 已经被很多连接充满了,没有办法接收更多的连接了
                if( i == INIT_SIZE)
                {
                    perror("can not hold so many clients");
                    return -1;
                }
                //加速优化能力,因为 poll 返回的一个整数,说明了这次 I/O 事件描述符的个数,
                //如果处理完监听套接字之后,就已经完成了这次 I/O 复用所要处理的事情,那么我们就可以跳过后面的处理,再次进入 poll 调用。
                if(--ready_number <= 0)
                    continue;
            }
            //循环处理是查看 event_set 里面其他的事件,也就是已连接套接字的可读事件
            for(i = 1; i < INIT_SIZE; i++)
            {
                int socket_fd;
                if((socket_fd = event_set[i].fd) < 0)
                    continue;
                //通过检测 revents 的事件类型是 POLLRDNORM 或者 POLLERR,我们可以进行读操作。
                if(event_set[i].revents & (POLLRDNORM | POLLERR))
                {
                    //读取数据正常之后,再通过 write 操作回显给客户端;
                    if((n = read(socket_fd, buf, MAXLINE)) > 0)
                    {
                        if(write(socket_fd, buf, n) < 0)
                        {
                            perror("write error");
                            return -1;
                        }
                    }
                    else if(n == 0 || errno == ECONNRESET)
                    {
                        //如果读到 EOF 或者是连接重置,则关闭这个连接,并且把 event_set 对应的 pollfd 重置
                        close(socket_fd);
                        event_set[i].fd = -1;
                    }
                    else 
                    {
                        perror("read error");
                        return -1;
                    }
                    if(--ready_number <= 0)
                        break;
                }
            }
        }
    }
    

    运行结果
    通过多个telnet客户端登录,互补影响

  • 相关阅读:
    俞敏洪:从马云、柳传志、王健林、马化腾看人与人的区别
    张瑞敏:人不成熟的五大特征
    为那么些在毕业季迷茫的人们,送上马云在清华经管院的毕业典礼演讲
    HTML5学习+javascript学习:打飞机游戏Service层Control层+源码
    HTML5学习+javascript学习:打飞机游戏简介以及Model层
    javascript的笔记
    搜狗主页页面CSS学习小记
    简单账务管理系统源码
    Mac SSH免密码登录 Linux服务器
    Flink写入数据到Elasticsearch示例
  • 原文地址:https://www.cnblogs.com/whiteBear/p/16029405.html
Copyright © 2020-2023  润新知