【Linux 35】多路转接 - poll

🌈 一、poll 初步认识

  • poll 的诞生主要是为了解决 select 以下两个缺点(select 剩下的缺点在 poll 中依然存在):
    1. select 可以等待的文件描述符的数量太少。
    2. select 的输入输出参数混合。
  • 系统调用 poll 也可以让程序同时监视多个文件描述符上的事件是否就绪,和 select 的定位相同 & 使用场景一致。

🌈 二、poll 函数原型

⭐ 1. poll 函数介绍

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

1. 参数说明

  • fds:一个由 poll 函数所监视的结构体数组,其中的每一个元素都包含三部分内容(文件描述符、监视的事件集合、就绪的事件集合)
  • nfds:fds 数组的长度
  • timeout:poll 函数的超时时间(单位 ms)

2. 参数 timeout 的取值

  • -1:调用 poll 后进行阻塞等待,直到被监视的文件描述符上有事件就绪为止。
  • 0:调用 poll 后进行非阻塞等待,无论被监视的文件描述符上的事件是否就绪,poll 都会立即返回。
  • 特定的时间值:调用 poll 后在指定的时间内进行阻塞等待,如果被监视的文件描述符上一直没有事件就绪,则在指定时间后 poll 进行超时返回。

3. 返回值说明

  • 调用 poll 函数成功:返回已经有事件就绪的文件描述符的个数。
  • 调用 poll 函数失败:返回 -1,同时设置错误码,错误码的可能取值如下:
    • EFAULT:fds数组不包含在调用程序的地址空间中。
    • EINTR:此调用被信号所中断。
    • EINVAL:nfds值超过RLIMIT_NOFILE值。
    • ENOMEM:核心内存不足。
  • timeout 时间耗尽:返回 0

⭐ 2. struct pollfd 结构体介绍

1. struct pollfd 的成员

  • 其中,fd 和 events 由用户填写,revents 由操作系统填写。
struct pollfd
{
    int fd;				// 监视的文件描述符,若设置为负数则忽略 events 字段且设置 revents 字段为 0
    short int events;	// 需要监视 fd 这个文件描述符上的哪些事件	
    short int revents;	// poll 函数返回时,告知用户 fd 这个文件描述符上的哪些事件已经就绪
};

2. events 和 revents 的取值

事件描述是否可作为输入是否可作为输出
POLLIN数据(包括普通数据和优先数据)可读
POLLRDNORM普通数据可读
POLLRDBAND优先级带数据可读(Linux 不支持)
POLLPRI高优先级数据可读,比如 TCP 带外数据
POLLOUT数据(包括普通数据和优先数据)可写
POLLWRNORM普通数据可写
POLLWRBAND优先级带数据可写
POLLRDHUPTCP连接被对方关闭,或者对方关闭了写操作,它由 GNU 引入
POLLERR错误
POLLHUP挂起。比如管道的写端被关闭后,读端描述符上将收到 POLLHUP 事件
POLLNVAL文件描述符没有打开

🌈 三、poll 的优缺点

⭐ 1. poll 的优点

  • struct pollfd 结构体中包含了 eventsrevents 两个字段,相当于将 select 的输入输出型参数进行分离。因此在每次调用poll之前,不需要像 select 一样重新对参数进行设置。
  • 由于 fds 数组的大小没有上限,就导致了 poll 函数能够监视的文件描述符的数量没有上限
  • poll 可以同时等待多个文件描述符,能够提高 IO 的效率。

⭐ 2. poll 的缺点

  • 和 select 函数一样,当poll返回后,需要遍历fds数组来获取就绪的文件描述符。
  • 每次调用 poll 时,都需要把大量的 struct pollfd 结构从用户态拷贝到内核态,这个开销也会随着 poll 监视的文件描述符数目的增多而增大。
  • 同时每次调用 poll 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时也很大。

🌈 sipoll 使用示例

#pragma once

#include <iostream>
#include <algorithm>
#include <sys/poll.h>

#include "socket.h"

using std::cerr;
using std::make_unique;
using std::max;
using std::unique_ptr;

using namespace socket_ns;

class poll_server
{
    const static int fd_max_num = 1024;
    const static int defauld_fd = -1;

private:
    uint16_t _port;
    unique_ptr<Socket> _listen_socket;
    bool _isrunning;
    struct pollfd* _rfds = nullptr;
    int _num;

public:
    poll_server(uint16_t port)
        : _port(port)
        , _listen_socket(make_unique<tcp_socket>())
        , _isrunning(false)
        , _num(fd_max_num)
    {}

    // 初始化服务器
    void init_server()
    {
        _listen_socket->build_listen_socket(_port);
        _rfds = new struct pollfd[_num];

        for (size_t i = 0; i < _num; i++)
        {
            _rfds[i].fd = defauld_fd;
            _rfds[i].events = 0;
            _rfds[i].revents = 0;
        }

        // 最初只有一个文件描述符
        _rfds[0].fd = _listen_socket->sockfd();
        _rfds[0].events |= POLLIN;
    }

    // 获取连接(处理 listen fd 的就绪事件)
    void accept_connection()
    {
        // 有新连接到来 - 连接事件就绪
        // 处理 listen 套接字
        Inet_addr addr;
        int sockfd = _listen_socket->accepter(&addr); // 由于 select 的原因,一定不会阻塞
        if (sockfd > 0)
        {
            LOG(DEBUG, "get a new link success, client info: %s:%d", addr.ip().c_str(), addr.port());

            // 将新的 fd 交给 poll 托管
            int pos = 0;
            for (; pos < _num; pos++)
            {
                if (_rfds[pos].fd == defauld_fd)
                {
                    _rfds[pos].fd = sockfd;
                    _rfds[pos].events |= POLLIN;
                    break;
                }
            }
            if (pos == _num)
            {
                close(sockfd);
            }
        }
    }

    // 处理 IO(处理普通 fd 的就绪事件)
    void handle_io(int i)
    {
        // 处理普通 sockfd,正常读写
        char buffer[1024];
        // 这里的读取不会阻塞,因为 select 已经判断出 fd 的读事件就绪
        ssize_t n = ::recv(_rfds[i].fd, buffer, sizeof(buffer) - 1, 0);
        if (n > 0)
        {
            buffer[n] = '\0';
            cout << "client say: " << buffer << endl;
            // 服务器给客户端回复
            string content = "<html><body><h1>hello world</h1></body></html>";
            string echo_str = "HTTP/1.0 200 OK\r\n";
            echo_str += "Content-Type: text/html\r\n";
            echo_str += "Content-Length: " + to_string(content.size()) + "\r\n\r\n";
            echo_str += content;
            // 发送数据
            ::send(_rfds[i].fd, echo_str.c_str(), echo_str.size(), 0);
        }
        else if (0 == n)
        {
            // 对端关闭连接
            LOG(INFO, "client quit, sockfd: %d", _rfds[i].fd);
            close(_rfds[i].fd);
            _rfds[i].fd = defauld_fd; // 让 select 不要再关心这个 fd 了
        }
        else
        {
            // 读取失败
            LOG(ERROR, "recv error, sockfd: %d", _rfds[i].fd);
            close(_rfds[i].fd);
            _rfds[i].fd = defauld_fd;
        }
    }

    // 处理已经就绪的事件 (一定会同时存在大量就绪的 fd,可能是普通 sockfd,也可能是 listensockfd)
    void handle_event()
    {
        for (size_t i = 0; i < _num; i++)
        {
            if (_rfds[i].fd == defauld_fd)
                continue;
            
            int fd = _rfds[i].fd;
            int revents = _rfds[i].revents;

            if (revents & POLLIN)
            {
                if (fd == _listen_socket->sockfd())
                    accept_connection();    // 处理连接事件
                else
                    handle_io(i);          // 处理 IO 事件
            }
        }
    }

    // 展示所有合法的 fd
    void print_all_legal_fd()
    {
        cout << "legal fd list: ";
        for (size_t i = 0; i < fd_max_num; i++)
            if (_rfds[i].fd != defauld_fd)
                cout << _rfds[i].fd << " ";
        cout << endl;
    }

    // 运行服务器
    void loop()
    {
        _isrunning = true;

        while (true)
        {
            int timeout = -1;
            int n = ::poll(_rfds, _num, timeout);
            
            switch (n)
            {
            case 0:
                LOG(DEBUG, "timeout, %d", timeout);
                break;
            case -1:
                LOG(ERROR, "poll error");
                break;
            default:
                LOG(INFO, "haved event ready, n: %d", n); // 如果事件就绪,但时不处理,select 会一直通知我,直到事件被处理为止
                handle_event();
                print_all_legal_fd();
                sleep(1);
                break;
            }
        }
    }

    ~poll_server()
    {
        if (_listen_socket)
            close(_listen_socket->sockfd());
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值