在之前我们讲过select是最古老的多路转接方案,古老就意味着他不是很方便使用,他需要用户手动保存fd_set这个位图结构,来表示读写事件的关注与否或者就绪性。
而且由于fd_set的大小是固定的,这就意味着他能管理的套接字文件描述符是有限的。针对这两个问题,人们研究出来了poll,他一方面不需要用户手动保存位图了,还能让poll管理的文件描述符是无限多的,只要你的内存足够大。
一、Poll的函数原型
参数说明:
fds
- 类型:
struct pollfd *
- 作用:指向
pollfd
结构体的数组,每个元素描述一个要监控的文件描述符及其关注的事件。正是由于这传的参数是一个结构体数组指针,所以理论上只要你的数组足够大,那么我poll能管理的文件描述符也就越多。nfds
- 类型:
nfds_t
(通常是无符号整数)- 作用:数组
fds
中的结构体个数,即要监控的文件描述符数量。timeout
- 类型:
int
- 作用:超时时间(毫秒)。
- 这是一个纯粹的输入型参数,和之前的select不同。
timeout > 0
:最多等待timeout
毫秒。timeout == 0
:非阻塞模式,立即返回。timeout < 0
:无限等待,直到有事件发生。
返回值:
> 0
:表示有事件发生,返回值为就绪的文件描述符数量。== 0
:超时,无任何事件发生。< 0
:发生错误,返回-1
并设置errno
。
二、Poll的第一个参数是如何做到不用手动保存的呢?
可以看到pollfd结构体的成员是这样的,其中第一个成员表示的是文件描述符的值,而第二个第三个成员则是用户需要让内核监控哪些事件、内核返还给用户哪些事件已经就绪了。
之前我们在使用select的时候,fd_set分为读、写、和异常,他们三个位图就管理了所有的文件描述符,所以造就了其复杂性。但是这里把文件描述符的值和事件分开了,一个pollfd就对应一个文件描述符及其关注的事件。从而让不同文件描述符不用相同的资源,而做到解耦。
三、使用poll的示例
这个示例和select的大同小异,仅仅更改select为poll
#pragma once
#include <iostream>
#include "socket.hpp"
#include <memory>
#include<poll.h>
#include "InetAddr.hpp"using namespace socket_ns;class PollServer
{const static int gdefaultfd = -1;const static int gnum=1024;public:PollServer(uint16_t port): _port(port), _listensock(std::make_unique<TcpSocket>()), _timeout(1000){}~PollServer() {}void InitServer(){_listensock->BuildListenSocket(_port);for(int i=0;i<gnum;i++){_events[i].fd=gdefaultfd;_events[i].events=0;_events[i].revents=0;}//把listen添加进来_events[0].fd=_listensock->sockfd();_events[0].events=POLLIN;//对读事件关心}void Accepter(){// listen套接字得到一个新连接请求-----读事件就绪// 因为已经就绪了,就不会被阻塞了,即accept不会再等了InetAddr client;SockPtr sock = _listensock->Accepter(&client);if (sock->sockfd() > 0){LOG(DEBUG, "get a new link,client info %s:%d\n", client.Ip().c_str(), client.Port());// 处理(但是这里不能直接处理,如果客户端不发消息那我仍然阻塞了)// 那么如何得知fd底层的数据是否就绪了呢?仍然是select!这些fd都要由select管理起来// 所以select中的文件描述符会越来越多// 只需要将新获得的连接套接字放入到fd_array中即可bool flag = false;// 看辅助数组中有没有空余位置给新fd使用,有则插入for (int pos = 1; pos < gnum; pos++){if (_events[pos].fd == gdefaultfd){flag = true;//将新获得的套接字,加入到poll管理的pollfd数组中_events[pos].fd = sock->sockfd();_events[pos].events=POLLIN;_events[pos].revents=0;LOG(INFO, "add %d to fd_array success!\n", sock->sockfd());break;}}// 遍历完成发现是满的if (flag == false){LOG(WARNING, "Server is Full!\n");// 因为处理不了了,所以直接关闭刚刚获得到的连接的套接字::close(sock->sockfd());}}}void Handler_IO(int i){char buffer[1024];// 这里读就不会阻塞了,因为select已经等过了ssize_t n = ::recv(_events[i].fd, buffer, sizeof(buffer) - 1, 0);if (n > 0){buffer[n] = 0;std::cout << "client say#" << buffer << std::endl;// 回复的时候也需要用select找到就绪的,但任何一个sockfd被创建的时候,他的读写缓冲区一定是空的,我们之前关心读事件,是关心// 读缓冲区有没有数据,所以读天然就是不就绪的,但是写天然是就绪的// std::string echo_str="[server echo info]";// echo_str+=buffer;std::string content = "<html><body></h1>hello world</body></html>";std::string echo_str = "HTTP/1.0 200 OK\r\n";echo_str += "Content-Type: text/html\r\n";echo_str += "Cotent-Length:" + std::to_string(content.size()) + "\r\n\r\n";echo_str += content;::send(_events[i].fd, echo_str.c_str(), echo_str.size(), 0);}// 对方把连接关了else if (n == 0){LOG(INFO, "client quit...\n");// 把该文件描述符从fd_array中拿出来::close(_events[i].fd);_events[i].events=0;_events[i].revents=0;_events[i].fd = gdefaultfd;}else{LOG(ERROR, "recv error!\n");}}// 一定有大量的fd就绪,可能是普通sockfd套接字,也可能是linsten套接字void HandlerEvent(){//变量查看合法的fdfor(int i=0;i<gnum;i++){if(_events[i].fd==gdefaultfd){continue;}//合法的文件描述符,判断是否就绪int fd=_events[i].fd;short revents=_events[i].revents;if(revents&POLLIN){//读就绪//判断是listen套接字就绪还是普通文件就绪,进行任务派发if(fd==_listensock->sockfd()){Accepter();}else{Handler_IO(i);}}if(revents&POLLOUT){//写就绪}}}void Loop(){while (1){// 3.调用selectstruct timeval timeout = {3, 0};// 由于select是一个输入输出型参数,所以必须要有一个辅助数组来保存fd信息,用来重置参数int n = ::poll(_events,gnum,_timeout);switch (n){case 0:LOG(DEBUG, "time out\n");break;case -1:LOG(ERROR, "select error\n");break;default:// // 如果事件已经就绪了,但是我没有做处理,则底层会一直通知我,告诉我有文件描述符就绪了,所以下一次调用select就不会再判断了,直接通知LOG(INFO, "have event ready,n: %d\n", n);// 处理HandlerEvent();PrintDebug();break;}// sleep(1);}}void PrintDebug(){std::cout << "fd list:" << std::endl;for (int i = 0; i < gnum; i++){if (_events[i].fd=gdefaultfd){continue;}std::cout << _events[i].fd << " ";std::cout << std::endl;}}private:uint16_t _port;std::unique_ptr<Socket> _listensock;struct pollfd _events[gnum];int _timeout;
};