epoll和ractor的粗浅理解

我们继续上篇的文章继续更新我们的代码。
首先就是介绍一下epoll的三个函数。

  • epoll_create
  • epoll_ctl
  • epoll_wait
    如何去理解这3个函数,我是这样去理解这个函数,
    就像我们去取快递一样,之前的Select模型,是通过轮询的方式一直去循环遍历客户端FD的列表,而EPOLL就相当于专门了一个快递柜,会将有读写事件的FD放到快递柜里面,而快递员只需要去快递柜进行取件和放件就可以了。
    epoll_create函数就相当于我们添加了一个快递柜在楼下,
    epoll_ctl就相当于我们添加快递或者取快递在快递柜中,
    epoll_wait就相当于快递员什么时候进行取件,什么时候取送件。

这就会有一些优点什么优点那?

  1. 不需要循环遍历所有fd
  2. 每一次取就绪集合,在固定位置;
  3. 异步解耦

那么我们在下面看一下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内核

posted @ 2022-10-10 11:15  飘雨的河  阅读(92)  评论(0编辑  收藏  举报