
半桔 :个人主页
🔥 个人专栏 : 《IO多路转接》《手撕面试算法》《C++从入门到入土》
🔖当你最认为困难的时候,其实就是你最接近成功的时候。《当幸福来敲门》
文章目录
- 前言
- [一. poll的接口](#一. poll的接口)
- [二. poll服务器实现](#二. poll服务器实现)
-
- [2.1 对网络套接字进行封装](#2.1 对网络套接字进行封装)
- [2.2 构建poll类](#2.2 构建poll类)
- [2.3 进行初始化](#2.3 进行初始化)
- [2.4 对任务进行派发](#2.4 对任务进行派发)
- [2.5 服务器主循环](#2.5 服务器主循环)
- [三. poll相对于select的优势](#三. 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是操作系统内提供的一个数据结构,用来存储要进行管理的相关信息:
cpp
struct pollfd {
int fd; // 要进行等待的文件描述符
short events; // 要进行等待的事件,是读事件,写事件还是什么
short revents; // 输出型参数,告诉用户那些事件已经就绪了
};
其中的fd可以设置为-1,表示该pollfd操作系统不需要进行处理。
- 参数一fds:告诉操作系统要对那些文件进行等待;
- 参数二nfds:一共要进行等待的文件个数;
- 参数三timeout:设置时间,时间到了/有事件就绪就返回;
- 返回值:表示有多少个事件已经就绪了,-1表示不进行等待。
二. poll服务器实现
此处我们仅仅是对poll服务器进行一个简单的实现,使用以下对应的接口,我们假设TCP接收时接收到的是一个完整的报文。
2.1 对网络套接字进行封装
首先我们先对网络套接字的接口进行封装:创建套接字,绑定,监听;关于这方面的知识可以查看之前的TCP相关内容,此时就直接贴实现方法:
cpp
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。
cpp
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 进行初始化
对于服务器的初始化,我们只进行一些简单的操作:
- 创建套接字;
- 绑定;
- 设置监听模式;
- 将网络套接字加入到等待数组中。
cpp
void AddToArray(int fd , short events)
{
// 1. 找空位置
// 2. 加入fd
int 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数组,找文件描述符已经就绪的位置; - 判断对应的文件描述符是不是套接字;
- 是套接字将建立好的连接拿上来;
- 是普通文件描述符就对缓冲区进行读写操作。
cpp
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 服务器主循环
服务器的主循环只需要进行等待即可:
cpp
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进行优化。
cpp
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存在的问题。