select 和 poll 都是 POSIX 标准规定的多路复用 IO 接口函数,它们都能够让程序同时监视多个文件描述符(如套接字、管道和文件等)的状态,并在有至少一个文件描述符就绪时通知应用程序进行相应的 IO 操作。
尽管 select 和 poll 都实现了多路复用 IO 的功能,但它们在一些细节上有所不同。具体来说,select 和 poll 之间的区别主要体现在以下几个方面:
API设计:select和poll在API设计上略有不同,例如select使用fd_set类型的集合来传递要监听的文件描述符,而poll则使用pollfd结构体数组来传递文件描述符信息。- 可扩展性:
select的文件描述符集合大小通常由系统定义的FD_SETSIZE宏限制,而poll做到了没有这样的限制。因此,poll的可扩展性更好,能够支持更多的文件描述符。 - 效率:
select的实现通常是使用轮询方式遍历整个描述符集合,当描述符集合中的某个描述符就绪时才返回;而poll的实现则使用链表管理描述符,只需要遍历已经就绪的描述符,因此poll的效率要稍微高一些。
需要注意的是,虽然 poll 在某些方面比 select 更优秀,但它的可移植性不如 select。select 已经成为了 POSIX 标准定义的接口,而 poll 目前还没有广泛采用,在某些操作系统上甚至不支持。
而下面的学习中,我们也只是稍微的了解一下 poll 即可,最重要的还是后面我们要学的 epoll,所以下面的学习包括代码,我们是从之前写的 select 代码中进行修改的!
Ⅱ. poll函数
1、函数原型
poll 系统调用的功能和 select 类似,也是在指定时间内轮询一定数量的文件描述符,以测试其是否有就绪事件。
其原型如下所示:
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);-
fds:
-
它是一个
pollfd结构类型的数组,它指定所有我们感兴趣的文件描述符上发生的可读、可写、异常等事件。其结构体定义如下所示:struct pollfd { int fd; /* 文件描述符 */ short events; /* 用户设置的感兴趣的事件 */ short revents; /* 系统返回的就绪事件 */ };fd变量指定文件描述符。就像我们之前所学的select中的_array[i]。events变量告诉poll要监听fd上的哪些事件,它是一系列事件的按位或。revents变量则由内核填充,以通知应用层fd上实际发生了哪些事件。- 也就是说我们不需要像
select一样每次都要在调用前重新设置监听集合了,现在有了pollfd这个结构体中的两个事件变量我们就能单独对其操作而不需要每次都去重新设置!
-
其中
events和revents的取值如下所示:
其中我们常用的就是
POLLIN、POLLOUT等事件!- 通常,应用层需要根据
recv函数的返回值来区分套接字上接收到的是有效数据还是对方关闭连接的请求,并做对应的处理。不过从linux2.6.17开始,GNU为poll函数增加了一个POLLRDHUP事件,它在套接字上接收到对方关闭连接的请求之后触发,这为我们区分上述两种情况提供了一种更简单的方式,但使用POLLRDHUP事件时,需要在代码最开始处定义_GUN_SOURCE。
- 通常,应用层需要根据
-
-
nfds:
-
指定被监听事件集合
fds的大小。其类型nfds_t的定义如下:typedef unsigned long int nfds_t;
-
-
timeout:
- 表示函数的超时时间,单位是毫秒。
timeout = -1,则poll函数将永远阻塞,直到某个事件发生。timeout = 0,则poll函数将立刻返回。timeout为具体时间,则如果在该时间段内没有就绪事件的话,则会超时返回0。
- 表示函数的超时时间,单位是毫秒。
-
返回值(与
select返回值是一样的):- 小于
0,表示poll函数出错。 - 等于
0,表示poll函数等待超时。 - 大于
0,表示poll函数由于监听的文件描述符就绪而返回。
- 小于
2、文件描述符的就绪条件
poll 函数和 select 函数的文件描述符就绪条件是一样的,这里直接复制:
可读就绪条件:
- 如果
socket内核的接收缓冲区中的字节数,大于等于其低水位标记SO_RCVLOWAT,此时可以无阻塞的读取该文件描述符,并且返回值大于0。- 在进行
TCP通信时候,如果对方关闭连接,则该文件描述符返回0。- 监听的文件描述符上有新的连接请求。
- 文件描述符中有未处理的错误,此时我们可以使用
getsockopt来读取和清除该错误。
可写就绪条件:
- 如果
socket内核的发送缓冲区中的可用字节数,大于等于低水标记SO_SNDLOWAT,此时可以无阻塞的写该文件描述符,并且返回值大于0。socket的写操作被关闭的时候(比如close或者shutdown)会触发SIGPIPE信号。socket使用非阻塞connect连接成功或失败(超时)之后。- 文件描述符中有未读取的错误,此时我们可以使用
getsockopt来读取和清除该错误。
异常就绪条件:
- 网络程序中,
select能处理的异常情况只有一种:socket上接收到带外数据。
3、poll的优点
poll 相对于 select 有如下优势:
pollfd结构包含了要监视的events和发生的revents,也就是说对于监听集合的输入输出是分离的,不需要我们在调用poll函数之前重新对参数进行设置,不再使用select“参数-值” 传递的方式,接口使用比select更方便。poll函数的第二个参数就解决了select函数中文件描述符的上限问题。(但是数量过大后性能也是会下降)
4、poll的缺点
当 poll 中监听的文件描述符数目增多时,其会有如下缺点:
- 和
select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。而同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。 - 每次调用
poll都需要把大量的pollfd结构从用户态拷贝到内核中。
Ⅲ. 编写poll代码
这里的代码准备,其实和之前我们写 select 是一样的,只不过需要改一下服务器的名称!具体的可以参考 select 笔记,这里最大的区别,是主服务器的头文件,所以下面我们只将服务器头文件的修改!
首先是头文件的主体框架,我们使用数组来维护 struct pollfd 结构体,并且做一些初始化,其中 pollfd 数组的长度我们这里设为 4096,但其实这个长度是可以不受限制的,可以改为动态增容版的,但是这里就不这样子处理了!
#pragma once
#include <iostream>
#include <poll.h>
#include "sock.hpp"
namespace poll_space
{
const static int default_port = 8080; // 服务器默认端口号
const static int free_num = -1; // 监听集合中空闲位置的默认值
const static nfds_t pollfd_size = 4096; // pollfd数组的长度
class poll_server
{
private:
int _listensock;
int _port;
struct pollfd* poll_array; // 存放pollfd结构体的数组
public:
poll_server(int port = default_port)
: _port(port), _listensock(-1), poll_array(nullptr)
{}
~poll_server()
{
if(_listensock != -1)
close(_listensock);
if(poll_array)
delete[] poll_array;
}
void print() {}
void Accepter() {} // 获取新链接函数
void Receiver(int pos) {} // 处理交互函数
void handler() {} // 获取新链接以及交互的处理函数
void init() {} // 初始化服务器
void run() {} // 运行服务器
};
} 接着就是初始化 init 函数,就是创建套接字、绑定套接字、监听套接字,然后初始化 pollfd 数组,不要忘了最后还要将 _listensock 先给初始化到数组中去,因为它是来负责监听新链接的!
void init()
{
_listensock = sock::Socket();
sock::Bind(_listensock, _port);
sock::Listen(_listensock);
// 初始化数组
poll_array = new struct pollfd[pollfd_size];
for(int i = 0; i < pollfd_size; ++i)
{
poll_array[i].fd = free_num;
poll_array[i].events = 0;
poll_array[i].revents = 0;
}
poll_array[0].fd = _listensock;
poll_array[0].events = POLLIN; // 设置为关心可读事件
} run 运行函数就是调用 poll 函数去进行多路转接,这里我们设置超时时间为两秒,其返回值的操作和 select 是一样的,并且我们可以看出,poll 调用是比 select 要简洁的多的!
void run()
{
while(true)
{
int n = poll(poll_array, pollfd_size, 2000); // 2秒超时时间
if(n == 0)
logMessage(Level::NORMAL, "timeout...");
else if(n == -1)
logMessage(Level::ERROR, "poll error, code: %d, err string: %s", errno, strerror(errno));
else
{
// 说明有事件就绪了
logMessage(Level::NORMAL, "有事件就绪了,一共有%d个", n);
handler();
sleep(1);
}
}
} 然后就是处理函数 handler() 了,和 select 一样,需要遍历处理数组中已经维护文件描述符判断是否有需要处理的就绪事件,这也可以看出来 poll 其实没有改善这个需要遍历集合的缺点!
接着筛选掉不符合的文件描述符,剩下的就判断是否为 _listensock,是的话让其执行 Accepter() 获取新链接,不是的话执行 Receiver() 进行交互处理。
只是在筛选的过程,和 select 不太一样,我们需要对 events 和 revents 进行按位与操作,来看看是否为 POLLIN 可读事件(我们这里只关心可读事件),如果不是的话,则过滤掉!
void handler()
{
// 和select一样,需要遍历处理数组中已经维护文件描述符判断是否有需要处理的就绪事件
for(int i = 0; i < pollfd_size; ++i)
{
// 过滤掉不符合的fd,即不存在、events不是可读事件、revents没就绪,则过滤掉
if (poll_array[i].fd == free_num ||
!(poll_array[i].events & POLLIN) ||
!(poll_array[i].revents == POLLIN))
continue;
// 走到这里一定是一个存在且就绪的可读事件!
if(poll_array[i].fd == _listensock)
Accepter();
else
Receiver(i);
}
} 接下来就是 Accepter() 函数,其实大体思路和 select 都是一样的,只不过修改了几个变量,并且在设置新连接进数组的时候,别忘了将其 events 设置为 POLLIN 可读事件,其它的没啥需要关注的!
void print()
{
std::cout << "当前数组中的fd有:";
for(int i = 0; i < pollfd_size; ++i)
if(poll_array[i].fd != free_num)
std::cout << poll_array[i].fd << " ";
std::cout << std::endl;
}
void Accepter()
{
// 1. 获取新链接
std::string clientip;
uint16_t clientport;
int newfd = sock::Accept(_listensock, &clientip, &clientport);
if(newfd < 0)
exit(ACCEPT_ERR);
// 2. 将新链接维护到数组中
// 2.1 首先找到数组中空闲的位置
int index = 0;
for(; index < pollfd_size; ++index)
if(poll_array[index].fd == free_num)
break;
// 2.2 若没有空闲位置,则关闭新链接,并且返回
if(index == pollfd_size)
{
close(newfd);
logMessage(ERROR, "数组中没有空闲位置,无法建立新链接!"); // 其实也可以搞成动态数组,这里就不弄了
return;
}
// 2.3 找到空闲位置则直接设置进数组即可,顺便打印一下数组的内容
poll_array[index].fd = newfd;
poll_array[index].events = POLLIN;
poll_array[index].revents = 0;
print();
} Receiver() 函数也是一样的,修改几个变量就搞定了,并且在关闭连接同时记得将文件描述符从数组中去除!
void Receiver(int pos)
{
// 1. 读取数据
// 目前我们不做自定义协议,当前我们认为能接收到一个完整的报文
char buffer[1024];
memset(buffer, 0, sizeof buffer);
ssize_t n = recv(poll_array[pos].fd, buffer, sizeof(buffer) - 1, 0);
if(n > 0)
{
buffer[n] = 0;
logMessage(NORMAL, "接收内容:%s", buffer);
}
else if(n == 0)
{
// 关闭同时记得将文件描述符从数组中去除
close(poll_array[pos].fd);
poll_array[pos].fd = free_num;
poll_array[pos].events = poll_array[pos].revents = 0;
logMessage(NORMAL, "客户端关闭连接");
return;
}
else
{
// 关闭同时记得将文件描述符从数组中去除
close(poll_array[pos].fd);
poll_array[pos].fd = free_num;
poll_array[pos].events = poll_array[pos].revents = 0;
logMessage(ERROR, "读写失败,错误码:%d,错误原因:%s", errno, strerror(errno));
return;
}
// 2. 业务处理(这里就不演示了,到后面epoll一起讲)
// 3. 响应数据,这里直接返回读取的数据
std::string response = std::string("响应: ") + std::string(buffer);
send(poll_array[pos].fd, response.c_str(), response.size(), 0);
}运行结果
至此 poll 服务器的头文件就写完啦,大体思路可以发现和 select 是一样的,只不过调用方式需要修改!下面我们来运行一下代码:
