概述
以下内容均为本人学习笔记,若有不当,感谢指出
上一篇学习了epoll 的基本概念和使用:epoll
今天学习了解epoll 的两种工作模式:水平触发 和 边缘触发
一、LT模式(Level Triggered)
epoll默认为LT工作模式,所以event.events选项中EPOLLET而没有LT
工作模式如下:
- 若缓冲区中有10k数据,我们可以多次进行读取
- 比如先读取2k数据,再次调用epoll_wait()
,并且会立刻通知socket读事件就绪,可以再次读取剩余的数据
- 再次调用epoll_wait
一、ET模式(Edge Triggered)
ET工作模式即我们将添加进epoll描述符时候使用EPOLLET标志,epoll进入工作模式
工作模式如下:
- 若缓冲区中有10k数据,第一次只读取了1k
- 再次调用epoll_wait
,已经不是就绪状态了,
- ET模式下,只有当缓冲区中数据由无到有,由少变多时才会进行读取数据
- 支持阻塞和非阻塞的读写
ET模式带来的问题
1. 因为只有当缓冲区中数据由无到有,由少变多时才会区读取数据,
所以一次要将缓冲区中的数据读完,否则剩下的数据可能就读不到了。
正常的读取数据时,我们若是要保证一次把缓冲区的数据读完,意为本次读被阻塞时即缓冲区中没有数据了,可是我们 epoll 服务器要处理多个用户的请求,read()不能被阻塞,所以采用非阻塞轮询的方式读取数据。
2.若轮询的将数据读完,对方给我们发9.5k的数据,我们采取每次读取1k的方式进行轮询读取,在读完9k的时候,下一次我们读到的数据为0.5k,我们就知道缓冲区中数据已经读完了就停止本次轮询。
但还有一种情况,对方给我们发的数据为10k,我们采取每次读取1k的方式轮询的读取数据,当我们已经读取了10k的时候,并不知道有没有数据了,我们仍旧还要尝试读取数据,这时read()就被阻塞了。
结论:epoll在ET模式下必须以非阻塞轮询的方式进行读取数据
三、epoll应用场景
适合用epoll的应用场景:对于连接特别多,活跃的连接特别少,这种情况等的时间特别久,典型的应用场景为一个需要处理上万的连接服务器,例如各种app的入口服务器,例如qq
不适合epoll的场景:连接比较少,数据量比较大,例如ssh
epoll 的惊群问题:因为epoll 多用于 多个连接,只有少数活跃的场景,但是万一某一时刻,epoll 等的上千个文件描述符都就绪了,这时候epoll 要进行大量的I/O,此时压力太大。
上一片篇中实现epoll的LT模式
四、epoll的ET模式举例
注意文件描述符设为非阻塞,加入epoll描述符中时,加EPOLLET标志
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <fcntl.h>
//使用epoll实现多路复用
//将文件描述符设置为非阻塞
void SetNoBlock(int fd)
{
int flag = fcntl(fd,F_GETFL);
fcntl(fd,F_SETFL, flag | O_NONBLOCK);
}
//启动服务器
int server_start(const char * ip,const short port)
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("socket");
return -1;
}
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip);
addr.sin_port = htons(port);
int ret = bind(sock,(sockaddr *)&addr,sizeof(addr));
if(ret < 0)
{
perror("bind");
return -1;
}
ret = listen(sock,5);
if(ret < 0)
{
perror("listen");
return -1;
}
return sock;
}
//处理连接socket就绪
//封装处理非阻塞轮询accept()
void Process_listen_socket(int epfd,int listen_socket,sockaddr_in * peer,socklen_t peer_len)
{
//就进行非阻塞式轮询accept()
//我们看到这里返回的events数据中并没有文件描述符
//所以我们最开始将data.fd赋值为文件描述符就是在这里用到
while(1)
{
int new_socket = accept(listen_socket,(sockaddr *)peer,&peer_len);
if(new_socket < 0 && errno ==EAGAIN)
{
//因为将文件描述符都设置为非阻塞,这里的accpt()为非阻塞的
//这里就需要轮询式的进行accpet()
//这里的listen()函数的第二个参数为5,表示排队等待连接的客户端最多有5个
//说明已经将所有的文件描述符进行accpet
perror("accpet");
return;
}
//如果创建new_socket成功之后
//就将new_socket 加入到epfd中,让epoll_wait()再去监视new_socket的状态
//将文件描述符设置为非阻塞
SetNoBlock(new_socket);
epoll_event event ;
event.data.fd = new_socket;
event.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_ADD,new_socket,&event);
}
}
//封装Read,实现非阻塞的轮询式的read()
ssize_t Read(int sock,char * buf,ssize_t max_size)
{
if(buf == NULL || max_size <= 0)
{
return -1;
}
ssize_t total_size = 0;
while(total_size < max_size)
{
//控制每次读取都不能使buf越界
//剩余空间如果大于1024,就一次性读取1024,否则就读取剩余的空间大小
int len = (max_size - total_size) > 1024 ? 1024 :(max_size - total_size);
ssize_t read_size = read(sock,buf+total_size,len);
if(read_size < 0 && errno == EAGAIN)
{
//这里的read()为非阻塞的
//说明为缓冲区中没有数据资源
//非阻塞轮询就结束了
printf("data not readyn");
break;
}
//如果这里为读取失败,read_size < 0 errno != EAGAIN
//让其再次尝试读取
if(read_size == 0)
{
printf("rad donen");
break;
}
else
{
//正常读取的情况
//修改total_size
total_size += read_size;
}
}
return total_size;
}
//处理连接上socket,进行读取数据
void Process_accept_socket(int epfd, int acc_socket,sockaddr_in * peer)
{
char buf[1024 * 10] = {0};
//非阻塞轮询进行读取
ssize_t read_size = Read(acc_socket,buf,sizeof(buf)-1);
if(read_size <= 0 )
{
close(acc_socket);
epoll_ctl(epfd,EPOLL_CTL_DEL,acc_socket,NULL);
printf("client[%s:%d] disconnect!n",inet_ntoa(peer->sin_addr),peer->sin_port);
return;
}
else
{
//正常读取的情况
buf[read_size] = '