本节是该系列文章的完结篇,将带来select、poll、epoll三者中性能最好的epoll的代码实现。上节我们基于poll实现了一个简单的TCP回显服务器的应用,本节将使用epoll代替poll,仍保持TCP回显服务器应用的功能不变。
1. 创建tcp回显服务器
本步骤包括创建服务器套接字,绑定IP地址和端口,启动监听三部分,具体的代码实现请见第三节I/O 多路复用,网络编程中的select、poll、epoll的发展历史、原理详解以及代码实现(三)
2. 使用epoll文件描述符进行监控
(1)创建epoll实例
int epfd = epoll_create(1);
if (epfd == -1) {
perror("epoll_create failed");
close(sockfd);
exit(EXIT_FAILURE);
}
epoll_create(1)
:epoll_create()
用来创建一个 epoll
实例,返回一个文件描述符 epfd
,该文件描述符用于后续的 epoll_ctl()
和 epoll_wait()
操作。
- 参数
1
是建议给epoll
实例分配的初始内存大小,但对于大多数应用来说,这个参数并不会影响实际的功能。 - 如果
epoll_create()
返回-1
,则表示创建失败,程序通过perror()
输出错误信息,并退出程序。
(2)注册监听事件
struct epoll_event ev;
ev.events = EPOLLIN; // 监听可读事件
ev.data.fd = sockfd; // 设置监听的套接字文件描述符
if (epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev) == -1) {
perror("epoll_ctl failed");
close(sockfd);
close(epfd);
exit(EXIT_FAILURE);
}
-
struct epoll_event ev
:epoll_event
是epoll
事件的结构体,它包含了事件类型(events
)和一些相关的数据(data
)。ev.events = EPOLLIN
:设置要监听的事件类型为EPOLLIN
,表示该文件描述符(即sockfd
)上有数据可读。EPOLLIN
是epoll
中最常见的事件之一,表示套接字可以读取数据。ev.data.fd = sockfd
:这里指定sockfd
作为要监听的文件描述符。ev.data
是一个联合体,包含了多种类型的数据。对于我们这里的情况,它存储的是一个文件描述符,用于标识哪个文件描述符(例如套接字)发生了事件。
-
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev)
:epoll_ctl()
是epoll
的控制函数,用来修改epoll
实例中的事件。EPOLL_CTL_ADD
表示将一个新的文件描述符和事件添加到epoll
实例中。- 传入的参数
sockfd
是要监听的套接字文件描述符,&ev
是需要监听的事件。
如果
epoll_ctl()
调用失败,则输出错误信息并退出。
(3)等待并处理事件
struct epoll_event events[MAX_EVENTS];
while (1) {
int nready = epoll_wait(epfd, events, MAX_EVENTS, -1);
if (nready == -1) {
perror("epoll_wait failed");
close(sockfd);
close(epfd);
exit(EXIT_FAILURE);
}
-
struct epoll_event events[MAX_EVENTS]
:epoll_wait()
函数将返回发生的事件,这里我们用events
数组来存储这些事件。MAX_EVENTS
是数组的大小,表示我们最多能同时处理MAX_EVENTS
个事件。 -
epoll_wait(epfd, events, MAX_EVENTS, -1)
:epoll_wait()
用来等待一个或多个事件的发生。- 第一个参数
epfd
是我们之前创建的epoll
实例的文件描述符。 - 第二个参数
events
是一个数组,用来存储发生的事件。 MAX_EVENTS
是我们希望epoll_wait()
最多处理的事件数量。-1
表示阻塞,直到有事件发生才返回。
如果
epoll_wait()
调用失败,返回-1
,则打印错误信息并退出程序。
(4)处理每个事件
for (int i = 0; i < nready; i++) {
int connfd = events[i].data.fd;
for (int i = 0; i < nready; i++)
:遍历所有发生的事件,nready
表示发生的事件数。int connfd = events[i].data.fd;
:从events[i]
中获取文件描述符fd
,即发生事件的套接字。对于监听套接字sockfd
来说,这个文件描述符表示有新的连接请求到来;对于其他客户端连接套接字来说,表示该套接字上有数据可以读或写。
(5)处理新连接请求
if (connfd == sockfd) { // 新连接
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
if (clientfd == -1) {
perror("accept failed");
continue;
}
printf("Client connected: %d\n", clientfd);
if (connfd == sockfd)
:如果当前事件是由监听套接字(sockfd
)触发的,表示有新的客户端连接请求。accept()
用来接受这个新连接。accept(sockfd, (struct sockaddr*)&clientaddr, &len)
:接受客户端连接并返回一个新的套接字文件描述符clientfd
,它用于与客户端进行通信。clientaddr
存储客户端的地址信息,len
是地址结构的大小。
(6)将客户端套接字添加到 epoll
中
ev.events = EPOLLIN;
ev.data.fd = clientfd;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev) == -1) {
perror("epoll_ctl failed for client");
close(clientfd);
continue;
}
ev.events = EPOLLIN
:将客户端套接字clientfd
设置为监听可读事件。epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev)
:将客户端套接字注册到epoll
实例中,监听它的EPOLLIN
事件。这样,epoll
就会监视客户端套接字上是否有数据到来。- 如果注册失败,打印错误信息并关闭该客户端连接。
(7)处理客户端数据
} else if (events[i].events & EPOLLIN) { // 有数据可读
char buffer[1024] = {0};
int count = recv(connfd, buffer, sizeof(buffer), 0);
if (count == 0) { // 客户端断开连接
printf("Client disconnected: %d\n", connfd);
close(connfd);
epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL);
} else if (count < 0) {
perror("recv failed");
} else {
printf("Received: %s\n", buffer);
send(connfd, buffer, count, 0); // 回传接收到的数据
}
}
}
else if (events[i].events & EPOLLIN)
:如果事件类型包含EPOLLIN
,说明套接字上有可读数据。recv(connfd, buffer, sizeof(buffer), 0)
:从客户端接收数据,保存在buffer
中。recv()
会阻塞直到有数据可读或发生错误。- 如果
recv()
返回0
,表示客户端已经断开连接,服务器关闭该连接并从epoll
实例中删除。 - 如果
recv()
返回小于0
的值,表示接收数据失败,打印错误信息。 - 如果接收到数据,打印数据并将接收到的数据回传给客户端。
- 如果
3. 总结
整个 epoll
部分的代码实现了高效的多路复用机制,用于处理多个客户端的并发连接。主要流程是:
- 创建
epoll
实例; - 注册监听的套接字
sockfd
; - 使用
epoll_wait()
等待和处理事件; - 如果是新连接,接受客户端并将其套接字添加到
epoll
; - 如果是客户端套接字上有数据到来,读取数据并回传给客户端。
通过 epoll
,可以高效地处理大量的并发连接,而不会像传统的 select
一样受到连接数的限制。
epoll在 Linux 系统中提供了一种高效的 I/O 事件通知机制,并且正是epoll的出现,使得Linux成为服务器操作系统的主流选择。虽然如此,其工作原理仍然基于同步 I/O,而不是异步 I/O。本系列结束后,后续会开启异步I/O的系列文章,包括协程,io_uring等,并会带来相关的项目。