多路复用 poll -- poll 的介绍,poll 的优缺点,poll 版本的 TCP 回显服务器

目录

[1. poll 的介绍](#1. poll 的介绍)

[1.1 poll 函数介绍](#1.1 poll 函数介绍)

[1.2 常用事件标记](#1.2 常用事件标记)

[2. poll 的优缺点](#2. poll 的优缺点)

[3. 使用 poll 实现 TCP 回显服务器](#3. 使用 poll 实现 TCP 回显服务器)

[3.1 前置代码](#3.1 前置代码)

[3.2 代码实现](#3.2 代码实现)


1. poll 的介绍

poll 是 Linux/Unix 系统中改进型的 I/O 多路复用机制 ,设计目标是解决 select 的核心痛点(如 FD 数量硬限制、fd_set 集合操作繁琐),同时保留 I/O 多路复用的核心逻辑(单个进程监控多个 FD)。

1.1 poll 函数介绍

cpp 复制代码
原型:
    int poll(struct pollfd *fds, nfds_t nfds, int timeout);
 
头文件:
    #include <poll.h>
 
参数:
    fds:struct pollfd 数组地址,存储所有待监控的 FD 及其事件配置。
    nfds:fds 数组的长度,即需要监控的 FD 总数,无上限。
    timeout:timeout > 0,阻塞 timeout 毫秒,若期间有 FD 准备就绪则立即返回,
    超时无 FD 就绪则返回0;timeout = 0,非阻塞模式,有无 FD 都立即返回;timeout = -1,
    阻塞等待,直到有 FD 就绪才返回。
 
返回值:
    >0:表示具体有多少个就绪的 fd。
    =0:表示在超时时间内没有 fd 准备就绪。
    <0:select 错误。
功能:
    让一个进程 / 线程同时监控多个文件描述符(File Descriptor,FD),
    等待其中任意一个或多个 FD 变为 "就绪状态"(可读、可写或发生异常),
    从而避免为每个 FD 单独创建线程 / 进程,提升 I/O 效率。
cpp 复制代码
// struct pollfd 结构体

struct pollfd {
    int   fd;         // 待监控的文件描述符(FD)
    short events;     // 期望监控的事件(输入参数,如 POLLIN 表示监控可读)
    short revents;    // 实际发生的事件(输出参数,由内核填充,如 POLLIN 表示可读就绪)
};

1.2 常用事件标记

下列常用事件标记的宏值,决定了 poll 监听 fd 的哪些事件的宏。

2. poll 的优缺点

优点:

  1. 无 FD 数量硬限制poll 用动态数组(pollfd 数组)替代 selectfd_set 位图,摆脱了 FD_SETSIZE(默认 1024)的硬限制。

  2. FD 与事件管理更灵活 :每个 FD 的 "监控事件"(如可读、可写)和 "就绪事件"(如实际发生的事件)通过 pollfd 结构体的 events(输入)和 revents(输出)分离,不会像 select 那样破坏原监控集合。减少代码冗余。

  3. 跨平台兼容性强 :遵循 POSIX 标准,支持 Linux、Unix、macOS 等主流类 Unix 系统,Windows 也可通过 WSAPoll 接口实现兼容(逻辑一致)。

缺点:

  1. 线性扫描效率低poll 返回后,无法直接获取 "就绪 FD 列表",仍需遍历整个 pollfd 数组,通过检查 revents 字段判断 FD 是否就绪。时间复杂度为 O (n),当 FD 数量达到万级以上时,遍历开销会急剧增大,成为性能瓶颈。

  2. 内核 / 用户态拷贝开销 :每次调用 poll 时,需将整个 pollfd 数组从用户态拷贝到内核态;返回时,内核需将更新后的 revents 字段(整个数组)拷贝回用户态。FD 越多,拷贝耗时越长,浪费 CPU 资源。

3. 使用 poll 实现 TCP 回显服务器

3.1 前置代码

参考多路复用 select 中的前置代码。

3.2 代码实现

cpp 复制代码
// pollServer.hpp -- poll 服务器类

#pragma once

#include <iostream>
#include <memory>
#include <unistd.h>
#include <poll.h>
#include "Socket.hpp"

using namespace SocketModule;
using namespace LogModule;

class PollServer
{
    const static int size = 4096;
    const static int defaultfd = -1;

public:
    PollServer(int port) : _listensock(std::make_unique<TcpSocket>()), _isrunning(false)
    {
        // 1. 创建套接字并绑定端口号并开始进行监听
        _listensock->BuildTcpSocketMethod(port);

        // 2. 将 pollfd 数组中的 fd 全部默认设置为 -1
        for (int i = 0; i < size; ++i)
            _fds[i].fd = defaultfd;

        // 3. 将  pollfd 数组中首元素的 fd 设置为监听套接字的 fd
        _fds[0].fd = _listensock->Fd();
        _fds[0].events = POLLIN;
    }

    void Start()
    {
        _isrunning = true;

        while (_isrunning)
        {
            PrintFd();
            int n = poll(_fds, size, -1);
            switch (n)
            {
            case -1:
                // poll 错误
                LOG(LogLevel::ERROR) << "poll error";
                break;
            case 0:
                // 2. poll 阻塞等待超时
                LOG(LogLevel::INFO) << "poll time out...";
                break;
            default:
                // 3. 读事件就绪(listen 套接字也是读事件就绪)
                LOG(LogLevel::DEBUG) << "读事件就绪..., n: " << n;
                Dispatcher(); // 进行事件派发
                break;
            }
        }

        _isrunning = false;
    }

    void Stop()
    {
        _isrunning = false;
    }

    ~PollServer() {}

    // 事件派发器
    void Dispatcher()
    {
        for (int i = 0; i < size; ++i)
        {
            // 1. 排除不合法的 fd
            if (_fds[i].fd == defaultfd)
                continue;
            // 2. 将合法且就绪的 fd 进行事件派发
            if (_fds[i].revents & POLLIN)
            {
                if (_fds[i].fd == _listensock->Fd())
                    Accepter();
                else
                    Recver(i);
            }
        }
    }

    // 连接管理器
    void Accepter()
    {
        InetAddr client;
        int sockfd = _listensock->Accept(&client); // 此时的 accept 不会被阻塞
        if (sockfd >= 0)
        {
            LOG(LogLevel::INFO) << "get a new link, sockfd: " << sockfd << ", client is: " << client.StringAddr();
            // 将新连接托管给 select
            int pos = 0;
            for (; pos < size; ++pos)
                if (_fds[pos].fd == defaultfd)
                    break;

            if (pos == size)
            {
                LOG(LogLevel::WARNING) << "poll server full";
                close(sockfd);
            }
            else {
                _fds[pos].fd = sockfd;
                _fds[pos].events = POLLIN;
                _fds[pos].revents = 0;
            }
        }
    }

    // IO处理器
    void Recver(int pos)
    {
        char buffer[1024];
        ssize_t n = recv(_fds[pos].fd, buffer, sizeof(buffer) - 1, 0); // 这里读的时候会有 bug,不能保证一次读取全部的数据
        if (n > 0)
        {
            // 1. 读取到客户端传入的数据
            buffer[n] = 0;
            std::cout << "sockfd: " << _fds[pos].fd << ", client say@ " << buffer << std::endl;
        }
        else if (n == 0)
        {
            // 2. 客户端退出 -- 关闭连接并将其 fd 从 pollfd 数组中移除
            LOG(LogLevel::INFO) << "sockfd: " << _fds[pos].fd << ", client quit...";
            close(_fds[pos].fd);

            _fds[pos].fd = defaultfd;
            _fds[pos].events = 0;
            _fds[pos].revents = 0;
        }
        else
        {
            // 3. 读取出错 -- 关闭连接并将其 fd 从 pollfd 数组中移除
            LOG(LogLevel::ERROR) << "recv error";
            close(_fds[pos].fd);

            _fds[pos].fd = defaultfd;
            _fds[pos].events = 0;
            _fds[pos].revents = 0;
        }
    }

    void PrintFd()
    {
        std::cout << "_fds[].fd: ";
        for (int i = 0; i < size; ++i)
            if (_fds[i].fd != defaultfd)
                std::cout << _fds[i].fd << " ";
        std::cout << std::endl;
    }

private:
    std::unique_ptr<Socket> _listensock;
    bool _isrunning;
    pollfd _fds[size];
};
cpp 复制代码
// main.cc -- 主函数

#include "PollServer.hpp"

int main(int argc, char* argv[]) {
    if (argc != 2) {
        std::cout << "Usage: " << argv[0] << " port" << std::endl;
        exit(USAGE_ERR);
    }

    Enable_Console_Log_Strategy();
    uint16_t port = std::stoi(argv[1]);

    std::unique_ptr<PollServer> svr = std::make_unique<PollServer>(port);
    svr->Start();
    return 0;
}

效果与使用 select 实现的服务器相同。

相关推荐
XiaoCCCcCCccCcccC1 小时前
多路复用 select -- select 的介绍,select 的优缺点,select 版本的 TCP 回显服务器
服务器·c++
陈奕昆1 小时前
n8n实战营Day2:复杂逻辑控制·HTTP请求+条件分支节点实操
网络·人工智能·python·网络协议·n8n
h***04772 小时前
IEEE 1588:电信网络的精确时间协议 (PTP)
网络
虾..2 小时前
Linux 进程等待
linux·运维·服务器
小π军2 小时前
STL利器:upper_bound与lower_bound的使用
c++
Han.miracle2 小时前
JavaEE-- 网络编程 http请求报头
运维·服务器·网络·网络协议·计算机网络·http
鹿鸣天涯2 小时前
使用VMware Workstation 17虚拟机安装红帽企业版系统RHEL10
linux·运维·服务器
SKYDROID云卓小助手2 小时前
三轴云台之控制协同技术
服务器·网络·图像处理·人工智能·算法
Zx623653 小时前
13.泛型编程 STL技术
java·开发语言·c++