基于epoll实现简单的web服务器

时间:2023-03-09 15:28:41
基于epoll实现简单的web服务器

1. 简介

epoll 是 Linux 平台下特有的一种 I/O 复用模型实现,于 2002 年在 Linux kernel 2.5.44 中被引入。在 epoll 之前,Unix/Linux 平台下的 I/O 复用模型包含 select 和 poll 两个系统调用。随着因特网的发展,因特网的用户量越来越大,C10K 问题出现。基于 select 和 poll 编写的网络服务已经不能满足不能满足用户的需求了,业界迫切希望更高效的系统调用出现。在此背景下,FreeBSD 的 kqueue 和 Linux 的 epoll 被研发了出来。kqueue 和 epoll 的出现,终结了 C10K 问题,C10K 问题就此作古。

因为 Linux 系统的广泛应用,所以大家在说 I/O 复用时,更多的是想到了 epoll,而不是 kqueue,本文也不例外。本篇文章不会涉及 kqueue,大家有兴趣可以自己看看。

2. 基于 epoll 实现 web 服务器

在 Linux 中,epoll 并不是一个系统调用,而是 epoll_create、epoll_ctl 和 epoll_wait 三个系统调用的统称。关于这三个系统调用的细节,这里就不说明了,大家可以自己去查 man-page。接下来,我们来直接看一个例子,这个例子基于 epoll 和 TinyHttpd 实现了一个 I/O 复用版的 HTTP Server。在上代码前,我们先来演示这个玩具版 HTTP Server 的效果。

基于epoll实现简单的web服务器

上面就是玩具版 HTTP Server 的运行效果了,看起来还行。在我第一次把它成功跑起来的时候,感觉很奇妙。好了,看完效果,接下来看代码吧,如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/sysinfo.h>
#include <sys/epoll.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/types.h>
#include "httpd.h" #define DEFAULT_PORT 8080
#define MAX_EVENT_NUM 1024
#define INFTIM -1 void process(int); void handle_subprocess_exit(int); int main(int argc, char *argv[])
{
struct sockaddr_in server_addr;
int listen_fd;
int cpu_core_num;
int on = 1; listen_fd = socket(AF_INET, SOCK_STREAM, 0);
fcntl(listen_fd, F_SETFL, O_NONBLOCK); // 设置 listen_fd 为非阻塞
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(DEFAULT_PORT); if (bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("bind error, message: ");
exit(1);
} if (listen(listen_fd, 5) == -1) {
perror("listen error, message: ");
exit(1);
} printf("listening 8080\n"); signal(SIGCHLD, handle_subprocess_exit); cpu_core_num = get_nprocs();
printf("cpu core num: %d\n", cpu_core_num);
// 根据 CPU 数量创建子进程,为了演示“惊群现象”,这里多创建一些子进程
for (int i = 0; i < cpu_core_num * 2; i++) {
pid_t pid = fork();
if (pid == 0) { // 子进程执行此条件分支
process(listen_fd);
exit(0);
}
} while (1) {
sleep(1);
} return 0;
} void process(int listen_fd)
{
int conn_fd;
int ready_fd_num;
struct sockaddr_in client_addr;
int client_addr_size = sizeof(client_addr);
char buf[128]; struct epoll_event ev, events[MAX_EVENT_NUM];
// 创建 epoll 实例,并返回 epoll 文件描述符
int epoll_fd = epoll_create(MAX_EVENT_NUM);
ev.data.fd = listen_fd;
ev.events = EPOLLIN; // 将 listen_fd 注册到刚刚创建的 epoll 中
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev) == -1) {
perror("epoll_ctl error, message: ");
exit(1);
} while(1) {
// 等待事件发生
ready_fd_num = epoll_wait(epoll_fd, events, MAX_EVENT_NUM, INFTIM);
printf("[pid %d]