Linux网络编程——epoll反应堆
前言
这是我自己的一个Linux网络编程学习路上的一个学习笔记,学习的过程中看过一些视频+博客,所以在学习过后根据记录的笔记来完成代码实现的过程中,可能会出现一大段文章内容和别人写的一样或者某些思想也会相同,如有侵权,请联系删除或者添加引用。(本文章不会作为商业用途)
一、epoll简介
epoll是Linux下多路复用IO接口select/poll的增强版本,epoll对于那些有大量并发连接但是只有很少部分活跃的情况下是很有用的。因为select、poll以及epoll都是系统内核来对网络通信中的通信套接字来进行监视,能够在与服务器连接的大量客户端中识别出与服务器请求了数据交换的客户端,并把它们所对应的套接字通过函数返回,交给服务器。此时服务器只需要和请求了数据交换的客户端进行通信即可,而其它的套接字则不做任何处理。 因此,比起服务器自身每次去轮询查询并处理每个套接字的效率要高很多。
二、select常用函数
select系统调用的目的是:在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常事件。
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
//nfds: 被监听的文件描述符总数,它会比文件描述符表集合中的文件描述符表的最大值大1,因为文件描述符从0开始计数
//readfds:需要监听的可读事件的文件描述符集合
//writefds:需要监听的可写事件的文件描述符集合,一般设为NULL
//exceptfds:需要监听的异常事件的文件描述符集合,一般设为NULL
//timeout:即告诉内核select等待多长时间之后就放弃等待。一般设为NULL 表示无动静就阻塞等待
//返回值:超时返回0;失败返回-1;成功返回大于0的整数,这个整数表示就绪描述符的数目。
//返回值同时传出fd_set *readfds或者fd_set *writefds或者fd_set *exceptfds,他们都是传出参数,监听了谁就传谁出来。传出的数组表示在监听时间内发生了监听类型变化的套接字数组。
FD_CLR(int fd, fd_set* set);
//用来清除描述词组set中相关fd 的位
FD_ISSET(int fd, fd_set *set);
//用来查看 fd 文件描述符是否在 set 数组中
FD_SET(int fd, fd_set *set);
//将 fd 添加到 set 数组中
FD_ZERO(fd_set *set);
//用来清除描述词组set的全部位
在这里看来,虽然 select 和 epoll 返回值都是有响应的套接字文件描述符,但是 select 返回的数组保持着数组下标不变,也即如果监听了0~1024个套接字,但是如果其中只有0、1、2、8、10、1023 这六个套接字有了响应,传出的数组中也只有下标为0、1、2、8、10、1024这几个值发生了变化,由于下标不变,所以要循环1024次来找出变化了的位置进行后续处理,这样的话,中间没有变化的地方也要遍历一次,效率很低。
但是epoll解决了这个问题,epoll操作的是一棵红黑树,在有事件触发时,返回的事件数组会把有响应的套接字放在时间数组前面的几个位置当中。如同上面的六个套接字收到数据交换请求,它就会把这些套接字放到数组的前六个位置。所以在循环的时候只需要访问前六个的值即可,极大程度上提高了服务器高并发处理事件的能力。
同时,select 还有 1024 个套接字处理上限,epoll则没有上限,自己可以修改上限值。
三、epoll常用函数
int epoll_create(int size);
//生成一个epoll专用的文件描述符,其中的参数是指定生成描述符的最大范围。
//成功,返回值为epoll专用的文件描述符epfd。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//用于操作某个文件描述符上的事件,可以注册事件、修改事件、删除事件。
//epfd:由 epoll_create 生成的epoll专用的文件描述符;
//op:要进行的操作,可能的取值EPOLL_CTL_ADD 注册、EPOLL_CTL_MOD 修改、EPOLL_CTL_DEL 删除;
//fd:要操作的文件描述符;
//event:指向epoll_event的指针;此处指向单个事件对象,而不是事件数组
//返回值:成功(0),失败(-1)
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
//该函数用于轮询I/O事件的发生,有监听到对应事件发生,返回。
//epfd:由epoll_create 生成的epoll专用的文件描述符;
//epoll_event:传出参数,用于回传待处理事件的数组;
//maxevents:每次能处理的事件数;
//timeout:等待I/O事件发生的超时值;-1:阻塞等待;0:不阻塞,立马返回;>0:设置一个超时时间
//返回值:监听的事件中发生响应的事件数量
epoll_event 结构体的events字段是表示感兴趣的事件和被触发的事件,可能的取值为:
EPOLLIN: 表示对应的文件描述符可以读;(常用)
EPOLLOUT: 表示对应的文件描述符可以写;(常用)
EPOLLPRI: 表示对应的文件描述符有紧急的数据可读;
EPOLLERR: 表示对应的文件描述符发生错误;(常用)
EPOLLHUP: 表示对应的文件描述符被挂断;
EPOLLET: 表示对应的文件描述符有事件发生;
四、epoll工作模式
epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的模式,ET是“高速”模式。其中,EPOLLLT表示水平触发,只要有数据都会触发;EPOLLET为边缘触发,只有数据来才触发,不管缓冲区中是否有数据。
所以工作方式有:
(1) epoll接口的一般使用
(2) epoll接口 + 非阻塞
(3) epoll接口 + 非阻塞 + 边沿触发
(4) epoll反应堆模型 (重点,Libevent库的核心思想)
前三个都比较简单,和一般的网络编程实现服务器差不多,只是用了 epoll 来代理,流程如下:
(1) epoll_create(); // 创建监听红黑树
(2) epoll_ctl(); // 向书上添加监听fd
(3) epoll_wait(); // 监听
(4) 有监听fd事件发送 —> 返回监听满足数组 —> 判断返回数组元素 —> lfd满足accept —> 返回cfd—> read() 读数据 —> write()给客户端回应。
第四个反应堆模型流程为:
(1) epoll_create(); // 创建监听红黑树
(2) epoll_ctl(); // 向书上添加监听fd
(3) epoll_wait(); // 监听
(4) 有客户端连接上来—>lfd调用acceptconn()—>将cfd挂载到红黑树上监听其读事件
(5) epoll_wait()返回cfd—>cfd回调recvdata()—>将cfd摘下来监听写事件
(6) epoll_wait()返回cfd—>cfd回调senddata()—>将cfd摘下来监听读事件—>…—>
五、epoll反应堆模型服务器实现
#include <iostream>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <time.h>
#define MAX_EVENTS 256
#define BUFLEN 1024
#define SERV_PROT 9999
using namespace std;
void setNoBlock(int fd)
{
int flags = fcntl(fd, F_GETFL);
flags |= O_NONBLOCK;
int ret = fcntl(fd, F_SETFL, flags);
if(ret < 0)
{
perror("fcntl error");
exit(1);
}
}
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
setNoBlock(sockfd);
int epfd = epoll_create(MAX_EVENTS);
if(epfd == -1)
{
perror("epoll_create error");
exit(1);
}
struct epoll_event ev,events[MAX_EVENTS];
ev.data.fd = sockfd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(SERV_PROT);
int ret = bind(sockfd, (sockaddr*)&server_addr, sizeof(server_addr));
if(ret == -1)
{
perror("bind error");
exit(1);
}
ret = listen(sockfd, MAX_EVENTS-1);
int readfds,connfd,optfd,nread;
char buf[BUFLEN];
while(1)
{
readfds = epoll_wait(epfd, events, 256, -1);
for(int i = 0; i < readfds; i++)
{
if(events[i].data.fd == sockfd)
{
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
connfd = accept(sockfd, (sockaddr*)&client_addr, &client_addr_len);
if(connfd == -1)
{
perror("accept error");
exit(1);
}
setNoBlock(connfd);
ev.data.fd = connfd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
}
else if((events[i].data.fd != sockfd) && (events[i].events == EPOLLIN))
{
optfd = events[i].data.fd;
nread = recv(optfd, buf, sizeof(buf), 0);
if(nread < 0)
{
close(optfd);
events[i].data.fd = -1;
}
else if(nread == 0)
{
close(optfd);
events[i].data.fd = -1;
}
else if(nread >0 )
{
write(STDOUT_FILENO, buf, nread);
for(int j = 0; j < nread; j++)
{
buf[j] = toupper(buf[j]);
}
write(STDOUT_FILENO, buf, nread);
}
ev.data.fd = optfd;
ev.events = EPOLLOUT | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_MOD, optfd, &ev);
if(events[i].events == EPOLLOUT)
{
cout<<"EPOLLOUT"<<endl;
}
}
else if ((events[i].data.fd != sockfd) && (events[i].events == EPOLLOUT))
{
optfd = events[i].data.fd;
send(optfd, buf, nread, 0);
ev.data.fd = optfd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_MOD, optfd, &ev);
}
}
}
return 0;
}