网络服务器的传统实现一般都是对于每个链接使用一个单独的进程或者线程。对于要并发处理数量庞大的客户端请求并且要高性能的应用场景,这种传统的方法表现并不会很好。因为诸如资源使用率,上下文环境切换,等因素,都将会影响并发处理客户请求的能力。一个可行的方法是在一个线程中使用非阻塞IO,然后通过就绪事件通知机制来捕获socket中有可读写数据的事件。
这篇文章将介绍Linux的epoll机制,epoll被认为是Linux系统中最好的就绪事件通知机制。这篇文章使用C语言实现了一个完整的TCP服务端实现的示例代码。本文假设你有C语言编程经验,知道如何在Linux上编译和运行C程序,知道如何查阅在示例代码中使用到的各种C函数的使用说明。
epoll是在Linux2.6中引入的,在其他的类Unix系统中并不可用。它提供了与select和poll类似的功能:
select可以同时监测FD_SETSIZE个文件描述符。FD_SETSIZE通常比较小,它在libc编译的时候定义。
poll并没有对同时可监测的文件描述符的个数做出限制,但是抛开这些,程序在每一次都需要遍历所有的文件描述符来检查是否有可读写事件通知。这是线性的,性能不高。
epoll没有文件描述符的个数限制,而且不需要进行线性的扫描,所以,它可以处理数量巨大的事件,并且有很好的性能。
一个epoll实例通过epoll_create或者epoll_create1(它们参数不同,都返回一个epoll实例)。Epoll用于epoll实例添加或者删除被监测的文件描述符。epoll_wait用于等待可读写事件,在事件发生之前,它将一直阻塞。请查阅函数手册获取更多相关的信息。
有以下两种模式添加文件描述符到epoll实例中:level_triggered 或者 edge_triggered。当使用leverl_triggered模式时,当用数据可以读,epoll_wait总会返回,此时,如果你没有完全读取所有的可读数据,然后再次调用epoll_wait监测这个文件描述符,epoll_wait将再次返回通知就绪事件,因为还有数据可读。相对而言,在edg_triggered模式下,如果没有完全的读取所有的可读数据情况下,再次使用epoll_wait将会阻塞而不会马上再次返回,因为这个模式下,同一个就绪事件通知只会执行一次。
epoll_ctl的参数Epoll event 数据结构如下,每一个被监测的描述符,user data可以与一个整形或者指针相关联。
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->ai_addrlen); if (s == 0) { /* We managed to bind successfully! */ break; } close (sfd); } if (rp == NULL) { fprintf (stderr, "Could not bind\n"); return -1; } freeaddrinfo (result); return sfd; }
|
create_and_bind()包含了一个简单便捷的方式获取IPv4或者IPv6 socket的代码片段。它接收一个port字符串作为参数来指定端口。getaddrinfo函数在result中返回一串addrinfo。这些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。create_and_bind()如果成功,返回socket描述符,否则返回-1。
接下来,编写一个函数设置socket模式为非阻塞。make_socket_non_blocking() 对文件描述符参数sfd设置O_NONBLOCK标志位:
static int make_socket_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;
|
接下来实现的main()函数是这个程序的主要部分,它包含了事件循环处理逻辑:
#define MAXEVENTS 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 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为非阻塞模式,接着调用listen监听端口。然后创建一个epoll实例efd,edge-triggered模式添加socketsfd到efd实例中,监听输入事件。
外部的while循环式主要的事件循环处理逻辑。调用epoll_wait,一直阻塞直到有事件发生。当事件发生时,epoll_wait在events参数中返回具体事件,events参数是一串epoll_event数据结构。
在事件循环处理逻辑中,当新的链接建立起来需要被监听,或者当链接被移除的时候,Epoll实例efd持续被更新。
当事件通知发生时,可能有以下三种情况:
1. 错误:当错误发生时,或者事件不是数据可读时,我们通常直接关闭相关的文件描述符。关闭文件描述符操作将会自动从epoll实例中移除对应的文件描述符;
2. 新的链接请求:当监听端口的socket描述符可读时,表示有一个或者多个新的链接请求达到。这时,调用accept接受链接请求,然后将新建立的socket设置为非阻塞模式,并且将它加入epoll实例的监听集合中;
3. 客户端数据:当与客户端的链接的socket描述符有可读的数据时,我们使用read函数在内循环中逐次读取512字节数据。因为在edge-triggered模式下,我们需要读取所有的数据,我们并不希望再次接收到这个事件的重复通知。读取到的数据在这个例子中将会写入标准输出。Read函数返回0表明读到文件结束,我们可以关闭与这个客户端的链接。Read如果返回-1,并且errorno的值为EAGAIN,表明这个事件通知的所有数据都已经读完,我们可以返回到主循环。
以上就是整个处理逻辑。在监听循环中,不断的添加或者删除监听集合中的文件描述符。
原文地址: https://banu.com/blog/2/how-to-use-epoll-a-complete-example-in-c/