欢迎光临
我们一直在努力

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

请添加图片描述

半桔:个人主页

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

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


文章目录

  • 前言
  • 一. poll的接口
  • 二. poll服务器实现
    • 2.1 对网络套接字进行封装
    • 2.2 构建poll类
    • 2.3 进行初始化
    • 2.4 对任务进行派发
    • 2.5 服务器主循环
  • 三. 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是操作系统内提供的一个数据结构,用来存储要进行管理的相关信息:

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

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

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

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

    2.1 对网络套接字进行封装

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

    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类

  • 设置一个上述网络套接字的类,对网络套接字接口进行封装;
  • 设置一个数组来管理每一个要进行等待的文件描述符,此处可以直接使用struct pollfd。
  • 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 进行初始化

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

  • 创建套接字;
  • 绑定;
  • 设置监听模式;
  • 将网络套接字加入到等待数组中。
  • 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函数对任务进行派发:

  • 遍历整个_fds_array数组,找文件描述符已经就绪的位置;
  • 判断对应的文件描述符是不是套接字;
  • 是套接字将建立好的连接拿上来;
  • 是普通文件描述符就对缓冲区进行读写操作。
  • 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 服务器主循环

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

    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相比:

  • poll等待的文件描述符的个数是没有限制的;
  • poll将输入型参数与输出型参数进行分离,使得用户使用的时候不需要每次都进行设置。
  • 但是与select一样,两者都需要对这个数组进行遍历进行检查,对于无效位置也要进行遍历,因此我们可以使用epoll进行优化。

    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的讲解,来解决poll和select存在的问题。

    赞(0)
    未经允许不得转载:171主机测评 » 【IO多路转接】深入解析 poll:从接口到服务器实现
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址