跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像AI 生图工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++

Poll 函数原理与 TCP 服务器实现详解

Poll 函数针对 select 的文件描述符数量限制和参数耦合问题进行了改进,通过 pollfd 结构体数组管理 fd,支持更多连接数且无需每次调用前重置。深入解析了 poll 的核心参数、事件宏定义及返回值处理,并提供了基于 C++ 的完整 TCP 服务器代码示例,涵盖监听 socket 初始化、事件循环及客户端连接管理。对比 select 与 poll 的差异后,分析了轮询机制在中低并发场景下的适用性,为后续学习 epoll 奠定基础。

极客零度发布于 2025/10/30更新于 2026/7/2537 浏览
Poll 函数原理与 TCP 服务器实现详解

Poll 函数原理与 TCP 服务器实现详解

select 作为早期 I/O 多路复用技术,存在文件描述符数量上限和参数耦合两个核心缺陷。poll 函数针对这些问题做了改进,突破 fd 限制并分离输入输出参数,但轮询本质未变。

一、Poll 函数核心解析

poll 的功能与 select 一致:监视并等待多个文件描述符的状态变化。区别在于管理方式更灵活。

1.1 核心参数:struct pollfd 数组

select 使用位图(fd_set)管理 fd,而 poll 通过 pollfd 结构体数组管理,每个结构体对应一个待监视的 fd 及其事件。

pollfd 结构体定义
struct pollfd {
    int fd;             // 待监视的文件描述符
    short events;       // 输入参数:用户要监视的事件(位掩码)
    short revents;      // 输出参数:内核返回的就绪事件(位掩码)
}; 
关键事件宏定义

events(输入)和 revents(输出)均通过位掩码表示事件,支持多种事件的组合(用 | 运算符)。

事件宏含义(events 输入 /revents 输出)对应 select 功能
POLLIN有数据可读(普通 / 优先数据)读事件(FD_SET 读集合)
POLLOUT写操作不会阻塞(普通 / 优先数据)写事件(FD_SET 写集合)
POLLERR输出专属:fd 发生错误异常事件
POLLHUP输出专属:fd 发生挂起(如客户端断开连接)异常事件
POLLNVAL输出专属:fd 非法(如未打开)异常事件

使用规则:

  • events 仅能设置'输入事件'(如 POLLIN、POLLOUT),设置 POLLERR 等输出事件无意义;
  • revents 由内核填充,可能包含 events 中的事件,也可能包含 POLLERR 等异常事件;
  • 事件判断示例:判断 fd 是否可读,需检查 revents & POLLIN(非 0 则就绪)。

1.2 参数二:nfds(数组大小)

nfds 表示 pollfd 数组中有效元素的数量,告诉内核需要遍历的结构体数量,避免访问越界。

1.3 参数三:timeout(超时时间)

timeout 指定 poll 的阻塞时长,单位为毫秒,是纯输入参数。

timeout 取值含义
-1无限阻塞,直到有 fd 就绪或被信号中断
0非阻塞模式,立即返回
>0
阻塞 timeout 毫秒,超时后返回

1.4 返回值处理

poll 的返回值直接反映调用结果:

返回值含义后续操作
-1调用失败检查 errno
0超时无需处理 fd,可重新调用
>0就绪 fd 的数量遍历数组,处理 revents 非 0 的 fd

1.5 简单示例:监视文件可读

以下代码演示如何用 poll 监视一个文件是否可读,超时时间 5 秒:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>

int main() {
    int fd = open("test.txt", O_RDONLY);
    if (fd < 0) {
        perror("Failed to open file");
        return EXIT_FAILURE;
    }

    struct pollfd fds[1];
    fds[0].fd = fd;
    fds[0].events = POLLIN;
    fds[0].revents = 0;

    int ret = poll(fds, 1, 5000); // 超时 5 秒
    if (ret == -1) {
        perror("poll failed");
        close(fd);
        return EXIT_FAILURE;
    } else if (ret == 0) {
        printf("No data within 5 seconds\n");
    } else if (fds[0].revents & POLLIN) {
        char buf[1024] = {0};
        ssize_t bytes_read = read(fd, buf, sizeof(buf) - 1);
        if (bytes_read > 0) {
            printf("Read %zd bytes: %s\n", bytes_read, buf);
        }
    }
    close(fd);
    return EXIT_SUCCESS;
}

1.6 Select 与 Poll 的核心差异

对比维度selectpoll
fd 数量限制有(默认 1024)无(仅受系统资源约束)
参数耦合性输入输出耦合输入输出分离
效率(fd 多时)低略高
异常事件处理需单独监视自动在 revents 返回

二、Poll 版 TCP 服务器实现

poll 版 TCP 服务器的逻辑与 select 版类似,核心差异在于用 pollfd 数组管理 fd,而非 fd_set 位图。

2.1 整体设计思路

  1. 初始化监听 socket:创建、绑定、监听端口;
  2. 管理 pollfd 数组:将监听 socket 加入数组,设置监视事件(POLLIN);
  3. 循环调用 poll:阻塞等待 fd 就绪,处理超时、错误、就绪三种情况;
  4. 事件处理:监听 socket 就绪接受新连接,通信 socket 就绪读取数据或处理断开。

2.2 完整代码实现

1. 工具类:Socket.hpp

封装了 socket 的基本操作,包括创建、绑定、监听和接受连接。

#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <errno.h>

enum { SocketErr = 2, BindErr, ListenErr };
const int backlog = 10;

class Sock {
public:
    Sock() : sockfd_(-1) {}
    ~Sock() {}

    void Socket() {
        sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd_ < 0) {
            perror("socket error");
            exit(SocketErr);
        }
        int opt = 1;
        setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    }

    void Bind(uint16_t port) {
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;
        if (bind(sockfd_, (struct sockaddr*)&local, sizeof(local)) < 0) {
            perror("bind error");
            exit(BindErr);
        }
    }

    void Listen() {
        if (listen(sockfd_, backlog) < 0) {
            perror("listen error");
            exit(ListenErr);
        }
    }

    int Accept(std::string* clientip, uint16_t* clientport) {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int newfd = accept(sockfd_, (struct sockaddr*)&peer, &len);
        if (newfd < 0) {
            perror("accept error");
            return -1;
        }
        char ipstr[64] = {0};
        inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));
        *clientip = ipstr;
        *clientport = ntohs(peer.sin_port);
        return newfd;
    }

    void Close() {
        if (sockfd_ != -1) {
            close(sockfd_);
            sockfd_ = -1;
        }
    }

    int Fd() const { return sockfd_; }

private:
    int sockfd_;
};
2. 服务器类:PollServer.hpp

核心逻辑,维护 pollfd 数组并处理事件循环。

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

const uint16_t default_port = 8877;
const std::string default_ip = "0.0.0.0";
const int default_fd = -1;
const int fd_num_max = 64;
const int non_event = 0;

class PollServer {
public:
    PollServer(uint16_t port = default_port, const std::string& ip = default_ip)
        : port_(port), ip_(ip) {
        for (int i = 0; i < fd_num_max; ++i) {
            event_fds_[i].fd = default_fd;
            event_fds_[i].events = non_event;
            event_fds_[i].revents = non_event;
        }
    }

    ~PollServer() { listensock_.Close(); }

    bool Init() {
        listensock_.Socket();
        listensock_.Bind(port_);
        listensock_.Listen();
        std::cout << "Server init success! Port: " << port_ << std::endl;
        return true;
    }

    void Start() {
        int listen_fd = listensock_.Fd();
        event_fds_[0].fd = listen_fd;
        event_fds_[0].events = POLLIN;
        int timeout = 3000;

        while (true) {
            int ret = poll(event_fds_, fd_num_max, timeout);
            switch (ret) {
                case -1:
                    perror("poll error");
                    break;
                case 0:
                    std::cout << "Poll timeout (3s)..." << std::endl;
                    break;
                default:
                    std::cout << "Found " << ret << " ready fd(s)!" << std::endl;
                    HandlerEvent();
                    break;
            }
        }
    }

private:
    void HandlerEvent() {
        for (int i = 0; i < fd_num_max; ++i) {
            int fd = event_fds_[i].fd;
            if (fd == default_fd) continue;

            if (event_fds_[i].revents & (POLLIN | POLLERR | POLLHUP)) {
                if (fd == listensock_.Fd()) {
                    Accept();
                } else {
                    Receiver(fd, i);
                }
            }
        }
    }

    void Accept() {
        std::string client_ip;
        uint16_t client_port;
        int new_fd = listensock_.Accept(&client_ip, &client_port);
        if (new_fd < 0) return;

        int i = 1;
        for (; i < fd_num_max; ++i) {
            if (event_fds_[i].fd == default_fd) break;
        }

        if (i == fd_num_max) {
            std::cout << "Server full! Close new connection." << std::endl;
            close(new_fd);
        } else {
            event_fds_[i].fd = new_fd;
            event_fds_[i].events = POLLIN;
            event_fds_[i].revents = non_event;
            std::cout << "New connection: " << client_ip << ":" << client_port << std::endl;
            PrintOnlineFds();
        }
    }

    void Receiver(int fd, int idx) {
        char buf[1024] = {0};
        ssize_t n = read(fd, buf, sizeof(buf) - 1);
        if (n > 0) {
            std::cout << "Received from fd " << fd << ": " << buf << std::endl;
        } else if (n == 0) {
            std::cout << "Client fd " << fd << " disconnected" << std::endl;
            close(fd);
            event_fds_[idx].fd = default_fd;
            PrintOnlineFds();
        } else {
            perror(("Read error on fd " + std::to_string(fd)).c_str());
            close(fd);
            event_fds_[idx].fd = default_fd;
            PrintOnlineFds();
        }
    }

    void PrintOnlineFds() {
        std::cout << "Online fds: ";
        for (int i = 0; i < fd_num_max; ++i) {
            if (event_fds_[i].fd != default_fd) {
                std::cout << event_fds_[i].fd << " ";
            }
        }
        std::cout << std::endl;
    }

private:
    uint16_t port_;
    std::string ip_;
    Sock listensock_;
    struct pollfd event_fds_[fd_num_max];
};
3. 主函数:Main.cc
#include "PollServer.hpp"
#include <memory>

int main() {
    std::unique_ptr<PollServer> server(new PollServer(8877));
    if (!server->Init()) {
        std::cerr << "Server init failed!" << std::endl;
        return 1;
    }
    server->Start();
    return 0;
}
4. 编译脚本:Makefile
# 生成服务器可执行文件
poll_server: main.cc
	g++ -o $@ $^ -std=c++11 -Wall

.PHONY: clean
clean:
	rm -rf poll_server

2.3 测试与运行

编译运行后,可用 telnet 或 nc 连接本地服务器测试。

telnet 127.0.0.1 8877

若 3 秒无事件,服务器会打印超时信息;有新连接时会更新在线 fd 列表。

三、Poll 的优缺点分析

3.1 优点

  1. 突破 fd 数量限制:pollfd 数组大小由用户自定义,无 select 的 1024 上限;
  2. 输入输出分离:events 和 revents 分离,无需每次调用前重新初始化;
  3. 异常事件自动返回:内核会在 revents 中自动标记 POLLERR、POLLHUP 等异常;
  4. fd 效率更高:仅遍历有效 pollfd 数组,fd 值较大时优于 select。

3.2 缺点

  1. 轮询机制效率低:需遍历所有监视的 fd,当 fd 数量庞大时效率线性下降;
  2. 用户态需维护数组:需手动管理 pollfd 数组,代码复杂度略高于 select;
  3. 数据拷贝开销:每次调用 poll 时,数组需从用户态拷贝到内核态;
  4. 无事件驱动机制:无法像 epoll 那样主动通知,高并发场景下性能不足。

四、总结

poll 是 select 的改进版,核心解决了 fd 数量上限和参数耦合问题,但其轮询本质未变,仍适用于中低并发场景。若需处理高并发,建议使用 Linux 特有的 epoll 技术。理解 poll 的设计逻辑,也为后续学习 epoll 的优势奠定基础。

目录

  1. Poll 函数原理与 TCP 服务器实现详解
  2. 一、Poll 函数核心解析
  3. 1.1 核心参数:struct pollfd 数组
  4. pollfd 结构体定义
  5. 关键事件宏定义
  6. 1.2 参数二:nfds(数组大小)
  7. 1.3 参数三:timeout(超时时间)
  8. 1.4 返回值处理
  9. 1.5 简单示例:监视文件可读
  10. 1.6 Select 与 Poll 的核心差异
  11. 二、Poll 版 TCP 服务器实现
  12. 2.1 整体设计思路
  13. 2.2 完整代码实现
  14. 1. 工具类:Socket.hpp
  15. 2. 服务器类:PollServer.hpp
  16. 3. 主函数:Main.cc
  17. 4. 编译脚本:Makefile
  18. 生成服务器可执行文件
  19. 2.3 测试与运行
  20. 三、Poll 的优缺点分析
  21. 3.1 优点
  22. 3.2 缺点
  23. 四、总结
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 为什么从 C# 转 Java 开发时常有落差感
  • Python 打造 AI 三剑客:文档总结、代码生成与智能检索
  • 前端高频面试题:TypeScript 篇
  • Python 数据分析入门:集中趋势与离散程度
  • Stable Diffusion 文生图基础详解
  • 低代码结合大模型:中小企业半天构建专属 SaaS 应用路径
  • WebSocket 与 MQTT 即时通讯协议对比与架构选型指南
  • Windows 系统 Git 安装与配置实战指南
  • Jupyter Notebook 安装与配置完整指南
  • 本地部署开源语言大模型 Llama3 及中文微调版教程
  • MCP 工具速成:npx vs. uvx 全流程安装指南
  • AI 生图去水印实用指南:5 种免费方法获取纯净原图
  • LoRA 训练入门:如何定制专属 AI 绘画模型
  • Python 装饰器核心原理解析
  • 漏洞扫描工具 AWVS 介绍及安装教程
  • Synfig Studio 入门指南:免费开源 2D 动画软件
  • 基于 LangChain 实现数据库问答机器人
  • OpenCode 开源 AI 编程助手使用教程
  • 树莓派智能家居毕设:AI 辅助开发与边缘推理实战
  • OpenClaw WebSocket 通道开发实战:构建自定义 AI 通信通道

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online