📢博客主页:https://blog.csdn.net/2301_779549673
📢博客仓库:https://gitee.com/JohnKingW/linux_test/tree/master/lesson
📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!
📢本文由 JohnKi 原创,首发于 CSDN🙉
📢未来很长,值得我们全力奔赴更美好的生活✨
文章目录
- 🏳️🌈一、什么是 POLL
- 🏳️🌈二、poll 函数接口
- 🏳️🌈三、poll 的优缺点
- 🏳️🌈四、模拟 poll
- 4.1 PollServer 类
- 4.1.1 基本结构
- 4.1.2 构造函数、析构函数
- 4.1.3 初始化函数 InitServer()
- 4.1.4 循环函数 Loop()
- 4.1.5 处理函数 HandlerEvent()
- 4.1.6 连接建立处理函数 HandlerNewConnection()
- 4.1.7 普通链接建立处理函数 HandlerIO()
- 4.2 主函数 PollServer.cpp
- 4.3 运行结果
- 🏳️🌈五、整体函数
- 5.1 PollServer.hpp
- 5.2 PollServer.cpp
- 👥总结
🏳️🌈一、什么是 POLL
上一篇文章中我们介绍了IO多路转接中的 select ,但是 select
存在4个明显的缺点
- 每次调用
select
,都需要手动设置 fd 集合,从接口使用角度来说是非常不便的 - 每次调用
select
,都需要把 fd 集合从用户态拷贝到内核态,这个开销在fd很多时是很大的 - 每次调用
select
,都需要在内核遍历传递进来的所有 fd,这个开销很大 select
支持的文件描述符数量太小
而这篇文章中介绍的 poll 将有效地解决上述两个问题 (1和4)
- 重新设定对 fd 和 关心的事件
- poll 等待的fd无上限
概念
poll
函数用于监视多个文件描述符以查看它们是否有 I/O(输入/输出)活动。
- 作用:为了等待多个fd,等待fd上面的新事件就绪,通知程序员,事件已经就绪,可以进行IO拷贝了!
- 定位:只负责进行等,等就绪事件派发!
🏳️🌈二、poll 函数接口
poll 函数
#include <poll.h>int poll(struct pollfd* fds, nfds_t nfds, int timeout);
fds
:指向 pollfd 结构体数组的指针,每个结构体指定一个要监视的文件描述符 及 要监视的事情nfds
:数组 fds 中包含的结构体数量,即要监视的文件描述符数量timeout
:超时时间(毫秒),-1 表示无限等待,0 表示立即返回,其他值表示等待指定的时间(毫秒)返回值
:成功时返回活跃的文件描述符数量,失败时返回 -1 并设置 errno
pollfd 结构体
struct pollfd{int fd; short events;short revents;
};
events 和 revents 的取值:
🏳️🌈三、poll 的优缺点
优点
不同于 select 使用三个位图来表示三个 fdset
的方式,poll 使用一个 pollfd
的指针实现.
pollfd
结构包含了要监视的 event 和发生的 event,不再使用 select“参数-值”传递的方式. 接口使用比 select 更方便.poll
并没有最大数量限制 (但是数量过大后性能也是会下降).
缺点
poll
中监听的文件描述符数目增多时
- 和 select 函数一样,poll 返回后,需要轮询 pollfd 来获取就绪的描述符.
- 每次调用 poll 都需要把大量的 pollfd 结构从用户态拷贝到内核中.
- 同时连接的大量客户端在一时刻可能只有很少的处于就绪状态, 因此随着监视的描述符数量的增长, 其效率也会线性下降.
🏳️🌈四、模拟 poll
4.1 PollServer 类
4.1.1 基本结构
PollServer类的成员变量与SelectServer类的成员变量基本一致,但是此处的数组(存放fd)类型是struct pollfd,还需要端口号和套接字
#include <iostream>
#include <poll.h>
#include "Socket.hpp"using namespace SocketModule;class PollServer{const static int gnum = sizeof(fd_set) * 8;const static int gdefault = -1;public:PollServer(uint16_t port);void InitServer();void HandlerNewConnection();void HandlerIO();void HandlerEvent();void Loop();void PrintDebug();~PollServer();private:uint16_t _port;SockPtr _listensock;struct pollfd fd_events[gnum];
};
4.1.2 构造函数、析构函数
基本不变,与 select一样
PollServer(uint16_t port): _port(port), _listensock(std::make_shared<TcpSocket>()) {_listensock->BuildListenSocket(_port);
}
~PollServer() {}
4.1.3 初始化函数 InitServer()
InitServer()
函数将结构体类型的数组fd成员设置为默认fd,其他两个事件先设置为0,
并将listensockfd添加到结构体数组的第一个元素的fd成员,并将events事件设置为读!
void InitServer() {for (int i = 0; i < gnum; ++i) {fd_events[i].fd = gdefault;fd_events[i].events = 0;fd_events[i].revents = 0;}fd_events[0].fd = _listensock->Sockfd();fd_events[0].events = POLLIN; // POLLIN: 可读
}
4.1.4 循环函数 Loop()
Loop()函数调用poll系统调用,根据返回值执行对应的操作:
-
返回值为0 :打印超时日志,并退出循环
-
返回值为-1 :打印出错日志,并退出循环
-
返回值大于0 :处理事件
void Loop() {while (true) {int timeout = 1000;int n = poll(fd_events, gnum, timeout);switch (n) {case 0:LOG(LogLevel::DEBUG) << "poll timeout";break;case -1:LOG(LogLevel::ERROR) << "poll error";break;default:LOG(LogLevel::INFO) << "haved fd ready , " << n;HandlerEvent();PrintDebug();break;}}
}
4.1.5 处理函数 HandlerEvent()
在执行
HandlerEvent()
函数之前,赋值数组中一定存在大量的fd就绪,可能是普通sockfd,也可能是listensockfd,此处主要分以下两步:
- 判断fd是否合法
- 判断fd是否就绪
2.1. 就绪是listensockfd
,调用Accepter()
处理新链接函数
2.2. 就绪是normal sockfd
,调用HandlerIO()
处理普通fd就绪函数
void HandlerEvent() {for (int i = 0; i < gnum; ++i) {// 1. 判断 fd 是否合法if (fd_events[i].fd == gdefaultfd)continue;// 2. 判断 fd 是否就绪// 必须使用 & 运算符,否则会出现错误if (fd_events[i].revents & POLLIN) {// 读事件就绪if (_listensock->Sockfd() == fd_events[i].fd) {HandlerNewConnection();}// 其他事件就绪else {HandlerIO(i);}}}
}
4.1.6 连接建立处理函数 HandlerNewConnection()
Accepter()
函数处理新链接,主要分为以下三步:
1、获取链接
2、获取链接成功将新的fd 和 读事件 添加到数组中
3、数组满了,需关闭sockfd,此处可以扩容并再次添加新的fd和事件
void HandlerNewConnection() {InetAddr client;// 这个时候一定不会阻塞,因为监听套接字已经就绪了int sockfd = _listensock->Accepter(&client);if (sockfd > 0) {LOG(LogLevel::DEBUG)<< "get a new connection from " << client.AddrStr().c_str()<< ", sockfd : " << sockfd;bool flag = false;for (int pos = 1; pos < gnum; ++pos) {if (fd_events[pos].fd == gdefaultfd) {fd_events[pos].fd = sockfd;fd_events[pos].events = POLLIN; // POLLIN: 可读LOG(LogLevel::DEBUG)<< "set fd_events[" << pos << "] to " << sockfd;break;}}// 数组满了if (!flag) {LOG(LogLevel::WARNING) << "fd_events is full";::close(sockfd);// 扩容// 添加}}
}
4.1.7 普通链接建立处理函数 HandlerIO()
HandlerIO()
函数处理普通fd情况,直接读取文件描述符中的数据,根据recv()函数的返回值做出不一样的决策,主要分为以下三种情况:
- 返回值大于0,读取文件描述符中的数据,并使用
send()
函数做出回应! - 返回值等于0,读到文件结尾,打印客户端退出的日志,关闭文件描述符,并将该下标的文件描述符设置为默认
fd
,事件都设置为0 - 返回值小于0,读取文件错误,打印接受失败的日志,然后同上!
void HandlerIO(int i) {char buffer[1024];// 这里同样不会被阻塞,因为是就绪后才会执行ssize_t n = ::recv(fd_events[i].fd, buffer, sizeof(buffer), 0);if (n > 0) {buffer[n] = 0;std::cout << "client say# " << buffer << std::endl;std::string content = "<html><body><h1>hello linux</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(LogLevel::DEBUG) << "client " << fd_events[i].fd << " closed";::close(fd_events[i].fd);fd_events[i].fd = gdefaultfd;fd_events[i].events = 0;fd_events[i].revents = 0;} else {LOG(LogLevel::ERROR) << "recv error";::close(fd_events[i].fd);fd_events[i].fd = gdefaultfd;fd_events[i].events = 0;fd_events[i].revents = 0;}
}
4.2 主函数 PollServer.cpp
#include "PollServer.hpp"int main(int argc, char* argv[]){if(argc != 2){std::cerr << "Usage: " << argv[0] << " local-port" << std::endl;exit(0);}uint16_t port = std::stoi(argv[1]);std::unique_ptr<PollServer> svr = std::make_unique<PollServer>(port);svr->InitServer();svr->Loop();return 0;
}
4.3 运行结果
🏳️🌈五、整体函数
5.1 PollServer.hpp
#pragma once#include <iostream>
#include <poll.h>
#include "Socket.hpp"using namespace SocketModule;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_shared<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; // POLLIN: 可读}void HandlerNewConnection(){InetAddr client;// 这个时候一定不会阻塞,因为监听套接字已经就绪了int sockfd = _listensock->Accepter(&client);if(sockfd > 0){LOG(LogLevel::DEBUG) << "get a new connection from " << client.AddrStr().c_str() << ", sockfd : " << sockfd;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; // POLLIN: 可读LOG(LogLevel::DEBUG) << "set fd_events[" << pos << "] to " << sockfd;break;}}//数组满了if(!flag){LOG(LogLevel::WARNING) << "fd_events is full" ;::close(sockfd);// 扩容// 添加}}}void HandlerIO(int i){char buffer[1024];// 这里同样不会被阻塞,因为是就绪后才会执行ssize_t n = ::recv(fd_events[i].fd, buffer,sizeof(buffer), 0);if(n > 0){buffer[n] = 0;std::cout << "client say# " << buffer << std::endl;std::string content = "<html><body><h1>hello linux</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(LogLevel::DEBUG) << "client " << fd_events[i].fd << " closed";::close(fd_events[i].fd);fd_events[i].fd = gdefaultfd;fd_events[i].events = 0;fd_events[i].revents = 0;} else{LOG(LogLevel::ERROR) << "recv error";::close(fd_events[i].fd);fd_events[i].fd = gdefaultfd;fd_events[i].events = 0;fd_events[i].revents = 0;}}void HandlerEvent(){for(int i = 0; i < gnum; ++i){// 1. 判断 fd 是否合法if(fd_events[i].fd == gdefaultfd)continue;// 2. 判断 fd 是否就绪// 必须使用 & 运算符,否则会出现错误if(fd_events[i].revents & POLLIN){// 读事件就绪if(_listensock->Sockfd() == fd_events[i].fd){HandlerNewConnection();}// 其他事件就绪else{HandlerIO(i);}}}}void Loop(){while(true){int timeout = 1000;int n = poll(fd_events, gnum, timeout);// 返回 ·0 表示超时,返回-1 表示出错switch(n){case 0:LOG(LogLevel::DEBUG) << "poll timeout";break;case -1:LOG(LogLevel::ERROR) << "poll error";break;default:// 如果事件就绪,但是不处理,select就会一直通知我,知道我处理了LOG(LogLevel::INFO) << "haved fd ready , " << n;HandlerEvent();PrintDebug();sleep(1);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;SockPtr _listensock;struct pollfd fd_events[gnum];
};
5.2 PollServer.cpp
#include "PollServer.hpp"int main(int argc, char* argv[]){if(argc != 2){std::cerr << "Usage: " << argv[0] << " local-port" << std::endl;exit(0);}uint16_t port = std::stoi(argv[1]);std::unique_ptr<PollServer> svr = std::make_unique<PollServer>(port);svr->InitServer();svr->Loop();return 0;
}
👥总结
本篇博文对 【Linux网络】I/O多路转接技术 - poll 做了一个较为详细的介绍,不知道对你有没有帮助呢
觉得博主写得还不错的三连支持下吧!会继续努力的~