Linux多路转接poll
1. poll()
poll()
结构包含了要监视的 event 和发生的 event ,接口使用比 select()
更方便。且 poll 并没有最大数量限制(但是数量过大后性能也是会下降)。
2. poll() 的工作原理
poll()
不再需要像 select()
那样自行设置文件描述符集合,它只需要用户在 pollfd
结构体中设置文件描述符及其关心的事件(events
),在输出时,结构体内的 revents
作为函数调用后事件就绪的结果, 就 select()
与poll()
的不同而言, poll()
支持用户自定义关心某些事件,同时将事件就绪的结构用不同的变量保存起来。
poll()
会返回就绪文件描述符的数量,用户需要自行设置判断条件,遍历pollfd
结构体中的events
,将其与自己关心的事件按位与(&
),然后执行 recv()
或其他函数读取或进行其他操作。
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,也可能是listensockfdvoid 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()
的底层,也需要操作系统遍历所有的文件描述符,来获取就绪的文件描述符和它的事件,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。