zoukankan      html  css  js  c++  java
  • 用C写一个web服务器(二) I/O多路复用之epoll

    前言

    继续更新“用 C 写一个 web 服务器”项目(上期链接:用C写一个web服务器(一) 基础功能),本次更新选择了 I/O 模型的优化,因为它是服务器的基础,这个先完成的话,后面的优化就可以选择各个模块来进行,不必进行全局化的改动了。

    I/O模型

    接触过 socket 编程的同学应该都知道一些 I/O 模型的概念,linux 中有阻塞 I/O、非阻塞 I/O、I/O 多路复用、信号驱动 I/O 和 异步 I/O 五种模型。

    其他模型的具体概念这里不多介绍,只简单地提一下自己理解的 I/O 多路复用:简单的说就是由一个进程来管理多个 socket,即将多个 socket 放入一个表中,在其中有 socket 可操作时,通知进程来处理, I/O 多路复用的实现方式有 select、poll 和 epoll。

    select/poll/epoll

    在 linux下,通过文件描述符(file descriptor, 下 fd)来进行 socket 的操作,所以下文均是对 fd 操作。

    首先说最开始实现的 select 的问题:

    • select 打开的 fd 最大数目有限制,一般为1024,在当前计算系统的并发量前显然有点不适用了。
    • select 在收到有 fd 可操作的通知时,是无法得知具体是哪个 fd 的,需要线性扫描 fd 表,效率较低。
    • 当有 fd 可操作时,fd 会将 fd 表复制到内核来遍历,消耗也较大。

    随着网络技术的发展,出现了 poll:poll 相对于 select,使用 pollfd 表(链表实现) 来代替 fd,它没有上限,但受系统内存的限制,它同样使用 fd 遍历的方式,在并发高时效率仍然是一个问题。

    最终,epoll 在 Linux 2.6 的内核面世,它使用事件机制,在每一个 fd 上添加事件,当fd 的事件被触发时,会调用回调函数来处理对应的事件,epoll 的优势总之如下:

    • 只关心活跃的 fd,精确定位,改变了poll的时间效率 O(n) 到 O(1);
    • fd 数量限制是系统能打开的最大文件数,会受系统内存和每个 fd 消耗内存的影响,以当前的系统硬件配置,并发数量绝对不是问题。
    • 内核使用内存映射,大量 fd 向内核态的传输不再是问题。

    为了一步到位,也是为了学习最先进的I/O多路复用模型,直接使用了 epoll 机制,接下来介绍一下 epoll 相关基础和自己服务器的实现过程。


    epoll介绍

    epoll 需要引入<sys/epoll.h>文件,首先介绍一下 epoll 系列函数:

    epoll_create

    int epoll_create(int size);

    创建一个 epoll 实例,返回一个指向此 epoll 实例的文件描述符,当 epoll 实例不再使用时,需要使用close()方法来关闭它。

    在最初的实现中, size 作为期望打开的最大 fd 数传入,以便系统分配足够大的空间。在最新版本的内核中,系统内核动态分配内存,已不再需要此参数了,但为了避免程序运行在旧内核中会有问题,还是要求此值必须大于0;

    epoll_ctl

    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

    • epfd 是通过 epoll_create 返回的文件描述符

    • op 则是文件描述符监听事件的操作方式,EPOLL_CTL_ADD/EPOLL_CTL_MOD/EPOLL_CTL_DEL 分别表示添加、修改和删除一个监听事件。

    • fd 为要监听的文件描述符。

    • event 为要监听的事件,可选事件和行为会在下面描述,它的结构如下:

    typedef union epoll_data {
                   void        *ptr;
                   int          fd;
                   uint32_t     u32;
                   uint64_t     u64;
               } epoll_data_t;
    
               struct epoll_event {
                   uint32_t     events;      /* epoll事件 */
                   epoll_data_t data;        /* 事件相关数据 */
               };

    epoll_wait

    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 监听 epoll 事件:

    • events 是 epoll 事件数组,epoll 事件的结构上面已经介绍过。
    • maxevents 是一次监听获取到的最大事件数目。
    • timeout 是一次监听中获取不到事件的最长等待时间,设置成 -1 会一直阻塞等待,0 则会立即返回。

    epoll行为

    在 epoll_ctl 的 event 参数中,事件 events 有如下可选项:

    EPOLLIN(可读)、EPOLLOUT(可写)、EPOLLRDHUP(连接关闭)、EPOLLPRI(紧急数据可读),此外 EPOLLERR(错误),EPOLLHUP(连接挂断)事件会被 epoll 默认一直监听。

    除了设置事件外,还可以对监听的行为设置:

    • level trigger:此行为被 epoll 默认支持,不必设置。在 epoll_wait 得到一个事件时,如果应用程序不处理此事件,在 level trigger 模式下,epoll_wait 会持续触发此事件,直到事件被程序处理;
    • EPOLLET(edge trigger):在 edge trigger 模式下,事件只会被 epoll_wait 触发一次,如果用户不处理此事件,不会在下次 epoll_wait 再次触发。在处理得当的情况下,此模式无疑是高效的。需要注意的是此模式需求 socket 处理非阻塞模式,下面会实现此模式。
    • EPOLLONESHOT:在单次命中模式下,对同一个文件描述符来说,同类型的事件只会被触发一次,若想重复触发,需要重新给文件描述符注册事件。
    • EPOLLWAKEUP:3.5版本加入,如果设置了单次命中和ET模式,而且进程有休眠唤醒能力,当事件被挂起和处理时,此选项确保系统不进入暂停或休眠状态。 事件被 epoll_wait 调起后,直到下次 epoll_wait 再次调起此事件、文件描述符被关闭,事件被注销或修改,都会被认为是处于处理中状态。
    • EPOLLEXCLUSIVE:4.5版本加入,为一个关联到目标文件描述符的 epoll 句柄设置独占唤醒模式。如果目标文件描述符被关联到多个 epoll 句柄,当有唤醒事件发生时,默认所有 epoll 句柄都会被唤醒。而都设置此标识后,epoll 句柄之一被唤醒,以避免“惊群”现象。

    当监听事件和行为需求同时设置时,使用运算符 |即可。


    代码实现

    整体处理逻辑

    使用 epoll 时的服务器受理客户端请求逻辑如下:

    1. 创建服务器 socket,注册服务器 socket 读事件;
    2. 客户端连接服务器,触发服务器 socket 可读,服务器创建客户端 socket,注册客户端socket 读事件;
    3. 客户端发送数据,触发客户端 socket 可读,服务器读取客户端信息,将响应写入 socket;
    4. 客户端关闭连接,触发客户端 socket 可读,服务器读取客户端信息为空,注销客户端 socket 读事件;

    代码实现如下(详细处理方式见 GitHub:我是地址):

    erver_fd = server_start();
        epoll_fd = epoll_create(FD_SIZE);
        epoll_register(epoll_fd, server_fd, EPOLLIN|EPOLLET);// 这里注册socketEPOLL事件为ET模式
    
        while (1) {
            event_num = epoll_wait(epoll_fd, events, MAX_EVENTS, 0);
            for (i = 0; i < event_num; i++) {
                fd = events[i].data.fd;
                // 如果是服务器socket可读,则处理连接请求
                if ((fd == server_fd) && (events[i].events == EPOLLIN)){
                    accept_client(server_fd, epoll_fd);
                // 如果是客户端socket可读,则获取请求信息,响应客户端
                } else if (events[i].events == EPOLLIN){
                    deal_client(fd, epoll_fd);
                } else if (events[i].events == EPOLLOUT)
                    // todo 数据过大,缓冲区不足的情况待处理
                    continue;
            }
        }

    需要注意的是,客户端socket在可读之后也是立刻可写的,我这里直接读取一次请求,然后将响应信息 write 进去,没有考虑读数据时缓冲区满的问题。

    这里提出的解决方案为:

    1. 设置一个客户端 socket 和 buffer 的哈希表;
    2. 在读入一次信息缓冲区满时 recv 会返回 EAGIN 错误,这时将数据放入 buffer,暂时不响应。
    3. 后续读事件中读取到数据尾后,再注册 socket 可写事件。
    4. 在处理可写事件时,读取 buffer 内的全部请求内容,处理完毕后响应给客户端。
    5. 最后注销 socket 写事件。

    设置epoll ET(edge trigger)模式

    上文说过,ET模式是 epoll 的高效模式,事件只会通知一次,但处理良好的情况下会更适用于高并发。它需要 socket 在非阻塞模式下才可用,这里我们实现它。

    sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    
        // 获取服务器socket的设置,并添加"不阻塞"选项
        flags = fcntl(sock_fd, F_GETFL, 0);
        fcntl(sock_fd, F_SETFL, flags|O_NONBLOCK);
        
        .....
        // 这里注册服务器socket EPOLL事件为ET模式
        epoll_register(epoll_fd, server_fd, EPOLLIN|EPOLLET);
    

    我将处理事件注掉后使用一次客户端连接请求进行了测试,很清晰地说明了 ET模式下,事件只触发一次的现象,前后对比图如下:


    小结

    Mac OS X 操作系统的某些部分是基于 FreeBSD 的,FreeBSD 不支持,MAC 也不支持(不过有相似的 kqueue),跑到开发机上开发的,作为一个最基础的 C learner, 靠着printf()fflush()两个函数来调试的,不过搞了很久总算是完成了,有用 C 的前辈推荐一下调试方式就最好了。。

    另外 epoll 在最新的内核中也更新了些内容,旧的很多博客都没有提到,话说照这样的发展速度,我这篇也会在一段时间后“过时”吧,哈哈~

    如果您觉得本文对您有帮助,可以点击下面的 推荐 支持一下我。博客一直在更新,欢迎 关注

    参考:

    IO多路复用之epoll总结

    epoll精髓

    epoll interface detail (很不错的英文文档,推荐)

  • 相关阅读:
    java OA系统 自定义表单 流程审批 电子印章 手写文字识别 电子签名 即时通讯
    flowable 获取当前任务流程图片的输入流
    最新 接口api插件 Swagger3 更新配置详解
    springboot 集成 activiti 流程引擎
    java 在线考试系统源码 springboot 在线教育 视频直播功能 支持手机端
    阿里 Nacos 注册中心 配置启动说明
    springboot 集成外部tomcat war包部署方式
    java 监听 redis 过期事件
    springcloudalibaba 组件版本关系
    java WebSocket 即时通讯配置使用说明
  • 原文地址:https://www.cnblogs.com/zhenbianshu/p/6606953.html
Copyright © 2011-2022 走看看