poll函数和与我上一篇文章介绍的select函数一样,都是系统提供的多路转接接口,允许进程或线程在同一时间监听多个文件描述符。
本篇文章的一部分内容与上一篇介绍select函数的文章练习很大。
poll函数原型如下:
参数timeout的取值:
返回值说明:
poll调用失败时,错误码可能被设置为:
struct pollfd结构
struct pollfd结构当中包含三个成员:
events和revents的取值都是以宏的方式进行定义的,它们的二进制序列当中有且只有一个比特位是1,且为1的比特位是各不相同的。
poll的工作流程和select是类似的,这里我们实现一个简单的poll服务器,该服务器也是读取客户端发来的数据并进行打印。
PollServer类当中也只需要包含监听套接字和端口号两个成员变量,在poll服务器绑定时直接将IP地址设置为INADDR_ANY即可。
#pragma once
#include "Socket.hpp"
#include
#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函数,当事件就绪时对应执行某种动作即可。
#pragma once
#include "Socket.hpp"
#include
#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服务器就应该对就绪事件进行处理了,事件处理结果如下:
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
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;
}
poll的优点
虽然代码中将fds数组的元素定义为1024,但是fds数组的大小是可以继续增大的,poll函数能够帮你监视多少文件描述符是由传入poll函数的第二个参数决定的。
而fd_set类型只有1024个比特位,因此select函数最大只能监视1024个文件描述符。
poll的缺点