【IO多路转接】深入解析 poll:从接口到服务器实现
文章目录
- 前言
- 一. poll的接口
- 二. poll服务器实现
- 2.1 对网络套接字进行封装
- 2.2 构建poll类
- 2.3 进行初始化
- 2.4 对任务进行派发
- 2.5 服务器主循环
- 三. poll相对于select的优势
前言
在高性能网络编程领域,IO 多路复用是应对高并发场景的核心技术之一 —— 它允许程序同时监控多个文件描述符(File Descriptor)的状态变化,从而高效处理多客户端的网络 IO 请求,解决了传统阻塞 IO 在高并发下效率低下的问题。poll 作为 IO 多路复用的经典实现机制,在 Linux 等操作系统中被广泛应用,是理解高并发服务器设计的重要基础。
本文将围绕 poll 展开,从技术原理到实践实现,逐步讲解如何基于 poll 构建高效的网络服务器。首先会剖析 poll 的核心接口与工作机制,为后续实践打下理论基础;随后聚焦于服务器的具体实现:从网络套接字的封装入手,逐步完成 poll 类的构建、初始化流程设计、任务派发策略,以及服务器主循环的实现(这部分是服务器 “持续工作” 的核心逻辑);最后,还会对比 poll 与更早出现的 select 机制,阐释 poll 在技术上的优势与改进。
通过本文的讲解,希望读者能深入理解 poll 的工作逻辑,并掌握基于 poll 开发高并发服务器的方法,为后续探索更复杂的网络编程技术(如 epoll)奠定基础。
一. poll的接口
int poll(struct pollfd *fds , nfds_t nfds , int timeuot):
struct pollfd是操作系统内提供的一个数据结构,用来存储要进行管理的相关信息:
struct pollfd {int fd; // 要进行等待的文件描述符short events; // 要进行等待的事件,是读事件,写事件还是什么short revents; // 输出型参数,告诉用户那些事件已经就绪了
};
其中的fd可以设置为-1,表示该pollfd操作系统不需要进行处理。
- 参数一fds:告诉操作系统要对那些文件进行等待;
- 参数二nfds:一共要进行等待的文件个数;
- 参数三timeout:设置时间,时间到了/有事件就绪就返回;
- 返回值:表示有多少个事件已经就绪了,-1表示不进行等待。
二. poll服务器实现
此处我们仅仅是对poll服务器进行一个简单的实现,使用以下对应的接口,我们假设TCP接收时接收到的是一个完整的报文。
2.1 对网络套接字进行封装
首先我们先对网络套接字的接口进行封装:创建套接字,绑定,监听;关于这方面的知识可以查看之前的TCP相关内容,此时就直接贴实现方法:
const std::string defaultip_ = "0.0.0.0";
enum SockErr
{SOCKET_Err,BIND_Err,
};class Sock
{
public:Sock(uint16_t port): port_(port),listensockfd_(-1){}void Socket(){listensockfd_ = socket(AF_INET, SOCK_STREAM, 0);if (listensockfd_ < 0){Log(Fatal) << "socket fail";exit(SOCKET_Err);}Log(Info) << "socket sucess";}void Bind(){struct sockaddr_in server;server.sin_family = AF_INET;server.sin_port = htons(port_);inet_pton(AF_INET, defaultip_.c_str(), &server.sin_addr);if (bind(listensockfd_, (struct sockaddr *)&server, sizeof(server)) < 0){Log(Fatal) << "bind fail";exit(BIND_Err);}Log(Info) << "bind sucess";}void Listen(){if (listen(listensockfd_, 10) < 0){Log(Warning) << "listen fail";}Log(Info) << "listen sucess";}int Accept(){struct sockaddr_in client;socklen_t len = sizeof(client);int fd = accept(listensockfd_ , (sockaddr*)&client , &len);if(fd < 0){Log(Warning) << "accept fail";}return fd;}int Accept(std::string& ip , uint16_t& port){struct sockaddr_in client;socklen_t len = sizeof(client);int fd = accept(listensockfd_ , (sockaddr*)&client , &len);if(fd < 0){Log(Warning) << "accept fail";}port = ntohs(client.sin_port);char bufferip[64];inet_ntop(AF_INET , &client.sin_addr , bufferip , sizeof(bufferip) - 1);ip = bufferip;return fd;}int Get_fd(){return listensockfd_;}~Sock(){close(listensockfd_);}private:uint16_t port_;int listensockfd_;
};
2.2 构建poll类
- 设置一个上述网络套接字的类,对网络套接字接口进行封装;
- 设置一个数组来管理每一个要进行等待的文件描述符,此处可以直接使用
struct pollfd。
const int defaultfd = -1;
class Pollserver
{static const int fds_array_num = 1024; // 设置默认要进行等待的数组长度
public:Pollserver(uint port):_sock_ptr(new Sock(port)){for(int i = 0 ; i < fds_array_num ; i++){_fds_array[i].fd = defaultfd; }}private:std::shared_ptr<Sock> _sock_ptr; // 套接字结构体struct pollfd _fds_array[fds_array_num]; // 存储所有文件描述符相关信息
};
2.3 进行初始化
对于服务器的初始化,我们只进行一些简单的操作:
- 创建套接字;
- 绑定;
- 设置监听模式;
- 将网络套接字加入到等待数组中。
void AddToArray(int fd , short events){// 1. 找空位置// 2. 加入fdint pos = 0;for(; pos < fds_array_num && _fds_array[pos].fd != -1 ; pos++) ;if(pos == fds_array_num){// 数组不够了// 1. 打印日志信息// 2. 关闭文件描述符Log(Warning) << "array is full";close(fd);}else{// 加入fd_fds_array[pos].fd = fd;_fds_array[pos].events = events;Log(Info) << "get a connect , fd : " << fd; }}void Init(){// 1. 创建套接字// 2. 进行绑定// 3. 设置监听模式// 4. 将网络套接字加到_fds_array数组中_sock_ptr->Socket();_sock_ptr->Bind();_sock_ptr->Listen();AddToArray(_sock_ptr->Get_fd() , POLLIN);}
2.4 对任务进行派发
当poll进行等待的时候有文件描述符读写事件就绪,我们就需要进行处理。
此时我们使用一个Dispatcher函数对任务进行派发:
- 遍历整个
_fds_array数组,找文件描述符已经就绪的位置; - 判断对应的文件描述符是不是套接字;
- 是套接字将建立好的连接拿上来;
- 是普通文件描述符就对缓冲区进行读写操作。
void Sockfd_Ready(){int listensock = _sock_ptr->Get_fd();int newfd = _sock_ptr->Accept();AddToArray(newfd , POLLIN);}void Normalfd_Ready(int fd , int pos){char buffer[1024];int n = read(fd , buffer , sizeof(buffer) - 1);if(n > 0){buffer[n] = 0;std::string ret = "server get a message : ";;ret += buffer;write(fd , ret.c_str() , ret.size());}else if(n == 0){// 对方断开连接了// 1. 将文件描述符从等待的队列中移除// 2. 关闭文件_fds_array[pos].fd = -1;close(fd);}else{// 出错了, 打印日志信息Log(Warning) << "read fail";}}void Dispatcher(){int listensock = _sock_ptr->Get_fd();for(int i = 0 ; i < fds_array_num ; i++){int fd = _fds_array[i].fd;short eventds = _fds_array[i].revents;if(fd == defaultfd || !(eventds & POLLIN)) continue;if(fd == listensock) {Sockfd_Ready();}else{Normalfd_Ready(fd , i);}}}
2.5 服务器主循环
服务器的主循环只需要进行等待即可:
void Run(){while(1){int n = poll(_fds_array , fds_array_num , -1);if(n > 0){Dispatcher();}else if(n == 0){Log(Info) << " no file is ready";}else{Log(Error) << "poll fail";}}}
以上就是整个pollserver服务器类的整个实现逻辑了。
三. poll相对于select的优势
与select相比:
- poll等待的文件描述符的个数是没有限制的;
- poll将输入型参数与输出型参数进行分离,使得用户使用的时候不需要每次都进行设置。
但是与select一样,两者都需要对这个数组进行遍历进行检查,对于无效位置也要进行遍历,因此我们可以使用epoll进行优化。
class Pollserver
{static const int fds_array_num = 1024; // 设置默认要进行等待的数组长度
public:Pollserver(uint port):_sock_ptr(new Sock(port)){for(int i = 0 ; i < fds_array_num ; i++){_fds_array[i].fd = -1; }}private:std::shared_ptr<Sock> _sock_ptr; // 套接字结构体struct pollfd _fds_array[fds_array_num]; // 存储所有文件描述符相关信息
};
后续我们将进行epoll的讲解,来解决poll和select存在的问题。
