3.2poll
由于select使用前要重置fd集,并且存在等待fd有上限等问题,所以使用poll修正select;
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
//返回值和select一样,0表示超时没有fd就绪,大于0表示有n个fd就绪,小于0表示出错;
//timeout表示一次最多等待的时间,单位是微秒;0表示非阻塞,-1表示阻塞,其他就是正常等待;
//fds是pollfd结构体指针,内部有文件描述符,请求事件,返回事件;这个结构将输入和输出事件进行了分类;
//nfds表示pollfd结构体的数量;
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */ //关心事件,读事件就设置POLLIN,写事件设置为POLLOUT
short revents; /* returned events */ //也是使用的位图结构表示事件就绪就绪
};
poll和select一样,都是进行关心fd的等待和派发就绪事件,只不过优化了重置fd_set和能等待的fd有限的问题;
如下事件都是宏,并且pollfd结构中的event是位图结构(16个比特位),来维护辅助关心fd和派发就绪事件;
本质就是通过输入输出事件分离解决fd_set重置,由用户维护结构体数组的大小,解决fd有限的问题;
3.2.1代码实现
#include <iostream>
#include <string>
#include <unistd.h>
#include <poll.h>
#include "Socket.hpp"
class pollserver
{
public:
pollserver(const uint16_t port = defaultport) : port_(port)
{
for (int i = 0; i < fd_num_max; i++)
{
event_fds_[i].fd = defaultsockfd;
event_fds_[i].events = no_event;
event_fds_[i].revents = no_event;
}
}
~pollserver()
{
listensock_.Close();
}
public:
bool init()
{
listensock_.Socket();
listensock_.Bind(port_);
listensock_.Listen();
return true;
}
void run()
{
event_fds_[0].fd = listensock_.Fd();
event_fds_[0].events = POLLIN;
int timeout = 5000;
for (;;)
{
int n = poll(event_fds_, fd_num_max, timeout);
switch (n)
{
case 0:
std::cout << "time out, timeout: " << timeout << std::endl;
break;
case -1:
std::cerr << "poll err" << std::endl;
break;
default:
// 表示有事件就绪
std::cout << "get a new link..." << std::endl;
dispatcher(); // 事件派发
break;
}
}
}
void print()
{
for (int i = 0; i < fd_num_max; i++)
{
if (event_fds_[i].fd == defaultsockfd)
continue;
std::cout << event_fds_[i].fd << " ";
}
std::cout << std::endl;
}
void accepter()
{
// 事件就绪,获取新连接
std::string clientip;
uint16_t clientport = 0;
int sockfd = listensock_.Accept(&clientip, &clientport); // 此时accept就不会阻塞了,因为事件已经就绪了
if (sockfd < 0)
{
return;
}
lg(Info, "accept success, clientip: %s, clientport: %d, sockfd: %d", clientip.c_str(), clientport, sockfd);
int j = 1;
for (; j < fd_num_max; j++)
{
if (event_fds_[j].fd == defaultsockfd)
{
event_fds_[j].fd = sockfd;
event_fds_[j].events = POLLIN;
event_fds_[j].revents = no_event;
print();
break;
}
}
if (j == fd_num_max)
{
lg(Warning, "server is full, close current fd: %d", sockfd);
close(sockfd);
return;
// 扩容逻辑
}
}
void recvr(int fd, int i)
{
// 其他文件描述符就绪,此处是读文件描述符就绪了
char buff[4096];
ssize_t n = read(fd, buff, sizeof(buff) - 1);
if (n > 0)
{
buff[n] = 0;
std::cout << buff << std::endl;
}
else if (n == 0)
{
std::cout << "client close fd, me too" << std::endl;
close(fd);
event_fds_[i].fd = defaultsockfd;
}
else
{
std::cerr << "read err" << std::endl;
close(fd);
event_fds_[i].fd = defaultsockfd;
}
}
void dispatcher()
{
for (int i = 0; i < fd_num_max; i++)
{
int fd = event_fds_[i].fd;
if (fd == defaultsockfd)
continue;
if (event_fds_[i].revents & POLLIN)
{
if (fd == listensock_.Fd())
{
accepter();
}
else
{
recvr(fd, i);
}
}
}
}
private:
Sock listensock_;
uint16_t port_;
static const uint16_t defaultport = 8080;
static const int fd_num_max = 64;
static const short no_event = 0;
int defaultsockfd = -1; // 因为0下标也被使用;
struct pollfd event_fds_[fd_num_max];
};
3.2.2poll优缺点
优点:
解决了每次使用select都要重置fd_set的问题,另外将接口限制了关心fd数量的大小转变为了用户决定(实际上还是受到了硬件的限制);
缺点:
没有解决输入输出型参数的多次拷贝和遍历,效率还是受到了影响;
3.3epoll
epoll主要解决poll要遍历大量文件描述符的问题;select和poll只是遍历文件描述符表;
是inux内核2.6之后公认的最高效的多路转接方案,memcache、redis、ngnix等都使用了epoll;
3.3.1epoll接口认识
#include <sys/epoll.h>
//1.创建一个epoll模型
int epoll_create(int size);//参数只要大于0即可;
//epoll_create() creates an epoll(7) instance. Since Linux 2.6.8, the size argument is ignored, but must be greater than zero;
//2.获取就绪事件
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
//第一个参数是创建epoll模型的返回值;
//第2-3个参数是将已经返回的就绪的fd和事件返回;
//第四个参数是毫秒和poll一样;
//返回值和poll一样,表示成功就绪的fd个数;
events可以是以下几个宏的集合:
EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
EPOLLOUT : 表示对应的文件描述符可以写;
EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
EPOLLERR : 表示对应的文件描述符发生错误;
EPOLLHUP : 表示对应的文件描述符被挂断;
EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要
再次把这个socket加入到EPOLL队列里
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
//3.将fd设置进epoll内
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//op选项如下
EPOLL_CTL_ADD
EPOLL_CTL_MOD
EPOLL_CTL_DEL,最后一个参数置为nullptr
//第3-4个参数是文件描述符和对应的事件;
3.3.2epoll模型原理
为了解决通过遍历大量的文件描述符来检测事件是否就绪的问题,操作系统内部维护了一个红黑树和就绪队列;
struct rb_node
{
int fd;
uint32_t event;//要关心的事件
//还包括大量的连接字段;
}
struct queue_node
{
int fd;
uint32_t event;//就绪的事件
}
当事件就绪了,操作系统会是使用硬件中断的方式将网卡的数据拷贝至网卡驱动层,然后网卡驱动层会自动调用回调函数,将数据进行向上交付并且交付到tcp的接收队列,然后查找红黑树,查找fd和对应关心的事件,之后就构建就绪队列节点,添加到队列里;
创建epoll模型就是,创建一个红黑树,就绪队列,还有注册底层的回调机制;操作系统为了管理epoll模型,创建了专门的struct结构来进行维护;
epoll_create就是创建文件对象和epoll模型并将epoll模型添加到文件对象内,就可以通过fd查找到epoll模型;
epoll_ctl就是增加,删除和修改红黑树的节点,而查找是由底层的回调机制来实现;
epoll_wait就是将就绪队列的就绪数据全部放到用户缓冲区的结构体数组内;
其实回调机制是可以和每一个节点对应,就不需要查找更高效;
3.3.3epoll优势
1.检测就绪是O(1);当队列为空时,说明不就绪,而select和poll需要遍历一遍;而获取就绪的时间复杂度是O(n);
2.fd和event没有上限;
3.epoll不需要用户自己维护数组,底层操作系统底层维护了一个红黑树;
4.返回值表示有n个fd就绪,因为是将就绪队列的节点一定是就绪的fd和事件,所以就绪队列的节点拷贝到结构体数组,数组中一定是连续的有效值,不需要像select和poll需要遍历查找过滤掉无效部分;
5.对于就绪队列的数据,如果上层满了就下一批再取;
6.一个文件描述符所关心的事件是否就绪,本质上就是是否添加到了就绪队列;当底层有数据但是上层可能太忙了没时间去取,就需要通知对方尽快将数据取走;即当事件就绪了,就将文件描述符及就绪事件链入到就绪队列,如果接收缓冲区满了,使得对方不能发送了,这时候对方也会发送一个PSH,让上层赶紧拿走接收缓冲区的数据,在底层强制调用回调机制,将就绪事件和对应的fd链入到就绪队列;PSH标志位的本质就是:读时间就绪;PSH仅仅是说明数据已经准备好了;
3.3.4代码实现
1.注意设置fd及关心事件时用一个epoll_event结构,就绪事件处理是使用的epoll_event数组;
2.在设置时将epoll_event.data.fd = sock,是为了将用户级数据返回;
3.当写端关闭了文件描述符之后,读端也应该关闭,要注意要保证文件描述符是合法的,所以要在删除关注文件描述符后再进行关闭文件描述符;
1.epoll封装
class nocopy
{
public:
nocopy();
nocopy(const nocopy &np) = delete;
nocopy &operator=(const nocopy &np) = delete;
};
#include <sys/epoll.h>
#include <cstring>
#include "nocopy.hpp"
#include "Log.hpp"
class epoll : public nocopy
{
public:
epoll()
{
epfd_ = epoll_create(128);
if (epfd_ < 0)
{
lg(Fatal, "epoll_create error, errno: %d, errstr: %s", errno, strerror(errno));
}
lg(Info, "epoll_create success, epfd: %d", epfd_);
}
int Wait(epoll_event event[], int size)
{
int n = epoll_wait(epfd_, event, size, timeout_);
return n;
}
int Update(int oper, int sockfd, uint32_t event)
{
int n = 0;
if (oper == EPOLL_CTL_DEL)
{
n = epoll_ctl(epfd_, oper, sockfd, nullptr);
if (n < 0)
{
lg(Error, "epoll_ctl_del error, errno: %d, errstr: %s", errno, strerror(errno));
}
}
else
{
epoll_event ev;
ev.events = event;
ev.data.fd = sockfd;
n = epoll_ctl(epfd_, oper, sockfd, &ev);
if (n < 0)
{
lg(Error, "epoll_ctl error, errno: %d, errstr: %s", errno, strerror(errno));
}
}
return n;
}
~epoll()
{
if (epfd_ > 0)
{
close(epfd_);
}
}
private:
int epfd_;
static const int timeout_ = 3000;
};
2.epollserver简单封装
#include <iostream>
#include <memory>
#include "Log.hpp"
#include "Socket.hpp"
#include "epoll.hpp"
uint32_t event_in = (EPOLLIN);
uint32_t event_out = (EPOLLOUT);
static const uint16_t defaultport = 8080;
class epollserver : public nocopy
{
public:
epollserver(uint16_t port = defaultport)
: listensock_ptr_(new Sock()), epoller_ptr_(new epoll()), port_(port) {}
void init()
{
listensock_ptr_->Socket();
listensock_ptr_->Bind(port_);
listensock_ptr_->Listen();
lg(Info, "create listen socket success, listensockfd: %d", listensock_ptr_->Fd());
}
void run()
{
epoller_ptr_->Update(EPOLL_CTL_ADD, listensock_ptr_->Fd(), event_in);
epoll_event event[numsize];
for (;;)
{
int n = epoller_ptr_->Wait(event, numsize);
if (n > 0)
{
// 事件就绪
lg(Debug, "event happend, fd is: %d", event[0].data.fd);
dispatcher(event, n); // 事件派发器
}
else if (n == 0)
{
// 没有事件就绪,超时
lg(Info, "time out...");
}
else
{
lg(Error, "Wait errror, errno: %d, errstr: %s", errno, strerror(errno));
}
}
}
void Accept()
{
// 监听套接字就执行监听逻辑
std::string clientip;
uint16_t clientport;
int sockfd = listensock_ptr_->Accept(&clientip, &clientport);
if (sockfd > 0)
{
// 将新的文件描述符添加到epoll中
epoller_ptr_->Update(EPOLL_CTL_ADD, sockfd, event_in);
lg(Info, "get a new link, client info: %s, %d", clientip.c_str(), clientport);
}
else
{
return;
}
}
void Recvr(int fd)
{
// 其他文件描述符就绪,此处是读文件描述符就绪了
char buff[4096];
ssize_t n = read(fd, buff, sizeof(buff) - 1); // 存在粘包问题
if (n > 0)
{
buff[n] = 0;
std::cout << buff << std::endl;
// 构建发送给客户端的数据
std::string echo_string = "server echo# ";
echo_string += buff;
write(fd, echo_string.c_str(), echo_string.size());
}
else if (n == 0)
{
// 对方将连接关闭了
std::cout << "client close fd, me too" << std::endl;
epoller_ptr_->Update(EPOLL_CTL_DEL, fd, 0);
close(fd);
}
else
{
std::cerr << "read err" << std::endl;
epoller_ptr_->Update(EPOLL_CTL_DEL, fd, 0);
close(fd);
}
}
void dispatcher(epoll_event events[], int size)
{
for (int i = 0; i < size; i++)
{
uint32_t event = events[i].events;
int fd = events[i].data.fd;
if (event & event_in)
{
// 读事件就绪了
if (fd == listensock_ptr_->Fd())
{
Accept(); // 连接管理器
}
else
{
// 其他文件描述符就执行读取逻辑
Recvr(fd); // 事件处理器
}
}
else if (event & event_out)
{
// 写事件就绪了
// 执行写逻辑
}
else
{
}
}
}
~epollserver()
{
listensock_ptr_->Close();
}
private:
static const int numsize = 64;
std::shared_ptr<Sock> listensock_ptr_;
std::shared_ptr<epoll> epoller_ptr_;
uint16_t port_;
};
3.3.5epoll两种工作模式LT和ET
LT,水平触发(Level Triggered),类似示波器中的波一直为高时才为真;
ET,边缘触发(Edge Triggered),类似示波器中的波由低变高才为真;
epoll默认的工作模式为LT模式,即当事件触发时,会一直发送消息,即一直有效,关注的是存在;而ET则是从无到有,从少到多,即变化的时候才会通知一次,关注的是变化;使用ET的时候要注意要将数据一次都处理完,否则就会造成数据的丢失,所以需要循环读取直至读取出错即阻塞,但是不可以阻塞防止服务器挂起,所以需要ET模式的fd都是非阻塞的;
ET比LT的通知效率要高,因为如果使用LT就会导致一直通知一个fd,其中有很多次的通知是无效的,使得单位时间内的有效通知减少,所以应该使用ET,只是通知了一次,只要保证处理数据时不丢包就行;
ET的IO效率更高,因为ET使得数据全被读取,对于TCP会使得发送端的滑动窗口更大,可以发送更多的数据;
其实LT也是可以设置第一次通知就将数据全部拿走,之后就不会通知了,所以效率不一定是ET高,具体看用户代码实现;
总结:LT和ET的区别就是事件就绪,向就绪队列添加多次还是一次;使用ET必须使用非阻塞文件描述符;