epoll和ractor的粗浅理解
我们继续上篇的文章继续更新我们的代码。
首先就是介绍一下epoll的三个函数。
- epoll_create
- epoll_ctl
- epoll_wait
如何去理解这3个函数,我是这样去理解这个函数,
就像我们去取快递一样,之前的Select模型,是通过轮询的方式一直去循环遍历客户端FD的列表,而EPOLL就相当于专门了一个快递柜,会将有读写事件的FD放到快递柜里面,而快递员只需要去快递柜进行取件和放件就可以了。
epoll_create函数就相当于我们添加了一个快递柜在楼下,
epoll_ctl就相当于我们添加快递或者取快递在快递柜中,
epoll_wait就相当于快递员什么时候进行取件,什么时候取送件。
这就会有一些优点什么优点那?
- 不需要循环遍历所有fd
- 每一次取就绪集合,在固定位置;
- 异步解耦
那么我们在下面看一下EPOLL实现的服务器的代码,我会把对应的注释标记到代码上
#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<fcntl.h>
#include <unistd.h>
#include<sys/epoll.h>
#include <string.h>
#define BUFFER_LENGTH 128
#define EVENTS_LENGTH 128
char rbuffer[BUFFER_LENGTH] = {0};
char wbuffer[BUFFER_LENGTH] = {0};
int main()
{
unsigned char buffer[BUFFER_LENGTH]={0};
int ret=0;
//socket有两个参数,第一个参数指定我们要使用IPV4,还是IPV6,第二个参数表明我们要使用套接字类型,这里我们使用的是流格式的套接字,第三个参数就是我们需要使用传输协议
//这里使用0,表示让系统自动推导我们需要使用的传输协议。
int listenfd= socket(AF_INET,SOCK_STREAM,0);
//如果返回值为-1,说明我们创建SOCKET失败,直接返回。
if (listenfd==-1)
{
return -1;
}
//我们需要绑定的信息
struct sockaddr_in serveraddr;
//使用IPV4
serveraddr.sin_family=AF_INET;
//我们需要绑定的IP地址,INADDR_ANY 就是0.0.0.0 ,就是所有网卡的所有IP段都可以连接到我们的创建的TCP服务器上。
serveraddr.sin_addr.s_addr=htonl(INADDR_ANY);
//我们需要绑定的端口,这里我们绑定的端口为9999
serveraddr.sin_port=htons(9999);
//第一个参数我们创建的套接字,第二个是我们填写的绑定信息,最后是我们的绑定信息结构体的大小。
if (-1==bind(listenfd,(const sockaddr*)&serveraddr,sizeof(serveraddr)))
{
return -2;
}
//监听我们创建的套接字,请求的队列数量,这里我们填写为10个
listen(listenfd,10);
//定义客户端的socket
//定义可读序列和可写序列
fd_set rfds,wfds,rset,wset;
//清空序列
FD_ZERO(&rfds);
//设置读的序列
FD_SET(listenfd,&rfds);
//清空可写的序列
FD_ZERO(&wfds);
int maxfd=listenfd;
//开始进行EPOLL的创建
int epfd = epoll_create(1);
struct epoll_event ev,events[EVENTS_LENGTH];
ev.events=EPOLLIN;
ev.data.fd=listenfd;
//添加我们的服务器通信的listenfd到EPFD中,
epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
//接下来开始接受 我们的客户端的连接请求
while (1)
{
//我们需要详细讲解一下这个函数的里面的各个参数的意义 ,以及它什么时候是阻塞的,什么时候是非阻塞的,
//第一个参数我们的EPFD的文件描述符,第二个我们的接收事件的缓冲器,第三个是我们事件数量的多少,最后一个参数就是我们等待的时长了。
//当是-1的时候就是一直等待连接的意思,没有连接就会 一直被阻塞住,
//当是0的时候就是一直有连接直接返回的意思,
//当是大于0的数的时候,就是在轮询查看是否有事件的时长,单位是MS。
int nready = epoll_wait(epfd,events,EVENTS_LENGTH,-1);
printf("----------%d\n",nready);
//开始遍历我们的事件
int i =0;
for (int i = 0; i < nready; i++)
{
int clientfd=events[i].data.fd;
if (listenfd==clientfd)
{
//如果是我们的监听的FD,说明是有客户端连入的事件
struct sockaddr_in client;
socklen_t len=sizeof(client);
//接受客户端的请求,
int connfd=accept(listenfd,(struct sockaddr*)&client,&len);
if (connfd==-1)
{
break;
}
printf("accept:%d\n",connfd);
//增加到我们的快递柜中
ev.events=EPOLLIN;
ev.data.fd=connfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
//如果是读的请求
}
else if (events[i].events & EPOLLIN)
{
//如果客户端在线可以接受到消息
int n=recv(clientfd,rbuffer,BUFFER_LENGTH,0);
if (n>0)
{
rbuffer[n]='\0';
printf("clientfd :%d recv: %s ,n:%d\n",clientfd,rbuffer,n);
memcpy(wbuffer,rbuffer,BUFFER_LENGTH);
ev.events=EPOLLOUT;
ev.data.fd=clientfd;
epoll_ctl(epfd,EPOLL_CTL_MOD,clientfd,&ev);
//客户端退出的时候会触发
}
else
{
ev.data.fd=clientfd;
epoll_ctl(epfd,EPOLL_CTL_DEL,clientfd,&ev);
}
}
else if(events[i].events & EPOLLOUT)
{
int sent = send(clientfd, wbuffer, BUFFER_LENGTH, 0); //
printf("sent: %d\n", sent);
ev.events = EPOLLIN;
ev.data.fd = clientfd;
epoll_ctl(epfd, EPOLL_CTL_MOD, clientfd, &ev);
}
}
}
return 0;
}
上面我已经把对应的注释以及注意的点已经写在了代码的上面,
这里我们还要说一个问题,就是EPOLLLT和EPOLLET的问题
LT模式
对于读事件 EPOLLIN,只要socket上有未读完的数据,EPOLLIN 就会一直触发,直到我们的数据接收完毕后才会停止;对于写事件 EPOLLOUT,只要socket可写,EPOLLOUT 就会一直触发。
在这种模式下,大家会认为读数据会简单一些,因为即使数据没有读完,那么下次调用epoll_wait()时,它还会通知你在上没读完的文件描述符上继续读,也就是人们常说的这种模式不用担心会丢失数据。
而写数据时,因为使用 LT 模式会一直触发 EPOLLOUT 事件,那么如果代码实现依赖于可写事件触发去发送数据,一定要在数据发送完之后移除检测可写事件,避免没有数据发送时无意义的触发。
ET模式
对于读事件 EPOLLIN,只有socket上的数据从无到有,EPOLLIN 才会触发;对于写事件 EPOLLOUT,只有在socket写缓冲区从不可写变为可写,EPOLLOUT 才会触发(刚刚添加事件完成调用epoll_wait时或者缓冲区从满到不满)
这种模式听起来清爽了很多,只有状态变化时才会通知,通知的次数少了自然也会引发一些问题,比如触发读事件后必须把数据收取干净,因为你不一定有下一次机会再收取数据了,即使不采用一次读取干净的方式,也要把这个激活状态记下来,后续接着处理,否则如果数据残留到下一次消息来到时就会造成延迟现象。
这种模式下写事件触发后,后续就不会再触发了,如果还需要下一次的写事件触发来驱动发送数据,就需要再次注册一次检测可写事件。
其次我们代码还有一个问题,就是公用同一个缓冲区的问题,这个问题,我们后面的文章再去解决。
推荐一个零声学院免费教程,个人觉得老师讲得不错,
分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,
fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,
TCP/IP,协程,DPDK等技术内容,点击立即学习:
服务器
音视频
dpdk
Linux内核