高级IO|从封装epoll服务器到实现Reactor服务器|Part1

从封装epoll_server到实现reactor服务器(part1)

  • 项目复习:从封装epoll_server到实现reactor服务器(part1)
  • EPOLL模式服务器初步
    • select, poll, epoll的优缺点
    • epoll的几个细节
    • 封装epoll_server
    • 基本框架先写好
    • 创建监听套接字和创建epoll模型
    • 可以Accept了吗?
    • 此时可以调用epoll_wait去让epoll关心这些文件描述符了
    • 一些细节
    • 进行一次测试
    • Accepter和Recver
    • epoll服务器最终测试
    • 这个epoll服务器没有问题吗?
  • 多路转接的工作模式
    • 基本概念
    • 为什么ET模式一定要是非阻塞的读取才行

仓库:https://github.com/Yufccode/Reactor-based-HyperWebServer/

EPOLL模式服务器初步

select, poll, epoll的优缺点

cite: https://blog.csdn.net/jgm20475/article/details/81083529

Linux中高级IO多路转接中select、poll和epoll的优缺点,这里主要谈select和poll的缺点以及epoll的优点。
一、select的缺点:

  1. 编写难度大
  2. 同时处理的文件描述符是有上限的
  3. 每次需要重新设定fd集合
  4. 性能会随用户的增多而效率降低
  5. 输入输出参数在一起

二、poll
poll是对select的一种改良,最突出的改良有两点:

  1. 文件描述符数量没有上限
  2. 将输入输出参数进行分离,不用每次设定
    那么poll的缺点是:
    poll中监听的文件描述符数目增多时:
    1、和select一样,poll返回后,需要轮询pollfd来获取就绪的描述符
    2、每次调用poll都需要大把大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。

三、epoll的优点:

  1. 文件描述符数目没有上限:通过epoll_ctl()来注册一个文件描述符,内核中使用红黑树的数据结构来管理所有需要监控的文件描述符。
  2. 基于事件就绪通知方式:一旦被监听的某个文件描述符就绪,内核会采用类似于callback的回调机制,迅速激活这个文件描述符,这样随着文件描述符数量的增加,也不会影响判定就绪的性能。
  3. 维护就绪队列:当文件描述符就绪,就会被放到内核中的一个就绪队列中,这样调用epoll_weit获取就绪文件描述符的时候,只要取队列中的元素即可,操作的时间复杂度恒为O(1)。

epoll的几个细节

  • 红黑树,是要有key的,文件描述符就是一个天然的key
  • 用户只需要设置关系,获取结果即可,不用关心任何对fd或者event对管理细节
  • epoll为什么高效呢?-> 红黑树
  • 底层只要有fd就绪了,OS会自己给我构建节点,链入到就绪队列中,上层只需要不断地从就绪队列中将数据拿走,就完成了获取就绪事件的任务
  • 所以本质上:epoll是一个生产者消费者模型!
  • 如果底层没有就绪事件呢?我们的上层应该怎么办?阻塞等待!(可以选择的!所以epoll_wait接口为什么有一个timeout参数,如果我选择不等,就直接返回->非阻塞了)

在这里插入图片描述

封装epoll_server

基本框架先写好

基本的东西先写好。

main.cc

#include "epoll_server.hpp"
#include <memory>
int main() {
    std::unique_ptr<ns_epoll::epoll_server> svr(new ns_epoll::epoll_server());
    svr->start();
    return 0;
}

epoll_server.hpp

#ifndef __YUFC_EPOLL_SERVER__
#define __YUFC_EPOLL_SERVER__
namespace ns_epoll {
class epoll_server {
};
} // namespace yufc
#endif

当然,以前写好的log.hpp, sock.hpp都要复制到目录里面。

创建监听套接字和创建epoll模型

这个很简单,直接写就行了,然后创建完监听套接字之后,就是要去创建epoll模型

epoll_server.hpp

    epoll_server(const int& port = default_port)
        : __port(port) {
        // 1. 创建监听套接字
        __listen_sock = Sock::Socket();
        Sock::Bind(__listen_sock, __port);
        Sock::Listen(__listen_sock);

    }

因为为了后面方便使用,epoll模型封装一下比较好,封装成epoll.hpp

创建epoll。

epoll.hpp

class __epoll {
public:
    static const int gsize = 256;

public:
    static int create_epoll() {
        int epfd = epoll_create(gsize);
        if (epfd > 0)
            return epfd;
        exit(5);
    }
};
epoll_create:
RETURN VALUE
       On success, these system calls return a file descriptor (a nonnegative integer).  On error, -1 is returned, and errno is
       set to indicate the error

所以创建成功返回epfd,创建失败就没得玩了,直接终止。

所以此时继续epoll_server.hpp

    epoll_server(const int& port = default_port)
        : __port(port) {
        // 1. 创建监听套接字
        __listen_sock = Sock::Socket();
        Sock::Bind(__listen_sock, __port);
        Sock::Listen(__listen_sock);
        // 2. 创建epoll模型
        __epoll_fd = __epoll::create_epoll();
        logMessage(DEBUG, "init success, listensock: %d, epfd: %d", __listen_sock, __epoll_fd); // 3, 4
    }

在这里插入图片描述

符合预期,因为文件描述符0,1,2已经在用了。

可以Accept了吗?

现在可以Accept了吗?下面这个代码可以吗?不可以!

    void start() {
        while (1) {
            int sock = Sock::Accept();
        }
    }

你怎么知道sock上已经有数据了?

多路转接的原则:不知道有没有数据的时候,不要去调用IO接口!

所以我们要:先将listen套接字添加到epoll中,让epoll去管理!

所以其实,epoll_server中的epoll模型,其实管理了两种sock,第一个叫做监听套接字,第二种叫做连接的套接字。

连接的套接字是越来越多的,但是listensock只有一个!epoll两种套接字都要管理!总结来说,多路转接中,任何文件描述符,都应该被管理!

怎么让epoll管理文件描述符呢?

认识一个接口。

在这里插入图片描述

参数:一句话搞定:让epfd这个epoll模型管理fd这个文件描述符,当fd中有event事件发生的时候,让epfd帮这个fd做op这个动作!

事件有哪些?可以查表。

常用的有这些。

  • EPOLLIN: 表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
  • EPOLLOUT: 表示对应的文件描述符可以写;
  • EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
  • EPOLLERR: 表示对应的文件描述符发生错误;
  • EPOLLHUP: 表示对应的文件描述符被挂断;
  • EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的.
  • EPOLLONESHOT: 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。

所以epoll.hpp这样写。

    static bool control_epoll(int epfd, int oper, int sock, uint32_t events) {
        struct epoll_event ev;
        ev.events = events;
        ev.data.fd = sock;
        int n = epoll_ctl(epfd, oper, sock, &ev);
        return n == 0;
    }

epoll_sever.hpp这样调用。

__epoll::control_epoll(__epoll_fd, EPOLL_CTL_ADD, __listen_sock, EPOLLIN);

表示把__listen_sock这个fd ADD到epoll模型中,关心这个fd的EPOLLIN事件。

此时可以调用epoll_wait去让epoll关心这些文件描述符了

当然到后面就绪的文件描述符可能很多,所以我希望一次性可以拿完所有就绪的文件描述符。

所以在epoll_server.hpp里面维护一个

struct epoll_event* __revs; // 后面epoll继续的元素都会被放在这里
int __revs_num; // __revs的大小

先把数组空间开好,然后到时候就绪的一起都拿出来。

epoll.hpp

    static int wait_epoll(int epfd, struct epoll_event revs[], int num, int timeout) {
        return epoll_wait(epfd, revs, num, timeout);
    }

epoll_server.hpp

    void loop_once(int timeout) {
        /**
         * timeout表示,外部决定,到底这个epoll_wait最多阻塞多久,可以选择不阻塞,可以选择阻塞的事件
         */
        int n = __epoll::wait_epoll(__epoll_fd, __revs, __revs_num, timeout);
        switch (n) {
        case 0:
            logMessage(DEBUG, "timeout ...");
            break;
        case -1:
            logMessage(WARNING, "epoll wait error; %s", strerror(errno));
            break;
        default:
            // epoll这一次成功等到了就绪的fd了!
            break;
        }
    }
    void start() {
        int timeout = 1000;
        while (true)
            loop_once(timeout);
    }

当timeout为0的时候,叫做非阻塞等待!当timeout为-1的时候,叫做阻塞式等待!

一次loop_once就应该这样写,如果wait_epoll成功了,就表明这一次loop等到了就绪的文件描述符!

一些细节

细节1: 如果底层就绪的sock非常多,revs放不下了,怎么办?

不影响,一次拿不完就下一次loop再拿就行!

细节2: 关于epoll_wait返回值的问题

表示有几个fd上的事件就绪,就返回几
但是epoll_wait的处理特别特别的优雅,epoll返回的时候,会将所有就绪的fd按照顺序放到revs数组中!一共有返回值个!很优雅!

进行一次测试

在这里插入图片描述

因为我们一直没处理这个继续的套接字,所以一直打印!

怎么处理,我们搞一个handler_event(n)就可以了!只需要传一个数字,告诉我处理几个就行了,因为东西都存在revs数组里面了!

epoll_server.hpp

    void handler_event(int n) {
        for (int i = 0; i < n; i++) {
            uint32_t revents = __revs[i].events;
            int sock = __revs[i].data.fd; // 这个就是就绪(什么事件就绪呢,看下面)的文件描述符!
            // 此时如果我去对这个fd做访问,一定不会阻塞
            // 如果是listensock,就去accept,此时accept不会阻塞!
            // 如果是普通的sock,那我们就读取发过来的信息就好了!
            // 读事件就绪了!
            if (revents & EPOLLIN) {
                // 1. listensock 就绪
                // 2. 普通的sock就绪 - read
                if (sock == __listen_sock)
                    Accepter();
                else
                    Recver();
            }
        }
    }

Accepter和Recver

    void Accepter(int listen_sock) {
        // accept这个监听套接字
    }
    void Recver(int sock) {
        // 读取这个普通套接字里面的内容
    }

Accepter这样写

    void Accepter(int listen_sock) {
        // accept这个监听套接字
        std::string client_ip;
        uint16_t client_port;
        int accept_errno = 0;
        int sock = Sock::Accept(listen_sock, &client_ip, &client_port, &accept_errno);
        if (sock < 0) {
            logMessage(WARNING, "accept error!");
            return;
        }
        // 此时能不能直接读取?不能,因为并不清楚是否有数据!
        // 交给epoll!
        if (__epoll::control_epoll(__epoll_fd, EPOLL_CTL_ADD, sock, EPOLLIN)) {
            logMessage(DEBUG, "add new sock: %d to epoll", sock);
        } else {
            return;
        }
    }

总之,成功Accept上来的sock是不能直接读取的!因为不知道有没有数据!所以交给epoll就行!

测试一下。

在这里插入图片描述

Recver这样写

    void Recver(int sock) {
        // 读取这个普通套接字里面的内容
        // 1. 读取数据
        // 2. 处理数据
        char buffer[10240];
        size_t n = recv(sock, buffer, sizeof(buffer) - 1, 0);
        if (n > 0) {
            // 假设这里就是读到了一个完整的报文
            buffer[n] = 0;
            __handler_request(buffer); // 进行回调!
        } else if (n <= 0) {
            // 对端关闭文件描述符
            // 让epoll不再关注这个文件描述符
            // 一定要先从epoll中去掉,才能close文件描述符
            bool res = __epoll::control_epoll(__epoll_fd, EPOLL_CTL_DEL, sock, 0);
            assert(res); // 保证是成功的,因为一般来说都是成功的,所以直接assert
            (void)res;
            close(sock);
            if (n == 0)
                logMessage(NORMAL, "client %d quit, me quit too ...", sock);
            else if (n < 0)
                logMessage(NORMAL, "client recv %d error, close error sock", sock);
        }
    }

这里有坑!一定要先从epoll中去掉,才能close,因为epoll中的fd都是合法的!

把回调方法搞好之后,epoll服务器我们就搞定了

main.cc

void change(std::string request) {
    // 完成业务逻辑
    std::cout << "change: " << request << std::endl;
}

int main() {
    std::unique_ptr<ns_epoll::epoll_server> svr(new ns_epoll::epoll_server(change));
    svr->start(-1); // 先暂时让他阻塞等待
    return 0;
}

epoll服务器最终测试

在这里插入图片描述

epoll服务器回调了方法。

这个epoll服务器没有问题吗?

肯定还是有问题的。

首先,如何保证Recver读到了完整的报文,这些都很熟悉了,都是要去解决的问题!

多路转接的工作模式

基本概念

epoll有两种工作模式,水平触发(LT)和边缘触发(ET)

LT模式: 如果我手里有你的数据,我就会一直通知
ET模式: 只有我手里你数据是首次到达,从无到有,从有到多(变化)的时候,我才会通知你

细节:

  1. 我为什么要听ET模式的?凭什么要立刻去走?我如果不取,底层再也不通知了,上层调用就 无法获取该fd的就绪事件了,无法再调用recv, 数据就丢失了。倒逼程序员,如果数据就绪, 就必须一次将本轮就绪的数据全部取走。
  2. 我可以暂时不处理LT中就绪的数据吗?可以! 因为我后面还有读取的机会。
  3. 如果LT模式,我也一次将数据取完的话,LT和ET的效率是没有区别的。

ET模式为什么更高效?

  1. 更少的返回次数(毕竟一次epoll_wait都是一次内核到用户)
  2. ET模式会倒逼程序员尽快将缓冲区中的数据全部取走,应用层尽快的去走了缓冲区中的数据,那么在单位时间下,该模式下工作的服务器,就可以在一定程度上,给发送方发送一 个更大的接收窗口,所以对方就可以拥有一个工大的滑动窗 口,一次向我们发送更多的数据,提高IO吞吐。

为什么ET模式一定要是非阻塞的读取才行

结论:et模式一定要是非阻塞读取。为什么?

首先,et模式要一次全部读完!怎么才能一次读完呢?我都不知道有多少,怎么保证一次读完?所以我们要连续读,一直读!循环读!读到没有数据为止!

ok!读到没有数据, recv就会阻塞!这就不行了,我们不允许阻塞!

所以怎么办?把这个sock设置成非阻塞的sock,这种sock有个特点:一直读,读到没数据了,不阻塞!直接返回报错,报一个错误:EAGAIN。而这个EAGAIN,可以告诉我们,读完了!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/579313.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

鸿蒙OpenHarmony【轻量系统 运行】 (基于Hi3861开发板)

运行 联网配置 由于Hi3861为WLAN模组&#xff0c;您可以在版本编译及烧录后&#xff0c;通过如下操作&#xff0c;使开发板实现联网功能。 保持Windows工作台和Hi3861 WLAN模组的连接状态&#xff0c;确认串口终端显示正常。 复位Hi3861 WLAN模组&#xff0c;终端界面显示“…

网络攻击日益猖獗,安全防护刻不容缓

“正在排队登录”、“账号登录异常”、“断线重连”......伴随着社交软件用户的一声声抱怨&#xff0c;某知名社交软件的服务器在更新上线2小时后&#xff0c;遭遇DDoS攻击&#xff0c;导致用户无法正常登录。在紧急维护几小时后&#xff0c;这款软件才恢复正常登录的情况。 这…

视频通话实时换脸:支持训练面部模型 | 开源日报 No.235

iperov/DeepFaceLive Stars: 19.7k License: GPL-3.0 DeepFaceLive 是一个用于 PC 实时流媒体或视频通话的人脸换装工具。 可以使用训练好的人脸模型从网络摄像头或视频中交换面部。提供多个公共面部模型&#xff0c;包括 Keanu Reeves、Mr. Bean 等。支持自己训练面部模型以…

基于数据挖掘的斗鱼直播数据可视化分析系统

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长 QQ 名片 :) 1. 项目简介 随着网络直播平台的兴起&#xff0c;斗鱼直播作为其中的佼佼者&#xff0c;吸引了大量用户和观众。为了更好地理解和分析斗鱼直播中的数据&#xff0c;本项目介绍了一个基于数据挖掘的斗鱼直播数据…

【图解计算机网络】简单易懂的https原理解析

简单易懂的https原理解析 https与http的区别混合加密对称加密非对称加密混合加密解析混合加密问题 摘要算法数字证书数字证书原理为什么通过CA证书可以解决中间人攻击的问题呢&#xff1f; https握手流程 https与http的区别 http是明文传输的&#xff0c;非常不安全&#xff0…

呆马科技——智慧应急执法监管平台

在当今社会&#xff0c;安全生产的重要性日益凸显。对于各级政府和企事业单位&#xff0c;当务之急是如何高效地对突发事件进行执法管理。平台应运而生&#xff0c;旨在通过信息化、智能化技术&#xff0c;提升安全管理的效率与准确性。 一、平台特点 整合各类平台的信息资源&…

添加github SSH Key

添加github SSH Key 使用 SSH 协议&#xff0c;您可以连接远程服务器和服务并对其进行身份验证。使用 SSH 密钥&#xff0c;您可以连接到 GitHub&#xff0c;而无需在每次访问时提供您的用户名和个人访问令牌。您还可以使用 SSH 密钥来签署提交。 #3224333333qq.com替换为你自己…

6.NVIC中断配置(ST的精简ARM中断体系)

void NVIC_SetPriorityGrouping(uint32_t PriorityGroup)//设置优先级分组&#xff0c;整个项目共用一个分组 uint32_t NVIC_EncodePriority (uint32_t PriorityGroup, uint32_t PreemptPriority, uint32_t SubPriority) //计算优先级编码值&#xff0c;&#xff08;组号&…

Python爬虫--Ajax异步抓取腾讯视频评论

在某些网站 &#xff0c;当我们滑下去的时候才会显示出后面的内容 就像淘宝一样&#xff0c;滑下去才逐渐显示其他商品 这个就是采用 Ajax 做的 然后我们现在就是要编写这样的爬虫。 规律分析&#xff1a; 这个时候就要用到我们的 Fiddler 了 我们需要分析加载评论的规律 …

GateWay具体的使用之局部过滤器接口耗时

1.找规律 局部过滤器命名规则 XXXGatewayFilterFactory&#xff0c; 必须以GatewayFilterFactory结尾。 /* 注意名称约定 * AddRequestHeaderGatewayFilterFactory 配置的时候写的是 AddRequestHeader * AddRequestParameterGatewayFilterFactory 配置的时候写的是 A…

【语音识别】搭建本地的语音转文字系统:FunASR(离线不联网即可使用)

参考自&#xff1a; 参考配置&#xff1a;FunASR/runtime/docs/SDK_advanced_guide_offline_zh.md at main alibaba-damo-academy/FunASR (github.com)参考配置&#xff1a;FunASR/runtime/quick_start_zh.md at 861147c7308b91068ffa02724fdf74ee623a909e alibaba-damo-aca…

上位机图像处理和嵌入式模块部署(树莓派4b下使用sqlite3)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 嵌入式设备下面&#xff0c;有的时候也要对数据进行处理和保存。如果处理的数据不是很多&#xff0c;一般用json就可以。但是数据如果量比较大&…

CISSP通关学习笔记:共计 9 个章节(已完结)

1. 笔记说明 第 0 章节为开篇介绍&#xff0c;不包括知识点。第 1 - 8 章节为知识点梳理汇总&#xff0c;8 个章节的知识框架关系如下图所示&#xff1a; 2. 笔记目录 「 CISSP学习笔记 」0.开篇「 CISSP学习笔记 」1.安全与风险管理「 CISSP学习笔记 」2.资产安全「 CISSP…

Ps 滤镜:置换(工作原理篇)

执行“置换”滤镜时&#xff0c;目标图像会发生位移变形&#xff0c;而变形的程度及方向与一个称为“置换图”的 PSD 文件有密切关系。 总体而言&#xff0c;置换所产生的位移变形是基于置换图的通道中的灰度信息进行的。 一、当置换图是灰度模式文件时 在灰度模式下&#xff0…

[leetcode] B树是不是A树的子结构

给定两棵二叉树 tree1 和 tree2&#xff0c;判断 tree2 是否以 tree1 的某个节点为根的子树具有 相同的结构和节点值 。 注意&#xff0c;空树 不会是以 tree1 的某个节点为根的子树具有 相同的结构和节点值 。 示例 1&#xff1a; 输入&#xff1a;tree1 [1,7,5], tree2 [6,…

《QT实用小工具·四十四》支持图片和动图的文本编辑器

1、概述 源码放在文章末尾 该项目实现了一个功能丰富的文本编辑器&#xff0c;除了包含文本常规的编辑功能&#xff0c;还包括图片的插入功能和动图的插入功能&#xff0c;项目demo演示如下所示&#xff1a; 项目部分代码如下所示&#xff1a; #include "imagehelper.…

基于深度学习神经网络的AI图片上色DDcolor系统源码

第一步&#xff1a;DDcolor介绍 DDColor 是最新的 SOTA 图像上色算法&#xff0c;能够对输入的黑白图像生成自然生动的彩色结果&#xff0c;使用 UNet 结构的骨干网络和图像解码器分别实现图像特征提取和特征图上采样&#xff0c;并利用 Transformer 结构的颜色解码器完成基于视…

循环单链表的介绍与操作

定义 区别 链表合并 整合代码 typedef struct node{int data;node* next;; }lnode,*linklist; lnode* n; linklist l;//定义 void init(linklist &l){lnode lnew lnode;l->nextl;lnode *rl; } //单循环链表的合并 linklist merge(linklist &a,linklist b){//存头结…

注意力机制:SENet详解

SENet&#xff08;Squeeze-and-Excitation Networks&#xff09;是2017年提出的一种经典的通道注意力机制&#xff0c;这种注意力可以让网络更加专注于一些重要的featuremap&#xff0c;它通过对特征通道间的相关性进行建模&#xff0c;把重要的特征图进行强化来提升模型的性能…