前言
在学习poll与epoll前,需要先了解select:
I/O 多路转接 之 poll
1. poll 函数原型
poll
函数-用于检查多个文件描述符的状态的函数,其原型如下:
#include <poll.h>
int poll(struct pollfd fds[], nfds_t nfds, int timeout);
struct pollfd fds[]
:是一个数组,用于指定要检查的文件描述符及其感兴趣的事件。nfds_t nfds
:是fds
数组中元素的数量。int timeout
:是超时时间,以毫秒为单位。如果设置为-1
,poll
函数将一直阻塞直到有事件发生;如果设置为0
,poll
函数将立即返回,否则poll
函数将等待指定的时间后返回。
struct pollfd
结构体定义如下:
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 感兴趣的事件 */
short revents; /* 实际发生的事件 */
};
fd
:是要检查的文件描述符。events
:是要监视的事件,可以是以下值的位掩码:POLLIN
:数据可读。POLLOUT
:数据可写。POLLPRI
:紧急数据可读。POLLERR
:发生错误。POLLHUP
:挂起事件。POLLNVAL
:文件描述符不是一个打开的文件。
revents
:是poll
函数返回时设置的实际发生的事件,它是events
的子集。
poll
函数会在指定的一组文件描述符上等待指定的事件发生,并将发生的事件保存在 revents
中。然后你可以检查每个文件描述符的 revents
字段来确定哪些事件发生了。
下面是events和revents的取值:
事件 | 含义 |
---|---|
POLLIN | 文件描述符中有数据可读取。 |
POLLPRI | 文件描述符中有紧急数据可读取。 |
POLLOUT | 文件描述符可以写入数据。 |
POLLERR | 文件描述符发生错误。 |
POLLHUP | 文件描述符挂起连接(例如,套接字关闭)。 |
POLLNVAL | 文件描述符不是一个打开的文件。 |
POLLWRNORM | 文件描述符可以普通写入数据。 |
POLLWRBAND | 文件描述符可以带外写入数据。 |
POLLRDHUP | 文件描述符挂起连接的一端关闭(例如,TCP连接的远程端关闭了连接的写半部)。 |
返回结果
- 返回值小于0, 表示出错;
- 返回值等于0, 表示poll函数等待超时;
- 返回值大于0, 表示poll由于监听的文件描述符就绪而返回
2. Socket 就绪条件
在使用套接字(Socket)进行网络编程时,套接字的就绪条件表示可以进行某种操作的条件,主要用于异步 I/O 操作,例如通过 select
、poll
、epoll
等函数实现的多路复用。
套接字的就绪条件通常包括以下几种:
-
读就绪(Read Ready):套接字缓冲区中有数据可供读取,即接收缓冲区中有数据到达,可以调用
recv
函数读取数据。 -
写就绪(Write Ready):套接字缓冲区有足够的空间可以写入数据,即发送缓冲区有足够的空间可以发送数据,可以调用
send
函数写入数据。 -
异常就绪(Exception Ready):套接字发生异常情况,如带外数据到达或者连接错误。这通常通过
select
或者poll
函数的异常集合来检查。 -
连接就绪(Connection Ready):套接字连接已经建立,可以进行数据交换。对于服务器套接字来说,连接就绪表示已经有客户端连接请求到达,可以调用
accept
函数接受连接。对于客户端套接字来说,连接就绪表示连接成功建立,可以进行数据交换。
这些就绪条件可以通过多路复用函数(如
select
、poll
、epoll
等)来监视,当套接字处于就绪状态时,这些函数会通知应用程序执行相应的操作。
3. poll 与 select 的不同(优缺点)
poll 与 select 大体还是很相似的,主要的优点在于其没有了文件描述符数量限制,下面还是列举给出poll的优缺点:
优点:
- 效率高
- 有大量连接,只有少量活跃
- 输入输出参数分离,不需要大量重置
- 没有文件描述符数量的限制(select 的fd最多是1024个,受限于fd_set)
缺点:
- poll依然需要大量遍历——在用户层检测事件就绪,在内核层检测fd就绪
- 用户依然需要维护数组
- 对于这点,下面会更进一步:epoll
- 依然需要“内核到用户,用户到内核”的拷贝
- 代码依然复杂(比select简洁一些:接口简洁性)
4. poll代码实例 - poll服务器
poll代码总体与select差别不大,其他文件都一致(Sock.hpp、Log.hpp)这里直接对Poll服务器代码进行编写:
成员变量:
private:
uint16_t _port; // 端口号
int _listensock; // 监听套接字
struct pollfd* _fds; // pollfd数组
int _nfds; // 设置的最大fd数量
int _timeout; // 设置超时时间(阻塞等待)
构造函数与析构函数:
public:
static const int nfds = 100; // 最大文件描述符个数
public:
// 构造:绑定并监听套接字
PollServer(const uint16_t port = 8080): _port(port), _nfds(nfds)
{
// 绑定监听
_listensock = Sock::Socket();
Sock::Bind(_listensock, _port);
Sock::Listen(_listensock);
logMessage(NORMAL, "%s", "create base socket success.");
// 初始化pollfd
_fds = new struct pollfd[_nfds];
for(int i = 0; i < _nfds; ++i)
{
_fds[i].fd = FD_NONE;
_fds[i].events = 0;
_fds[i].revents = 0;
}
_fds[0].fd = _listensock;
_fds[0].events = POLLIN;
_timeout = 1000;
}
~PollServer()
{
// 关闭套接字
if(_listensock != FD_NONE)
close(_listensock);
// 释放资源
if(_fds) delete []_fds;
}
启动函数:
void Start()
{
DebugPrint();
while(true)
{
int n = poll(_fds, _nfds, _timeout); // 等待事件
switch(n)
{
case 0: // 超时
logMessage(DEBUG, "%s", "time out...");
break;
case -1:
logMessage(WARNING, "poll error | %d : %s", errno, strerror(errno));
default:
logMessage(NORMAL, "poll success: get a new link event.");
HandlerEvent();
break;
}
}
}
DebugPrint():
- 调试目的打印当前文件描述符数组 _fds[] 中的有效文件描述符
void DebugPrint()
{
cout << "_fds[].fd: ";
for(int i = 0; i < _nfds; ++i)
{
if(_fds[i].fd != FD_NONE)
cout << _fds[i].fd << " ";
}
cout << endl;
}
HandlerEvent(事件处理函数):
// 处理事件
void HandlerEvent()
{
for(int i = 0; i < _nfds; ++i)
{
int& fd = _fds[i].fd;
// 去掉无效 fd
if(fd == FD_NONE) continue;
if(_fds[i].revents & POLLIN)
{
// 此时的 fd 读事件就绪
if(fd == _listensock) Accepter(); // 建立连接
else Recver(i); // 接收数据
}
}
}
Acceptrt(建立连接):
- 接受客户端的连接,并将连接的套接字添加到一个文件描述符数组中进行管理
void Accepter()
{
string clientIp;
uint16_t clientPort;
int sock = Sock::Accept(_listensock, &clientIp, &clientPort);
if(sock < 0){
logMessage(WARNING, "accept error | %d : %s", errno, strerror(errno));
return;
}
logMessage(NORMAL, "accept a new link from %s : %d", clientIp.c_str(), clientPort);
// 添加fd
int pos = 1;
for(; pos < _nfds; ++pos)
if(_fds[pos].fd == FD_NONE) break;
// 文件描述符存满了,扩容
if(pos == _nfds) {
logMessage(WARNING, "%s:%d", "poll server already full, close: %d", clientIp.c_str(), clientPort, sock);
close(sock);
} else {
_fds[pos].fd = sock;
_fds[pos].events = POLLIN;
// _fds[pos].revents = 0; // 已经初始化过
}
}
Recver():
- 处理接收到的数据或客户端断开连接的情况
void Recver(int pos)
{
// 此时读事件就绪,进行读取:
pollfd& pfd = _fds[pos];
logMessage(DEBUG, "message in, get IO event: %d", pfd);
char buffer[1024];
int n = recv(pfd.fd, buffer, sizeof(buffer) - 1, 0);
if(n > 0){ // 读取成功
buffer[n] = 0;
logMessage(NORMAL, "recv success, client[%d]# %s", pfd.fd, buffer);
} else if (n == 0) { // 对端关闭
logMessage(DEBUG, "client quit[%d], me too.", pfd.fd, buffer);
// 关闭该fd
close(pfd.fd);
// poll 不必再关心该fd
pfd.fd = FD_NONE;
pfd.events = 0;
} else { // 读取失败
logMessage(WARNING, "recv error[%d] | %d : %s", pfd.fd, errno, strerror(errno));
// 关闭fd
close(pfd.fd);
// poll不必再关心该fd
pfd.fd = FD_NONE;
pfd.events = 0;
}
}
对于上面的代码,验证是和SelectServer无异的,我们主要了解poll与select的不同以及优缺点,正如前面提到的:
- poll依然需要大量遍历——在用户层检测事件就绪,在内核层检测fd就绪
- 用户依然需要维护数组
为了避免这个问题,引入了一个新的模型:epoll