Linux网络编程:多路转接--poll/epoll

news2024/11/15 11:12:24

1. poll

        poll也是一种多路转接的方案,解决了select的fd有上限和每次调用都要重新设置关心的fd的问题。

2. poll接口

#include

int poll(struct pollfd* fds, nfds_t nfds, int timeout);

参数:fds:可以看成是动态数组/数组/结构体数组

           nfds:数组类长度

           timeout:单位ms(毫秒)  >0:在timeout内阻塞,否则非阻塞

                                                    =0:非阻塞等待

                                                    <0:阻塞等待

返回值:同select

poll的作用和select一模一样:只负责等待

struct pollfd{
	int fd;			//文件描述符     用户->内核:要帮我关心一下fd
	short events;	//等待的事件     用户->内核:关心fd上的事件
	short revents;	//实际发生的事件 内核->用户:要关心的fd上面的events中,有哪些事件就绪了
};

输入时看:fd+events

输出时看:fd+revents 

poll将输入输出进行了分离,使得不需要对参数进行重新设定

poll中的events和revents的取值:

pollfd这个数组可以由程序员自己决定,解决了selectfd有上限的问题。 

3. poll代码实现

poll的代码基于前一篇文章的的select修改得来的,只修改了selectserver.hpp:

pollserver.hpp:

#pragma once

#include <string>
#include <iostream>
#include <functional>
#include "sock.hpp"
#include "log.hpp"
#include "err.hpp"

namespace poll_ns
{
    static const int defaultport = 8080;
    static const int num = 2048;
    static const int defaultfd = -1;

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

    class PollServer
    {
    public:
        PollServer(func_t func, int port = defaultport)
            : _port(port), _listenSock(-1), _func(func), _rfds(nullptr)
        {
        }

        void initServer()
        {
            _listenSock = Sock::Socket();
            Sock::Bind(_listenSock, _port);
            Sock::Listen(_listenSock);
            // logMessage(NORMAL, "creat socket..");
            _rfds = new struct pollfd[num];
            for (int i = 0; i < num; i++) ResetItem(i);

            _rfds[0].fd = _listenSock; // 不变了
            _rfds[0].events = POLLIN;
        }

        void Print()
        {
            std::cout << "fd list: ";
            for (int i = 0; i < num; i++)
            {
                if (_rfds[i].fd != defaultfd)
                    std::cout << _rfds[i].fd << " ";
            }
            std::cout << std::endl;
        }

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

        void Accepter(int listenSock)
        {
            // 走到这里accept不会阻塞 listensock套接字已经就绪了
            string clientIp;
            uint16_t clientPort = 0;
            int sock = Sock::Accept(listenSock, &clientIp, &clientPort);
            if (sock < 0)
                return;
            logMessage(NORMAL, "accept success [%s:%d]", clientIp.c_str(), clientPort);

            // sock 我们能直接recv/read吗?--不能 整个代码 只有select有资格检测事件是否就绪
            // 将新的sock交给select
            // 将新的sock托管给select的本质,将sock添加到_fdArray数组中
            int i;
            for (i = 0; i < num; i++)
            {
                if (_rfds[i].fd != defaultfd)
                    continue;
                else
                    break;
            }
            if (i == num)
            {
                logMessage(WARNING, "server is full, please wait!");
                close(sock);
            }
            else
            {
                _rfds[i] .fd= sock; // 将新创建的sock设置到rfds中
                _rfds[i].events = POLLIN;
                _rfds[i].revents = 0;
            }
            Print();
        }

        void Recver(int pos)
        {
            // 1.读取
            // 这样读取有问题!不能保证是否读取到一个完整的报文
            char buffer[1024];
            ssize_t s = recv(_rfds[pos].fd, buffer, sizeof(buffer) - 1, 0); // 这里在进行recv'时,不会被阻塞,因为走到这里时文件描述符已经就绪了
            if (s > 0)
            {
                buffer[s] = 0;
                logMessage(NORMAL, "client# %s", buffer);
            }
            else if (s == 0)
            {
                close(_rfds[pos].fd);
                ResetItem(pos);
                logMessage(NORMAL, "client quit");
                return;
            }
            else
            {
                close(_rfds[pos].fd);
                ResetItem(pos);
                logMessage(ERROR, "client quit: %s", strerror(errno));
                return;
            }

            // 2.处理request
            std::string response = _func(buffer);
            
            // 3.返回response
            // write
            write(_rfds[pos].fd, response.c_str(), response.size());
        }

        // handler event 中 不仅仅是有一个fd就绪,可能有多个
        // 我们的select只处理了read
        void HandlerReadEvent()
        {
            for (int i = 0; i < num; i++)
            {
                // 过滤掉非法的fd
                if (_rfds[i].fd == defaultfd)
                    continue;
                if(!(_rfds[i].revents & POLLIN)) continue;
                // 下面的为正常的fd
                // 正常的fd不一定就绪
                // 目前一定是listen套接字
                if (_rfds[i].fd == _listenSock && (_rfds[i].revents & POLLIN))
                    Accepter(_listenSock);
                else if(_rfds[i].revents & POLLIN)
                    Recver(i);
                else
                {
                        
                }
            }
        }

        void start()
        {
            int timeout = -1;
            for (;;)
            {
                int n = poll(_rfds, num, timeout);
                switch (n)
                {
                case 0:
                    logMessage(NORMAL, "timeout...");
                    break;
                case -1:
                    logMessage(WARNING, "poll error, code: %d, err string: %s", errno, strerror(errno));
                    break;
                default:
                    // 说明有时间就绪了,目前只有一个监听事件就绪
                    logMessage(NORMAL, "get a new link...");
                    HandlerReadEvent();
                    break;
                }
            }
        }

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

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

得到了与select相同的实验结果,并且结局了select的两个缺点:

poll的缺点主要是遍历的问题:

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

4. epoll

按照man 手册的说法 : 是为处理大批量句柄而作了改进的 poll .
它是在 2.5.44 内核中被引进的 (epoll(4) is a new API introduced in Linux kernel 2.5.44)  

它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法.

5. epoll接口 

#include <sys/epoll.h>

int epoll_create(int size);  -- 创建一个epoll模型,size>0

返回值:成功则返回一个文件描述符,失败返回-1


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

参数:epfd:为epoll_create的返回值

           op:增、改、删:fd对应的事件的类型 EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL

           fd:用户告诉内核:你要帮我关心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 event */
     epoll_data_t data;      /* User data variable */
};

其中events表示感兴趣的事件和被触发的事件,可能的取值为:
EPOLLIN:表示对应的文件描述符可以读;
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数可读;

EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET:   ET的epoll工作模式;

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

返回值:>0:有几个fd就绪  ==0:超时返回  <0:调用失败

参数:epfd:epoll_create的返回值

           events:输出型参数,数组,内核告诉用户:哪些关心的fd事件就绪

           maxevents:events的最大容量

           timeout:>0: ms  ==0:非阻塞等待  <0: 阻塞等待

6. epoll底层原理

红黑树的节点表示:用户告诉内核,哪些sock上的哪些events需要OS关心

还有一个就绪队列,双向链表,每个节点表示:内核告诉用户,哪些sock上的events已经就绪了

就绪的过程就相当于是从红黑树节点转移到就绪队列节点的过程,两种数据结构使用的是同一个节点,只需更该指针即可

每一个节点对应的是一个fd,底层有对应的struct file对象,其中有一个void* private_data字段,指向一个回调函数,这个回调函数的作用就是转移节点的,从而不需要遍历树来确定某个fd对应的事件是否就绪。

epoll模型:

epoll_create:创建epoll模型

epoll_ctl:向红黑树中增、删、改

epoll_wait:找到对应的epoll莫i选哪个,拿到就绪队列中的就绪事件(就绪队列中的事件已经就绪了);不需要遍历检测(检测事件就绪),只需要遍历拷贝

epoll_wait将所有就绪的事件按照顺序放入用户传入的数组中,有几个就绪事件则返回几。就绪队列中数据很多时,一次拿不完也没事(队列先进先出)。epoll不需要自己维护辅助数组,换为了OS维护的红黑树。

7. epoll demo

这里只写epollserver,其余的代码模块与select和poll中的相同,代码如下:

#pragma once

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

#include "err.h"
#include "log.hpp"
#include "sock.hpp"

namespace epoll_ns
{
    class EpollServer
    {
        static const int defaultPort = 8081;
        static const int size = 128;
        static const int defaultValue = -1;
        static const int defaultnum = 64;

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

    public:
        EpollServer(func_t func, uint16_t port = defaultPort, int num = defaultnum)
            :_func(func)
            ,_num(num)
            ,_port(port)
        {
        }

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

            // 2.创建epoll模型
            _epfd = epoll_create(size);
            if(_epfd < 0)
            {
                logMessage(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; // 当事件就绪 被重新捞取上来的时候,我们要知道是哪一个fd就绪了
            epoll_ctl(_epfd, EPOLL_CTL_ADD, _listenSock, &ev);

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

            logMessage(NORMAL, "init server success!");
        }

        void Print(int readyNum)
        {
            std::cout << "fd list: ";
            for(int i = 0; i < readyNum; i++)
            {
                std::cout << _revs[i].data.fd << " ";
            }
            std::cout << std::endl;
        }

        void HandlerEvent(int readyNum)
        {
            logMessage(DEBUG, "Handler in");
            for(int i = 0; i < readyNum; i++)
            {
                uint32_t events = _revs[i].events; 
                int sock = _revs[i].data.fd;
                Print(readyNum);
                if(sock == _listenSock && (events & EPOLLIN))
                {
                    // _listensock的读事件就绪,获取新连接
                    std::string clientip;
                    uint16_t clientport;
                    int fd = Sock::Accept(sock, &clientip, &clientport);

                    if(fd < 0)
                    {
                        logMessage(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)
                {
                    // 普通的读事件就绪
                    // 读取是有问题的 没有协议定制
                    logMessage(DEBUG, "recv start");
                    char buffer[1024];
                    // 把本轮读完也不一定读到一个完整的请求
                    int n = recv(sock, buffer, sizeof(buffer)-1, 0);
                    if(n > 0)
                    {
                        buffer[n] = 0;
                        logMessage(DEBUG, "client# %s", buffer);

                        std::string response = _func(buffer);

                        send(sock, response.c_str(), response.size(), 0);
                    }
                    else if(n == 0)
                    {
                        // 建议先从epoll溢出,才close 文件描述符
                        epoll_ctl(_epfd, EPOLL_CTL_DEL, sock, nullptr);
                        close(sock);
                        logMessage(NORMAL, "client quit");
                    }
                    else
                    {
                        epoll_ctl(_epfd, EPOLL_CTL_DEL, sock, nullptr);
                        close(sock);
                        logMessage(ERROR, "recv error, code: %d, errstring: %s", errno, strerror(errno));
                    }
                }
                else if(events & EPOLLOUT)
                {}
            }
            logMessage(DEBUG, "Handler out");
        }

        void start()
        {
            int timeout = -1;
            for(;;)
            {
                int n = epoll_wait(_epfd, _revs, _num, timeout);
                switch(n)
                {
                case 0:
                    logMessage(NORMAL, "timeout...");
                    break;
                case -1:
                    logMessage(WARNING, "epoll_wait failed, code: %d, errstring: %s", errno, strerror(errno));
                    break;
                default:
                    logMessage(NORMAL, "have event ready");
                    HandlerEvent(n); // 有n个事件就绪
                    break;
                }
            }
        }

        ~EpollServer()
        {
            if(_listenSock != defaultValue) close(_listenSock);
            if(_epfd != defaultValue) close(_epfd);
            if(_revs) delete[] _revs;
        }

    private:
        uint16_t _port;
        int _listenSock;
        int _epfd;
        struct epoll_event *_revs;
        int _num;
        func_t _func;
    };
}

得到的实验结果也是与预期相符的: 

8. 再谈epoll原理 (LT/ET)

什么叫做事件就绪:底层的IO条件满足了,可以进行某种IO行为了。

select/poll/epoll 为等待 :IO就绪事件的通知机制 

通知机制的策略,epoll的不同工作模式:

  • Level Triggered(LT):水平触发,只要底层有数据epoll就会一直通知用户要读取数据
  • Edge Triggered(ET):边缘触发,底层如果数据没有被读完,epoll不在通知用户,除非底                                           层数据变化的时候(数据增多),才会再一次通知一次

select/poll/epoll默认工作模式为LT

EP模式只有数据从无到有/从有到多的时候,才会通知上层且只通知一次。使得程序员将本轮就绪的数据全部读取到上层,那么是如何吧本次就绪的底层数据全部读完的呢?-- 循环读取,直到读不到数据;一般的fd是阻塞式的,但是ET模式下对应的fd必须是非阻塞式的,因为若是我们读到最后一次底层没有数据时,阻塞式的fd会导致程序在读取时阻塞。

在epoll采用ET(Edge Triggered)工作模式时,如果一次没有读完底层来的数据,epoll_wait将不会再次触发事件,直到数据完全被读取或者关闭连接。这意味着,如果数据处理不完全,代码可能会阻塞在epoll_wait上,等待更多数据。

LT的fd可以是阻塞/非阻塞的。LT模式下也可以模仿ET的工作方式。并不能说LT/ET谁的工作模式更高效,要结合特定的场景来看。

一般情况下,ET的高效不只体现在通知机制上,还有会尽快让上层把数据取走。-- 当上层把数据取走后TCP可以更新出一个更大的滑动窗口,提高底层的发送效率,更阿红的利用诸如TCP应答机制等策略。 -- TCP中PSH的作用就是让底层数据就绪,再让上层知道。

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

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

相关文章

【生日视频制作】一群美女挥手拉蓝横幅条幅AE模板修改文字软件生成器教程特效素材【AE模板】

一群美女挥手拉蓝条横幅生日视频制作教程AE模板修改文字生成器 怎么如何做的【生日视频制作】一群美女挥手拉蓝横幅条幅AE模板修改文字软件生成器教程特效素材【AE模板】 生日视频制作步骤&#xff1a; 安装AE软件下载AE模板把AE模板导入AE软件修改图片或文字渲染出视频

Ai+若依(系统工具-->表单构建):【02篇】

系统工具 表单构建 介绍 允许用户通过拖放等可视化操作创建表单,比如用来收集数据的表格或调查问卷。 可以自定义表单的各个部分,比如添加不同的输入项和设置验证规则,无需编写代码。 提供了导出数据、导入数据、分享表单和设置权限的功能,方便数据管理和共享。 案例 通…

RoboCopy文件快速拷贝工具

RoboCopy是Windows平台(从Windows Vista/Window 7开始)自带的文件快速拷贝工具,它是xcopy命令工具的升级版,解除了xcopy拷贝文件时4GB的容量限制,同时,又支持多线程、给文件夹制作镜像、复制指定日期的文件等功能。 1 全部拷贝 假设从alice文件夹,全部拷贝到bob文件夹,则…

leetcode 80 删除有序数组中的重复项 II

正文 仍旧使用双指针&#xff0c; 思想与 leetcode 26 删除有序数组中的重复项 一致。只是此时因为要求保留重复元素两次&#xff0c;我们的左侧指针可以从第二个数据开始&#xff0c;且右侧指针需要和两个元素的值进行判断。 class Solution:def removeDuplicates(self, nums…

WPF—资源的使用

资源的使用 资源是可以在应用中的不同位置重复使用的对象。 资源的示例包括画笔和样式。 <Window.Resources><!--定义颜色资源--><SolidColorBrush x:Key"MyBrush" Color"#05E0E9"/><!--定义样式资源--><Style TargetType&quo…

前端技术(五)—— 使用Node.js编写简单的项目

一、 初始化项目 1. 创建项目 ⑴ 新建 api_kjzt_server 文件夹作为项目根目录&#xff0c;并初始化包管理配置文件 并在项目根目录中运行如下的命令&#xff0c;初始化包管理配置文件&#xff1a; npm init -y⑵ 运行如下的命令&#xff0c;安装特定版本的 express npm i e…

企业级WEB应用服务器TOMCAT详解

一、什么是TOMCAT 1.1来源 Tomcat是Apache 软件基金会&#xff08;Apache Software Foundation&#xff09;的Jakarta 项目中的一个核心项目&#xff0c;由Apache、Sun 和其他一些公司及个人共同开发而成。由于有了Sun 的参与和支持&#xff0c;最新的Servlet 和JSP 规范总是能…

A Neural Probabilistic Language Model

摘要 统计语言建模的一个目标是学习单词序列的联合概率函数。由于维度的诅咒&#xff0c;这在本质上是困难的:我们建议用它自己的武器来对抗它。在提出的方法中&#xff0c;人们同时学习(1)每个单词的分布式表示&#xff08;词向量&#xff09;(即单词之间的相似性)以及(2)表示…

TypeScript 类型注解(二)

一、TypeScript类型约束--对象 对象其实和数组类似&#xff0c;不是限制对象本身的类型&#xff0c;而是对对象属性类型进行限制 结构简化&#xff1a; 对对象做类型限制的好处&#xff1a; 大家都学习过一段时间编程了&#xff0c;会发现咱们经常操作的类型就是对象&#xf…

使用miniconda构建数据科学环境

背景 数据科学中&#xff0c;不同时期项目代码往往是由不同的版本的python和第三方数据科学包构建&#xff0c;这些不同版本往往会存在兼容性问题&#xff0c;要运行这些代码&#xff0c;需要管理不同的版本的安装包。Conda是一个开源的包管理和环境管理系统&#xff0c;环境管…

Linux | 编译和使用自定义动静态库的全面解析

新竹高于旧竹枝&#xff0c;全凭老干为扶持。 - 《新竹》(郑燮) 2024.8.25 目录 1、静态库和动态库简介 静态库&#xff08;.a&#xff09;&#xff1a; 动态库&#xff08;.so&#xff09;&#xff1a; 动态库和静态库的比较&#xff1a; 2、静态库的生成和使用&#xff…

GCB | 首次揭示!气候变化对常绿和落叶植物物候差异化影响的机制

气候变化引起的植物物候改变已对全球范围内生物多样性和生态系统产生重大影响&#xff08;Nature高引文章 | 北京大学朴世龙院士等&#xff1a;全球变暖对植被物候的影响及其机制&#xff1b;Nature Ecology & Evolution | 南京大学张永光教授团队揭示延长的植被物候期受CO…

set容器的所有操作

1.基本概念 2.构造和赋值 3.大小和交换 4.插入和删除 5.查找和统计 6.set和multiset的区别 7.pair对组创建 用p.first和p.second调用前后两个属性。 8.仿函数实现降序排列 自定义数据类型也一样用仿函数&#xff1a;

【领域驱动设计 打通DDD最小闭环】领域建模

本篇BLOG为DDD流程的第二步&#xff0c;在模型的建立阶段&#xff0c;领域专家与技术人员通过领域建模来完成更为细致的模型建立讨论 领域建模的目的 领域建模主要有两个目的&#xff1a; 将知识可视化&#xff0c;准确、深刻地反映领域知识&#xff0c;并且在业务和技术人…

Python优化算法14——海鸥优化算法(SOA)

科研里面优化算法都用的多&#xff0c;尤其是各种动物园里面的智能仿生优化算法&#xff0c;但是目前都是MATLAB的代码多&#xff0c;python几乎没有什么包&#xff0c;这次把优化算法系列的代码都从底层手写开始。 需要看以前的优化算法文章可以参考&#xff1a;Python优化算…

【图文并茂】ant design pro 如何给后端发送 json web token - 请求拦截器的使用

上一节有讲过 【图文并茂】ant design pro 如何对接后端个人信息接口 还差一个东西&#xff0c;去获取个人信息的时候&#xff0c;是要发送 token 的&#xff0c;不然会报 403. 就是说在你登录之后才去获得个人信息。这样后端才能知道是谁的信息。 token 就代码了某个人。 …

工作实战-项目压测记录

1-1-1每分钟的单量 1-1-2第二版测试 2022年5月16日 17:43:11 成功 失败 其它(nginx) 真实入库单量 总单量 52 1 447 500 2022年5月16日 19:42:18 成功 失败 其它(nginx) 真实入库单量 总单量 311 689 306 1000 2-0-1. 20线程-2000单执行结果 2-1-0. 40线…

金融科技 API 接口:提升金融服务效率的关键

金融科技是应用技术手段和创新理念来提升金融服务效率的重要途径。而其中的API接口则是实现金融科技的关键。API接口的简单定义是提供计算机程序之间通信的规范和工具&#xff0c;提供一种方法和数据的交互形式&#xff0c;以便开发人员能够利用现有的软件来创建新的应用和服务…

前端网格布局display: grid;

display: grid; 块级网格 &#xff08;常用&#xff09; display: inline-grid; 行内块级网格 &#xff08;一般不用&#xff09; HTML 元素将 display 属性设置为 grid 或 inline-grid 后&#xff0c;它就变成了一个网格容器&#xff0c;这个元素的所有直系子元素将…

Mobile-Agent项目部署与学习总结(DataWhale AI夏令营)

前言 你好&#xff0c;我是GISer Liu&#xff0c;一名热爱AI技术的GIS开发者&#xff0c;本文是DataWhale 2024 AI夏令营的最后一期——Mobile-Agent赛道&#xff0c;这是作者的学习文档&#xff0c;这里总结一下&#xff0c;和作者一起学习这个多模态大模型新项目吧&#x1f6…