多路转接之poll
IO多路转接之poll
因为select有着那种那样的问题!所以就有了一个新的实现多路转接的方式poll!
详细可看笔者的上一篇文章
使用poll来实现的多路转接方案主要解决了select的两个问题!
1.select的fd有上限的问题!
2.select每次调用都要重新设置关心的fd的问题!
poll函数
fds参数
——可以认为是一种动态数组!由new或者malloc出来的结构体数组!这个结构体有三个成员!
==fd——就是用户告诉内核,要监视哪一个fd==
==events——就是用户告诉内核,fd上的事件是什么!我们会把对应的事件设置进events里面!==
==所以我们输入的是只要看fd+events——就是用户告诉操作系统,要监视fd上的什么事件!==
==返回的时候就是操作系统告诉用户,用户要求监视的fd中的events里面有哪些事件已经就绪了!==
==revents——就是用来告诉哪些已经就绪的事件!==
==所以返回的时候我们看fd+revents==
这样的设计解决了输入输出分离的问题!——fd不变的!返回的时候用的是revents,输入的是用的是events!这样就不会出现select里面的,返回修改了位图后导致了我们的要重新设定位图的问题!
这样就poll就不需要就参数继续重新设定!
==那么events和revents的取值是什么呢?==
==这些值本质都是一个宏!这些宏每一个都占一个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_;//用来传入对请求的处理函数! }; }
==相比select的代码!poll的代码明显更加的简单!==
poll的优缺点
poll的优点
1.等待的fd是没有上限的!
2.输入输出分离!
poll的缺点
遍历问题!——因为我们交给poll多个文件描述符的时候!poll的底层也依旧需要遍历式的去查找!如果遍历一遍没有就绪!那么poll就会阻塞!会把这个进程添加进每一个关系的描述符下所对应的struct file的等待队列下面!直到有一个就绪了就会醒来!然后重新遍历!
只要有遍历的问题,虽然我们文件描述符没有上限!但是当文件描述符数量太多的时候!我们poll照样使用线性遍历的方式!这势必会带来效率的降低!
==poll的主要问题就是遍历的问题!==