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

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

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

目录

[1、I/O 多路转接之 poll](#1、I/O 多路转接之 poll)

1.1、初识poll

[1.2、poll 函数接口](#1.2、poll 函数接口)

[1.3、poll 的优点](#1.3、poll 的优点)

[1.4、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];
};
相关推荐
鲤籽鲲23 分钟前
C# 整型、浮点型 数值范围原理分析
开发语言·c#
Hacker_LaoYi24 分钟前
计算机网络:虚拟机虚拟网络配置
网络·计算机网络·php
重生之绝世牛码1 小时前
Java设计模式 —— 【行为型模式】命令模式(Command Pattern) 详解
java·大数据·开发语言·设计模式·命令模式·设计原则
-Harvey1 小时前
ubuntu为Docker配置代理
linux·ubuntu·docker
thehunters1 小时前
win10 ubuntu 使用Android ndk 问题:clang-14: Exec format error
android·linux·ubuntu
大丈夫立于天地间2 小时前
OSPF - 1类LSA(Router-LSA)
网络·网络协议·学习·信息与通信
Ven%2 小时前
如何让后台运行llamafactory-cli webui 即使关掉了ssh远程连接 也在运行
运维·人工智能·chrome·python·ssh·aigc
晚风_END2 小时前
node.js|浏览器插件|Open-Multiple-URLs的部署和使用,实现一键打开多个URL的强大工具
服务器·开发语言·数据库·node.js·dubbo
黑客Jack2 小时前
网络安全应急响应技术原理与应用
网络·安全·web安全
黑客K-ing2 小时前
什么是黑客和白帽子
网络·安全·web安全