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
系统调用的功能和 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
函数由于监听的文件描述符就绪而返回。 poll
函数和 select
函数的文件描述符就绪条件是一样的,这里直接复制:
可读 就绪条件:
socket
内核的接收缓冲区中的字节数,大于等于其低水位标记 SO_RCVLOWAT
,此时可以无阻塞的读取该文件描述符,并且返回值大于 0
。TCP
通信时候,如果对方关闭连接,则该文件描述符返回 0
。getsockopt
来读取和清除该错误。 可写 就绪条件:
socket
内核的发送缓冲区中的可用字节数,大于等于低水标记 SO_SNDLOWAT
,此时可以无阻塞的写该文件描述符,并且返回值大于 0
。socket
的写操作被关闭的时候(比如 close
或者 shutdown
)会触发 SIGPIPE
信号。socket
使用 非阻塞 connect
连接成功或失败(超时)之后。getsockopt
来读取和清除该错误。 异常 就绪条件:
select
能处理的异常情况只有一种:socket
上接收到带外数据。poll
相对于 select
有如下优势:
pollfd
结构包含了要监视的 events
和发生的 revents
,也就是说 对于监听集合的输入输出是分离的,不需要我们在调用 poll
函数之前重新对参数进行设置,不再使用 select
“参数-值” 传递的方式,接口使用比 select
更方便。poll
函数的第二个参数就 解决了 select
函数中文件描述符的上限问题。(但是数量过大后性能也是会下降)当 poll
中监听的文件描述符数目增多时,其会有如下缺点:
select
函数一样,poll
返回后,需要轮询 pollfd
来获取就绪的描述符。而同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。poll
都需要把大量的 pollfd
结构从用户态拷贝到内核中。 这里的代码准备,其实和之前我们写 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
是一样的,只不过调用方式需要修改!下面我们来运行一下代码: