epoll进阶--事件模型
1 事件模型
EPOLL事件有两种模型:
- Edge Triggered (ET) 边缘触发只有数据到来才触发,不管缓存区中是否还有数据。
- Level Triggered (LT) 水平触发只要有数据都会触发。(epoll默认为LT模式)
思考如下步骤:
- 假定我们已经把一个用来从管道中读取数据的文件描述符(rfd)添加到epoll描述符。
- 管道的另一端写入了2KB的数据
- 调用epoll_wait,并且它会返回rfd,说明它已经准备好读取操作
- 读取1KB的数据
- 调用epoll_wait……
在这个过程中,有两种工作模式:
ET模式
ET模式即Edge Triggered工作模式。
如果我们在第1步将rfd添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。
- 基于非阻塞文件句柄
- 只有当read或者write返回EAGAIN(非阻塞读,暂时无数据)时才需要挂起、等待。但这并不是说每次read时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。
LT模式
LT模式即Level Triggered工作模式。
与ET模式不同的是,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll,无论后面的数据是否被使用。
2 ET和LT比较
LT(level triggered):LT是缺省的工作方式,并且同时支持block和no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。
ET(edge-triggered):ET是高速工作方式,只支持no-block socket(需要忙轮询读)。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知。请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once).
3 示例
3.1 基于管道epoll ET触发模式
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/epoll.h>
int main()
{
int fd[2] = {0}; // 管道的读写两端
int ret = pipe(fd); // 创建管道
if (-1 == ret)
{
perror("pipe error");
exit(1);
}
pid_t pid = fork();
if (-1 == pid)
{
perror("fork error");
exit(1);
}
else if (0 == pid) // 子进程
{
close(fd[0]); // 关闭管道读端
char ch = 'a';
while (1)
{
char buf[10] = {0};
int ii = 0;
for(ii = 0; ii < 4; ++ii)
{
buf[ii] = ch;
}
buf[ii] = '\n'; // aaaa\n
ch += 1; // b
for(ii = 5;ii < 9; ++ii)
{
buf[ii] = ch;
}
buf[ii] = '\n'; // bbbb\n
// buf aaaa\nbbbb\n
write(fd[1], buf, sizeof(buf));
ch += 1;
sleep(5);
}
}
else // 父进程
{
close(fd[1]); // 关闭管道写端
int epfd = epoll_create(1); // 创建一颗红黑树
if (-1 == epfd)
{
perror("epoll_create error");
exit(1);
}
struct epoll_event tep{};
struct epoll_event ep{};
//tep.events = EPOLLIN; // LT模式(水平触发)
tep.events = EPOLLIN | EPOLLET; // ET模式(边缘触发)
tep.data.fd = fd[0];
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd[0], &tep); // 将fd[0]添加到红黑树上
if (-1 == ret)
{
perror("epoll_ctl error");
exit(1);
}
while (1)
{
int nReady = epoll_wait(epfd, &ep, 1, -1); // 阻塞监听fd[0]的读事件
if (-1 == nReady)
{
perror("epoll_wait error");
exit(1);
}
char buf[10] = {0};
int n = read(fd[0], buf, 5);
if (-1 == n)
{
perror("read error");
exit(1);
}
printf("%s", buf);
}
}
return 0;
}
3.2 基于网络C/S模型的epoll ET触发模式
服务端
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <sys/epoll.h>
int main()
{
int listenfd, connfd;
listenfd = socket(AF_INET, SOCK_STREAM, 0); // 创建socket
if (-1 == listenfd)
{
perror("socket error");
exit(1);
}
// 端口复用
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in saddr, caddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(8000);
saddr.sin_addr.s_addr = inet_addr("192.168.71.132");
int ret = bind(listenfd, (struct sockaddr*)&saddr, sizeof(saddr)); // 将socket与服务端的地址结构进行绑定
if (-1 == ret)
{
perror("bind error");
close(listenfd);
exit(1);
}
ret = listen(listenfd, 5); // 设置监听上限
if (-1 == ret)
{
perror("listen error");
exit(1);
}
int epfd = epoll_create(1024); // 创建红黑树
if (-1 == epfd)
{
perror("epoll_create error");
exit(1);
}
struct epoll_event tep{};
struct epoll_event ep[1024]{};
tep.events = EPOLLIN;
tep.data.fd = listenfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &tep); // 将listenfd添加到红黑树上
if (-1 == ret)
{
perror("epoll_ctl error");
exit(1);
}
while (1)
{
int nReady = epoll_wait(epfd, ep, 1024, -1); // 阻塞监听
if (-1 == nReady)
{
perror("epoll_wait error");
exit(1);
}
for (int ii = 0; ii < nReady; ++ii)
{
if (ep[ii].data.fd == listenfd) // 有客户端连接上来
{
socklen_t caddrLen = sizeof(caddr);
connfd = accept(listenfd, (struct sockaddr*)&caddr, &caddrLen);
if (-1 == connfd)
{
perror("accept error");
exit(1);
}
printf("建立连接成功:%s:%d\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
//tep.events = EPOLLIN;
tep.events = EPOLLIN | EPOLLET; // ET模式
tep.data.fd = connfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &tep); // 将connfd添加到红黑树上
if (-1 == ret)
{
perror("epoll_ctl error");
epoll_ctl(epfd, EPOLL_CTL_DEL, listenfd, NULL);
close(connfd); close(listenfd);
exit(1);
}
}
else // 有客户端发送数据
{
int sockfd = ep[ii].data.fd;
char buf[10] = {0};
int n = read(sockfd, buf, 5);
if (-1 == n)
{
perror("read error");
exit(1);
}
else if (0 == n) // 如果客户端断开连接不会到此处 bug(因为客户端已经断开,没有条件再触发读事件)
{
printf("断开连接\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
close(sockfd);
}
else
{
printf("%s", buf);
}
}
}
}
close(listenfd);
return 0;
}
客户端
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <ctype.h>
int main()
{
int cfd;
// 1.创建socket
cfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == cfd)
{
perror("socket error");
exit(1);
}
struct sockaddr_in saddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(8000);
inet_pton(AF_INET, "192.168.71.132", &saddr.sin_addr.s_addr);
// 2.与服务端建立连接
int ret = connect(cfd, (struct sockaddr*)&saddr, sizeof(saddr));
if (-1 == ret)
{
perror("connect error");
exit(1);
}
char ch = 'a';
while (1)
{
char buf[10] = {0};
int ii = 0;
for(ii = 0; ii < 4; ++ii)
{
buf[ii] = ch;
}
buf[ii] = '\n'; // aaaa\n
ch += 1; // b
for(ii = 5;ii < 9; ++ii)
{
buf[ii] = ch;
}
buf[ii] = '\n'; // bbbb\n
// buf aaaa\nbbbb\n
write(cfd, buf, sizeof(buf));
ch += 1;
sleep(5);
}
// 4.关闭socket
close(cfd);
return 0;
}
3.3 基于网络C/S非阻塞模型的epoll ET触发模式 ★★
服务端
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
int main()
{
int listenfd, connfd;
listenfd = socket(AF_INET, SOCK_STREAM, 0); // 创建socket
if (-1 == listenfd)
{
perror("socket error");
exit(1);
}
// 端口复用
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in saddr, caddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(8000);
saddr.sin_addr.s_addr = inet_addr("192.168.71.132");
int ret = bind(listenfd, (struct sockaddr*)&saddr, sizeof(saddr)); // 将socket与服务端的地址结构进行绑定
if (-1 == ret)
{
perror("bind error");
close(listenfd);
exit(1);
}
ret = listen(listenfd, 5); // 设置监听上限
if (-1 == ret)
{
perror("listen error");
exit(1);
}
int epfd = epoll_create(1024); // 创建红黑树
if (-1 == epfd)
{
perror("epoll_create error");
exit(1);
}
struct epoll_event tep{};
struct epoll_event ep[1024]{};
tep.events = EPOLLIN;
tep.data.fd = listenfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &tep); // 将listenfd添加到红黑树上
if (-1 == ret)
{
perror("epoll_ctl error");
exit(1);
}
while (1)
{
int nReady = epoll_wait(epfd, ep, 1024, -1); // 阻塞监听
if (-1 == nReady)
{
perror("epoll_wait error");
exit(1);
}
for (int ii = 0; ii < nReady; ++ii)
{
if (ep[ii].data.fd == listenfd) // 有客户端连接上来
{
socklen_t caddrLen = sizeof(caddr);
connfd = accept(listenfd, (struct sockaddr*)&caddr, &caddrLen);
if (-1 == connfd)
{
perror("accept error");
exit(1);
}
printf("建立连接成功:%s:%d\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
// 设置文件描述符非阻塞
int flag = fcntl(connfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(connfd, F_SETFL, flag);
//tep.events = EPOLLIN;
tep.events = EPOLLIN | EPOLLET; // ET模式
tep.data.fd = connfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &tep); // 将connfd添加到红黑树上
if (-1 == ret)
{
perror("epoll_ctl error");
epoll_ctl(epfd, EPOLL_CTL_DEL, listenfd, NULL);
close(connfd); close(listenfd);
exit(1);
}
}
else // 有客户端发送数据
{
int sockfd = ep[ii].data.fd;
while (1) // 非阻塞,轮询读
{
char buf[10] = {0};
int n = read(sockfd, buf, 5);
if (-1 == n)
{
if (errno == EAGAIN) // 非阻塞读没有数据
break;
perror("read error");
exit(1);
}
else if (0 == n)
{
printf("断开连接\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
close(sockfd);
break;
}
else
{
printf("%s", buf);
}
}
}
}
}
close(listenfd);
return 0;
}
客户端与示例3.2为同一个