当前位置: 首页 > article >正文

【Linux网络编程】第二十一弹---深入解析I/O多路转接技术之poll函数:优势、缺陷与实战代码

个人主页: 熬夜学编程的小林

💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】【Linux网络编程】

目录

1、I/O 多路转接之 poll

1.1、初识poll 

1.2、poll 函数接口

1.3、poll 的优点

1.4、poll 的缺点

1.5、代码演示

1.5.1、主函数

1.5.2、PollServer类

1.5.3、运行结果

1.6、完整代码

1.6.1、Main.cc

1.6.2、Makefile

1.6.3、PollServer.hpp 


1、I/O 多路转接之 poll

select缺点

上一弹我们知道了select有四个缺点poll能够解决select中的两个缺点

1.1、初识poll 

poll函数用于监视多个文件描述符以查看它们是否有 I/O(输入/输出)活动

作用:为了等待多个fd,等待fd上面的新事件就绪,通知程序员,事件已经就绪,可以进行IO拷贝了! 

定位:只负责进行等,等就绪事件派发!

1.2、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: 指向 pollfd 结构体数组的指针,每个结构体指定一个要监视的文件描述符及其感兴趣的事件。
  • nfds: 数组 fds 中的元素数量,即要监视的文件描述符数量。
  • timeout: 超时时间(毫秒)
    • 如果为 -1,   则 poll 将阻塞直到某个文件描述符准备好
    • 如果为  0,   则 poll 立即返回,检查是否有文件描述符准备好,而不阻塞
    • 如果为正数,则 poll 将阻塞指定的毫秒数

返回值:

  • 成功时,返回准备就绪的文件描述符数量revents 不为零的 pollfd 结构体的数量)。
    • 返回值等于 0, 表示 poll 函数等待超时;
    • 返回值大于 0, 表示 poll 由于监听的文件描述符就绪而返回.
  • 失败时,返回 -1,并设置 errno 以指示错误。

events 和 revents 的取值: 

1.3、poll 的优点

不同于 select 使用三个位图来表示三个 fdset 的方式,poll 使用一个 pollfd 的指针实现.

  • pollfd 结构包含了要监视的 event 和发生的 event,不再使用 select“参数-值”传递的方式. 接口使用比 select 更方便.
  • poll 并没有最大数量限制 (但是数量过大后性能也是会下降).

1.4、poll 的缺点

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

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

1.5、代码演示

此处对上一弹select版本的多路转接进行修改!

1.5.1、主函数

老规矩,根据主函数反向实现类和成员函数!

// ./poll_server 8888
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " locak-port" << std::endl;
        exit(0);
    }
    uint16_t port = std::stoi(argv[1]);
    EnableScreen(); // 开启日志
    std::unique_ptr<PollServer> svr = std::make_unique<PollServer>(port);
    svr->InitServer();
    svr->Loop();

    return 0;
}

1.5.2、PollServer类

PollServer类的成员变量与SelectServer类的成员变量基本一致,但是此处的数组(存放fd)类型是struct pollfd,还需要端口号和套接字

基本结构

class PollServer
{
    const static int gnum = sizeof(fd_set) * 8;
    const static int gdefaultfd = -1;

public:
    PollServer(uint16_t port);
    void InitServer();
    // 处理新链接
    void Accepter();
    // 处理普通fd就绪
    void HandlerIO(int i);
    // 一定存在大量的fd就绪,可能是普通sockfd,也可能是listensockfd
    void HandlerEvent();
    void Loop();
    void PrintDebug();
    ~PollServer();

private:
    uint16_t _port;
    std::unique_ptr<Socket> _listensock;

    struct pollfd fd_events[gnum];
};

构造析构函数

构造函数初始化端口号并根据端口号创建监听套接字对象析构函数暂时不做处理

PollServer(uint16_t port) : _port(port), _listensock(std::make_unique<TcpSocket>())
{
    _listensock->BuildListenSocket(_port);
}

~PollServer() 
{}

InitServer()

InitServer()函数将结构体类型的数组fd成员设置为默认fd,其他两个事件先设置为0,并将listensockfd添加到结构体数组的第一个元素的fd成员,并将events事件设置为读

void InitServer()
{
    for (int i = 0; i < gnum; i++)
    {
        fd_events[i].fd = gdefaultfd;
        fd_events[i].events = 0;
        fd_events[i].revents = 0;
    }
    fd_events[0].fd = _listensock->Sockfd(); // 默认直接添加sockfd到数组中
    fd_events[0].events = POLLIN;
}

Loop()

Loop()函数调用poll系统调用,根据返回值执行对应的操作:

1、返回值为0    :打印超时日志,并退出循环

2、返回值为-1   :打印出错日志,并退出循环

3、返回值大于0 :处理事件

void Loop()
{
    while (true)
    {
        int timeout = 1000;
        int n = ::poll(fd_events,gnum,timeout);
        switch (n)
        {
        case 0:
            LOG(DEBUG, "time out\n");
            break;
        case -1:
            LOG(ERROR, "poll error\n");
            break;
        default:
            // 如果事件就绪,但是不做处理,select会一直通知我,直到我处理了!
            LOG(INFO, "haved event ready,n : %d\n", n); // 几个文件描述符就绪
            HandlerEvent();
            PrintDebug();
            // sleep(1);
            break;
        }
    }
}

注意:Loop函数中的timeout变量后序会用于测试!

HandlerEvent()

在执行HandlerEvent()函数之前,赋值数组中一定存在大量的fd就绪,可能是普通sockfd,也可能是listensockfd此处主要分以下两步:

  • 1、判断fd是否合法
  • 2、判断fd是否就绪    
    • 2.1、就绪是listensockfd  
      • 2.1.1、调用Accepter()处理新链接函数
    • 2.2、就绪是normal sockfd
      • 2.2.1、调用HandlerIO()处理普通fd就绪函数
// 一定存在大量的fd就绪,可能是普通sockfd,也可能是listensockfd
void HandlerEvent()
{
    // 事件派发
    for (int i = 0; i < gnum; i++)
    {
        // 1.判断fd是否合法
        if (fd_events[i].fd == gdefaultfd)
            continue;
        // 2.判断fd是否就绪
        // fd一定是合法的fd
        // 合法的fd不一定就绪,判断fd是否就绪?
        if (fd_events[i].revents & POLLIN)
        {
            // 读事件就绪
            // 2.1 listensockfd 2.2 normal sockfd
            if (_listensock->Sockfd() == fd_events[i].fd)
            {
                // listensockfd
                // 链接事件就绪,等价于读事件就绪
                Accepter();
            }
            else
            {
                // normal sockfd,正常的读写
                HandlerIO(i);
            }
        }
    }
}

Accepter()

Accepter()函数处理新链接主要分为以下三步:

  • 1、获取链接
  • 2、获取链接成功将新的fd 和 读事件 添加到数组中
  • 3、数组满了,需关闭sockfd,此处可以扩容并再次添加新的fd和事件
// 处理新链接
void Accepter()
{
    InetAddr addr;
    int sockfd = _listensock->Accepter(&addr); // 会不会阻塞!一定不会,因为已经就绪了!
    if (sockfd > 0)
    {
        LOG(DEBUG, "get a new link,client info %s:%d\n", addr.Ip().c_str(), addr.Port());
        // 已经获得了一个新的sockfd
        // 接下来我们可以读取?绝对不能读,条件不一定满足
        // 只要将新的fd添加到fd_events中即可!
        bool flag = false;
        for (int pos = 1; pos < gnum; pos++)
        {
            if (fd_events[pos].fd == gdefaultfd)
            {
                flag = true;
                fd_events[pos].fd = sockfd; // 把新的fd放入数组中
                fd_events[pos].events = POLLIN;
                LOG(INFO, "add %d to fd_array success \n", sockfd);
                break;
            }
        }
        // 数组满了
        if (!flag)
        {
            LOG(WARNING, "Server Is Full\n");
            ::close(sockfd);
            // 扩容
            // 添加
        }
    }
}

HandlerIO()

HandlerIO()函数处理普通fd情况直接读取文件描述符中的数据根据recv()函数的返回值做出不一样的决策,主要分为以下三种情况:

1、返回值大于0,读取文件描述符中的数据,并使用send()函数做出回应!

2、返回值等于0,读到文件结尾,打印客户端退出的日志,关闭文件描述符,并将该下标的文件描述符设置为默认fd,事件都设置为0

3、返回值小于0,读取文件错误,打印接受失败的日志,然后同上!

注意:此处的读取是有问题的,因为正确的读取是有协议的,此处是直接读取! 

// 处理普通fd就绪
void HandlerIO(int i)
{
    char buffer[1024];
    ssize_t n = ::recv(fd_events[i].fd, buffer, sizeof(buffer) - 1, 0); // 这里读取会阻塞?不会,因为读事件就绪了
    if (n > 0)
    {
        buffer[n] = 0;
        std::cout << "client say# " << buffer << std::endl;

        std::string content = "<html><body><h1>hello linux</h1></body></html>";
        std::string echo_str = "HTTP/1.0 200 OK\r\n";
        echo_str += "Content-Type: text/html\r\n";
        echo_str += "Content-Length: " + std::to_string(content.size()) + "\r\n\r\n";
        echo_str += content;
        ::send(fd_events[i].fd, echo_str.c_str(), echo_str.size(), 0);
    }
    else if (n == 0)
    {
        LOG(INFO, "client quit...\n");
        // 关闭fd
        ::close(fd_events[i].fd);
        // select 不再关心这个fd了
        fd_events[i].fd = gdefaultfd;
        fd_events[i].events = 0;
        fd_events[i].revents = 0;
    }
    else
    {
        LOG(ERROR, "recv error\n");
        // 关闭fd
        ::close(fd_events[i].fd);
        // select 不再关心这个fd了
        fd_events[i].fd = gdefaultfd;
        fd_events[i].events = 0;
        fd_events[i].revents = 0;
    }
}

PrintDebug()

PrintDebug()遍历数组,将合法的文件描述符打印出来!

void PrintDebug()
{
    std::cout << "fd list: ";
    for (int i = 0; i < gnum; i++)
    {
        if (fd_events[i].fd == gdefaultfd)
            continue;
        std::cout << fd_events[i].fd << " ";
    }
    std::cout << "\n";
}

1.5.3、运行结果

Loop()函数中timeout = 1000时,即1000毫秒,即1秒等待一次!

Loop()函数中timeout = 0时,即1000毫秒,即非阻塞等待! 

 Loop()函数中timeout = -1时,即阻塞等待! 

1.6、完整代码

1.6.1、Main.cc

#include <iostream>
#include <memory>

#include "PollServer.hpp"

// ./poll_server 8888
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " locak-port" << std::endl;
        exit(0);
    }
    uint16_t port = std::stoi(argv[1]);
    EnableScreen(); // 开启日志
    std::unique_ptr<PollServer> svr = std::make_unique<PollServer>(port);
    svr->InitServer();
    svr->Loop();

    return 0;
}

1.6.2、Makefile

poll_server:Main.cc
	g++ -o $@ $^ -std=c++14
.PHONY:clean
clean:
	rm -rf poll_server

1.6.3、PollServer.hpp 

#pragma once

#include <iostream>
#include <poll.h>
#include "Socket.hpp"
#include "InetAddr.hpp"

using namespace socket_ns;

class PollServer
{
    const static int gnum = sizeof(fd_set) * 8;
    const static int gdefaultfd = -1;

public:
    PollServer(uint16_t port) : _port(port), _listensock(std::make_unique<TcpSocket>())
    {
        _listensock->BuildListenSocket(_port);
    }
    void InitServer()
    {
        for (int i = 0; i < gnum; i++)
        {
            fd_events[i].fd = gdefaultfd;
            fd_events[i].events = 0;
            fd_events[i].revents = 0;
        }
        fd_events[0].fd = _listensock->Sockfd(); // 默认直接添加sockfd到数组中
        fd_events[0].events = POLLIN;
    }
    // 处理新链接
    void Accepter()
    {
        InetAddr addr;
        int sockfd = _listensock->Accepter(&addr); // 会不会阻塞!一定不会,因为已经就绪了!
        if (sockfd > 0)
        {
            LOG(DEBUG, "get a new link,client info %s:%d\n", addr.Ip().c_str(), addr.Port());
            // 已经获得了一个新的sockfd
            // 接下来我们可以读取?绝对不能读,条件不一定满足
            // 只要将新的fd添加到fd_events中即可!
            bool flag = false;
            for (int pos = 1; pos < gnum; pos++)
            {
                if (fd_events[pos].fd == gdefaultfd)
                {
                    flag = true;
                    fd_events[pos].fd = sockfd; // 把新的fd放入数组中
                    fd_events[pos].events = POLLIN;
                    LOG(INFO, "add %d to fd_array success \n", sockfd);
                    break;
                }
            }
            // 数组满了
            if (!flag)
            {
                LOG(WARNING, "Server Is Full\n");
                ::close(sockfd);
                // 扩容
                // 添加
            }
        }
    }
    // 处理普通fd就绪
    void HandlerIO(int i)
    {
        char buffer[1024];
        ssize_t n = ::recv(fd_events[i].fd, buffer, sizeof(buffer) - 1, 0); // 这里读取会阻塞?不会,因为读事件就绪了
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << "client say# " << buffer << std::endl;

            std::string content = "<html><body><h1>hello linux</h1></body></html>";
            std::string echo_str = "HTTP/1.0 200 OK\r\n";
            echo_str += "Content-Type: text/html\r\n";
            echo_str += "Content-Length: " + std::to_string(content.size()) + "\r\n\r\n";
            echo_str += content;
            ::send(fd_events[i].fd, echo_str.c_str(), echo_str.size(), 0);
        }
        else if (n == 0)
        {
            LOG(INFO, "client quit...\n");
            // 关闭fd
            ::close(fd_events[i].fd);
            // select 不再关心这个fd了
            fd_events[i].fd = gdefaultfd;
            fd_events[i].events = 0;
            fd_events[i].revents = 0;
        }
        else
        {
            LOG(ERROR, "recv error\n");
            // 关闭fd
            ::close(fd_events[i].fd);
            // select 不再关心这个fd了
            fd_events[i].fd = gdefaultfd;
            fd_events[i].events = 0;
            fd_events[i].revents = 0;
        }
    }
    // 一定存在大量的fd就绪,可能是普通sockfd,也可能是listensockfd
    void HandlerEvent()
    {
        // 事件派发
        for (int i = 0; i < gnum; i++)
        {
            // 1.判断fd是否合法
            if (fd_events[i].fd == gdefaultfd)
                continue;
            // 2.判断fd是否就绪
            // fd一定是合法的fd
            // 合法的fd不一定就绪,判断fd是否就绪?
            if (fd_events[i].revents & POLLIN)
            {
                // 读事件就绪
                // 2.1 listensockfd 2.2 normal sockfd
                if (_listensock->Sockfd() == fd_events[i].fd)
                {
                    // listensockfd
                    // 链接事件就绪,等价于读事件就绪
                    Accepter();
                }
                else
                {
                    // normal sockfd,正常的读写
                    HandlerIO(i);
                }
            }
        }
    }
    void Loop()
    {
        while (true)
        {
            int timeout = -1;
            // int timeout = 0;
            // int timeout = 1000;
            int n = ::poll(fd_events,gnum,timeout);
            switch (n)
            {
            case 0:
                LOG(DEBUG, "time out\n");
                break;
            case -1:
                LOG(ERROR, "poll error\n");
                break;
            default:
                // 如果事件就绪,但是不做处理,select会一直通知我,直到我处理了!
                LOG(INFO, "haved event ready,n : %d\n", n); // 几个文件描述符就绪
                HandlerEvent();
                PrintDebug();
                // sleep(1);
                break;
            }
        }
    }
    void PrintDebug()
    {
        std::cout << "fd list: ";
        for (int i = 0; i < gnum; i++)
        {
            if (fd_events[i].fd == gdefaultfd)
                continue;
            std::cout << fd_events[i].fd << " ";
        }
        std::cout << "\n";
    }
    ~PollServer()
    {
    }

private:
    uint16_t _port;
    std::unique_ptr<Socket> _listensock;

    struct pollfd fd_events[gnum];
};


http://www.kler.cn/a/471166.html

相关文章:

  • 数据库回滚:大祸临头时
  • 黄仁勋演讲总结(2种显卡,1个开源大模型,1个数据采集平台)
  • 121 买入股票的最佳时机
  • 性能测试01|性能测试理论
  • 网页数据如何正确copy到postman中
  • CART、XGBoost 、LightGBM详解及分析
  • git①111
  • HDFS架构原理
  • TextMeshPro保存偏移数据
  • React18实现账单管理项目(三):日期分组与图标适配
  • 请求是如何通过k8s service 路由到对应的pod
  • Express 加 sqlite3 写一个简单博客
  • Oracle SQL子查询实例
  • UE4_用户控件_4_Widgets嵌套Widgets构建复杂系统
  • VLMs之Agent之CogAgent:CogAgent的简介、安装和使用方法、案例应用之详细攻略
  • Yolov8训练方式以及C#中读取yolov8+onnx模型进行目标检测.NET 6.0
  • 分布式与集群
  • 基于SpringBoot+Vue的考研百科网站
  • UG NX二次开发(C++)-UFun函数-按照特定方向提取轮廓线
  • el-table拖拽表格
  • 吉客云与金蝶云星空数据集成技术详解
  • 20250107在WIN10下使用无线ADB连接Andorid7.1.2
  • vulnhub whowantstobeking靶机
  • C++中,typename
  • 初学stm32 --- 电源监控
  • 数据库6讲