Linux多路转接poll

发布于:2025-02-10 ⋅ 阅读:(36) ⋅ 点赞:(0)

Linux多路转接poll

1. poll()

poll() 结构包含了要监视的 event 和发生的 event ,接口使用比 select() 更方便。且 poll 并没有最大数量限制(但是数量过大后性能也是会下降)。

2. poll() 的工作原理

poll() 不再需要像 select() 那样自行设置文件描述符集合,它只需要用户在 pollfd 结构体中设置文件描述符及其关心的事件(events,在输出时,结构体内的 revents 作为函数调用后事件就绪的结果, 就 select()poll() 的不同而言, poll() 支持用户自定义关心某些事件,同时将事件就绪的结构用不同的变量保存起来。

poll() 会返回就绪文件描述符的数量,用户需要自行设置判断条件,遍历pollfd 结构体中的events,将其与自己关心的事件按位与&),然后执行 recv() 或其他函数读取或进行其他操作。

Poll1

3. 函数声明

#include <poll.h>

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

struct pollfd *fds*fds 表示”数组“的起始地址。下面给出 struct pollfd 的具体内容。

nfds_t nfds:表示数组有效元素的个数。 nfds_t unsigned long typedef 出来的类型。

timeout:表示超时时间,以毫秒为单位,等于 0 表示非阻塞等待, -1 表示阻塞等待。

reval:返回值大于 0 ,返回事件就绪文件描述符的数量;返回值小于 0 表示出错;返回值等于 0 表示超时。

4. struct pollfd

struct pollfd
{
	int   fd;         /* 文件描述符 */
	short events;     /* 等待的事件集 */
	short revents;    /* 实际发生的事件集 */
};

events:表示调用 poll()希望检测的事件类型

revents :表示在 poll() 调用之后,实际发生的事件集poll() 会在返回时填充该字段,以指示文件描述符上发生了哪些事件。

常见的事件类型有:

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

5. poll() 的使用

#pragma once

#include <iostream>
#include <poll.h>
#include "Socket.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"

using namespace socket_ns;

class PollServer
{
    const static int gnum = sizeof(fd_set) * 8;
    const static int gdefaultfd = -1;

public:
    PollServer(uint16_t port) : _port(port), _listensock(std::make_unique<TcpSocket>())
    {
        _listensock->BuildListenSocket(_port);
    }
    void InitServer()
    {
        for (int i = 0; i < gnum; i++)
        {
            fd_events[i].fd = gdefaultfd;
            fd_events[i].events = 0;
            fd_events[i].revents = 0;

        }
        fd_events[0].fd = _listensock->Sockfd(); 
        fd_events[0].events = POLLIN;
    }
    void Accepter()
    {
        InetAddr addr;
        int sockfd = _listensock->Accepter(&addr);
        if (sockfd > 0)
        {
            LOG(DEBUG, "get a new link, client info %s:%d\n", addr.Ip().c_str(), addr.Port());
       
            bool flag = false;
            for (int pos = 1; pos < gnum; pos++)
            {
                if (fd_events[pos].fd == gdefaultfd)
                {
                    flag = true;
                    fd_events[pos].fd = sockfd;
                    fd_events[pos].events = POLLIN;
                    LOG(INFO, "add %d to fd_array success!\n", sockfd);
                    break;
                }
            }
            if (!flag)
            {
                LOG(WARNING, "Server Is Full!\n");
                ::close(sockfd);
                // 扩容
                // 添加
            }
        }
    }
    // 处理普通的fd就绪的
    void HandlerIO(int i)
    {
        char buffer[1024];
        ssize_t n = ::recv(fd_events[i].fd, buffer, sizeof(buffer) - 1, 0); 
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << "client say# " << buffer << std::endl;
            std::string content = "<html><body><h1>hello bite</h1></body></html>";
            std::string echo_str = "HTTP/1.0 200 OK\r\n";
            echo_str += "Content-Type: text/html\r\n";
            echo_str += "Content-Length: " + std::to_string(content.size()) + "\r\n\r\n";
            echo_str += content;
            ::send(fd_events[i].fd, echo_str.c_str(), echo_str.size(), 0); 
        }
        else if (n == 0)
        {
            LOG(INFO, "client quit...\n");
            ::close(fd_events[i].fd);
            fd_events[i].fd = gdefaultfd;
            fd_events[i].events = 0;
            fd_events[i].revents = 0;
        }
        else
        {
            LOG(ERROR, "recv error\n");
            ::close(fd_events[i].fd);
            fd_events[i].fd = gdefaultfd;
            fd_events[i].events = 0;
            fd_events[i].revents = 0;
        }
    }
    // 一定会存在大量的fd就绪,可能是普通sockfd,也可能是listensockfd
    void HandlerEvent()
    {
        // 事件派发
        for (int i = 0; i < gnum; i++)
        {
            if (fd_events[i].fd == gdefaultfd)
                continue;
            // fd一定是合法的fd
            // 合法的fd不一定就绪, 判断fd是否就绪
            if (fd_events[i].revents & POLLIN)
            {
                // 读事件就绪
                // 1. listensockfd 2. normal sockfd就绪
                if (_listensock->Sockfd() == fd_events[i].fd)
                {
                    Accepter();
                }
                else
                {
                    HandlerIO(i);
                }
            }
        }
    }
    void Loop()
    {
        int timeout = -1;
        while (true)
        {
            int n = ::poll(fd_events, gnum, timeout); 
            switch (n)
            {
            case 0:
                LOG(DEBUG, "time out\n");
                break;
            case -1:
                LOG(ERROR, "poll error\n");
                break;
            default:
                LOG(INFO, "haved event ready, n : %d\n", n); 
                HandlerEvent();
                PrintDebug();
                break;
            }
        }
    }

    void PrintDebug()
    {
        std::cout << "fd list: ";
        for (int i = 0; i < gnum; i++)
        {
            if (fd_events[i].fd == gdefaultfd)
                continue;
            std::cout << fd_events[i].fd << " ";
        }
        std::cout << "\n";
    }
    ~PollServer() {}

private:
    uint16_t _port;
    std::unique_ptr<Socket> _listensock;

    struct pollfd fd_events[gnum];
};

6. poll() 的缺点

poll() 返回后,需要轮询pollfd 来获取就绪的文件描述符。

每次调用 poll() 都要把大量的 pollfd 结构从用户态拷贝到内核态。

poll()的底层,也需要操作系统遍历所有的文件描述符,来获取就绪的文件描述符和它的事件,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。