【IO多路转接】深入解析 poll:从接口到服务器实现

半桔个人主页
🔥 个人专栏 : 《IO多路转接》《手撕面试算法》《C++从入门到入土》

🔖当你最认为困难的时候,其实就是你最接近成功的时候。《当幸福来敲门》


文章目录

  • 前言
  • [一. poll的接口](#一. poll的接口)
  • [二. poll服务器实现](#二. poll服务器实现)
    • [2.1 对网络套接字进行封装](#2.1 对网络套接字进行封装)
    • [2.2 构建poll类](#2.2 构建poll类)
    • [2.3 进行初始化](#2.3 进行初始化)
    • [2.4 对任务进行派发](#2.4 对任务进行派发)
    • [2.5 服务器主循环](#2.5 服务器主循环)
  • [三. poll相对于select的优势](#三. poll相对于select的优势)

前言

在高性能网络编程领域,IO 多路复用 是应对高并发场景的核心技术之一 ------ 它允许程序同时监控多个文件描述符(File Descriptor)的状态变化,从而高效处理多客户端的网络 IO 请求,解决了传统阻塞 IO 在高并发下效率低下的问题。poll 作为 IO 多路复用的经典实现机制,在 Linux 等操作系统中被广泛应用,是理解高并发服务器设计的重要基础。

本文将围绕 poll 展开,从技术原理到实践实现,逐步讲解如何基于 poll 构建高效的网络服务器。首先会剖析 poll 的核心接口与工作机制,为后续实践打下理论基础;随后聚焦于服务器的具体实现:从网络套接字的封装入手,逐步完成 poll 类的构建、初始化流程设计、任务派发策略,以及服务器主循环的实现(这部分是服务器 "持续工作" 的核心逻辑);最后,还会对比 poll 与更早出现的 select 机制,阐释 poll 在技术上的优势与改进。

通过本文的讲解,希望读者能深入理解 poll 的工作逻辑,并掌握基于 poll 开发高并发服务器的方法,为后续探索更复杂的网络编程技术(如 epoll)奠定基础。

一. poll的接口

int poll(struct pollfd *fds , nfds_t nfds , int timeuot)

struct pollfd是操作系统内提供的一个数据结构,用来存储要进行管理的相关信息:

cpp 复制代码
struct pollfd {
    int fd;              // 要进行等待的文件描述符
    short events;        // 要进行等待的事件,是读事件,写事件还是什么
    short revents;       // 输出型参数,告诉用户那些事件已经就绪了
};

其中的fd可以设置为-1,表示该pollfd操作系统不需要进行处理。

  1. 参数一fds:告诉操作系统要对那些文件进行等待;
  2. 参数二nfds:一共要进行等待的文件个数;
  3. 参数三timeout:设置时间,时间到了/有事件就绪就返回;
  4. 返回值:表示有多少个事件已经就绪了,-1表示不进行等待。

二. poll服务器实现

此处我们仅仅是对poll服务器进行一个简单的实现,使用以下对应的接口,我们假设TCP接收时接收到的是一个完整的报文。

2.1 对网络套接字进行封装

首先我们先对网络套接字的接口进行封装:创建套接字,绑定,监听;关于这方面的知识可以查看之前的TCP相关内容,此时就直接贴实现方法:

cpp 复制代码
const std::string defaultip_ = "0.0.0.0";
enum SockErr
{
    SOCKET_Err,
    BIND_Err,
};

class Sock
{
public:
    Sock(uint16_t port)
        : port_(port),
          listensockfd_(-1)
    {
    }
    void Socket()
    {
        listensockfd_ = socket(AF_INET, SOCK_STREAM, 0);
        if (listensockfd_ < 0)
        {
            Log(Fatal) << "socket fail";
            exit(SOCKET_Err);
        }
        Log(Info) << "socket sucess";
    }
    void Bind()
    {
        struct sockaddr_in server;
        server.sin_family = AF_INET;
        server.sin_port = htons(port_);
        inet_pton(AF_INET, defaultip_.c_str(), &server.sin_addr);
        if (bind(listensockfd_, (struct sockaddr *)&server, sizeof(server)) < 0)
        {
            Log(Fatal) << "bind fail";
            exit(BIND_Err);
        }
        Log(Info) << "bind sucess";
    }
    void Listen()
    {
        if (listen(listensockfd_, 10) < 0)
        {
            Log(Warning) << "listen fail";
        }
        Log(Info) << "listen sucess";
    }
    int Accept()
    {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        int fd = accept(listensockfd_ , (sockaddr*)&client , &len);
        if(fd < 0)
        {
            Log(Warning) << "accept fail";
        }
        return fd;
    }
    int Accept(std::string& ip , uint16_t& port)
    {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        int fd = accept(listensockfd_ , (sockaddr*)&client , &len);
        if(fd < 0)
        {
            Log(Warning) << "accept fail";
        }

        port = ntohs(client.sin_port);
        char bufferip[64];
        inet_ntop(AF_INET , &client.sin_addr , bufferip , sizeof(bufferip) - 1);
        ip = bufferip;

        return fd;
    }
    int Get_fd()
    {
        return listensockfd_;
    }
    ~Sock()
    {
        close(listensockfd_);
    }

private:
    uint16_t port_;
    int listensockfd_;
};

2.2 构建poll类

  1. 设置一个上述网络套接字的类,对网络套接字接口进行封装;
  2. 设置一个数组来管理每一个要进行等待的文件描述符,此处可以直接使用struct pollfd
cpp 复制代码
const int defaultfd = -1;
class Pollserver
{
    static const int fds_array_num = 1024;         // 设置默认要进行等待的数组长度
public:
    Pollserver(uint port)
        :_sock_ptr(new Sock(port))
    {
        for(int i = 0 ; i < fds_array_num ; i++)
        {
            _fds_array[i].fd = defaultfd;             
        }
    }

private:
    std::shared_ptr<Sock> _sock_ptr;               // 套接字结构体
    struct pollfd _fds_array[fds_array_num];       // 存储所有文件描述符相关信息
};

2.3 进行初始化

对于服务器的初始化,我们只进行一些简单的操作:

  1. 创建套接字;
  2. 绑定;
  3. 设置监听模式;
  4. 将网络套接字加入到等待数组中。
cpp 复制代码
    void AddToArray(int fd , short events)
    {
        // 1. 找空位置
        // 2. 加入fd

        int pos = 0;
        for(; pos < fds_array_num && _fds_array[pos].fd != -1 ; pos++) 
        ;

        if(pos == fds_array_num)
        {
            // 数组不够了
            // 1. 打印日志信息
            // 2. 关闭文件描述符

            Log(Warning) << "array is full";
            close(fd);
        }
        else
        {
            // 加入fd
            _fds_array[pos].fd = fd;
            _fds_array[pos].events = events;
            Log(Info) << "get a connect  , fd : " << fd; 
        }
    }

    void Init()
    {
        // 1. 创建套接字
        // 2. 进行绑定
        // 3. 设置监听模式
        // 4. 将网络套接字加到_fds_array数组中

        _sock_ptr->Socket();
        _sock_ptr->Bind();
        _sock_ptr->Listen();

        AddToArray(_sock_ptr->Get_fd() , POLLIN);
    }

2.4 对任务进行派发

poll进行等待的时候有文件描述符读写事件就绪,我们就需要进行处理。

此时我们使用一个Dispatcher函数对任务进行派发:

  1. 遍历整个_fds_array数组,找文件描述符已经就绪的位置;
  2. 判断对应的文件描述符是不是套接字;
  3. 是套接字将建立好的连接拿上来;
  4. 是普通文件描述符就对缓冲区进行读写操作。
cpp 复制代码
    void Sockfd_Ready()
    {
        int listensock = _sock_ptr->Get_fd();
        int newfd = _sock_ptr->Accept();
        AddToArray(newfd , POLLIN);
    }

    void Normalfd_Ready(int fd  , int pos)
    {
        char buffer[1024];
        int n = read(fd  , buffer , sizeof(buffer) - 1);
        if(n > 0)
        {
            buffer[n] = 0;
            std::string ret = "server get a message : ";;
            ret += buffer;
            write(fd  , ret.c_str() , ret.size());
        }
        else if(n == 0)
        {
            // 对方断开连接了
            
            // 1. 将文件描述符从等待的队列中移除
            // 2. 关闭文件

            _fds_array[pos].fd = -1;

            close(fd);
        }
        else
        {
            // 出错了, 打印日志信息
            Log(Warning) << "read  fail";
        }
    }


    void Dispatcher()
    {
        int listensock = _sock_ptr->Get_fd();
        for(int i = 0 ; i < fds_array_num ; i++)
        {
            int fd = _fds_array[i].fd;
            short eventds = _fds_array[i].revents;
            if(fd == defaultfd || !(eventds & POLLIN)) continue;
            
            if(fd == listensock) 
            {
                Sockfd_Ready();
            }
            else
            {
                Normalfd_Ready(fd , i);
            }
        }
    }

2.5 服务器主循环

服务器的主循环只需要进行等待即可:

cpp 复制代码
    void Run()
    {
        while(1)
        {
            int n = poll(_fds_array , fds_array_num , -1);
            if(n > 0)
            {
                Dispatcher();
            }
            else if(n == 0)
            {
                Log(Info) << " no file is ready";
            }
            else
            {
                Log(Error) << "poll fail";
            }
        }
    }

以上就是整个pollserver服务器类的整个实现逻辑了。

三. poll相对于select的优势

与select相比:

  1. poll等待的文件描述符的个数是没有限制的;
  2. poll将输入型参数与输出型参数进行分离,使得用户使用的时候不需要每次都进行设置。

但是与select一样,两者都需要对这个数组进行遍历进行检查,对于无效位置也要进行遍历,因此我们可以使用epoll进行优化。

cpp 复制代码
class Pollserver
{
    static const int fds_array_num = 1024;         // 设置默认要进行等待的数组长度
public:
    Pollserver(uint port)
        :_sock_ptr(new Sock(port))
    {
        for(int i = 0 ; i < fds_array_num ; i++)
        {
            _fds_array[i].fd = -1;             
        }
    }

private:
    std::shared_ptr<Sock> _sock_ptr;               // 套接字结构体
    struct pollfd _fds_array[fds_array_num];       // 存储所有文件描述符相关信息
};

后续我们将进行epoll的讲解,来解决pollselect存在的问题。

相关推荐
xx.ii4 小时前
k8s:service资源详解
运维·网络·容器·kubernetes
hello_2504 小时前
Shell脚本高效编写技巧
运维·shell
Dovis(誓平步青云)4 小时前
《静态库与动态库:从编译原理到实战调用,一篇文章讲透》
linux·运维·开发语言
不开心就吐槽4 小时前
linux安装kafka
linux·运维·kafka
孙同学要努力4 小时前
《Linux篇》进程等待(wait、waitpid)与进程程序替换(exec等接口)
linux·服务器·网络
柯衍ky4 小时前
Mac通过命令行开启ssh服务
运维·macos·ssh
_w_z_j_4 小时前
Linux----进程控制
linux·运维·服务器
2501_938963964 小时前
服务器数据库安全:MySQL 开启 SSL 连接,禁止远程 root 登录并限制 IP 访问
服务器·mysql·ssl
Icoolkj4 小时前
手机物理内存云共享—技术挑战与突破路径分析
java·服务器·智能手机