多路转接之poll

IO多路转接之poll

因为select有着那种那样的问题!所以就有了一个新的实现多路转接的方式poll!

详细可看笔者的上一篇文章

使用poll来实现的多路转接方案主要解决了select的两个问题!

1.select的fd有上限的问题!

2.select每次调用都要重新设置关心的fd的问题!

poll函数

image-20231116103124454

fds参数——可以认为是一种动态数组!由new或者malloc出来的结构体数组!

image-20231116103637037

这个结构体有三个成员!

==fd——就是用户告诉内核,要监视哪一个fd==

==events——就是用户告诉内核,fd上的事件是什么!我们会把对应的事件设置进events里面!==

==所以我们输入的是只要看fd+events——就是用户告诉操作系统,要监视fd上的什么事件!==

==返回的时候就是操作系统告诉用户,用户要求监视的fd中的events里面有哪些事件已经就绪了!==

==revents——就是用来告诉哪些已经就绪的事件!==

==所以返回的时候我们看fd+revents==

这样的设计解决了输入输出分离的问题!——fd不变的!返回的时候用的是revents,输入的是用的是events!这样就不会出现select里面的,返回修改了位图后导致了我们的要重新设定位图的问题!

这样就poll就不需要就参数继续重新设定!

==那么events和revents的取值是什么呢?==

Quicker_20231116_105916_result

==这些值本质都是一个宏!这些宏每一个都占一个bit位!在events和revents中可以表示不同的事件!==

==所以用户关心那些事件,就把对应的宏值设置进events里面!操作系统返回的时候也只要将对应的宏值设置进revents里面!==

==我们可以通过按位与,按位或的方式,来判断事件有没有就绪!==

==我们常用的事件有==

1.POLLIN——就是那些事件好了可以读取了!

2.POLLOUT——那些事件好了可以写入了!

nfds参数——是数组对应的长度!

==poll还解决了select等待的fd有上限的这个问题!==

为什么呢?这里不是还有一个长度吗?

select是一种具体的数据类型是fd_set这就决定了,数据类型的大小最终只能有编译环境自己决定!

但是poll的数组是由我们自己说的算!我们可以自己来决定它有多长!poll能等待的文件描述符个数,被poll本身限制!而是被进程本身的文件所能接收的描述符锁个数决定!

timeout参数——这是一个纯输入型参数,时间单位是毫秒(ms)

这个参数有三种设置方式

小于0——让poll等待多个文件描述符的时候以阻塞的方式进行等待!

等于0——就是让poll等待多个文件描述符的时候以非阻塞的方式进行等待!

大于0——比如说1000,就是在1000ms以内是阻塞的!一旦超过timeout时间就已非阻塞的方式返回一次!

==RETURN VALUE——和select的作用是一模一样!==

如果==返回值是大于0==,值是几就是表明有几个fd就绪了!例如:5,就表明有5个文件描述符就绪了!但是这个大小不会大于我们监视的文件描述符数!(监视10个不可能返回11个)

==如果返回值等于0==,代表的就是超时返回了!即:如果我们时间设置5s就说明超过5s返回了!如果是设置0s,也是超过0s后超时返回!

==如果返回值小于0==说明poll等待的时候失败了!例如:我们现在服务器中只打开了3,4,5三个文件描述符!只有这三个合法的文件描述符!但是现在我们要让把10,20号的文件描述符都管理起来!但是这两个文件描述符在进程里面压根没有被打开!那么此时select就会调用失败!

==当调用失败就会返回-1,错误码会被设置!==

==poll的作用和select是一模一样的!——也就是只负责等!而不负责拷贝!==

poll服务器

#pragma once
#include"Sock.hpp"
#include<iostream>
#include<functional>
#include<string>
#include<poll.h>

namespace poll_ns
{
    class PollServer
    {
        static const int default_port = 8080;
        static const int num = 2048;
        static const int defaultfd = -1;


        using func_t = std::function<std::string(const std::string&)>;
     public:
        PollServer(func_t func,int port = default_port)
            : port_(port),listensock_(-1),pollrfds_(nullptr),func_(func)
            {}

        void ResetItem(int pos)
        {
            pollrfds_[pos].fd = defaultfd;
            pollrfds_[pos].events = 0;
            pollrfds_[pos].revents = 0;
        }
        void initServer()
        {
            listensock_ = Sock::Socket();//创建套接字
            Sock::Bind(listensock_,port_);//绑定套接字
            Sock::Listen(listensock_);    // 把套接字设置为监听状态!
            pollrfds_ = new struct pollfd[num];

            for(int i = 0;i<num;i++)//初始化
                ResetItem(i);

            pollrfds_[0].fd = listensock_;
            pollrfds_[0].events = POLLIN;//我们想让他关心读事件!

        }
        void Print()
        {
            for(int i = 0;i<num;i++)
            {
                if (pollrfds_[i].fd != defaultfd)
                    std::cout << "fd list: " << pollrfds_[i].fd << " ";
            }
            std::cout << std::endl;
        }


        void Accepter(int listensock)
        {
            // select告诉我们,listensock读事件就绪了!
            std::string ClientIp;
            uint16_t ClientPort = 0;
            int sock = Sock::Accept(listensock, &ClientIp, &ClientPort); // 获取新连接
            // 走到这里accept函数不会阻塞!
            if (sock == ACCEPT_ERR)
                return;

            // 将新的sock托管给select!
            // 而托管给select的本质就是,将sock添加到fdarray_这数组中!
            // 当下一次循环的时候,就会被托管进select里面!
            int i = 0;
            for (i = 0; i < num; i++)
            {
                if (pollrfds_[i].fd != defaultfd)
                    continue;
                break;
            }

            if (i == num) // 这说明所有的数组元素都是合法的!
            {
                LogMessage(WARNING, "server is full! please wait!");
                close(sock); // 关闭这个新的套接字
            }
            else
            {
                pollrfds_[i].fd = sock;
                pollrfds_[i].events = POLLIN;//我们是要从该连接中进行读取
                pollrfds_->revents = 0;
            }
            Print();
        }


        void Recver(int pos)//pos参数是为了方便我们需要的时候删除掉这个sock
        {
            //1.读取
            char buffer[1024];
            ssize_t s = recv(pollrfds_[pos].fd,buffer,sizeof(buffer) -1,0);//这里再进行读取的时候就不会被阻塞了!
            //这样子的读写时有问题的!因为不能肯定能不能把缓冲区的数据都读取上来!
            //也不能保证读一半的时候该怎么办!序列化和反序列化该怎么处理!但是为了方便我演示我们就暂时怎么写!
            if(s > 0)
            {
                buffer[s] = 0;
                LogMessage(NORMAL,"Client# %s",buffer);
            }
            else if(s == 0)//对方关闭文件描述符!
            {
                close(pollrfds_[pos].fd);
                ResetItem(pos);

                LogMessage(NORMAL,"client quit! me too!");
                return;
            }
            else //读取失败
            {
                close(pollrfds_[pos].fd);//我们也要关闭自己的这边的文件描述符! //然后让select不要再关心这个文件描述符了!
                ResetItem(pos);

                LogMessage(ERROR,"read error! errno: %s,error string: %s",errno,strerror(errno));
                return;
            }

            //2.处理request
            std::string response = func_(buffer);

            //返回response
            //write——我们这里打算把结果给用户返回!
            //但是我们如何保证写入的时候写事件就就绪了呢?——那么我们就要给select多加一个新的writefds
            //同时也要有一个新的数组来维护这些fd!但是因为这样子会让代码更加的复杂!我们这里就不进行实现!
            //所以我们就直接写回去了!暂时不考虑写回去的暂停问题!

            write(pollrfds_[pos].fd, response.c_str(), response.size());
        }
        void HandlerEvent()
        {
            //我们无法知道那个文件描述符对应的事件是什么!
            //所以我们只能对所有的文件描述符进行遍历!
            for(int i = 0;i<num;i++)
            {
                //过滤非法fd
                if(pollrfds_[i].fd== defaultfd) continue;//非法fd不进行处理!

                if(!(pollrfds_[i].events & POLLIN)) continue;//如果不是读事件就进入下一次循环!


                if (pollrfds_[i].fd == listensock_ && (pollrfds_[i].revents & POLLIN))//如果当前读标志位被设置
                {
                    std::cout << "this is accept"<<std::endl;
                    Accepter(listensock_);
                }
                else if(pollrfds_[i].revents & POLLIN)//该文件描述符已经就绪
                {

                    std::cout << "this is Recver"<<std::endl;
                    Recver(i);
                }
            }
        }
        void start()
        {
            int timeout = 1000;//1000ms
            for(;;)
            {
                int n = poll(pollrfds_,num,timeout);
                switch(n)
                {
                    case 0://如果返回0说明超时了!
                        LogMessage(NORMAL,"timeout....");
                        break;
                    case -1:
                        LogMessage(WARNING,"select error,code: %d,err string: %s",errno,strerror(errno));
                        break;
                    default:
                        // LogMessage(NORMAL,"get a new link ....");
                        LogMessage(NORMAL,"have a event ready");
                        HandlerEvent();
                        //说明事件就绪了!但是目前只有一个监听时间!
                        break;
                }


            }

        }
        ~PollServer()
        {
            if(listensock_< 0) close(listensock_);
            if (pollrfds_ != nullptr) delete[] pollrfds_;
        }

    private:
        int port_;
        int listensock_;
        struct pollfd *pollrfds_;
        func_t func_;//用来传入对请求的处理函数!

    };
}

image-20231116164445030

==相比select的代码!poll的代码明显更加的简单!==

poll的优缺点

poll的优点

1.等待的fd是没有上限的!

2.输入输出分离!

poll的缺点

遍历问题!——因为我们交给poll多个文件描述符的时候!poll的底层也依旧需要遍历式的去查找!如果遍历一遍没有就绪!那么poll就会阻塞!会把这个进程添加进每一个关系的描述符下所对应的struct file的等待队列下面!直到有一个就绪了就会醒来!然后重新遍历!

只要有遍历的问题,虽然我们文件描述符没有上限!但是当文件描述符数量太多的时候!我们poll照样使用线性遍历的方式!这势必会带来效率的降低!

==poll的主要问题就是遍历的问题!==