写在前面,原文出处: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,意味着这个事件已经被读过了,我们可以跳回至主循环中。
本次译文到此结束,如果想了更多,请看原文或者使用用户手册。