如何使用Linux Epoll来进行网络程序开发(译文)

写在前面,原文出处https://banu.com/blog/2/how-to-use-epoll-a-complete-example-in-c/
原文标题:How to use epoll? A complete example in C
传统网络服务器的实现主要是使用一个连接对应一个进程或者线程。对于需要高性能的应用(服务器)需要同时处理大量的客户(连接),而(前面)这种方法就不是很合适,由于诸多因素比如资源使用和上下文切换时间,会对处理众多客户(连接)的能力造成影响。一个可替代的方法是:在一个线程里使用非阻塞I/O,以及一些事件就绪通知方法,即:当你能在某个socket上进行读/写操作时,会发出一些就绪信号。
本文是对Linux下的epoll()工具的介绍,而epoll正式Linux系统中最好的就绪通知工具。我们将用C写一个示范代码来实现一个完整的TCP服务器。我建议读者拥有C语言的编程经历,指导如何在Linux系统上编译、运行程序,以及能够阅读用户帮助手册来查询我们用到的各种函数。
epoll实在Linux2.6中引进的,不兼容其他的UNIX系统。他提供了类似于select()和poll()类似的函数。

  • select() 最多能同时管理FD_SETSIZE个描述符,通常是一个很小的数字,决定于函数库的编译期(在select实现中,有相关字段指定size的大小)。

  • poll()并没有改进能在同一时刻能够管理的描述符数量,但是将其他的东西做了分离,甚至我们自己需要去做线性扫描来检查出就绪事件,时间复杂度为O(n),并且很慢。

epoll没有这些僵硬的限制,并且也不是线性扫描。今后,它能表现出更高的性能,处理更多的事件。

一个epoll实例是通过epoll_create()或者epoll_create1()(两者要求的参数不同)。epoll_ctl()是用来向epoll实例添加或者删除描述符的函数。在事件监视集合中等待事件(到来时),使用epoll_wait()函数,这个函数将会一直阻塞直到有事件到来。您可以阅读用户手册获取更多信息。

当描述符添加进一个epoll实例中时,可以使用两种模式:level triggered和edge triggered,即水平触发和边缘触发。当使用水平触发模式,数据就绪可用来读时,epoll_wait()将一直会把就绪事件返回。如果没有将数据完全读出,再次调用epoll_wait()来观察这个描述符时,它将再次返回这个就绪事件,因为数据仍旧是可用的。
在边缘触发模式中,你仅仅会得到一次就绪事件通知。如果没有将数据全部读完,再次调用epoll_wait()来监视这个描述符,它将会一直阻塞在等待就绪事件中。(因为上次的你要读的那个事件已经通知过了。)

传递给epoll_ctl()的epoll事件数据结构如下所示。因为每个描述符都在被监视,你能使用一个整形数字或一个指针来作为用户数据。

typedef union epoll_data
{
 void *ptr;
 int fd;
 __uint32_t u32;
 __uint64_t u64;
}epoll_data_t;

struct epoll_event
{
 __uint32_t events;/*Epoll events*/
 epoll_data_t data;/*User data variable */
}

让我们立刻开始代码~~
我们将要实现一个小巧的TCP服务器:将传递给socket的任何东西都通过标准输出打印出来。我们以写一个create_and_bind()函数作为开始,这个函数的功能是创建、绑定一个TCP socket:

static int create_and_bind(char* port)
{
    struct addrinfo hints;
    struct addrinfo *result,*rp;
    int s,sfd;

    memset(&hints,0,sizeof(struct addrinfo));
    hints.ai_family = AF_UNSPEC; //Return IpV4 AND ipv6 choices
    hints.ai_socktype = SOCK_STREAM;//we want a TCP socket
    hints.ai_flags = AI_PASSIVE;// all interfaces

    s = getaddrinfo(NULL,port,&hints,&result);
    if(s!=0)
    {
        fprintf(stderr,"getaddrinfo:%s \n",gai_strerror(s));
        return -1;
    }

    for(rp = result;rp != NULL;rp = rp->ai_next)
    {
        sfd = socket(rp->ai_family,rp->ai_socktype,rp->ai_protocol);
        if(sfd == -1)
            continue;
        s = bind(sfd,rp->ai_addr,rp->addrlen);
        if(s==0)
        {
            //we managed to bind successful
            break;
        }
        close(sfd);
    }
    if(rp == NULL)
    {
        fprintf(stderr,"Could not bind!\n");
        return -1;
    }
    freeaddrinfo(result);
}

create_and_bind()包含一个标准的代码块,用来方便的获取IPv4 或者 IPv6套接字。 它接收一个字符串类型port参数,可以通过argv[1]来传递过来。而getaddrinfo()函数的结果返回一串addrinfo 结构,与hints中的参数是兼容的。addrinfo结构如下:

struct addrinfo
{
    int     ai_flags;
    int     ai_family;
    int     ai_socktype;
    int     ai_protocol;
    size_t  ai_addrlen;
    struct sockaddr* ai_addr;
    char     *ai_canonname;
    struct addrinfo *ai_next;
};

我们一个接一个的尝试这个结构体里面的各种类型,一直到我们能够创建、绑定一个socket。如果我们成功的创建了socket,这个函数会返回一个文件描述符;反之,返回 -1。

接下来,我们写一个函数来让socket成为非阻塞类型。make_scket_non_blocking() 在这个描述符上设置 O_NONBLOCK 标志通过sfd参数。

static int make_non_blocking(int sfd)
{
    int flags,s;
    flags = fcntl(sfd,F_GETFL,0);
    if(flags == -1)
    {
        perror("fcntl");
        return -1;
    }

    flags |= O_NONBLOCK;
    s = fcntl(sfd,F_SETFL,flags);
    if(s == -1)
    {
        perror("fcntl");
        return -1;
    }
    return 0;
}

现在,在主函数代码中包含了事件循环。这是程序的主体:

#define MAXEVENT 64
int main(int argc,char *argv[])
{
    int sfd,s;
    int efd;
    struct epoll_event event;
    struct epoll_event *events;
    if(argc != 2)
    {
        fprintf(stderr,"Usage:%s [port]\n",argv[0]);
        exit(EXIT_FAILURE);
    }
    sfd = create_and_bind(argv[1]);
    if(sfd == -1)
    {
        abort();
    }

    s = make_socket_non_blocking(sfd);
    if(s == -1)
    {
        abort();
    }

    s = listen(sfd,SOMAXCONN);
    if(s == -1)
    {
        perror("listen");
        abort();
    }

    efd = epoll_create1(0);
    if(efd == -1)
    {
        perror("epoll_create");
        abort();
    }

    event.data.fd = sfd;
    event.events = EPOLLIN | EPOLLET;
    s = epoll_ctl(efd,EPOLL_CTL_ADD,sfd,&event);
    if(s == -1)
    {
        perror("epoLL_ctl");
        abort();
    }

    //buffer where events are returned
    events = calloc(MAXEVENTS,sizeof event);
    //the events loop
    /* The event loop */
  while (1)
    {
      int n, i;

      n = epoll_wait (efd, events, MAXEVENTS, -1);
      for (i = 0; i < n; i++)
    {
      if ((events[i].events & EPOLLERR) ||
              (events[i].events & EPOLLHUP) ||
              (!(events[i].events & EPOLLIN)))
        {
              /* An error has occured on this fd, or the socket is not
                 ready for reading (why were we notified then?) */
          fprintf (stderr, "epoll error\n");
          close (events[i].data.fd);
          continue;
        }

      else if (sfd == events[i].data.fd)
        {
              /* We have a notification on the listening socket, which
                 means one or more incoming connections. */
              while (1)
                {
                  struct sockaddr in_addr;
                  socklen_t in_len;
                  int infd;
                  char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];

                  in_len = sizeof in_addr;
                  infd = accept (sfd, &in_addr, &in_len);
                  if (infd == -1)
                    {
                      if ((errno == EAGAIN) ||
                          (errno == EWOULDBLOCK))
                        {
                          /* We have processed all incoming
                             connections. */
                          break;
                        }
                      else
                        {
                          perror ("accept");
                          break;
                        }
                    }

                  s = getnameinfo (&in_addr, in_len,
                                   hbuf, sizeof hbuf,
                                   sbuf, sizeof sbuf,
                                   NI_NUMERICHOST | NI_NUMERICSERV);
                  if (s == 0)
                    {
                      printf("Accepted connection on descriptor %d "
                             "(host=%s, port=%s)\n", infd, hbuf, sbuf);
                    }

                  /* Make the incoming socket non-blocking and add it to the
                     list of fds to monitor. */
                  s = make_socket_non_blocking (infd);
                  if (s == -1)
                    abort ();

                  event.data.fd = infd;
                  event.events = EPOLLIN | EPOLLET;
                  s = epoll_ctl (efd, EPOLL_CTL_ADD, infd, &event);
                  if (s == -1)
                    {
                      perror ("epoll_ctl");
                      abort ();
                    }
                }
              continue;
            }
          else
            {
              /* We have data on the fd waiting to be read. Read and
                 display it. We must read whatever data is available
                 completely, as we are running in edge-triggered mode
                 and won't get a notification again for the same
                 data. */
              int done = 0;

              while (1)
                {
                  ssize_t count;
                  char buf[512];

                  count = read (events[i].data.fd, buf, sizeof buf);
                  if (count == -1)
                    {
                      /* If errno == EAGAIN, that means we have read all
                         data. So go back to the main loop. */
                      if (errno != EAGAIN)
                        {
                          perror ("read");
                          done = 1;
                        }
                      break;
                    }
                  else if (count == 0)
                    {
                      /* End of file. The remote has closed the
                         connection. */
                      done = 1;
                      break;
                    }

                  /* Write the buffer to standard output */
                  s = write (1, buf, count);
                  if (s == -1)
                    {
                      perror ("write");
                      abort ();
                    }
                }

              if (done)
                {
                  printf ("Closed connection on descriptor %d\n",
                          events[i].data.fd);

                  /* Closing the descriptor will make epoll remove it
                     from the set of descriptors which are monitored. */
                  close (events[i].data.fd);
                }
            }
        }
    }

  free (events);

  close (sfd);

  return EXIT_SUCCESS;
}

main()函数中,首先调用create_and_bind()函数,而后者是用来设置socket:设置socket为non-blocking模式,然后调用listen()函数,接着创建一个描述符为efd的epoll实例,继而将使用边缘触发的、监听中的socket sfd添加到input events中。

外部的循环是主函数中的主要的事件循环。它调用epoll_wait(),直到有事件到来为止一直处于阻塞状态。当事件可用(到来)时,epoll_wait()返回这些事件:一簇epoll_event结构体。

当我们添加新的连接到监视中,移除那些已经断开的连接,epoll实例是一直处于更新状态中的。

当事件是可用状态时,有三种状态:

  • Errors :当一个错误条件出现或者这个事件并不是一个关于数据可读的通知,我们仅仅需要关闭这个描述符。关闭描述符会自动的从epoll监视中移除。

  • New connections:当监听的描述符就绪可读时,它意味着一个或者更多个连接的到来。当有新的连接,accept()这些连接,打印关于这个链接的消息,是这个socket为non-blocking并添加到epoll实例的监视集中。

  • Client data:当任何一个客户连接就绪可读时,我们使用read()在一个内部循环中每次读取512字节的数据,这样做是为将数据全部读完。更深层次原因是:我们使用的是边缘触发模式。使用write()写数据。如果read()返回0,这意味着一个EOF,我们可以关闭这个连接了。而如果返回-1,并且errno被置为EAGAIN,意味着这个事件已经被读过了,我们可以跳回至主循环中。

本次译文到此结束,如果想了更多,请看原文或者使用用户手册。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值