IO复用-epoll模型

158 阅读4分钟

阻塞/非阻塞IO

阻塞:程序在调用结果返回之前,会等待,得到结果之后才会返回
非阻塞:不管调用是否得到结果,程序都不会等待,立即返回
缺省阻塞的函数:connect(), send(), revc(), accept()

epoll模型:

epoll只在有linux下才有,windows中没有,包含sys/epoll.h头文件

水平触发&边缘触发
    select和poll采用水平触发
    epoll有水平触发和边缘触发两种机制
    水平触发:
        如果接收缓冲区不为空,表示有数据可读,如果数据没有被读取完,再次调用epoll_wait的时候,读事件一直触发
        如果发送缓冲区没有满,表示可以写入数据,只要缓冲区没有写满,再次调用epoll_wait的时候,写事件一直触发
        读:    
            有客户端连接上来,会触发读事件,如果一直不处理客户端连接(注释此语句块代码),就会一直触发
            如果客户端有报文过来,会触发读事件,不处理(注释此语句块代码), 一直触发 
        写:
            如果关注客户端的写事件,可以向客户端缓冲发送报文的话,一直触发写事件,直到缓冲区写满
            每个连接的客户端都有发送缓冲区,各自填满为止

    边缘触发:
        socket加入epoll后,如果接收缓冲区不为空,触发可读事件,如果有新的数据到达,再次触发可读事件
        边缘触发(如果有客户端连接请求未处理,只触发一次)
        epoll触发可读事件后,不管程序有没有处理可读的事件,epoll都不会再触发,只有当新的数据到达时,才再次触发可读事件
        
        可写事件:
            socket加入epoll后,如果发送缓冲区不为空,触发可写事件,如果发送缓冲区由满变成有空时,再次触发可写事件
            网上有文章说边缘触发模式下,只要发送缓冲区有变化,就会触发写事件,是不对的。

IO复用的场景要使用非阻塞IO

1.当数据达到socket缓冲区的时候,可能会因为某些原因被内核丢弃,比如校验和错误,这时候,如果采用了阻塞的IO
唤醒的程序读到不到数据,acceptrecv函数就会阻塞
2.达到缓冲区的数据有可能被别人取走,比如多个进程accept同一个socket时引发的惊群现象,只有一个连接到来,
但是所有的监听进程都会被唤醒,最终只有一个进程可以accept到这个请求,其他进程accept会被阻塞
3.ET边缘触发模式必须要使用非阻塞IO,因为程序中需要循环读和写,直到EAGAN出现,如果使用阻塞IO容易被阻塞住
    读的方法:
        如果接收缓冲区中有事件没有处理或有数据没有读完
        水平触发:epoll_wait会重复报告,不必担心遗漏事件
        边缘触发:epoll_wait不会重复报告,程序要用一个循环处理全部的事件或读取全部的数据
    写的方法:
        想写就直接写,出现EAGAIN就别写了
        水平触发:epoll_wait会重复报告,如果不想写了,可以注销事件,否则一直触发
        边缘触发:epoll_wait不会重复报告,不想写了就不写,不必注销事件
百度为什么IO多路复用要搭配非阻塞IO
/*
 * 程序名:tcpepoll.cpp,此程序用于演示采用epoll模型的使用方法。
 *
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/types.h>          /* See NOTES */
#include <arpa/inet.h>
#include <sys/fcntl.h>
#include <sys/epoll.h>

// 初始化服务端的监听端口。
int initserver(int port);

int main(int argc,char *argv[])
{
  if (argc != 2) { printf("usage: ./tcpepoll port\n"); return -1; }

  // 初始化服务端用于监听的socket。
  int listensock = initserver(atoi(argv[1]));
  printf("listensock=%d\n",listensock);

  if (listensock < 0) { printf("initserver() failed.\n"); return -1; }

  // 创建epoll句柄。
  int epollfd=epoll_create(1);

  // 为监听的socket准备可读事件。
  struct epoll_event ev;  // 声明事件的数据结构。
  ev.events=EPOLLIN;      // 读事件。
  //ev.events=EPOLLIN|EPOLLET;     // 读事件。+边缘触发(如果有客户端连接请求未处理,只触发一次)
  ev.data.fd=listensock;  // 指定事件的自定义数据,会随着epoll_wait()返回的事件一并返回。

  // 把监听的socket的事件加入epollfd中。
  epoll_ctl(epollfd,EPOLL_CTL_ADD,listensock,&ev);

  struct epoll_event evs[10];      // 存放epoll返回的事件。

  while (true)
  {
    // 等待监视的socket有事件发生。第四个参数是超时间间
    int infds=epoll_wait(epollfd,evs,10,-1);

    // 返回失败。
    if (infds < 0)
    {
      perror("epoll() failed"); break;
    }

    // 超时。
    if (infds == 0)
    {
      printf("epoll() timeout.\n"); continue;
    }

    // 如果infds>0,表示有事件发生的socket的数量。
    // 遍历epoll返回的已发生事件的数组evs。
    for (int ii=0;ii<infds;ii++)
    {
      printf("events=%d,data.fd=%d\n",evs[ii].events,evs[ii].data.fd);

      // 如果发生事件的是listensock,表示有新的客户端连上来。
      if (evs[ii].data.fd==listensock)
      {
        //有客户端连接上来,会触发读事件,如果一直不处理客户端连接(注释此语句块代码),就会一直触发 (水平触发)
        //printf("有客户端连接。。。\n");

        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        int clientsock = accept(listensock,(struct sockaddr*)&client,&len);

        printf ("accept client(socket=%d) ok.\n",clientsock);

        // 为新客户端准备可读事件,并添加到epoll中。
        ev.data.fd=clientsock;
        ev.events=EPOLLIN;
        //ev.events=EPOLLOUT; //关注客户端的写事件
        epoll_ctl(epollfd,EPOLL_CTL_ADD,clientsock,&ev);
      }
      else
      {
        //如果客户端有报文过来,会触发读事件,不处理(注释此语句块代码), 一直触发  (水平触发)
        //printf("客户端有可读报文\n");

        //如果关注客户端的写事件,可以向客户端缓冲发送报文的话,一直触发写事件,直到缓冲区写满
        //每个连接的客户端都有发送缓冲区,各自填满为止
        //printf("可以向客户端发送报文\n");

        // 如果是客户端连接的socket有事件,表示有报文发过来或者连接已断开。
        char buffer[1024]; // 存放从客户端读取的数据。
        memset(buffer,0,sizeof(buffer));
        if (recv(evs[ii].data.fd,buffer,sizeof(buffer),0)<=0)
        {
          // 如果客户端的连接已断开。
          printf("client(eventfd=%d) disconnected.\n",evs[ii].data.fd);
          close(evs[ii].data.fd);    // 关闭客户端的socket,系统会将关闭的socket从epoll中删除
        }
        else
        {
          // 如果客户端有报文发过来。
          printf("recv(eventfd=%d):%s\n",evs[ii].data.fd,buffer);
          // 把接收到的报文内容原封不动的发回去。
          send(evs[ii].data.fd,buffer,strlen(buffer),0);
        }
      }
    }
  }

  return 0;
}

// 初始化服务端的监听端口。
int initserver(int port)
{
  int sock = socket(AF_INET,SOCK_STREAM,0);
  if (sock < 0)
  {
    perror("socket() failed"); return -1;
  }

  int opt = 1; unsigned int len = sizeof(opt);
  setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,len);

  struct sockaddr_in servaddr;
  servaddr.sin_family = AF_INET;
  servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
  servaddr.sin_port = htons(port);

  if (bind(sock,(struct sockaddr *)&servaddr,sizeof(servaddr)) < 0 )
  {
    perror("bind() failed"); close(sock); return -1;
  }

  if (listen(sock,5) != 0 )
  {
    perror("listen() failed"); close(sock); return -1;
  }

  return sock;
}

/*
 * 程序名:tcpepoll.cpp,此程序用于演示采用epoll模型的使用方法。
 * 
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/types.h>          /* See NOTES */
#include <arpa/inet.h>
#include <sys/fcntl.h>
#include <sys/epoll.h>

// 初始化服务端的监听端口。
int initserver(int port);

int main(int argc,char *argv[])
{
  if (argc != 2) { printf("usage: ./tcpepoll port\n"); return -1; }

  // 初始化服务端用于监听的socket。
  int listensock = initserver(atoi(argv[1]));
  printf("listensock=%d\n",listensock);

  if (listensock < 0) { printf("initserver() failed.\n"); return -1; }

    //设置socket为阻塞模式
    //fcntl(listensock, F_SETFL, fcntl(listensock, F_GETFD,0)|O_NONBLOCK);

  // 创建epoll句柄。
  int epollfd=epoll_create(1);

  // 为监听的socket准备可读事件。
  struct epoll_event ev;  // 声明事件的数据结构。
  ev.events=EPOLLIN|EPOLLET;      // 读事件+边缘触发
  ev.data.fd=listensock;  // 指定事件的自定义数据,会随着epoll_wait()返回的事件一并返回。

  // 把监听的socket的事件加入epollfd中。
  epoll_ctl(epollfd,EPOLL_CTL_ADD,listensock,&ev);

  struct epoll_event evs[10];      // 存放epoll返回的事件。

  while (true)
  {
    // 等待监视的socket有事件发生。
    int infds=epoll_wait(epollfd,evs,10,-1);

    // 返回失败。
    if (infds < 0)
    {
      perror("epoll() failed"); break;
    }

    // 超时。
    if (infds == 0)
    {
      printf("epoll() timeout.\n"); continue;
    }

    // 如果infds>0,表示有事件发生的socket的数量。
    // 遍历epoll返回的已发生事件的数组evs。
    for (int ii=0;ii<infds;ii++)
    {
      printf("events=%d,data.fd=%d\n",evs[ii].events,evs[ii].data.fd);

      // 如果发生事件的是listensock,表示有新的客户端连上来。
      if (evs[ii].data.fd==listensock)
      {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        int clientsock = accept(listensock,(struct sockaddr*)&client,&len);

        printf ("accept client(socket=%d) ok.\n",clientsock);
fcntl(clientsock, F_SETFL, O_NONBLOCK);       // 把客户端socket连接设置为非阻塞。
        // 为新客户端准备可读事件,并添加到epoll中。
        ev.data.fd=clientsock;
        ev.events=EPOLLOUT|EPOLLET;  //写事件+边缘触发
        epoll_ctl(epollfd,EPOLL_CTL_ADD,clientsock,&ev);
// printf("有新客户端连上来。\n");
      }
      else
      {
printf("可以向客户端写数据。\n");
for (int uu=0;uu<10000;uu++)
{
  if (send(evs[ii].data.fd,"dssssssssssssssssssssdddddddddddddddd",30,0)<0)
    if (errno==EAGAIN) break;  // 如果发送缓冲区已填满就不再发送。 
}
printf("写完。\n");
/*
        // 如果是客户端连接的socke有事件,表示有报文发过来或者连接已断开。
        char buffer[1024]; // 存放从客户端读取的数据。
        memset(buffer,0,sizeof(buffer));
        int iret = 0;
        char *ptr=buffer;
        while(true)     //用一个循环,一直读,读到EAGAIN为止
        {
          if ( iret = recv(evs[ii].data.fd,ptr,10,0)<=0)
          {
            if(errno == EAGAIN) break;
            // 如果客户端的连接已断开。
            printf("client(eventfd=%d) disconnected.\n",evs[ii].data.fd);
            close(evs[ii].data.fd);            // 关闭客户端的socket
          }
          ptr=ptr+iret;
        }

        if(strlen(buffer) > 0)
        {
          // 如果客户端有报文发过来。
          printf("recv(eventfd=%d):%s\n",evs[ii].data.fd,buffer);
          // 把接收到的报文内容原封不动的发回去。
          send(evs[ii].data.fd,buffer,strlen(buffer),0);
        }
*/
      }
    }
  }

  return 0;
}

// 初始化服务端的监听端口。
int initserver(int port)
{
  int sock = socket(AF_INET,SOCK_STREAM,0);
  if (sock < 0)
  {
    perror("socket() failed"); return -1;
  }

  int opt = 1; unsigned int len = sizeof(opt);
  setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,len);

  struct sockaddr_in servaddr;
  servaddr.sin_family = AF_INET;
  servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
  servaddr.sin_port = htons(port);

  if (bind(sock,(struct sockaddr *)&servaddr,sizeof(servaddr)) < 0 )
  {
    perror("bind() failed"); close(sock); return -1;
  }

  if (listen(sock,5) != 0 )
  {
    perror("listen() failed"); close(sock); return -1;
  }

  return sock;
}