epoll反应堆代码深入分析

本文通过代码和注释深入分析epoll反应堆模型的工作原理,包括服务器和客户端代码实现细节,探讨模型设计的原因及优势。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


前言

本篇文章为笔者的读书笔记,未经允许请勿转载。如果对你有帮助记得点个赞(●’◡’●)
本篇文章会用图片加代码以及注释的方式帮助读者理解epoll反应堆的实现,我会在文章最后将自己绘制的函数代码关系图送给大家参考(第一次使用xmind绘制的,有点杂乱,献丑了)。


一,epoll反应堆模型

1.传统的epoll服务器模型

监听可读事件⇒ 数据到来 ⇒ 触发读事件 ⇒
epoll_wait()返回 ⇒ read消息 ⇒ write回射信息 ⇒ 继续epoll_wait()
⇒ 直到程序停止前都是这么循环

2.epoll反应堆服务器模型

监听可读事件 ⇒ 数据到来 ⇒ 触发读事件 ⇒
epoll_wait()返回 ⇒
read完数据; 节点下树; 设置监听写事件和对应写回调函数; 节点上树(可读事件回调函数内)
.
监听可写事件 ⇒ 对方可读 ⇒ 触发事件 ⇒
epoll_wait()返回 ⇒
write数据; 节点下树; 设置监听读事件和对应可读回调函数; 节点上树(可写事件回调函数内)
⇒ 直到程序停止前一直这么交替循环
这里的节点下树和上树涉及未决和非未决的转换,详细请看下图:
在这里插入图片描述
事件分为三个状态:非未决态(事件已经初始化了但未去监听),未决态(初始化了也去监听了等待事件发生),激活态。

3.为什么epoll反应堆模型要这样设计?

传统的epoll服务器模型中,完成收发信息只需要一个树上位置。epoll反应堆模型中,对于同一个socket而言,完成收发信息至少占用两个树上的位置。会频繁的在未决事件和非未决事件间切换,它这样设计有它的道理,虽然会浪费部分性能,但是这样的设计可以保证服务器的稳定性,避免因服务器收发数据时产生错误或阻塞(详细见下文),而传统epoll模型不具备这样的功能。
.

滑动窗口机制

服务器read到客户端的数据后,假设刚好此时客户端的接收滑动窗口满,假设不进行可写事件设置,并且客户端是有意让自己的接受滑动窗口满黑客。那么,当前服务器将阻塞在send函数处,不能向客户端发送数据,导致服务器程序阻塞。

SIGPIPE信号

客户端send完数据后,突然由于异常停止,这将导致一个FIN发送给服务器。如果服务器不设置可写事件监听,那么服务器在read完数据后,直接向没有读端的套接字中写入数据,TCP协议栈将会给服务器发送SIGPIPE信号,导致服务器进程终止。

二,epoll反应堆模型Server代码

#include <stdio.h>
#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 <stdlib.h>
#include <time.h>

#define MAX_EVENTS  1024
#define SERVER_PORT 8888

struct my_events {
    void       *m_arg;                                       //泛型参数,难点
    int        m_event;                                      //监听的事件  
    int        m_fd;                                         //监听的文件描述符
    void       (*call_back)(int fd, int event, void *arg);   //回调函数

    char       m_buf[BUFSIZ];
    int        m_buf_len;
    int        m_status;                                     //是否在红黑树上, 1->在, 0->不在
    time_t     m_lasttime;                                   //最后放入红黑树的时间
};

int                    ep_fd;                                //红黑树根(epoll_create返回的句柄)
struct my_events       ep_events[MAX_EVENTS];                //定义于任何函数体之外的变量被初始化为0(bss段)

/*初始化监听socket*/
void initlistensocket(int ep_fd, unsigned short port);
/*将结构体成员变量初始化*/
void eventset(struct my_events *my_ev, int fd, void (*call_back)(int fd, int event, void *arg), void *event_arg);
/*向红黑树添加 文件描述符和对应的结构体*/
void eventadd(int ep_fd, int event, struct my_events *my_ev);
/*从红黑树上删除 文件描述符和对应的结构体*/
void eventdel(int ep_fd, struct my_events *ev);
/*发送数据*/
void senddata(int client_fd, int event, void *arg);
/*接收数据*/
void recvdata(int client_fd, int event, void *arg);
/*回调函数: 接收连接*/
void acceptconnect(int listen_fd, int event, void *arg);


int main(void)
{
   unsigned short port = SERVER_PORT;

   ep_fd = epoll_create(MAX_EVENTS);                         //创建红黑树,返回给全局变量ep_fd;
   if (ep_fd <= 0)
      printf("create ep_fd in %s error: %s \n", __func__, strerror(errno));
  
   /*初始化监听socket*/
   initlistensocket(ep_fd, port);

   int checkpos = 0;
   int i;
   struct epoll_event events[MAX_EVENTS]; //epoll_wait的传出参数(数组:保存就绪事件的文件描述符)
   while (1)
   {
      /*超时验证,每次测试100个连接,60s内没有和服务器通信则关闭客户端连接*/
      long now = time(NULL); //当前时间
      for (i=0; i<100; i++,checkpos++) //一次循环检测100个,使用checkpos控制检测对象
      {
         if (checkpos == MAX_EVENTS-1)
             checkpos = 0;
         if (ep_events[i].m_status != 1) //不在红黑树上
             continue;

         long spell_time = now - ep_events[i].m_lasttime; //客户端不活跃的时间
         if (spell_time >= 600) //如果时间超过60s
         {
             printf("[fd= %d] timeout \n", ep_events[i].m_fd);  
             close(ep_events[i].m_fd); //关闭与客户端连接
             eventdel(ep_fd, &ep_events[i]); //将客户端从红黑树摘下
         }     
      }
	  
      /*监听红黑树,将满足条件的文件描述符加至ep_events数组*/ 
      int n_ready = epoll_wait(ep_fd, events, MAX_EVENTS, 1000); //1秒没事件满足则返回0
      if (n_ready < 0 && errno != EINTR)//EINTR:interrupted system call
      {
          perror("epoll_wait");
          break;
      }

      for (i=0; i<n_ready; i++)
      {
		   //将传出参数events[i].data的ptr赋值给"自定义结构体ev指针"
           struct my_events *ev = (struct my_events *)(events[i].data.ptr); 
           if ((events[i].events & EPOLLIN) && (ev->m_event & EPOLLIN))  //读就绪事件
               ev->call_back(ev->m_fd, events[i].events, ev->m_arg);
           if ((events[i].events & EPOLLOUT) && (ev->m_event & EPOLLOUT)) //写就绪事件
               ev->call_back(ev->m_fd, events[i].events, ev->m_arg);
      }
   }
   return 0;
}     

/*初始化监听socket*/
void initlistensocket(int ep_fd, unsigned short port)
{
	int                  listen_fd;
	struct sockaddr_in   listen_socket_addr;

	printf("\n initlistensocket() \n");  

	int opt = 1;
	setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));//设置为可以端口复用
   /*SO_REUSEADDR:(这通常是重启监听服务器时出现,若不设置此选项,则bind时将出错。)
     SOL_SOCKET:To manipulate options at the sockets API level, level is  specified  as  SOL_SOCKET.*/

	/*申请一个socket*/
	listen_fd = socket(AF_INET, SOCK_STREAM, 0);
	fcntl(listen_fd, F_SETFL, O_NONBLOCK); //将socket设置为非阻塞模式
   /*使I/O变成非阻塞模式(non-blocking),在读取不到数据或是写入缓冲区已满会马上return,而不会阻塞等待。*/
	
   /*绑定前初始化*/
	bzero(&listen_socket_addr, sizeof(listen_socket_addr));
	listen_socket_addr.sin_family      = AF_INET;
	listen_socket_addr.sin_port        = htons(port);
	listen_socket_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	/*绑定*/
	bind(listen_fd, (struct sockaddr *)&listen_socket_addr, sizeof(listen_socket_addr));
	/*设置监听上限*/
	listen(listen_fd, 128);

	/*将listen_fd初始化*/
	eventset(&ep_events[MAX_EVENTS-1], listen_fd, acceptconnect, &ep_events[MAX_EVENTS-1]);    
	/*将listen_fd挂上红黑树*/
	eventadd(ep_fd, EPOLLIN, &ep_events[MAX_EVENTS-1]);

    return ;
}

/*将结构体成员变量初始化*/
void eventset(struct my_events *my_ev, int fd, void (*call_back)(int, int, void *), void *event_arg)
{
   my_ev->m_fd       = fd;
   my_ev->m_event    = 0; //开始不知道关注的是什么事件,因此设置为0
   my_ev->m_arg      = event_arg;//指向自己
   my_ev->call_back  = call_back;
   
   my_ev->m_status   = 0; //0表示没有在红黑树上
   my_ev->m_lasttime = time(NULL);//调用eventset函数的绝对时间
   return ;
}

/*向红黑树添加文件描述符和对应的结构体*/
void eventadd(int ep_fd, int event, struct my_events *my_ev)
{
  int op;
  struct epoll_event epv;
  epv.data.ptr = my_ev;//让events[i].data.ptr指向我们初始化后的my_events,在注册的时候决定的,等到事件为激活态的时候,再将ptr的内容取出,进行比较,回调即可。
  epv.events   = my_ev->m_event = event; //EPOLLIN或EPOLLOUT,默认LT

  if (my_ev->m_status == 0)
  {
    op = EPOLL_CTL_ADD;
  }
  else
  {
    printf("\n add error: already on tree \n");
    return ;
  }
  
  if (epoll_ctl(ep_fd, op, my_ev->m_fd, &epv) < 0) //实际添加/修改
  {
     printf("\n event add/mod false [fd= %d] [events= %d] \n", my_ev->m_fd, my_ev->m_event);
  }
  else
  {
     my_ev->m_status = 1;
     printf("\n event add ok [fd= %d] [events= %d] \n", my_ev->m_fd, my_ev->m_event);
  }

  return ;
}
/*从红黑树上删除 文件描述符和对应的结构体*/
void eventdel(int ep_fd, struct my_events *ev)
{
  if(ev->m_status != 1)
     return ;

  epoll_ctl(ep_fd, EPOLL_CTL_DEL, ev->m_fd, NULL);
  ev->m_status = 0;
 
  return ;
}

/*回调函数: 接收连接*/
void acceptconnect(int listen_fd, int event, void *arg)
{
  int                 connect_fd;
  int                 i;//标识ep_events数组下标
  int                 flag=0;
  char                str[BUFSIZ];
  struct sockaddr_in  connect_socket_addr;
  socklen_t           connect_socket_len;// a value-result argument
  /*the caller must initialize it  to  contain the  size (in bytes) of the structure pointed to by addr;
   on return it will contain the actual size of the peer address.*/

  if ( (connect_fd=accept(listen_fd, (struct sockaddr *)&connect_socket_addr, &connect_socket_len)) <0 )
   /*addrlen不能用&+sizeof来获取,sizeof表达式的结果是一个unsigned的纯右值,无法对其引用。
   主要原因__addr_len是一个传入传出参数*/
  {
     if (errno != EAGAIN && errno != EINTR)
        {/*暂时不处理*/}
     printf("\n %s: accept, %s \n", __func__, strerror(errno));
     return ;
  }
  
  do
  {
    for(i=0; i<MAX_EVENTS; i++) //从全局数组ep_events中找一个空闲位置i(类似于select中找值为-1的位置)
        if(ep_events[i].m_status == 0) 
           break;
    if(i >= MAX_EVENTS)
     {
        printf("\n %s : max connect [%d] \n", __func__, MAX_EVENTS);
        break;
     }      
    
	/* 设置非阻塞 */
    if((flag = fcntl(connect_fd, F_SETFL, O_NONBLOCK)) <0)
    {
       printf("\n %s: fcntl nonblocking false, %s \n", __func__, strerror(errno));
       break;
    }

    eventset(&ep_events[i], connect_fd, recvdata, &ep_events[i]);
    eventadd(ep_fd, EPOLLIN, &ep_events[i]);

  }while(0);

   printf("\n new connection [%s:%d]  [time:%ld]  [pos:%d] \n", inet_ntop(AF_INET, &connect_socket_addr.sin_addr, str, sizeof(str)), 
                                ntohs(connect_socket_addr.sin_port), ep_events[i].m_lasttime, i);
   return ;
}

/*接收数据*/
void recvdata(int client_fd, int event, void *arg)
{
  int              len;
  struct my_events *ev = (struct my_events *)arg;

  len = recv(client_fd, ev->m_buf, sizeof(ev->m_buf), 0);
  //1.将ev对应的文件描述符和结构体从红黑树拿下
  eventdel(ep_fd, ev);                                      

  if (len >0)
  {
      ev->m_buf_len      = len;
      ev->m_buf[len] = '\0'; //手动添加结束标记
      printf("\n Client[%d]: %s \n", client_fd, ev->m_buf);

      eventset(ev, client_fd, senddata, ev); //2.设置client_fd对应的回调函数为senddata
      eventadd(ep_fd, EPOLLOUT, ev); //3.将ev对应的文件描述符和结构体放上红黑树,监听写事件EPOLLOUT
  }
  else if (len == 0)
  {
      close(ev->m_fd);
      eventdel(ep_fd, ev);
      printf("\n [Client:%d] disconnection \n", ev->m_fd);
  }
  else
  {
      close(ev->m_fd);
      eventdel(ep_fd, ev);
      printf("\n error: [Client:%d] disconnection\n", ev->m_fd);
  }
 
  return ;
}

/*发送数据*/
void senddata(int client_fd, int event, void *arg)
{
  int              len; 
  struct my_events *ev = (struct my_events *)arg;
 
  len = send(client_fd, ev->m_buf, ev->m_buf_len, 0);   //回写

  if (len > 0)
  {
     printf("\n send[fd=%d], [len=%d] %s \n", client_fd, len, ev->m_buf);
     eventdel(ep_fd, ev);  //1.将ev对应的文件描述符和结构体从红黑树拿下
     eventset(ev, client_fd, recvdata, ev); //2.设置client_fd对应的回调函数为recvdata
     eventadd(ep_fd, EPOLLIN, ev); //3.将ev对应的文件描述符和结构体放上红黑树,监听读事件EPOLLIN 
  }
  else
  {
     close(ev->m_fd);
     eventdel(ep_fd, ev);
     printf("\n send[fd=%d] error \n", client_fd);
  }
  return ;
}

三,epoll反应堆模型client代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>


#define MAX_LINE (1024)
#define SERVER_PORT (8888)

void setnoblocking(int fd)
{
  int opts=fcntl(fd,F_GETFL);
  opts=opts|O_NONBLOCK;
  fcntl(fd,F_SETFL,opts);
}

int main(int argc,char* argv[])
{
  int sockfd;
  char recvbuf[MAX_LINE+1]={0};/*变量在函数体内的默认初始化值是未定义的(不同编译器不同手段),
  尽量手动初始化,{0}只能初始化靠前的元素,剩下的被初始化成默认值(\000)。*/

  struct sockaddr_in server_addr;

  /*
  if(argc!=2)
  {
    fprintf(stderr,"usage ./cli <SERVER_IP> \n");
    exit(0);
  }
  */

  if((sockfd=socket(AF_INET,SOCK_STREAM,0))<0)
  {
    fprintf(stderr,"socket error");
    exit(0);
  }

  bzero(&server_addr,sizeof(server_addr));
  server_addr.sin_family=AF_INET;
  server_addr.sin_port=htons(SERVER_PORT);
  server_addr.sin_addr.s_addr=inet_addr("127.0.0.1");  
  /*
  if(inet_pton(AF_INET,argv[1],&server_addr.sin_addr)<=0)
  {
    fprintf(stderr,"inet_pton error for %s",argv[1]);
    exit(0);
  }
  */
  if(connect(sockfd,(struct sockaddr*)&server_addr,sizeof(server_addr))<0)
  {
    perror("connect");
    fprintf(stderr,"connect error\n");
    exit(0);
  }
  setnoblocking(sockfd);

  char input[100];
  int n=0;
  int count=0;

  while(fgets(input,100,stdin)!=NULL)//一次读取最多99个字符,直到stdin缓冲区读取完毕
  /* fgets() reads in at most one less than size characters from stream and stores them into the buffer pointed to by s.*/
  {
    printf("[send]:%s\n",input);
    n=send(sockfd,input,strlen(input),0);
    if(n<0)
    {
      perror("send");
    }
    n=0;
    count=0;

    while(1)
    {
      n=read(sockfd,recvbuf+count,MAX_LINE);/*报错:Resource temporarily unavailable,原因发送和接收的时间间隔太短
(这个错误表示资源暂时不够,能read时,读缓冲区没有数据,或者write时,写缓冲区满了。)导致read读不到数据而EAGAIN*/
      if(n<0)
      {
        if(errno == EAGAIN)//解决办法
          continue;
        perror("recv");
        break;
      }
      else 
      {

        count+=n;
        recvbuf[count]=0;//同'0'
        printf("[recv]:%s\n",recvbuf);
        break;
      }
    }
  }

  return 0;
}

附加内容

下面这张图片是我在网上查资料的时候看到的(没记错的话这张图来自黑马linux网络编程),对于理解epoll反应堆在底层的工作原理有一定帮助。这部分的内核和服务进程很好理解,但是用户空间这个mmap在此处的工作机制是否为下图所述,笔者现在无法笃定。我会在websever项目完成后继续回来深入研究(敬请期待:)
在这里插入图片描述

附件

更多代码详细说明在这个xmind里面(注意看是0积分下载,别被VIP字样吓到了:)
epoll反应堆函数代码关系图

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值