C++网络编程I/O 多路复用之poll

前言

学到这,感觉没什么可说的,poll 是对select 的改进,但是它是个半成品,相对 select 提升不大。更多的是把poll当成一个过渡,大家并没有给poll过多的关注,而是直接epoll了。所以着重解释了从select到poll改变了什么,思路的变化,到如何使用上其实就是调函数,也没有太多细节要注意。

回顾上文select

上节通过解释和代码,我们可以总结select的缺点主要有:

  1. select函数的监听事件的文件描述符集合参数,是传入传出参数,导致每次调用select都要重新设置
  2. 由于传入传出,fdset的位图bitmap反复产生用户态和内核态之间的拷贝,这需要大量的开销。
  3. 传出的位图内容改变了,但是大小没有变化。select函数返回后,是通过遍历fdset,找到就绪的描述符fd。
  4. 最主要是select关心的文件描述符是有上限的(底层是位图,有固定大小,在linux下,bitmap的长度不能超过1024,虽然可以调整,但是描述符数量越大,效率越低,调整的意义不大。)

poll的改进

解决了select的两件事情:

  1. poll以链表的形式存储文件描述符,解决了文件描述符的数量限制
  2. poll的数据结构,将监听事件集合和返回事件集合分离,所以每次循环都不需要重新设置。 其实就是指struct pollfd结构体里的events和revents。

poll

poll也是操作系统提供的系统调用函数,机制与select类似,管理多个描述符也是进行轮询,即每次调用都要扫描整个注册文件描述符的集合,并将其中就绪的文件描述符返回给用户程序,因此他们检测就绪事件的算法的时间复杂度为O(n)。

pollfd结构体

struct pollfd {
    int   fd;         /* file descriptor */
    short events;     /* requested events */
    short revents;    /* returned events */
};
  • fd:待监听的文件描述符
  • events:用户告诉内核,需要监听该文件描述符的哪些事件。
  • revents:内核告诉用户,监听到该文件描述符的哪些事件就绪。

poll支持的事件类型如下表,其中,可以在events中填充的事件类型,如果关注多种事件类型,可以使用按位或将这些事件合到一起:

image.png

poll函数

 #include <poll.h>
 int poll(struct pollfd *fds, nfds_t nfds, int timeout);     

参数:

  • fds:struct pollfd类型的数组,用来传递用户关注的文件描述符以及事件类型,每一个元素中, 包含了三部分内容: 文件描述符, 监听的事件集合, 返回的事件集合
  • nfds:表示fds数组的长度
  • timeout:poll函数的超时时间, 单位是毫秒(ms)。设置为0,表示poll非阻塞等待;设置为-1,表示阻塞等待;设置大于0,则表示阻塞等待一次的时间。超过时间后,会超时返回,跟select一样。

返回值

返回值和select的完全一样。

  • 返回0:超时,表示超出设置的timeout时间后还没有文件描述符就绪。
  • 返回-1:发生错误,并设置 errno 为下列值之一。
    1. EBADF:一个或多个结构体中指定的文件描述符无效。
    2. EFAULT:fds 指针指向的地址超出进程的地址空间。
    3. EINTR:请求的事件之前产生一个信号,调用可以重新发起。
    4. EINVAL:nfds 参数超出 PLIMIT_NOFILE 值。
    5. ENOMEM:可用内存不足,无法完成请求。
  • 返回大于0的值:表示poll由于监听的文件描述符就绪而返回,返回就绪文件描述符的总数。

poll服务器实现思路

  1. 首先lfd=socket()创建TCP套接字bind()绑定端口号,对TCP套接字进行listen(),这时就会得到一个监听套接字lfd;
  2. 创建pollfd结构体数组cfds[OPEN_MAX] ,把对lfd读事件监听添加到数组的第一个元素,并用-1初始化cfds[OPEN_MAX]里剩下元素的fd;
  3. 进行poll()函数阻塞轮询,根据不同的返回值,进行不同的处理:
    • 0:timeout超时,继续循环回去轮询监听;
    • -1:出错返回,根据errno设置处理方式,或exit退出,或continue;
    • 大于0:说明这时有事件就绪或者有新连接到来
  4. 如果返回值大于0,对cfds[]数组进行判断操作:
    • 先判断cfds[0].revents & POLLIN,为真则处理客户端链接请求,cfd=accept(),此时进行accept不会被阻塞,并将cfd添加到cfds[]中的未使用位置,并设置监听读事件;
    • 再判断其他文件描述符cfds[i].revents & POLLIN,为真则为读事件,有客户端发来了数据,这时就能对客户端发来的数据进行read,read(cfds[i].fd, buf, sizeof(buf))),这时的read也不会阻塞

poll总结

优点

(1)poll()的nfds参数,不要求像select那样计算最大文件描述符加一的大小。
(2)poll没有最大连接数的限制,原因是它是基于链表来存储的(但是数量过大后性能也是会下降)。
(3)pollfd结构包含了要监视的event和就绪的revent,不再使用select"参数-值"传递的方式,在调用函数时,只需要对参数进行一次设置就好了。
(4)优化了编程接口。select()函数有5个参数,而poll()减少到了3个参数。

缺点

  1. 内存拷贝开销,仍然如select一样的,避免不了调用poll时用户到内核,poll返回时内核到用户的数据拷贝。包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。

系统调用函数的执行是发生在内核区的,而用户程序的执行是发生在用户区的,所以会存在内核区与用户区之间的内存复制的系统开销。

  1. 遍历问题,与select一样,poll返回后,需要需要通过遍历获取就绪事件的文件描述符,它的开销随着文件描述符数量的增加而线性增大。
  2. 时间复杂度,虽然没有数量上限,但同时连接的大量客户端在一时刻可能只有很少的就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降

与select一样,每次poll系统调用时,需要在内核遍历传入的整个文件描述符集合,逐个检测,查看是否有就绪的文件描述符,然后返回就绪文件描述符的个数。也就是说,poll也是线性扫描的方式,当注册的文件描述符fd的数量很多时,效率会较低,时间复杂度为O(n)。

  1. 工作模式,poll 只能工作在水平触发(LT)模式下。

poll代码

//
// Created by 11406 on 2022/5/26.
//
#include <string.h>
#include <strings.h>
#include<netinet/in.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <pthread.h>
#include <signal.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <poll.h>
#include "wrap.h"

#define PORT 6266
#define OPEN_MAX 1024

int main(int argc,char *argv[]){
    int lfd,cfd;
    int maxi,i,nready,ret;

    char buf[BUFSIZ],clie_ip[INET_ADDRSTRLEN];

    struct pollfd client[OPEN_MAX];

    struct sockaddr_in srv_addr,clt_addr;
    socklen_t clt_addr_len;

    bzero(&srv_addr, sizeof(srv_addr));
    srv_addr.sin_family=AF_INET;
    srv_addr.sin_port= htons(PORT);
    srv_addr.sin_addr.s_addr= htonl(INADDR_ANY);

    lfd= Socket(AF_INET,SOCK_STREAM,0);
    int opt=1;
    setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt, sizeof(opt));
    Bind(lfd,(struct sockaddr*)&srv_addr, sizeof(srv_addr));
    Listen(lfd,128);

    client[0].fd=lfd;
    client[0].events=POLLIN;
    //client[0].revents=0;
    maxi=0;   /* client[]数组有效元素中最大元素下标 */

    for(i=1;i<OPEN_MAX;i++){
        client[i].fd=-1;
    }

    while(1){
        nready= poll(client,maxi+1,-1);
        /*if(nready==-1){
            if(errno == EINTR){
                continue;;
            } else{
                sys_err("poll error");
            }
        }*/
        if(client[0].revents & POLLIN) {
            clt_addr_len = sizeof(clt_addr);
            cfd = Accept(lfd, (struct sockaddr *) &clt_addr, &clt_addr_len);
            printf("received from %s at PORT %d\n",inet_ntop(AF_INET, &(clt_addr.sin_addr.s_addr), clie_ip, sizeof(clie_ip)), ntohs(clt_addr.sin_port));

            for (i = 1; i < OPEN_MAX; i++) {
                if (client[i].fd == -1) {
                    client[i].fd = cfd;
                    client[i].events = POLLIN;
                    client[i].revents = 0;
                    break;
                }
            }
            if (i == OPEN_MAX) {
                sys_err("too many clients");
            }
            if (i > maxi) {
                maxi = i;
            }
            if (--nready == 0) {
                continue;
            }
        }

        for (i = 1; i <= maxi; i++) {
            if (client[i].fd == -1) {
                continue;
            }
            if (client[i].revents & POLLIN) {
                ret = read(client[i].fd, buf, sizeof(buf));
                printf("----------%d\n", ret);
                if (ret == 0) {

                    Close(client[i].fd);
                    client[i].fd == -1;

                } else if (ret < 0) {  //ECONNRESET
                    sys_err("read");
                } else {
                    //write(STDOUT_FILENO,buf,ret);
                    for (int j = 0; j < ret; j++) {
                        buf[j] = toupper(buf[j]);
                    }
                    write(STDOUT_FILENO, buf, ret);
                    write(client[i].fd, buf, ret);
                }
                if (--nready <= 0) {   //有事件,且处理了事件才减1!!!
                    break;
                }
            }

        }
    }

    Close(lfd);
    return 0;

}
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: libevent是一个开源的C语言网络编程库,主要用于处理高并发网络连接。它提供了对事件驱动的支持,使得开发者可以方便地编写高效的并发网络应用程序。 libevent的核心是事件循环机制。在传统的网络编程中,通常需要使用多线程或多进程来处理并发连接,而使用libevent可以通过一个事件循环来处理多个连接。在事件循环中,可以注册多个事件,并定义回调函数来处理事件的触发。当有事件发生时,libevent会调用相应的回调函数来处理事件的处理逻辑。这样可以大大简化并发编程的复杂性,并提高程序的性能。 libevent的事件模型基于操作系统提供的I/O多路复用机制,如select、poll和epoll等。它可以在不同的操作系统平台上运行,并提供一致的接口和高效的事件处理机制。借助这些机制,libevent可以同时处理大量的并发连接,并保持低延迟和高吞吐量。 除了处理网络连接,libevent还提供了其他常用的功能,如定时器和信号处理等。它允许开发者在事件循环中注册定时器事件,可以用于定时任务的调度。同时,libevent还可以处理来自操作系统的信号,并提供了对信号的处理接口,以便开发者能够处理各种系统事件。 总之,libevent是一个功能强大、简单易用的高并发网络编程库,适用于开发各种类型的网络应用。无论是开发服务器、代理、聊天程序还是实时应用,libevent都能帮助开发者快速编写高性能的并发网络程序。 ### 回答2: libevent是一个开源的C/C++网络库,用于高性能的事件驱动编程。它提供了一个轻量级、可移植的框架,用于开发高并发的网络应用程序。 它的设计目标是提供一个高效的事件处理器,可以处理成千上万个并发连接,并且支持多线程并发处理。libevent基于事件驱动模型,通过异步I/O和回调函数来实现高并发处理网络请求。 libevent提供了一系列的函数来注册和监听各种网络事件,包括读、写、超时和信号等等。当一个事件发生时,libevent会调用相应的回调函数来处理事件。通过这种方式,我们可以非常方便地处理并发连接,并实现高性能的网络编程。 libevent的优点主要包括: 1. 高性能:libevent使用异步I/O和事件驱动模型,能够处理成千上万个并发连接,具有很高的处理能力。 2. 可移植性:libevent提供了统一的接口,可以在多种操作系统上运行,包括Linux、Windows、Mac等。 3. 易用性:libevent简单易用,只需注册感兴趣的事件和相应的回调函数,就可以实现高效的网络编程。 4. 多线程支持:libevent支持多线程并发处理,可以充分利用多核CPU的性能优势。 总之,libevent是一款非常适合高并发网络编程的开源库,它可以帮助我们实现高性能的服务器程序,提升系统的并发处理能力。无论是开发网络服务器还是网络应用程序,libevent都是一个不错的选择。 ### 回答3: libevent 是一个用于高并发网络编程的 C/C++ 库。它提供了一个跨平台的异步事件驱动的网络编程框架,能够实现高效地处理大量并发连接的需求。 libevent 的主要特点包括: 1. 异步事件驱动:libevent 使用事件驱动模型,主要利用非阻塞 I/O 和事件回调机制,能够高效地处理大量并发事件。 2. 跨平台支持:libevent 提供了跨不同操作系统的支持,包括 Windows、Linux、Unix 等,并且提供了统一的 API 接口,方便开发者进行跨平台开发。 3. 支持多种网络协议:libevent 支持 TCP、UDP、HTTP 等多种网络协议,为开发者提供了丰富的网络编程能力。 4. 高性能:libevent 的设计目标之一是高性能,它通过使用多路复用技术,将系统资源高效地利用起来,能够同时处理大量并发连接,并且保持低延迟。 5. 灵活易用:libevent 提供了简洁的 API,使用起来非常方便,可以快速实现高并发网络编程的需求。 总之,libevent 是一个强大而灵活的 C/C++ 库,适用于各种需要处理高并发连接的网络应用程序。无论是开发高性能服务器、代理、负载均衡器还是其他类似应用,libevent 都是一个值得推荐的选择。它的高效性能、跨平台支持和简洁易用的 API 接口使得开发者能够快速构建稳定可靠的高并发网络应用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值