欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 房产 > 家装 > 【Linux网络】I/O多路转接技术 - poll

【Linux网络】I/O多路转接技术 - poll

2025/5/2 5:37:18 来源:https://blog.csdn.net/2301_77954967/article/details/147627305  浏览:    关键词:【Linux网络】I/O多路转接技术 - poll

📢博客主页: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个明显的缺点

  1. 每次调用 select都需要手动设置 fd 集合,从接口使用角度来说是非常不便的
  2. 每次调用 select都需要把 fd 集合从用户态拷贝到内核态,这个开销在fd很多时是很大的
  3. 每次调用 select都需要在内核遍历传递进来的所有 fd,这个开销很大
  4. select 支持的文件描述符数量太小

而这篇文章中介绍的 poll 将有效地解决上述两个问题 (1和4)

  1. 重新设定对 fd 和 关心的事件
  2. 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系统调用,根据返回值执行对应的操作:

  1. 返回值为0 :打印超时日志,并退出循环

  2. 返回值为-1 :打印出错日志,并退出循环

  3. 返回值大于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,此处主要分以下两步:

  1. 判断fd是否合法
  2. 判断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()函数的返回值做出不一样的决策,主要分为以下三种情况:

  1. 返回值大于0,读取文件描述符中的数据,并使用 send() 函数做出回应!
  2. 返回值等于0,读到文件结尾,打印客户端退出的日志,关闭文件描述符,并将该下标的文件描述符设置为默认fd,事件都设置为0
  3. 返回值小于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 做了一个较为详细的介绍,不知道对你有没有帮助呢

觉得博主写得还不错的三连支持下吧!会继续努力的~

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词