文章目录
- 1. poll函数原型
- 2. poll服务器
- 3. poll的优点和确定
1. poll函数原型
poll函数和与我上一篇文章介绍的select函数一样,都是系统提供的多路转接接口,允许进程或线程在同一时间监听多个文件描述符。
本篇文章的一部分内容与上一篇介绍select函数的文章练习很大。
poll函数原型如下:
参数说明:
- fds:一个poll函数监视的结构列表,每一个元素包含三部分内容:文件描述符、监视的事件集合、就绪的事件集合。
- nfds:表示fds数组的长度。
- timeout:表示poll函数的超时时间,单位是毫秒(ms)。
参数timeout的取值:
- -1:poll调用后进行阻塞等待,直到被监视的某个文件描述符上的某个事件就绪。
- 0:poll调用之后进行非阻塞等待,无论被监视的文件描述符上的事件是否就绪,poll检测之后都会立即返回。
- 特定的时间值:poll调用后在指定的时间内进行阻塞等待,如果被监视的文件描述符上一直没有事件就绪,则在该时间后poll进行超时返回。
返回值说明:
- 如果函数调用成功,则返回所有事件就绪的文件描述符个数。
- 如果timeout时间耗尽,返回0。
- 如果函数调用失败,返回-1,同时错误码会被设置。
poll调用失败时,错误码可能被设置为:
- EFAULT:fds数组不包含在调用程序的地址空间内。
- EINTR:此调用被信号所中断。
- EINVAL:nfds值超过RLIMIT_NOFILE值
- ENOMEM:核心内存不足
struct pollfd结构
struct pollfd结构当中包含三个成员:
- fd:特定的文件描述符,若设置为负值则忽略events字段并且revents字段返回0。
- events:需要监视文件描述符上的哪些事件。
- revents:poll函数返回时告知用户该文件描述符上的哪些事件已经就绪。
events和revents的取值如下:
events和revents的取值都是以宏的方式进行定义的,它们的二进制序列当中有且只有一个比特位是1,且为1的比特位是各不相同的。
- 在调用poll函数之前,可以通过“或”运算符将要检测的事件添加到events成员当中。
- 在poll函数返回后,可以通过“与”运算符检测revents成员中是否包含特定事件,以得知对应文件描述符的特定事件是否就绪。
2. poll服务器
poll的工作流程和select是类似的,这里我们实现一个简单的poll服务器,该服务器也是读取客户端发来的数据并进行打印。
- PollServer类
PollServer类当中也只需要包含监听套接字和端口号两个成员变量,在poll服务器绑定时直接将IP地址设置为INADDR_ANY即可。
- 在构造PollServer对象时,需要指明服务器的端口号,当然也可以在初始化poll服务器的时候指明。
- 在初始化poll服务器的时候调用Socket类中的函数,依此进行套接字的创建、绑定和监听即可,这里的Socket类和之前实现的一模一样。
- 在析构函数中可以选择调用close函数将监听套接字进行关闭,但实际也可以不进行该动作,因为服务器运行后一般是不退出的。
#pragma once
#include "Socket.hpp"
#include <poll.h>
#define BACK_LOG 5
class PollServer
{
public:
PollServer(int port)
: _port(port)
{}
void InitPollServer()
{
_listen_sock = Socket::SocketCreate();
Socket::SocketBind(_listen_sock, _port);
Socket::SocketListen(_listen_sock, BACK_LOG);
}
~PollServer()
{
if (_listen_sock >= 0) close(_listen_sock);
}
private:
int _listen_sock; // 监听套接字
int _port; // 端口号
};
运行服务器
服务器初始化完毕之后就可以开始运行了,而poll服务器要做的就是不断调用poll函数,当事件就绪时对应执行某种动作即可。
- 首先,在poll服务器开始死循环调用poll函数之前,需要定义一个fds数组,该数组当中的每个位置都是一个struct pollfd结构,后续调用poll函数时会作为参数进行传入。先将fds数组中每个位置初始化为无效,并将监听套接字添加到fds数组当中,表示服务器刚开始运行时只需要监视监听套接字的读事件。
- 此后,poll服务器就不断调用poll函数监视读事件是否就绪。如果poll函数的返回值大于0,则说明poll函数调用成功,此时已经有文件描述符的读事件就绪,接下来就应该对就绪事件进行处理。如果poll函数的返回值等于0,则说明timeout时间耗尽,此时直接准备下一个调用即可。如果poll函数的返回值为-1,则说明poll调用失败,此时也让服务器准备进行下一次poll调用,但实际应该进一步判断错误码,根据错误码来判断是否应该继续调用。
#pragma once
#include "Socket.hpp"
#include <poll.h>
#define BACK_LOG 5
#define NUM 1024
#define DFL_FD -1
class PollServer
{
public:
PollServer(int port)
: _port(port)
{}
void InitPollServer()
{
_listen_sock = Socket::SocketCreate();
Socket::SocketBind(_listen_sock, _port);
Socket::SocketListen(_listen_sock, BACK_LOG);
}
~PollServer()
{
if (_listen_sock >= 0) close(_listen_sock);
}
void Run()
{
struct pollfd fds[NUM];
ClearPollfds(fds, NUM, DFL_FD);
SetPollfds(fds, NUM, _listen_sock);
while (1)
{
switch (poll(fds, NUM, -1))
{
case 0:
std::cout << "timeout..." << std::endl;
break;
case -1:
std::cerr << "poll error" << std::endl;
break;
default:
HandleEvent(fds, NUM);
break;
}
}
}
private:
void ClearPollfds(struct pollfd fds[], int num, int default_fd)
{
for (int i = 0; i < num; ++i)
{
fds[i].fd = default_fd;
fds[i].events = 0;
fds[i].revents = 0;
}
}
bool SetPollfds(struct pollfd fds[], int num, int fd)
{
for (int i = 0; i < num; ++i)
{
if (fds[i].fd == DFL_FD)
{
fds[i].fd = fd;
fds[i].events |= POLLIN; // 添加事件到events中
return true;
}
return false;
}
}
int _listen_sock; // 监听套接字
int _port; // 端口号
};
事件处理
当poll检测到有文件描述符的读事件就绪,就会在其对应的struct pollfd结构中的revents成员中添加读事件并返回,接下来poll服务器就应该对就绪事件进行处理了,事件处理结果如下:
- 首先遍历fds数组的每个struct pollfd结构,如果该结构当中的fd有效,且revents当中包含读事件,则说明该文件描述符的读事件就绪,接下来就需要进一步判断该文件描述符是监听套接字还是与客户端建立的套接字。
- 如果是监听套接字的读事件就绪,则调用accept函数将底层建立好的连接获取上来,并将获取到的套接字添加到fds数组当中,表示下一次调用poll函数时需要监视该套接字的读事件。
- 如果是客户端建立的连接对应的读事件就绪,则调用read函数读取客户端发来的数据,并将读取到的数据在服务端进行打印。
- 如果在调用read函数时发现客户端将连接关闭或者read函数调用失败,则poll函数会直接关闭对应的连接,并将该连接对应的文件描述符从fds数组当中清除,表示下一次调用poll函数时无需再监视该套接字的读事件。
void HandleEvent(struct pollfd fds[], int num)
{
for (int i = 0; i < num; ++i)
{
if (fds[i].fd == DFL_FD) continue; // 跳过无效位置
if (fds[i].fd == _listen_sock && fds[i].revents & POLLIN)
{
// 连接事件就绪
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
socklen_t len = sizeof(peer);
int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
if (sock < 0)
{
std::cerr << "accept error" << std::endl;
continue;
}
std::string peer_ip = inet_ntoa(peer.sin_addr);
int peer_port = ntohs(peer.sin_port);
std::cout << "get a new link[" << peer_ip << ":" << peer_port << "]" << std::endl;
if (!SetPollfds(fds, NUM, sock))
{
close(sock);
std::cout << "poll server is full, close fd: " << sock << std::endl;
}
}
else if (fds[i].revents & POLLIN)
{
// 读事件就绪
char buffer[1024];
ssize_t size = read(fds[i].fd, buffer, sizeof(buffer) - 1);
if (size > 0) // 读取成功
{
buffer[size-1] = 0;
std::cout << "echo# " << buffer << std::endl;
}
else if (size == 0) // 对端连接关闭
{
std::cout << "client quit!" << std::endl;
close(fds[i].fd);
UnSetPollfds(fds, i); // 将该文件从fds数组中清除
}
else
{
std::cerr << "read error" << std::endl;
close(fds[i].fd);
UnSetPollfds(fds, i); // 将该文件从fds数组中清除
}
}
}
}
private:
void UnSetPollfds(struct pollfd fds[], int pos)
{
fds[pos].fd = DFL_FD;
fds[pos].events = 0;
fds[pos].revents = 0;
}
因为这里fds数组的大小是固定设置的,因此将新获取连接对应的文件描述符添加到fds数组时,可能会因为fds数组已满而添加失败,这时poll服务器只能将刚刚获取上来的连接对应的套接字进行关闭。
运行poll服务器
#include "PollServer.hpp"
#include <string>
int main(int argc, char* argv[])
{
if (argc != 2)
{
std::cout << "Usage: " << "./PollServer port" << std::endl;
exit(1);
}
int port = atoi(argv[1]);
PollServer* svr = new PollServer(port);
svr->InitPollServer();
svr->Run();
return 0;
}
运行结果如下:
3. poll的优点和确定
poll的优点
- struct pollfd结构当中包含了events和revents,相当于select的输入输出参数进行分离,因此在每次调用poll之前,不需要像select一样重新对参数进行设置。
- poll可监控的文件描述符数量没有限制。
- poll和select一样,可以同时等待多个文件描述符,提高IO效率。
虽然代码中将fds数组的元素定义为1024,但是fds数组的大小是可以继续增大的,poll函数能够帮你监视多少文件描述符是由传入poll函数的第二个参数决定的。
而fd_set类型只有1024个比特位,因此select函数最大只能监视1024个文件描述符。
poll的缺点
- 和select函数一样,当poll返回后,需要遍历fds数组来获取就绪的文件描述符。
- 每次调用poll,都需要把大量的struct pollfd结构用用户态拷贝到内核态,这个开销也会随着poll监视的文件描述符数目的增多而增多。
- 同时每次调用poll都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大。