二、poll
2.1 poll函数
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数说明:
fds:一个poll函数监视的结构列表,每一个元素包含三部分内容:文件描述符、监视的事件集合、就绪的事件集合
nfds:表示fds数组的长度
timeout:表示poll函数的超时时间,单位是毫秒(ms)
参数timeout的取值:
-1:poll调用后进行阻塞等待,直到被监视的某个文件描述符上的某个事件就绪
0:poll调用后进行非阻塞等待,无论被监视的文件描述符上的事件是否就绪,poll检测后都会立即返回
特定的时间值:poll调用后在指定的时间内进行阻塞等待,若被监视的文件描述符上没有事件就绪,则在该时间后poll进行超时返回
返回值说明:
若函数调用成功,则返回有事件就绪的文件描述符个数
若timeout时间耗尽,则返回0
若函数调用失败,则返回-1,同时错误码被设置
poll调用失败时,错误码可能被设置为:
EFAULT:fds数组不包含在调用程序的地址空间中
EINTR:此调用被信号所中断
EINVAL:nfds值超过RLIMIT_NOFILE值
ENOMEM:核心内存不足
struct pollfd结构
fd:特定的文件描述符,若设置为负值则忽略events字段并且revents字段返回0
events:需要监视该文件描述符上的哪些事件
revents:poll函数返回时告知用户该文件描述符上的哪些事件已经就绪
events和revents的取值:
这些值都以宏的方式定义,二进制序列中有且只有一个bit位是1,且为1的bit位各不相同
在调用poll函数之前,可以通过或运算符将要监视的事件添加到events成员中
在poll函数返回后,可以通过与运算符检测revents成员中是否包含特定事件,以得知对应文件描述符的特定事件是否就绪
2.2 poll服务器
poll的工作流程和select基本类似,下面也实现一个简单poll服务器,只读取客户端发来的数据并进行打印
PollServer类
#ifndef __POLL_SVR_H__ #define __POLL_SVR_H__ #include <iostream> #include <string> #include <poll.h> #include "Socket.hpp" #include "Log.hpp" #include <unistd.h> #include <cstring> #include <cerrno> using namespace std; #define FD_NONE -1 // 只完成读取,写入和异常不做处理 class PollServer { public: PollServer(const nfds_t nfds, const uint16_t &port = 9090) : _port(port), _nfds(nfds), _fds(nullptr) { _listenSocketFd = Socket::SocketCreate(); Socket::Bind(_listenSocketFd, _port); Socket::Listen(_listenSocketFd); LogMessage(DEBUG, "create base socket success"); _fds = new struct pollfd[_nfds]; _fds[0].fd = _listenSocketFd; _fds[0].events = POLLIN; for(int i = 1; i < _nfds; ++i) { _fds[i].fd = FD_NONE; _fds[i].events = _fds[i].revents = 0; } _timeout = 1000; } ~PollServer() { if (_listenSocketFd > 0) close(_listenSocketFd); if (_fds != nullptr) delete[] _fds; } public: void Start() { while (true) { DebugPrint(); int number = poll(_fds, _nfds, _timeout); switch (number) { case 0: LogMessage(DEBUG, "%s", "Time Out ..."); break; case -1: LogMessage(WARNING, "Poll Fail: %d : %s", errno, strerror(errno)); break; default: HandlerEvent(); break; } } } private: void Accepter() { string clientIp; uint16_t clientPort = 0; int socketfd = Socket::Accept(_listenSocketFd, &clientIp, &clientPort); if (socketfd < 0) { LogMessage(ERROR, "accept error"); return; } LogMessage(DEBUG, "Get a link success : [%s : %d] , socketFd : %d", clientIp.c_str(), clientPort, socketfd); int pos = 1; for (; pos < _nfds; ++pos) if (_fds[pos].fd == FD_NONE) break; if (pos == _nfds) { // 满了 //可以进行自动扩容 LogMessage(ERROR, "%s:%d", "PollServer already full, close:", socketfd); close(socketfd); } else { // 找到空位置 _fds[pos].fd = socketfd; _fds[pos].events = POLLIN; } } void Recver(int i) { LogMessage(DEBUG, "message in , get IO event:%d", _fds[i].fd); char buffer[1024]; int num = recv(_fds[i].fd, buffer, sizeof(buffer) - 1, 0); if(num > 0) { buffer[num] = 0; LogMessage(DEBUG, "client[%d]#%s", _fds[i].fd, buffer); } else if(num == 0) { LogMessage(DEBUG, "client[%d] link close, me too...", _fds[i].fd); close(_fds[i].fd); _fds[i].fd = FD_NONE; _fds[i].events = _fds[i].revents = 0; } else { LogMessage(WARNING, "%d recv error, %d : %s", _fds[i].fd, errno, strerror(errno)); close(_fds[i].fd); _fds[i].fd = FD_NONE; _fds[i].events = _fds[i].revents = 0; } } void HandlerEvent() { for (int i = 0; i < _nfds; ++i) { // 去掉不合法的fd if (_fds[i].fd == FD_NONE) continue; // 判断是否就绪 if (_fds[i].revents & POLLIN) { if (_fds[i].fd == _listenSocketFd) Accepter(); //链接事件 else Recver(i);// 读事件 } } } void DebugPrint() { cout << "fds[]:"; for(int i = 0; i < _nfds; ++i) { if(_fds[i].fd == FD_NONE) continue; cout << _fds[i].fd << " "; } cout << endl; } private: uint16_t _port; int _listenSocketFd; struct pollfd* _fds; nfds_t _nfds = 100; int _timeout; }; #endif
_fds数组的大小是固定设置的,因此在将新获取连接对应的文件描述符添加到fds数组时,可能会因为fds数组已满而添加失败,这时poll服务器只能将刚刚获取上来的连接对应的套接字进行关闭
poll服务器测试
在调用poll函数时,将timeout的值设置成1000,因此运行服务器后每隔1000毫秒没有客户端发来连接请求,那么服务器就会超时返回
用telnet工具连接poll服务器后,poll函数在检测到监听套接字的读事件就绪后就会调用accept获取建立好的连接,并打印输出客户端的IP和端口号等信息,此时客户端发来的数据也能成功被poll服务器收到并进行打印输出
poll服务器也是单进程、单线程服务器,同样可以为多个客户端服务
当服务器端检测到客户端退出后,也会关闭对应的连接,并将对应的套接字从_fds数组中清除
2.3 poll的优点 && 缺点
优点
struct pollfd结构中包含了events和revents,相当于将select的输入输出型参数进行分离,因此在每次调用poll之前,不需像select一样重新对参数进行设置
poll可监控的文件描述符数量没有限制
poll也可以同时等待多个文件描述符,提高IO效率
说明一下:
虽然代码中将_fds数组的元素个数定义为100,但_fds数组的大小可以增大,poll函数能监视多少文件描述符由poll函数的第二个参数决定
而fd_set类型只有1024个bit位,因此select函数最多只能监视1024个文件描述符
缺点
和select函数一样,当poll返回后,需要遍历_fds数组来获取就绪的文件描述符
每次调用poll,都需将大量struct pollfd结构从用户态拷贝到内核态,这个开销会随着poll监视的文件描述符数目增多而增大
同时每次调用poll都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大