首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【网络编程】十六、多路转接之 poll

【网络编程】十六、多路转接之 poll

作者头像
利刃大大
发布于 2025-05-28 00:11:10
发布于 2025-05-28 00:11:10
12100
代码可运行
举报
文章被收录于专栏:csdn文章搬运csdn文章搬运
运行总次数:0
代码可运行

Ⅰ. select和poll的区别

selectpoll 都是 POSIX 标准规定的多路复用 IO 接口函数,它们都能够让程序同时监视多个文件描述符(如套接字、管道和文件等)的状态,并在有至少一个文件描述符就绪时通知应用程序进行相应的 IO 操作。

​ 尽管 selectpoll 都实现了多路复用 IO 的功能,但它们在一些细节上有所不同。具体来说,selectpoll 之间的区别主要体现在以下几个方面:

  1. API 设计:selectpollAPI 设计上略有不同,例如 select 使用 fd_set 类型的集合来传递要监听的文件描述符,而 poll 则使用 pollfd 结构体数组来传递文件描述符信息。
  2. 可扩展性select 的文件描述符集合大小通常由系统定义的 FD_SETSIZE 宏限制,而 poll 做到了没有这样的限制。因此,poll 的可扩展性更好,能够支持更多的文件描述符。
  3. 效率:select 的实现通常是使用轮询方式遍历整个描述符集合,当描述符集合中的某个描述符就绪时才返回;而 poll 的实现则使用链表管理描述符,只需要遍历已经就绪的描述符,因此 poll 的效率要稍微高一些。

​ 需要注意的是,虽然 poll 在某些方面比 select 更优秀,但它的可移植性不如 selectselect 已经成为了 POSIX 标准定义的接口,而 poll 目前还没有广泛采用,在某些操作系统上甚至不支持。

​ 而下面的学习中,我们也只是稍微的了解一下 poll 即可,最重要的还是后面我们要学的 epoll,所以下面的学习包括代码,我们是从之前写的 select 代码中进行修改的!

Ⅱ. poll函数

1、函数原型

poll 系统调用的功能和 select 类似,也是在指定时间内 轮询 一定数量的文件描述符,以测试其是否有就绪事件。

​ 其原型如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

fds

它是一个 pollfd 结构类型的数组,它指定所有我们感兴趣的文件描述符上发生的可读、可写、异常等事件。其结构体定义如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct pollfd 
{
    int   fd;         /* 文件描述符 */
    short events;     /* 用户设置的感兴趣的事件 */
    short revents;    /* 系统返回的就绪事件 */
};
  • fd 变量指定文件描述符。就像我们之前所学的 select 中的 _array[i]
  • events 变量告诉 poll 要监听 fd 上的哪些事件,它是一系列事件的按位或。
  • revents 变量则由内核填充,以通知应用层 fd 上实际发生了哪些事件。
  • 也就是说 我们不需要像 select 一样每次都要在调用前重新设置监听集合了,现在有了 pollfd 这个结构体中的两个事件变量我们就能单独对其操作而不需要每次都去重新设置!

其中 eventsrevents 的取值如下所示:

其中我们常用的就是 POLLINPOLLOUT 等事件!

  • 通常,应用层需要根据 recv 函数的返回值来区分套接字上接收到的是有效数据还是对方关闭连接的请求,并做对应的处理。不过从 linux2.6.17 开始,GNUpoll 函数增加了一个 POLLRDHUP 事件,它在套接字上接收到对方关闭连接的请求之后触发,这为我们区分上述两种情况提供了一种更简单的方式,但使用 POLLRDHUP 事件时,需要在代码最开始处定义 _GUN_SOURCE

nfds

指定被监听事件集合 fds 的大小。其类型 nfds_t 的定义如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
typedef unsigned long int nfds_t;

timeout

  • 表示函数的超时时间,单位是毫秒
    • timeout = -1,则 poll 函数将永远阻塞,直到某个事件发生。
    • timeout = 0,则 poll 函数将立刻返回。
    • timeout 为具体时间,则如果在该时间段内没有就绪事件的话,则会超时返回 0

返回值(与 select 返回值是一样的)

  • 小于 0,表示 poll 函数出错。
  • 等于 0,表示 poll 函数等待超时。
  • 大于 0,表示 poll 函数由于监听的文件描述符就绪而返回。

2、文件描述符的就绪条件

poll 函数和 select 函数的文件描述符就绪条件是一样的,这里直接复制:

可读 就绪条件:

  • 如果 socket 内核的接收缓冲区中的字节数,大于等于其低水位标记 SO_RCVLOWAT,此时可以无阻塞的读取该文件描述符,并且返回值大于 0
  • 在进行 TCP 通信时候,如果对方关闭连接,则该文件描述符返回 0
  • 监听的文件描述符上有 新的连接请求
  • 文件描述符中有未处理的错误,此时我们可以使用 getsockopt 来读取和清除该错误。

可写 就绪条件:

  • 如果 socket 内核的发送缓冲区中的可用字节数,大于等于低水标记 SO_SNDLOWAT,此时可以无阻塞的写该文件描述符,并且返回值大于 0
  • socket 的写操作被关闭的时候(比如 close 或者 shutdown)会触发 SIGPIPE 信号。
  • socket 使用 非阻塞 connect 连接成功或失败(超时)之后。
  • 文件描述符中有未读取的错误,此时我们可以使用 getsockopt 来读取和清除该错误。

异常 就绪条件:

  • 网络程序中,select 能处理的异常情况只有一种:socket 上接收到带外数据

3、poll的优点

poll 相对于 select 有如下优势:

  1. pollfd 结构包含了要监视的 events 和发生的 revents,也就是说 对于监听集合的输入输出是分离的,不需要我们在调用 poll 函数之前重新对参数进行设置,不再使用 select “参数-值” 传递的方式,接口使用比 select 更方便
  2. poll 函数的第二个参数就 解决了 select 函数中文件描述符的上限问题。(但是数量过大后性能也是会下降)

4、poll的缺点

poll 中监听的文件描述符数目增多时,其会有如下缺点:

  1. select 函数一样,poll 返回后,需要轮询 pollfd 来获取就绪的描述符。而同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。
  2. 每次调用 poll 都需要把大量的 pollfd 结构从用户态拷贝到内核中。

Ⅲ. 编写poll代码

​ 这里的代码准备,其实和之前我们写 select 是一样的,只不过需要改一下服务器的名称!具体的可以参考 select 笔记,这里最大的区别,是主服务器的头文件,所以下面我们只将服务器头文件的修改!

​ 首先是头文件的主体框架,我们使用数组来维护 struct pollfd 结构体,并且做一些初始化,其中 pollfd 数组的长度我们这里设为 4096,但其实这个长度是可以不受限制的,可以改为动态增容版的,但是这里就不这样子处理了!

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#pragma once
#include <iostream>
#include <poll.h>
#include "sock.hpp"

namespace poll_space
{
    const static int default_port = 8080;    // 服务器默认端口号
    const static int free_num = -1;          // 监听集合中空闲位置的默认值
    const static nfds_t pollfd_size = 4096;  // pollfd数组的长度

    class poll_server
    {
    private:
        int _listensock;
        int _port;
        struct pollfd* poll_array; // 存放pollfd结构体的数组
    public:
        poll_server(int port = default_port)
            : _port(port), _listensock(-1), poll_array(nullptr)
        {}
        ~poll_server()
        {
            if(_listensock != -1)
                close(_listensock);
            if(poll_array)
                delete[] poll_array;
        }

        void print() {}
        void Accepter() {}	      // 获取新链接函数
        void Receiver(int pos) {} // 处理交互函数
        void handler() {}		  // 获取新链接以及交互的处理函数
        
        void init() {} // 初始化服务器
        void run() {}  // 运行服务器
    };
}

​ 接着就是初始化 init 函数,就是创建套接字、绑定套接字、监听套接字,然后初始化 pollfd 数组,不要忘了最后还要将 _listensock 先给初始化到数组中去,因为它是来负责监听新链接的!

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void init()
{
    _listensock = sock::Socket();
    sock::Bind(_listensock, _port);
    sock::Listen(_listensock);

    // 初始化数组
    poll_array = new struct pollfd[pollfd_size];
    for(int i = 0; i < pollfd_size; ++i)
    {
        poll_array[i].fd = free_num;
        poll_array[i].events = 0; 
        poll_array[i].revents = 0;
    }
    poll_array[0].fd = _listensock;
    poll_array[0].events = POLLIN; // 设置为关心可读事件
}

run 运行函数就是调用 poll 函数去进行多路转接,这里我们设置超时时间为两秒,其返回值的操作和 select 是一样的,并且我们可以看出,poll 调用是比 select 要简洁的多的!

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void run()
{
    while(true)
    {
        int n = poll(poll_array, pollfd_size, 2000); // 2秒超时时间
        if(n == 0)
            logMessage(Level::NORMAL, "timeout...");
        else if(n == -1)
            logMessage(Level::ERROR, "poll error, code: %d, err string: %s", errno, strerror(errno));
        else
        {
            // 说明有事件就绪了
            logMessage(Level::NORMAL, "有事件就绪了,一共有%d个", n);
            handler();
            sleep(1);
        }
    }
}

​ 然后就是处理函数 handler() 了,和 select 一样,需要遍历处理数组中已经维护文件描述符判断是否有需要处理的就绪事件,这也可以看出来 poll 其实没有改善这个需要遍历集合的缺点

​ 接着筛选掉不符合的文件描述符,剩下的就判断是否为 _listensock,是的话让其执行 Accepter() 获取新链接,不是的话执行 Receiver() 进行交互处理。

​ 只是在筛选的过程,和 select 不太一样,我们需要对 eventsrevents 进行按位与操作,来看看是否为 POLLIN 可读事件(我们这里只关心可读事件),如果不是的话,则过滤掉!

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void handler()
{
    // 和select一样,需要遍历处理数组中已经维护文件描述符判断是否有需要处理的就绪事件
    for(int i = 0; i < pollfd_size; ++i)
    {
        // 过滤掉不符合的fd,即不存在、events不是可读事件、revents没就绪,则过滤掉
        if (poll_array[i].fd == free_num || 
        !(poll_array[i].events & POLLIN) || 
        !(poll_array[i].revents == POLLIN))
            continue;

        // 走到这里一定是一个存在且就绪的可读事件!
        if(poll_array[i].fd == _listensock)
            Accepter();
        else
            Receiver(i);
    }
}

​ 接下来就是 Accepter() 函数,其实大体思路和 select 都是一样的,只不过修改了几个变量,并且在设置新连接进数组的时候,别忘了将其 events 设置为 POLLIN 可读事件,其它的没啥需要关注的!

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void print()
{
    std::cout << "当前数组中的fd有:";
    for(int i = 0; i < pollfd_size; ++i)
        if(poll_array[i].fd != free_num)
            std::cout << poll_array[i].fd << " ";
    std::cout << std::endl;
}

void Accepter()
{
    // 1. 获取新链接
    std::string clientip;
    uint16_t clientport;
    int newfd = sock::Accept(_listensock, &clientip, &clientport);
    if(newfd < 0)
        exit(ACCEPT_ERR);

    // 2. 将新链接维护到数组中
    // 2.1 首先找到数组中空闲的位置
    int index = 0;
    for(; index < pollfd_size; ++index)
        if(poll_array[index].fd == free_num)
            break;

    // 2.2 若没有空闲位置,则关闭新链接,并且返回
    if(index == pollfd_size)
    {
        close(newfd);
        logMessage(ERROR, "数组中没有空闲位置,无法建立新链接!"); // 其实也可以搞成动态数组,这里就不弄了
        return;
    }

    // 2.3 找到空闲位置则直接设置进数组即可,顺便打印一下数组的内容
    poll_array[index].fd = newfd;
    poll_array[index].events = POLLIN;
    poll_array[index].revents = 0;
    print();
}

Receiver() 函数也是一样的,修改几个变量就搞定了,并且在关闭连接同时记得将文件描述符从数组中去除!

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void Receiver(int pos)
{
    // 1. 读取数据
    // 目前我们不做自定义协议,当前我们认为能接收到一个完整的报文
    char buffer[1024];
    memset(buffer, 0, sizeof buffer);
    ssize_t n = recv(poll_array[pos].fd, buffer, sizeof(buffer) - 1, 0);
    if(n > 0)
    {
        buffer[n] = 0;
        logMessage(NORMAL, "接收内容:%s", buffer);
    }
    else if(n == 0)
    {
        // 关闭同时记得将文件描述符从数组中去除
        close(poll_array[pos].fd);
        poll_array[pos].fd = free_num;
        poll_array[pos].events = poll_array[pos].revents = 0;
        logMessage(NORMAL, "客户端关闭连接");
        return;
    }
    else
    {
        // 关闭同时记得将文件描述符从数组中去除
        close(poll_array[pos].fd);
        poll_array[pos].fd = free_num;
        poll_array[pos].events = poll_array[pos].revents = 0;
        logMessage(ERROR, "读写失败,错误码:%d,错误原因:%s", errno, strerror(errno));
        return;
    }

    // 2. 业务处理(这里就不演示了,到后面epoll一起讲)
    // 3. 响应数据,这里直接返回读取的数据
    std::string response = std::string("响应: ") + std::string(buffer);
    send(poll_array[pos].fd, response.c_str(), response.size(), 0);
}
运行结果

​ 至此 poll 服务器的头文件就写完啦,大体思路可以发现和 select 是一样的,只不过调用方式需要修改!下面我们来运行一下代码:

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-05-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
LV.1
这个人很懒,什么都没有留下~
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档