【Hello Linux】多路转接之 poll

简介: 【Hello Linux】多路转接之 poll

poll函数初识

poll是系统提供的一个多路转接接口

它的作用和select函数基本一致

在学习poll函数之前我们先回顾下select函数的缺点

  1. 为了维护第三方数组 select服务器充满大量的遍历
  2. 每一次都要对select参数进行重新设定
  3. 能够同时管理的fd有上限
  4. 每个参数都是输入输出形的 所以要经历大量的内核用户拷贝
  1. 编码比较复杂

对于我们的poll来说

  1. 它的输出输出参数是分离的 所以解决了缺点2
  2. poll能同时管理的参数个数是有上限了 所以解决了缺点3

poll函数

poll函数的函数原型如下:

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

下面我们依次介绍下这个函数的参数和返回值

返回值:int n

  • 如果返回大于0的数字就表示 有n个文件描述符就绪
  • 如果timeout时间到了还没有文件描述符就绪则返回0
  • 如果出错则返回-1

参数:

参数一: struct pollfd *fds

一个结构体 里面包含着文件描述符表 我们需要监视的文件描述符合集 就绪的文件描述符合集

我们在下面会详细介绍该参数

参数二: nfds

fds数组的长度

参数三: timeout

超时时间

  • 单位是毫秒 比如说我们设置为1000 就是等待1秒
  • 如果设置为0 就表示非阻塞模式
  • 如果设置为-1 就表示阻塞模式

struct pollfd详细介绍

既然poll和select的功能相同 那么它就应该回答和select相同的两个问题

  • 用户如何告诉内核 我需要关心的文件描述符有哪些
  • 内核如何告诉用户 你关心的文件描述符有哪些就绪了

而实际上这两个问题都在结构体 struct pollfd 中解决了

struct pollfd在linux中的定义如下图

c432d9d0def544629076b03392ddcd9c.png

  • fd 特定的文件描述符值
  • events 用户告诉内核 哪些事件需要关心
  • revents 内核告诉用户 哪些事件就绪了

但是我们这里就有一个问题了 events是一个short类型的数据啊

我们怎么让一个short类型的数据 来表示很多时间是否就绪呢?

不知道大家还记不记得在基础IO章节我们曾经说过一句话 在Linux操作系统中 为了节省空间 要表示一个事件存不存在 我们只需要用一个比特位表示就够了

所以说 我们只要规定 short 类型数据的每个比特位的含义 就能使用该比特位的1或0来表示该事件是否存在了

以下是events和revents的取值

b426ef75319a4dfdbab7dc4971334960.png

我们需要特别注意的有三个 分别是

  • POLLIN 可读
  • POLLOUT 可写
  • POLLERR 错误

为什么说poll能够接收的fd是无上限的呢?

我们在学习完毕struct pollfd之后明白了 一个文件描述符实际上就是对应一个struct pollfd

所以说理论上 只要有多少个数组 我们的poll就能检测多少的文件描述符

poll服务器

我们选择将原来的select服务器改写成一个poll服务器 让大家更直观的感受到poll对比于select的优点

它的私有成员变化如下

          int _port;    
          int _listensock;         
          struct pollfd *_rfds;    
          func_t _func;    

对比于我们select的第三方数组来说 我们这里多了一个数组指针和数组大小

在初始化的时候 我们首先new出一个 struct pollfd 数组出来 (大小自己指定)

并且遍历初始化一下

              _rfds[i].fd = defaultfd;                       
              _rfds[i].events = 0;
              _rfds[i].revents = 0;

对于数据如何判断就绪 我们可以使用按位与来判断

_rfds[i].revents & POLLIN

托管文件描述符给poll也很简单 和select一样 找到没有被使用过的数组然后依次设置struct即可

整体代码如下

#pragma once
#include <iostream>
#include <string>
#include <functional>
#include "sock.hpp"
namespace select_ns
{
    static const int defaultport = 8081;
    static const int fdnum = sizeof(fd_set) * 8;
    static const int defaultfd = -1;
    using func_t = std::function<std::string (const std::string&)>;
    class SelectServer
    {
    public:
        SelectServer(func_t f, int port = defaultport) : func(f), _port(port), _listensock(-1), fdarray(nullptr)
        {
        }
        void initServer()
        {
            _listensock = Sock::Socket();
            Sock::Bind(_listensock, _port);
            Sock::Listen(_listensock);
            fdarray = new int[fdnum];
            for (int i = 0; i < fdnum; i++)
                fdarray[i] = defaultfd;
            fdarray[0] = _listensock; // 不变了
        }
        void Print()
        {
            std::cout << "fd list: ";
            for (int i = 0; i < fdnum; i++)
            {
                if (fdarray[i] != defaultfd)
                    std::cout << fdarray[i] << " ";
            }
            std::cout << std::endl;
        }
        void Accepter(int listensock)
        {
            logMessage(DEBUG, "Accepter in");
            // 走到这里,accept 函数,会不会阻塞???1 0
            // select 告诉我, listensock读事件就绪了
            std::string clientip;
            uint16_t clientport = 0;
            int sock = Sock::Accept(listensock, &clientip, &clientport); // accept = 等 + 获取
            if (sock < 0)
                return;
            logMessage(NORMAL, "accept success [%s:%d]", clientip.c_str(), clientport);
            // sock我们能直接recv/read 吗?不能,整个代码,只有select有资格检测事件是否就绪
            // 将新的sock 托管给select!
            // 将新的sock托管给select的本质,其实就是将sock,添加到fdarray数组中即可!
            int i = 0;
            for (; i < fdnum; i++)
            {
                if (fdarray[i] != defaultfd)
                    continue;
                else
                    break;
            }
            if (i == fdnum)
            {
                logMessage(WARNING, "server if full, please wait");
                close(sock);
            }
            else
            {
                fdarray[i] = sock;
            }
            Print();
            logMessage(DEBUG, "Accepter out");
        }
        void Recver(int sock, int pos)
        {
            logMessage(DEBUG, "in Recver");
            // 1. 读取request
            // 这样读取是有问题的!
            char buffer[1024];
            ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0); // 这里在进行读取的时候,会不会被阻塞?1, 0
            if (s > 0)
            {
                buffer[s] = 0;
                logMessage(NORMAL, "client# %s", buffer);
            }
            else if (s == 0)
            {
                close(sock);
                fdarray[pos] = defaultfd;
                logMessage(NORMAL, "client quit");
                return;
            }
            else
            {
                close(sock);
                fdarray[pos] = defaultfd;
                logMessage(ERROR, "client quit: %s", strerror(errno));
                return;
            }
            // 2. 处理request
            std::string response = func(buffer);
            // 3. 返回response
            // write bug
            write(sock, response.c_str(), response.size());
            logMessage(DEBUG, "out Recver");
        }
        // 1. handler event rfds 中,不仅仅是有一个fd是就绪的,可能存在多个
        // 2. 我们的select目前只处理了read事件
        void HandlerReadEvent(fd_set &rfds)
        {
            for (int i = 0; i < fdnum; i++)
            {
                // 过滤掉非法的fd
                if (fdarray[i] == defaultfd)
                    continue;
                // 正常的fd
                // 正常的fd不一定就绪了
                // 目前一定是listensock,只有这一个
                if (FD_ISSET(fdarray[i], &rfds) && fdarray[i] == _listensock)
                    Accepter(_listensock);
                else if(FD_ISSET(fdarray[i], &rfds))
                    Recver(fdarray[i], i);
                else{}
            }
        }
        void start()
        {
            for (;;)
            {
                fd_set rfds;
                // fd_set wfds;
                FD_ZERO(&rfds);
                int maxfd = fdarray[0];
                for (int i = 0; i < fdnum; i++)
                {
                    if (fdarray[i] == defaultfd)
                        continue;
                    FD_SET(fdarray[i], &rfds); // 合法 fd 全部添加到读文件描述符集中
                    if (maxfd < fdarray[i])
                        maxfd = fdarray[i]; // 更新所有fd中最大的fd
                }
                logMessage(NORMAL, "max fd is: %d", maxfd);
                // struct timeval timeout = {1, 0};
                // int n = select(_listensock + 1, &rfds, nullptr, nullptr, &timeout); // ??
                // 一般而言,要是用select,需要程序员自己维护一个保存所有合法fd的数组!
                int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr); // ??
                switch (n)
                {
                case 0:
                    logMessage(NORMAL, "timeout...");
                    break;
                case -1:
                    logMessage(WARNING, "select error, code: %d, err string: %s", errno, strerror(errno));
                    break;
                default:
                    // 说明有事件就绪了,目前只有一个监听事件就绪了
                    logMessage(NORMAL, "have event ready!");
                    HandlerReadEvent(rfds);
                    // HandlerWriteEvent(wfds);
                    break;
                }
                // std::string clientip;
                // uint16_t clientport = 0;
                // int sock = Sock::Accept(_listensock, &clientip, &clientport); // accept = 等 + 获取
                // if(sock<0) continue;
                // // 开始进行服务器的处理逻辑
            }
        }
        ~SelectServer()
        {
            if (_listensock < 0)
                close(_listensock);
            if (fdarray)
                delete[] fdarray;
        }
    private:
        int _port;
        int _listensock;
        int *fdarray;
        func_t func;
    };
}

poll的优缺点

优点

  • 效率高
  • 适合有大量连接 少量活跃
  • 输入输出分离 不需要大量的充值
  • poll参数级别 没有可管理的fd上限

缺点

  • poll依旧需要不少的遍历
  • poll需要内核到用户的拷贝 – 这个少不了的
  • poll的代码虽然比select容易 但是也很复杂
相关文章
|
6月前
|
监控 网络协议 Linux
Linux多路转接or多路复用模型
【2月更文挑战第5天】
56 1
|
29天前
|
Linux C++
Linux C/C++之IO多路复用(poll,epoll)
这篇文章详细介绍了Linux下C/C++编程中IO多路复用的两种机制:poll和epoll,包括它们的比较、编程模型、函数原型以及如何使用这些机制实现服务器端和客户端之间的多个连接。
22 0
Linux C/C++之IO多路复用(poll,epoll)
|
5月前
|
Linux C++
c++高级篇(三) ——Linux下IO多路复用之poll模型
c++高级篇(三) ——Linux下IO多路复用之poll模型
|
6月前
|
Linux C语言 Windows
在Linux写自己的第一个程序“hello Linux”
在Linux写自己的第一个程序“hello Linux”
在Linux写自己的第一个程序“hello Linux”
|
6月前
|
Ubuntu Linux
Linux 驱动开发基础知识——Hello驱动程序(一)
Linux 驱动开发基础知识——Hello驱动程序(一)
78 0
Linux 驱动开发基础知识——Hello驱动程序(一)
|
6月前
|
网络协议 Linux
【Linux C TCP服务器端-poll案例】
本文主要介绍了linux下Select的TCP通信流程,实现了客户端和服务器的通信,主要实现了消息的回发,即服务器将消息原封不动的回发给客户端。
45 0
|
6月前
|
网络协议 Linux API
Linux C TCP编程(socket,select/poll/epoll)
本文主要介绍了linux下标准的TCP通信流程,实现了客户端和服务器的通信,主要实现了消息的回发,即服务器将消息原封不动的回发给客户端。如果对接口不熟悉可以参考socket api介绍或者参考其他博客。
67 0
|
6月前
|
Linux API
Linux网络编程(多路IO复用poll)
Linux网络编程(多路IO复用poll)
64 0
|
6月前
|
Linux
Linux网络编程(多路IO复用select函数使用)
Linux网络编程(多路IO复用select函数使用)
74 0
|
6月前
|
存储 监控 Linux
linux系统应用中select函数与poll函数详解
linux系统应用中select函数与poll函数详解
74 0