碌碌无为,则余生太长;
欲有所为,则人生苦短。
--- 中岛敦 《山月记》---
上一篇文章我们学习了多路转接中的Select,其操作很简单,但有一些缺陷:
而poll方案可以解决其中的两个缺点:
那么接下来我们就来看poll是怎样实现的。
首先poll的作用与select一模一样:等待多个文件描述符!只负责等待!
我们来看看poll接口:
OLL(2) Linux Programmer's Manual POLL(2)
NAME
poll, ppoll - wait for some event on a file descriptor
SYNOPSIS
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <signal.h>
#include <poll.h>
int ppoll(struct pollfd *fds, nfds_t nfds,
const struct timespec *tmo_p, const sigset_t *sigmask);
poll接口中只有三个参数:
struct pollfd *fds
:这时一个文件描述符数组,其中每个元素是一个结构体,其中包含文件描述符,需要处理的事件类型。nfds_t nfds
:表示文件描述符的数量!timeout
:输入性参数,这里直接采用的是毫秒,不使用结构体!等于0时是非阻塞IO,等于-1时是阻塞IO!我们来看看struct pollfd
内部是怎么样的 :
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
我们对比一下select,select需要传入三个事件集,输入输出性参数,每次都会发生改变!所以才需要每次调用都要进行初始化。而poll使用一个结构体,对于这个文件描述符有两种事件:requested events 与 returned events
!输入输出并不互相干扰!那么就解决了select需要不断初始化的问题。
那么事件类型有哪些呢?
宏定义 | 描述 |
---|---|
POLLIN | 普通或优先级带数据可读 |
POLLRDNORM | 同POLLIN |
POLLRDBAND | 数据可读(优先级带数据) |
POLLPRI | 高优先级数据可读 |
POLLOUT | 普通数据可写 |
POLLWRNORM | 同POLLOUT |
POLLWRBAND | 数据可写(优先级带数据) |
POLLERR | 发生错误 |
POLLHUP | 挂起,对方关闭连接 |
POLLNVAL | 描述字不是一个打开的文件 |
这些都是宏定义,short events;
是一个16位位图,可以通过宏定义进行匹配设置!我们想要查看哪些事件,或者有哪些事件就绪了,就都可以通过位运算进行判断就可以了!
通过结构体的两个位图:
我们仅仅需要对select的代码做出一些修改即可:
首先,poll需要一个struct pollfd数组,这里储存需要处理的fd。初始化事遍历进行将对应fd设置为-1,事件设置为0,将listen套接字加入就可以:
void Initserver()
{
// 对数组进行初始化
for (int i = 0; i < gnum; i++)
{
fd_array[i].fd = gdefault;
fd_array[i].events = 0;
fd_array[i].revents = 0;
}
// 加入监听套接字
fd_array[0].fd = _listensock->GetSockfd();
fd_array[0].events = POLLIN;
}
//...
// poll
struct pollfd fd_array[gnum];
然后对Loop函数进行修改,我们不在需要对数据遍历更新rfds了,这样代码看起来就整洁了许多!
void Loop()
{
// 进入服务
while (true)
{
// 创建timeout
int timeout = 1000;
// 进行select
int n = ::poll(fd_array, gnum, timeout);
switch (n)
{
case 0:
// 超时
LOG(DEBUG, "timeout \n");
break;
case -1:
// 出错了
LOG(ERROR, "select error\n");
break;
default:
// 正常
LOG(INFO, "have event ready: n = %d\n", n);
// 处理事件
HandlerEvent();
PrintDebug();
break;
}
}
}
接下来就是HandlerEvent函数,进行判断的策略依然是遍历,这里只关心读事件:
void HandlerEvent()
{
// 遍历fd_array判断是否有就绪的新事件
for (int i = 0; i < gnum; i++)
{
if (fd_array[i].fd == gdefault)
continue;
// 如果有新事件
if (fd_array[i].revents & POLLIN)
{
// 进行判断是scokfd 还是普通fd
if (fd_array[i].fd == _listensock->GetSockfd())
{
Accepter();
}
// 普通fd 进行正常读写
else
{
HandlerIO(fd_array[i]);
}
}
}
}
然后就是对于普通套接字和监听套接字的处理,针对数组进行稍微修改即可:
void Accepter()
{
// 连接事件就绪
InetAddr addr;
int sockfd = _listensock->Accepter(&addr); // 已经就绪 ,不会阻塞
// 这时会得到一个新连接
if (sockfd > 0)
{
LOG(DEBUG, "get a new link , client info %s:%d\n", addr.Ip().c_str(), addr.Port());
// 将新获取的fd加入到数组中
LOG(INFO, "get new fd :%d\n", sockfd);
bool flag = false;
for (int i = 0; i < gnum; i++)
{
if (fd_array[i].fd == gdefault)
{
flag = true;
fd_array[i].fd = sockfd;
fd_array[i].events = POLLIN;
break;
}
else
continue;
}
if (flag == false)
{
LOG(WARNING, "fd_array have fill!\n");
return;
// 可以进行扩容
}
}
}
void HandlerIO(struct pollfd &sp)
{
char buffer[1024];
int n = ::recv(sp.fd, buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
// 读取到了数据
buffer[n] = 0;
std::string echo_str = "[client say]#";
echo_str += buffer;
std::cout << echo_str << std::endl;
// 返回一个报文
std::string content = "<html><body><h1>hello bite</h1></body></html>";
std::string ret_str = "HTTP/1.0 200 OK\r\n";
ret_str += "Content-Type: text/html\r\n";
ret_str += "Content-Length: " + std::to_string(content.size()) + "\r\n\r\n";
ret_str += content;
// echo_str += buffer;
::send(sp.fd, ret_str.c_str(), ret_str.size(), 0); // 临时方案
}
else if (n == 0)
{
// 此时fd退出了
LOG(INFO, "fd:%d quit!\n", sp.fd);
::close(sp.fd);
sp.fd = gdefault;
sp.events = 0;
sp.revents = 0;
}
else
{
LOG(ERROR, "recv error! errno:%d\n", errno);
::close(sp.fd);
sp.fd = gdefault;
}
}
来看效果:
很好的实现了我们的需求!代码也比select更加的简单了!
Poll的底层其实也是遍历,对我们传入的数据进行遍历,这样的效率其实比select并不能高出太多!也就是说poll依然有这样的缺点:
这样poll 的处境就很尴尬,没有select资历早,适配性不如select。性能又比不过epoll! 下一篇文章我们来学习epoll!