看过上期的都知道,我是搞java的,所以对这些可能理解不是很清楚,各位看完可以尽情发言。
事件循环和非阻塞IO
在服务器端网络编程中,有三种处理并发连接的方法。
它们是:**分叉**、**多线程**和**事件循环**。分叉为每个客户端连接创建新进程,以实现并发性。多线程使用线程而不是进程。事件循环使用轮询和非阻塞IO,通常在单个线程上运行。由于进程和线程的开销,大多数现代生产级软件使用事件循环进行网络连接。
我们服务器的事件循环的简化伪代码是:
all_fds = [...]
while True:
active_fds = poll(all_fds)
for each fd in active_fds:
do_something_with(fd)
def do_something_with(fd):
if fd is a listening socket:
add_new_client(fd)
elif fd is a client connection:
while work_not_done(fd):
do_something_to_client(fd)
def do_something_to_client(fd):
if should_read_from(fd):
data = read_until_EAGAIN(fd)
process_incoming_data(data)
while should_write_to(fd):
write_until_EAGAIN(fd)
if should_close(fd):
destroy_client(fd)
all_fds
是一个包含所有需要监控的文件描述符的列表。while True:
是一个无限循环,它会一直运行,直到程序被外部中断。active_fds = poll(all_fds)
调用poll函数,返回一个包含所有准备好进行I/O操作的文件描述符的列表。for each fd in active_fds:
遍历这个列表,对每个文件描述符执行相应的操作。do_something_with(fd)
是一个函数,根据文件描述符的类型执行相应的操作。def do_something_with(fd):
定义了这个函数。if fd is a listening socket:
如果文件描述符是一个监听套接字,那么调用add_new_client(fd)
函数添加新的客户端连接。elif fd is a client connection:
如果文件描述符是一个客户端连接,那么进入一个循环,直到工作完成为止。在这个循环中,调用do_something_to_client(fd)
函数处理客户端的数据。def do_something_to_client(fd):
定义了这个函数。if should_read_from(fd):
如果应该从文件描述符读取数据,那么调用read_until_EAGAIN(fd)
函数读取数据,然后调用process_incoming_data(data)
函数处理数据。while should_write_to(fd):
如果应该向文件描述符写入数据,那么调用write_until_EAGAIN(fd)
函数写入数据。if should_close(fd):
如果应该关闭文件描述符,那么调用destroy_client(fd)
函数销毁客户端连接。我们不只是对fd做一些事情(读、写或接受),而是使用轮询操作来告诉我们哪些fd可以立即操作而不阻塞。当我们在fd上执行IO操作时,该操作应该在非阻塞模式下执行。
epoll的使用
在Linux上,除了poll系统调用,还有select和epoll。古老的select系统调用基本上与轮询相同,只是最大fd数被限制为一个小数目,这使得它在现代应用程序中已经过时了。epoll API由3个系统调用组成:epoll\_create、epoll\_wait和epoll\_ctl。epoll API是有状态的,而不是提供一组fd作为系统调用参数,epoll\_ctl用于操作由epoll\_create创建的fd集,epoll\_wait正在对其进行操作。
**epoll API**执行与poll()类似的任务:监视多个文件描述符,以查看其中任何一个文件是否可以进行I/O操作。epoll API可以用作边缘触发或水平触发接口,并且可以很好地扩展到大量监视的文件描述符。以下提供系统调用来创建和管理epoll实例:
**epoll\_create()**创建一个epoll实例并返回一个引用该实例的文件描述符。
然后通过**epoll\_ctl()**注册对特定文件描述符的兴趣。当前在epoll上注册的文件描述符集,实例有时称为epoll集。
**epoll\_wait()**等待I/O事件,如果当前没有事件可用,则阻塞调用线程。
#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int listen_sock, conn_sock, nfds, epollfd;
/* Set up listening socket, 'listen_sock' (socket(),
bind(), listen()) */
epollfd = epoll_create(10);
if (epollfd == -1) {
perror("epoll_create");
exit(EXIT_FAILURE);
}
ev.events = EPOLLIN;
ev.data.fd = listen_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
perror("epoll_ctl: listen_sock");
exit(EXIT_FAILURE);
}
for (;;) {
nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_pwait");
exit(EXIT_FAILURE);
}
for (n = 0; n < nfds; ++n) {
if (events[n].data.fd == listen_sock) {
conn_sock = accept(listen_sock,
(struct sockaddr *) &local, &addrlen);
if (conn_sock == -1) {
perror("accept");
exit(EXIT_FAILURE);
}
setnonblocking(conn_sock);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = conn_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,
&ev) == -1) {
perror("epoll_ctl: conn_sock");
exit(EXIT_FAILURE);
}
} else {
do_use_fd(events[n].data.fd);
}
}
}
这段代码的主要功能是创建一个监听套接字,并使用epoll机制来处理客户端连接请求。以下是代码的详细解析:
#define MAX_EVENTS 10
:定义了一个宏常量MAX_EVENTS
,表示最多可以处理的事件数量为10。struct epoll_event ev, events[MAX_EVENTS];
:定义了一个结构体数组events
,用于存储epoll事件。每个事件由一个epoll_event
结构体表示。int listen_sock, conn_sock, nfds, epollfd;
:定义了四个整型变量,分别表示监听套接字、新连接的套接字、返回的文件描述符数量和epoll实例的文件描述符。epollfd = epoll_create(10);
:创建了一个epoll实例,最大文件描述符数量为10。如果创建失败,将打印错误信息并退出程序。ev.events = EPOLLIN;
:设置事件类型为可读事件(EPOLLIN)。ev.data.fd = listen_sock;
:将监听套接字的文件描述符设置为事件的关联数据。if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1)
:将监听套接字添加到epoll实例中,以便接收新的连接请求。如果添加失败,将打印错误信息并退出程序。for (;;)
:无限循环,不断处理事件。nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
:调用epoll_wait
函数等待事件发生。参数-1
表示等待所有类型的事件。返回值表示实际发生的事件数量。如果发生错误,将打印错误信息并退出程序。for (n = 0; n < nfds; ++n)
:遍历所有发生的事件。if (events[n].data.fd == listen_sock)
:判断事件是否与监听套接字相关联。如果是,表示有新的连接请求。conn_sock = accept(listen_sock, (struct sockaddr *) &local, &addrlen);
:接受新的连接请求,并将新连接的套接字赋值给conn_sock
。如果接受失败,将打印错误信息并退出程序。setnonblocking(conn_sock);
:将新连接的套接字设置为非阻塞模式,以便后续处理。ev.events = EPOLLIN | EPOLLET;
:设置事件类型为可读事件(EPOLLIN),并启用边缘触发(EPOLLET)。ev.data.fd = conn_sock;
:将新连接的套接字的文件描述符设置为事件的关联数据。if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock, &ev) == -1)
:将新连接的套接字添加到epoll实例中,以便后续处理。如果添加失败,将打印错误信息并退出程序。else
:如果事件与监听套接字无关,表示是一个已连接的客户端发送的数据。调用do_use_fd
函数进行处理。