【Linux后端服务器开发】poll/epoll多路转接IO服务器

news2024/9/29 7:15:40

目录

一、poll原理

二、poll实现多路转接IO服务器

三、epoll函数接口

四、epoll的工作原理

五、epoll实现多路转接IO服务器


一、poll原理

poll函数接口

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

// pollfd结构
struct pollfd 
{
    int fd;         /* file descriptor */
    short events;   /* requested events */
    short revents;  /* returned events */
}
  • fds是一个poll函数监听的结构列表,每一个元素,包含了三部分内容:文件描述符、监听事件的集合、返回的事件集合
  • nfds表示fds数组的长度
  • timeout表示poll函数的超时时间,单位是毫秒:①timeout > 0 ,timeout时间以内阻塞等待,否则非阻塞返回一次;②timeout == 0,非阻塞等待;③timeout < 0,阻塞等待
events和revents的取值
事件描述是否可作为输入是否可作为输出
POLLIN数据(普通数据、优先数据)可读
POLLRDNORM普通数据可读
POLLRDBAND优先级带数据可读(Linux不支持)
POLLPRI高优先级数据可读,如TCP带外数据
POLLOUT数据(普通数据、优先数据)可写 是
POLLWRNORM普通数据可写
POLLWRBAND优先级带数据可写
POLLRDHUPTCP连续被对方关闭或对方关闭了写操作,它由GNU引入
POLLERR错误
POLLHUP挂起。比如通道的写端被关闭后,读端描述符上将收到POLLHUP事件
POLLVNAL文件描述符没有打开
  • 返回值小于0,表示出错
  • 返回值等于0,表示poll函数等待超时
  • 返回值大于0,表示poll用于监听的文件描述符就绪而返回

poll的优点

poll多路转接的提出是为了解决select存在的部分缺陷,不同于select使用三个位图结构来表示三个fd_set的方式,poll使用一个pollfd的指针实现:

  • pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式,接口使用比select方便,不需要每次调用都重新设置需要关心的fd
  • events参数:用户告诉内核你需要帮我监视哪些fd
  • revents参数:内核告诉用户哪些正在监视的fd已经就绪了
  • poll并没有最大数量限制(但是最大数量限制是由系统能接受的最大数量决定,监视的fd数量过大时性能也会下降)

poll的缺点

poll中监听的文件描述符增多时:

  • 和select一样,poll返回后,需要轮询所有pollfd来获取就绪的描述符
  • 每次调用poll都需要把大把的pollfd结构从用户态拷贝到内核中
  • 同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监听的文件描述符数量的增长,其效率也会线性下降

二、poll实现多路转接IO服务器

Log.hpp、Sock.hpp、main.cc与select中的多路转接是一样的:【Linux后端服务器开发】select多路转接IO服务器_命运on-9的博客-CSDN博客

PollServer.hpp

#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <poll.h>

#include "Sock.hpp"

using namespace std;

static const int g_defaultport = 8080;
static const int g_num = 2024;
static const int g_defaultfd = -1;

using func_t = function<string (const string&)>;

class PollServer
{
public:
    PollServer(func_t f, int port = g_defaultport)
        : _func(f), _port(port), _listensock(-1), _rfds(nullptr)
    {}

    void Init()
    {
        _listensock = Sock::Socket();
        Sock::Bind(_listensock, _port);
        Sock::Listen(_listensock);

        _rfds = new struct pollfd[g_num];
        for (int i = 0; i < g_num; ++i)
            Reset_Item(i);
        _rfds[0].fd = _listensock;
        _rfds[0].events = POLLIN;
    }

    void Reset_Item(int i)
    {
        _rfds[i].fd = g_defaultfd;
        _rfds[i].events = 0;
        _rfds[i].revents = 0;
    }

    void Print_Rfds()
    {
        cout << "rfd list: ";
        for (int i = 0; i < g_num; ++i)
            if (_rfds[i].fd != g_defaultfd)
                cout << _rfds[i].fd << " ";
        cout << endl;
    }

    void Accepter(int listensock)
    {
        Log_Message(DEBUG, "Accepter in");

        string clientip;
        uint16_t clientport = 0;
        int sock = Sock::Accept(listensock, &clientip, &clientport);
        if (sock < 0)
            return;
        Log_Message(NORMAL, "accept success [%s: %d]", clientip.c_str(), clientport);

        int i = 0;
        for (; i < g_num; ++i)
        {
            if (_rfds[i].fd != g_defaultfd)
                continue;
            else
                break;
        }
        if (i == g_num)
        {
            Log_Message(WARNING, "server is full, please wait");
            close(sock);
        }
        else
        {
            _rfds[i].fd = sock;
            _rfds[i].events = POLLIN;
            _rfds[i].revents = 0;
        }

        Print_Rfds();
        Log_Message(DEBUG, "Accepter out");
    }

    void Recver(int pos)
    {
        Log_Message(DEBUG, "in Recver");

        // 1. 读取request
        char buffer[1024];
        ssize_t s = recv(_rfds[pos].fd, buffer, sizeof(buffer) - 1, 0);
        if (s > 0)
        {
            buffer[s] = 0;
            Log_Message(NORMAL, "client# %s", buffer);
        }
        else if (s == 0)
        {
            close(_rfds[pos].fd);
            Reset_Item(pos);
            Log_Message(NORMAL, "client quit");
            return;
        }
        else
        {
            close(_rfds[pos].fd);
            Reset_Item(pos);
            Log_Message(ERROR, "client quit: %s", strerror(errno));
            return;
        }

        // 2. 处理request
        string response = _func(buffer);

        // 3. 返回response
        write(_rfds[pos].fd, response.c_str(), response.size());

        Log_Message(DEBUG, "out Recver");
    }

    void Handler_Read_Event()
    {
        for (int i = 0; i < g_num; ++i)
        {
            // 过滤掉非法的fd
            if (_rfds[i].fd == g_defaultfd)
                continue;
            if (!(_rfds[i].events & POLLIN))
                continue;

            if (_rfds[i].fd == _listensock && (_rfds[i].revents & POLLIN))
                Accepter(_listensock);
            else if (_rfds[i].revents & POLLIN)
                Recver(i);
        }
    }

    void Start()
    {
        int timeout = -1;
        while (1)
        {
            int n = poll(_rfds, g_num, timeout);
            switch (n)
            {
            case 0:
                Log_Message(NORMAL, "timeout ...");
                break;
            case -1:
                Log_Message(WARNING, "poll error, code:%d, err string: %s", errno, strerror(errno));
                break;
            default:
                Log_Message(NORMAL, "have event ready!");
                Handler_Read_Event();
                break;
            }
        }
    }

    ~PollServer()
    {
        if (_listensock < 0)
            close(_listensock);
        if (_rfds)
            delete[] _rfds;
    }

private:
    int _port;
    int _listensock;
    struct pollfd* _rfds;
    func_t _func;
};

三、epoll函数接口

epoll的官方说法是为了处理大批量句柄而做了改进的poll,但是在底层原理的设计上,epoll与poll有着天差地别的区别(老婆与老婆饼的关系),epoll的设计远远优于poll。

它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44),几乎弥补了前两种多路转接接口(select、poll)的所有缺陷,被公认为Linux2.6之后性能最好的多路I/O就绪通知方法。

epoll_create

int epoll_create(int size);

创建一个epoll模型,自从2.6.8之后size参数是被忽略的,用完之后必须调用close关闭。

epoll_ctl

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

epoll的事件注册函数,它不同于select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。

第一个参数是epoll_create()的返回值(epoll模型),第二个参数表示动作(用三个宏表示),第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事。

第二个参数的取值:①EPOLL_CTL_ADD,注册新的fd到epfd中;②EPOLL_CTL_MOD,修改已经注册的fd监听事件;③EPOLL_CTL_DEL,从epfd中删除一个fd。

epoll_wait

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

收集在epoll监听的事件中已经发生的事件:

  • 参数events是分配好的epoll_event结构体
  • epoll将会把发生的事件赋值到events数组中(events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存)
  • maxevents参数是告知内核这个events多大,这个maxevents的值不能大于创建的epoll_create()时的size
  • 参数timeout是超时时间(ms,0会立即返回,-1是阻塞等待)
  • 如果函数调用成功,返回I/O已经准备好的文件描述符数目,如返回0表示已超时,返回值小于0表示报错

struct epoll_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 events */
  epoll_data_t data;	/* User data variable */
} __EPOLL_PACKED;

events可以是以下几个宏的集合:

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

四、epoll的工作原理

  1. 通过epoll_create()创建epoll模型
  2. 通过epoll_ctl()将需要监视的文件描述符告诉OS,由OS维护红黑树进行监视
  3. 当数据从硬件层传入网卡驱动再写入操作系统中监视的文件缓冲区,会通过每个文件的回调机制将受监视的fd节点传入就绪队列中
  4. 上层直接对就绪队列进行事件处理,而不用对所有监视进行遍历

什么是事件就绪?底层的IO条件满足了,可以进行某种IO行为了,即事件就绪。

select / poll / epoll  ---->   IO事件的通知机制(等)

事件的通知有什么策略呢?LT工作模式和ET工作模式

关于事件的通知,这里有一个取快递的示例:

假设你网购了一些东西,快递到了,这时快递员张三就把快递送到你家楼下,打电话通知你让你下去取快递,你不下去他就一直给你打,直到你去将你的所有快递取完了他才离开。

之后你又网购了一些东西,快递到了,这时换了一个快递员变成李四了,他也将快递送到了你家楼下,但是他不会一直给你打电话,只要你接了电话他就离开了,也不管你有没有下来取快递。

这两种不同的通知策略,对应的就是LT水平触发模式和ET边缘触发模式,张三是LT,李四是ET。

epoll如何进行事件通知呢?通过文件的回调机制将红黑树中的就绪节点添加到就绪队列中

  • LT水平触发:只要就绪队列中的数据没有处理完,epoll就会一直通知用户
  • ET边缘触发:只要epoll将数据放入就绪队列里了,无论用户有没有将就绪队列里的数据读完,epoll就不再通知用户,除非底层的数据变化(数据增多),才会再次进行通知

epoll默认的是LT工作模式,我们也可以设置events将其改为ET模式。

为什么ET模式必须是非阻塞读取呢?

ET模式   --->   底层只有数据从无到有、从有到多变化的时候,才会进行通知上层   --->   只会通知一次   --->   倒逼程序员将本轮就绪的数据全部读取完   --->   你怎么知道你把就绪的数据全部读取完毕了呢?循环读取,直到读取不到数据了   --->   一般的fd,是阻塞式读取,但是ET模式下,必须是非阻塞读取,防止最后一次因读取不到数据而阻塞

为什么ET模式更高效?因为ET高效不仅体现在通知机制上,还会倒逼程序一次把就绪数据读取完,TCP通信时让接收方可以给发送方提供一个更大的窗口大小,即让对端更新出一个更大的滑动窗口,提高网络通信的数据吞吐量。

所以TCP中PSH标志的作用?让底层就绪事件,再次通知给上层。

五、epoll实现多路转接IO服务器

Log.hpp、Sock.hpp、main.cc与poll版本的一样

EpollServer.hpp

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <functional>
#include <sys/epoll.h>

#include "Sock.hpp"

using namespace std;

static const int g_defaultport = 8080;
static const int g_size = 2024;
static const int g_defaultvalue = -1;
static const int g_defaultnum = 64;

using func_t = function<string (const string&)>;

class EpollServer
{
public:
    EpollServer(func_t f, uint16_t port = g_defaultport, int num = g_defaultnum)
        : _func(f), _num(num), _revs(nullptr), _port(port), _listensock(g_defaultvalue), _epfd(g_defaultvalue)
    {}

    void Init()
    {
        // 1. 创建socket
        _listensock = Sock::Socket();
        Sock::Bind(_listensock, _port);
        Sock::Listen(_listensock);

        // 2. 创建epoll模型
        _epfd = epoll_create(g_size);
        if (_epfd < 0)
        {
            Log_Message(FATAL, "epoll create error: %s", strerror(errno));
            exit(EPOLL_CREATE_ERR);
        }

        // 3. 添加listensock到epoll中
        struct epoll_event ev;
        ev.events = EPOLLIN;
        ev.data.fd = _listensock;
        epoll_ctl(_epfd, EPOLL_CTL_ADD, _listensock, &ev);

        // 4. 申请就绪事件的空间
        _revs = new struct epoll_event[_num];

        Log_Message(NORMAL, "init server success");
    }

    void Handler_Event(int ready_num)
    {
        Log_Message(DEBUG, "Handler_Event in");

        // 遍历就绪队列
        for (int i = 0; i < ready_num; ++i)
        {
            uint32_t events = _revs[i].events;
            int sock = _revs[i].data.fd;

            if (sock == _listensock && (events & EPOLLIN))
            {
                // _listensock事件就绪,建立新连接
                string clientip;
                uint16_t clientport;
                int fd = Sock::Accept(sock, &clientip, &clientport);
                if (fd < 0)
                {
                    Log_Message(WARNING, "accept error");
                    continue;
                }

                // 获取fd成功,可以直接读取吗?不可以,放入epoll
                struct epoll_event ev;
                ev.events = EPOLLIN;
                ev.data.fd = fd;
                epoll_ctl(_epfd, EPOLL_CTL_ADD, fd, &ev);
            }
            else if (events & EPOLLIN)
            {
                // 普通事件就绪
                char buffer[1024];
                int n = recv(sock, buffer, sizeof(buffer), 0);
                if (n > 0)
                {
                    buffer[n] = 0;
                    Log_Message(NORMAL, "client say# %s", buffer);

                    // TODO
                    string response = response = _func(buffer);
                    send(sock, response.c_str(), response.size(), 0);
                }
                else if (n == 0)
                {
                    // 先从epoll移除,再close fd
                    epoll_ctl(_epfd, EPOLL_CTL_DEL, sock, nullptr);
                    close(sock);
                    Log_Message(NORMAL, "client quit");
                }
                else
                {
                    epoll_ctl(_epfd, EPOLL_CTL_DEL, sock, nullptr);
                    close(sock);
                    Log_Message(ERROR, "recv error, code: %d, errstring: %s", errno, strerror(errno));
                }
            }
        }

        Log_Message(DEBUG, "Handler_Event out");
    }

    void Start()
    {
        int timeout = -1;
        while (1)
        {
            int n = epoll_wait(_epfd, _revs, _num, timeout);
            switch (n)
            {
            case 0:
                Log_Message(NORMAL, "timeout ...");
                break;
            case -1:
                Log_Message(WARNING, "epoll_wait failed, code: %d, err string: %s", errno, strerror(errno));
                break;
            default:
                Log_Message(NORMAL, "have event ready");
                Handler_Event(n);
                break;
            }
        }
    }

private:
    uint16_t _port;
    int _listensock;
    int _epfd;                      // epoll模型
    struct epoll_event* _revs;      // 就绪队列
    int _num;
    func_t _func;
};

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

c高级:day3

作业: 1. 整理思维导图 2.判断家目录下,普通文件的个数和目录文件的个数 #!/bin/bash ######################################################################## # File Name: zy1.sh # Created Time: 2023年08月04日 星期五 19时13分08秒 ##############################…

知识体系总结(八)SSM框架体系

文章目录 Spring基础1-1、Spring、SpringMVC、Mybatis与SpringBoot的区别1-2、Spring中常用的注解及作用1-3、Spring 框架中用到了哪些设计模式&#xff1f; Spring IoC 、 DI、Bean2-1、Spring IoC是什么&#xff0c;有什么好处&#xff0c;Spring中是怎么实现的&#xff1f;2…

《Kali渗透基础》13. 无线渗透(三)

kali渗透 1&#xff1a;无线通信过程1.1&#xff1a;Open 认证1.2&#xff1a;PSK 认证1.3&#xff1a;关联请求 2&#xff1a;加密2.1&#xff1a;Open 无加密网络2.2&#xff1a;WEP 加密系统2.3&#xff1a;WPA 安全系统2.3.1&#xff1a;WPA12.3.2&#xff1a;WPA2 3&#…

修复 Adob​​e After Effects 预览无法工作/播放的方法技巧

Adobe After Effects 允许您预览视频和音频&#xff0c;而无需将其渲染为最终输出。当您无法在此应用程序中预览视频和音频时&#xff0c;一定会感到沮丧。不过不用担心&#xff0c;您可以尝试以下方法来修复 After Effects 预览不起作用的问题。 技巧1&#xff1a;重启After …

SHEIN还说TEMU,2023跨境电商怎么选?

2023年要说跨境热门的平台有哪些&#xff0c;SHEIN与TEMU应该是名列前茅的。这两家一直以来给人感觉也都是比较相似的&#xff0c;他们的跨境斗法从未停歇。其实两者有相似之处&#xff0c;也有不同之处!作为跨境玩家&#xff0c;我们应该如何选择适合自己的平台呢?往下看。 一…

Qt 6. 其他类调用Ui中的控件

1. 把主类指针this传给其他类&#xff0c;tcpClientSocket new TcpClient(this); //ex2.cpp #include "ex2.h" #include "ui_ex2.h"Ex2::Ex2(QWidget *parent): QDialog(parent), ui(new Ui::Ex2) {ui->setupUi(this);tcpClientSocket new TcpClient…

一百四十一、Kettle——kettle8.2在Windows本地开启carte服务以及配置子服务器

一、目的 在kettle建好共享资源库后&#xff0c;为了给在服务器上部署kettle的carte服务躺雷&#xff0c;先在Windows本地测试一下怎么玩carte服务 二、Kettle版本以及在Windows本地安装路径 kettle版本是8.2 pdi-ce-8.2.0.0-342 kettle本地安装路径是D:\j…

【动态规划刷题 4】礼物的最大价值下降路径最小和

礼物的最大价值 在一个 m*n 的棋盘的每一格都放有一个礼物&#xff0c;每个礼物都有一定的价值&#xff08;价值大于 0&#xff09;。你可以从棋盘的左上角开始拿格子里的礼物&#xff0c;并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值…

Flutter iOS 集成使用 fluter boost

在 Flutter项目中集成完 flutter boost&#xff0c;并且已经使用了 flutter boost进行了路由管理&#xff0c;这时如果需要和iOS混合开发&#xff0c;这时就要到 原生端进行集成。 注意&#xff1a;之前建的项目必须是 Flutter module项目&#xff0c;并且原生项目和flutter m…

Kotlin~Visitor访问者模式

概念 将数据结构和操作分离&#xff0c;使操作集合可以独立于数据结构变化。 角色介绍 Visitor&#xff1a;抽象访问者&#xff0c;为对象结构每个具体元素类声明一个访问操作。Element&#xff1a;抽象元素&#xff0c;定义一个accept方法ConcreteElement&#xff1a;具体元…

HTML编码

目录 1.HTML编码概述2.实体编码3.URLcode编码4.unicode编码5.解码实例 1.HTML编码概述 通常一个网页中可解析的总共有三种编码&#xff0c;每种编码都能用来代替表示字符&#xff0c;按解析顺序依次是“html实体编码”“urlcode码”“Unicode码”&#xff0c;在执行过程中会在…

Flowable-顺序流

目录 顺序流标准顺序流定义图形标记XML内容使用示例 条件顺序流定义图形标记XML内容界面操作 默认顺序流定义图形标记XML内容使用示例视频教程 顺序流 顺序流是一端带有箭头的实线&#xff0c;可在流程图中连接流程内的各个元素&#xff0c;并显示各个元素的执行顺序。 Flowa…

Flink学习教程

最近因为用到了Flink&#xff0c;所以博主开了《Flink教程》专栏来记录Flink的学习笔记。 【Apache Flink v1.16 中文文档】 【官网 - Apache Flink v1.3 中文文档】 一、基础 参考链接如下&#xff1a; Flink教程&#xff08;01&#xff09;- Flink知识图谱Flink教程&…

[Docker实现测试部署CI/CD----自由风格的CI操作[最终架构](5)]

目录 11、自由风格的CI操作&#xff08;最终&#xff09;Jenkins容器化实现方案修改 docker.sock 权限修改 Jenkins 启动命令后重启 Jenkins构建镜像推送到Harbor修改 daemon.json 文件Jenkins 删除构建后操作Jenkins 添加 shell 命令重新构建 Jenkins通知目标服务器拉取镜像目…

Java阶段五Day18

Java阶段五Day18 文章目录 Java阶段五Day18缓存方案面试题整理 项目功能新增审核业务流程图 账户账户表格和ER图账号服务功能账号的创建当前实现功能时序图&#xff08;对应全景图&#xff09; 抢单相关时序图供应商和需求单 附录布隆过滤器 缓存方案 面试题整理 目标&#xf…

【Unity3D应用案例系列】Unity3D中实现文字转语音的工具开发

推荐阅读 CSDN主页GitHub开源地址Unity3D插件分享简书地址我的个人博客 大家好&#xff0c;我是佛系工程师☆恬静的小魔龙☆&#xff0c;不定时更新Unity开发技巧&#xff0c;觉得有用记得一键三连哦。 一、前言 在开发中&#xff0c;会遇到将文字转语音输出的需求&#xff0…

[每周一更]-(第57期):用Docker、Docker-compose部署一个完整的前后端go+vue分离项目

文章目录 1.参考项目2.技能点3.GO的Dockerfile配置后端的结构如图Dockerfile先手动docker调试服务是否可以启动报错 4.Vue的Dockerfile配置前端的结构如图nginx_docker.confDockerfile构建 5.docker-compose 整合前后端docker-compose.yml错误记录&#xff08;1&#xff09;ip端…

宇树Unitree Z1机械臂使用教程

宇树Unitree Z1机械臂使用教程 作者&#xff1a;Herman Ye Galbot Auromix Auromix是一个机器人爱好者组织&#xff0c;欢迎参与我们Github上的开源项目 更新日期&#xff1a;2023/08/04 注意&#xff1a;此文档在该日期下测试有效。 以下内容参考宇树官方的Z1 Docs。 由宇树…

idea调节文字大小、日志颜色、git改动信息、单击打开代码覆盖原标签问题

idea调节菜单栏文字大小&#xff1a; 调节代码文字大小&#xff1a; 按住ctrl滚动滑轮可以调节代码文字大小&#xff1a; ctrl单击打开代码覆盖原标签问题&#xff1a; idea在控制台对不同级别的日志打印不同颜色 &#xff1a; “grep console”插件 点击某一行的时候&#x…

docker-compose --version报错

在部署docker-compose后&#xff0c;查看版本时有如下报错: 解决方法: 解决方法&#xff1a; 直接在release中下载对应的linux发行版【docker-compose-linux-x86_64】 https://github.com/docker/compose/releases/tag/v2.18.1 下载完后将软件上传至 Linux的【/usr/local/bin】…