【高级IO-3】I/O多路转接 之 poll【概念及代码实例】

前言

在学习poll与epoll前,需要先了解select:

I/O 多路转接之 select

I/O 多路转接 之 poll

1. poll 函数原型

poll 函数-用于检查多个文件描述符的状态的函数,其原型如下:

#include <poll.h>

int poll(struct pollfd fds[], nfds_t nfds, int timeout);
  • struct pollfd fds[]:是一个数组,用于指定要检查的文件描述符及其感兴趣的事件。
  • nfds_t nfds:是 fds 数组中元素的数量。
  • int timeout:是超时时间,以毫秒为单位。如果设置为 -1poll 函数将一直阻塞直到有事件发生;如果设置为 0poll 函数将立即返回,否则 poll 函数将等待指定的时间后返回。

struct pollfd 结构体定义如下:

struct pollfd {
    int   fd;         /* 文件描述符 */
    short events;     /* 感兴趣的事件 */
    short revents;    /* 实际发生的事件 */
};
  • fd:是要检查的文件描述符。
  • events:是要监视的事件,可以是以下值的位掩码:
    • POLLIN:数据可读。
    • POLLOUT:数据可写。
    • POLLPRI:紧急数据可读。
    • POLLERR:发生错误。
    • POLLHUP:挂起事件。
    • POLLNVAL:文件描述符不是一个打开的文件。
  • revents:是 poll 函数返回时设置的实际发生的事件,它是 events 的子集。

poll 函数会在指定的一组文件描述符上等待指定的事件发生,并将发生的事件保存在 revents 中。然后你可以检查每个文件描述符的 revents 字段来确定哪些事件发生了。

下面是events和revents的取值:

事件含义
POLLIN文件描述符中有数据可读取。
POLLPRI文件描述符中有紧急数据可读取。
POLLOUT文件描述符可以写入数据。
POLLERR文件描述符发生错误。
POLLHUP文件描述符挂起连接(例如,套接字关闭)。
POLLNVAL文件描述符不是一个打开的文件。
POLLWRNORM文件描述符可以普通写入数据。
POLLWRBAND文件描述符可以带外写入数据。
POLLRDHUP文件描述符挂起连接的一端关闭(例如,TCP连接的远程端关闭了连接的写半部)。

返回结果

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

2. Socket 就绪条件

在使用套接字(Socket)进行网络编程时,套接字的就绪条件表示可以进行某种操作的条件,主要用于异步 I/O 操作,例如通过 selectpollepoll 等函数实现的多路复用。

套接字的就绪条件通常包括以下几种:

  1. 读就绪(Read Ready):套接字缓冲区中有数据可供读取,即接收缓冲区中有数据到达,可以调用 recv 函数读取数据。

  2. 写就绪(Write Ready):套接字缓冲区有足够的空间可以写入数据,即发送缓冲区有足够的空间可以发送数据,可以调用 send 函数写入数据。

  3. 异常就绪(Exception Ready):套接字发生异常情况,如带外数据到达或者连接错误。这通常通过 select 或者 poll 函数的异常集合来检查。

  4. 连接就绪(Connection Ready):套接字连接已经建立,可以进行数据交换。对于服务器套接字来说,连接就绪表示已经有客户端连接请求到达,可以调用 accept 函数接受连接。对于客户端套接字来说,连接就绪表示连接成功建立,可以进行数据交换。

这些就绪条件可以通过多路复用函数(如 selectpollepoll
等)来监视,当套接字处于就绪状态时,这些函数会通知应用程序执行相应的操作。


3. poll 与 select 的不同(优缺点)

poll 与 select 大体还是很相似的,主要的优点在于其没有了文件描述符数量限制,下面还是列举给出poll的优缺点:

优点:

  • 效率高
  • 有大量连接,只有少量活跃
  • 输入输出参数分离,不需要大量重置
  • 没有文件描述符数量的限制(select 的fd最多是1024个,受限于fd_set)

缺点:

  • poll依然需要大量遍历——在用户层检测事件就绪,在内核层检测fd就绪
    • 用户依然需要维护数组
    • 对于这点,下面会更进一步:epoll
  • 依然需要“内核到用户,用户到内核”的拷贝
  • 代码依然复杂(比select简洁一些:接口简洁性)

4. poll代码实例 - poll服务器

poll代码总体与select差别不大,其他文件都一致(Sock.hpp、Log.hpp)这里直接对Poll服务器代码进行编写:

成员变量:

private:
    uint16_t _port; // 端口号
    int _listensock; // 监听套接字
    struct pollfd* _fds; // pollfd数组
    int _nfds; // 设置的最大fd数量
    int _timeout; // 设置超时时间(阻塞等待)

构造函数与析构函数:

public:
    static const int nfds = 100; // 最大文件描述符个数
public:
    // 构造:绑定并监听套接字
    PollServer(const uint16_t port = 8080): _port(port), _nfds(nfds)
    {
        // 绑定监听
        _listensock = Sock::Socket();
        Sock::Bind(_listensock, _port);
        Sock::Listen(_listensock);
        logMessage(NORMAL, "%s", "create base socket success.");

        // 初始化pollfd
        _fds = new struct pollfd[_nfds];
        for(int i = 0; i < _nfds; ++i)
        {
            _fds[i].fd = FD_NONE;
            _fds[i].events = 0;
            _fds[i].revents = 0;
        }
        _fds[0].fd = _listensock;
        _fds[0].events = POLLIN;

        _timeout = 1000;
    }

    ~PollServer()
    {
        // 关闭套接字
        if(_listensock != FD_NONE)
            close(_listensock);
        // 释放资源
        if(_fds) delete []_fds;
    }

启动函数:

void Start()
{
    DebugPrint();
    while(true)
    {
        int n = poll(_fds, _nfds, _timeout); // 等待事件
        switch(n)
        {
        case 0: // 超时
            logMessage(DEBUG, "%s", "time out...");
            break;
        case -1:
            logMessage(WARNING, "poll error | %d : %s", errno, strerror(errno));
        default:
            logMessage(NORMAL, "poll success: get a new link event.");

            HandlerEvent();
            break;
        }
    }
}

DebugPrint():

  • 调试目的打印当前文件描述符数组 _fds[] 中的有效文件描述符
void DebugPrint()
    {
        cout << "_fds[].fd: ";
        for(int i = 0; i < _nfds; ++i)
        {
            if(_fds[i].fd != FD_NONE)
                cout << _fds[i].fd << " ";
        }
        cout << endl;
    }

HandlerEvent(事件处理函数):

// 处理事件
void HandlerEvent()
{
    for(int i = 0; i < _nfds; ++i)
    {
        int& fd = _fds[i].fd;
        // 去掉无效 fd
        if(fd == FD_NONE) continue;
        if(_fds[i].revents & POLLIN)
        {
            // 此时的 fd 读事件就绪
            if(fd == _listensock) Accepter(); // 建立连接
            else Recver(i); // 接收数据
        }
    }
}

Acceptrt(建立连接):

  • 接受客户端的连接,并将连接的套接字添加到一个文件描述符数组中进行管理
void Accepter()
    {
        string clientIp;
        uint16_t clientPort;
        int sock = Sock::Accept(_listensock, &clientIp, &clientPort);
        if(sock < 0){
            logMessage(WARNING, "accept error | %d : %s", errno, strerror(errno));
            return;
        }

        logMessage(NORMAL, "accept a new link from %s : %d", clientIp.c_str(), clientPort);
        // 添加fd
        int pos = 1;
        for(; pos < _nfds; ++pos)
            if(_fds[pos].fd == FD_NONE) break;
        
        // 文件描述符存满了,扩容
        if(pos == _nfds) {
            logMessage(WARNING, "%s:%d", "poll server already full, close: %d", clientIp.c_str(), clientPort, sock);
            close(sock);
        } else {
            _fds[pos].fd = sock;
            _fds[pos].events = POLLIN;
            // _fds[pos].revents = 0; // 已经初始化过
        }
    }

Recver():

  • 处理接收到的数据或客户端断开连接的情况
void Recver(int pos)
{
    // 此时读事件就绪,进行读取:
    pollfd& pfd = _fds[pos];
    logMessage(DEBUG, "message in, get IO event: %d", pfd);

    char buffer[1024];
    int n = recv(pfd.fd, buffer, sizeof(buffer) - 1, 0);
    if(n > 0){ // 读取成功
        buffer[n] = 0;
        logMessage(NORMAL, "recv success, client[%d]# %s", pfd.fd, buffer);
    } else if (n == 0) { // 对端关闭
        logMessage(DEBUG, "client quit[%d], me too.", pfd.fd, buffer);
        // 关闭该fd
        close(pfd.fd);
        // poll 不必再关心该fd
        pfd.fd = FD_NONE;
        pfd.events = 0;
    } else { // 读取失败
        logMessage(WARNING, "recv error[%d] | %d : %s", pfd.fd, errno, strerror(errno));
        // 关闭fd
        close(pfd.fd);
        // poll不必再关心该fd
        pfd.fd = FD_NONE;
        pfd.events = 0;
    }   
}

对于上面的代码,验证是和SelectServer无异的,我们主要了解poll与select的不同以及优缺点,正如前面提到的:

  • poll依然需要大量遍历——在用户层检测事件就绪,在内核层检测fd就绪
    • 用户依然需要维护数组

为了避免这个问题,引入了一个新的模型:epoll

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值