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

简介: 1. 简介epoll 是 Linux 平台下特有的一种 I/O 复用模型实现,于 2002 年在 Linux kernel 2.5.44 中被引入。在 epoll 之前,Unix/Linux 平台下的 I/O 复用模型包含 select 和 poll 两个系统调用。

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 的效果。

img_4f7aae7c7ff84b438b4b4d98e24369b8.gif

上面就是玩具版 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]  震惊!我又被唤醒了...\n", getpid());
        if (ready_fd_num == -1) {
            perror("epoll_wait error, message: ");
            continue;
        }
        for(int i = 0; i < ready_fd_num; i++) {
            if (events[i].data.fd == listen_fd) { // 有新的连接
                conn_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_addr_size);
                if (conn_fd == -1) {
                    sprintf(buf, "[pid %d]  accept 出错了: ", getpid());
                    perror(buf);
                    continue;
                }

                // 设置 conn_fd 为非阻塞
                if (fcntl(conn_fd, F_SETFL, fcntl(conn_fd, F_GETFD, 0) | O_NONBLOCK) == -1) {
                    continue;
                }

                ev.data.fd = conn_fd;
                ev.events = EPOLLIN;
                if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn_fd, &ev) == -1) {
                    perror("epoll_ctl error, message: ");
                    close(conn_fd);
                }
                printf("[pid %d]  收到来自 %s:%d 的请求\n", getpid(), inet_ntoa(client_addr.sin_addr), client_addr.sin_port);
                
            } else if (events[i].events & EPOLLIN) {    // 某个 socket 数据已准备好,可以读取了
                printf("[pid %d]  处理来自 %s:%d 的请求\n", getpid(), inet_ntoa(client_addr.sin_addr), client_addr.sin_port);
                conn_fd = events[i].data.fd;
                // 调用 TinyHttpd 的 accept_request 函数处理请求
                accept_request(conn_fd, &client_addr);
                close(conn_fd);
            } else if (events[i].events & EPOLLERR) {
                fprintf(stderr, "epoll error\n");
                close(conn_fd);
            }
        }
    }
}

void handle_subprocess_exit(int signo)
{
    printf("clean subprocess.\n");
    int status;  
    while(waitpid(-1, &status, WNOHANG) > 0);
}

上面的代码有点长,不过还好,基本上都是模板代码,没什么特别复杂的逻辑。希望大家耐心看一下。

上面的代码基于epoll + 多进程的方式实现,开始,主进程会通过系统调用获取 CPU 核心数,然后根据核心数创建子进程。为了演示“惊群现象”,这里多创建了一倍的子进程。关于惊群现象,下一章会讲到,大家先别急哈。创建好子进程后,主进程不需再做什么事了,核心逻辑都会在子线程中执行。首先,每个子进程都会调用 epoll_create 在内核创建 epoll 实例,然后再通过 epoll_ctl 将 listen_fd 注册到 epoll 实例中,由内核进行监控。最后,再调用 epoll_wait 等待感兴趣的事件发生。当 listen_fd 中有新的连接时,epoll_wait 会返回。此时子进程调用 accept 接受连接,并把客户端 socket 注册到 epoll 实例中,等待 EPOLLIN 事件发生。当该事件发生后,即可接受数据,并根据 HTTP 请求信息返回相应的页面了。

这里说明一下,上面代码中处理 HTTP 请求的逻辑是写在 TinyHttpd 项目中的,TinyHttpd 是一个只有 500 行左右的超轻量型Http Server,很适合学习使用。为了适应需求,我对其源码进行了一定的修改,并添加了一些注释。本章的测试代码已经放到了 github 上,需要的同学自取,传送门 -> epoll_multiprocess_server.c

3. 惊群及演示

“惊群现象”是指并发环境下,多线程或多进程等待同一个 socket 事件,当这个事件发生时,多线程/多进程被同时唤醒,这就是“惊群现象”。对应上面的代码,多个子进程通过调用 epoll_wait 等待 listen_fd 上某个事件发生。当有新连接进来时,多个进程会被同时唤醒去处理这个事件。但最终只有一个进程可以去处理事件,其他进程重新进入等待状态。使用上面的代码可以演示惊群现象,如下:

img_58132b6edc1ac8ea6d172af9a8501f3b.jpe

从上图可以看出,当 listen_fd 上有新连接事件发生时,进程19571和19573被唤醒。但最终进程19573成功处理了新连接事件,进程19571则失败了。

惊群现象会影响服务器性能,因为多个进程被唤醒,但最终只有一个进程可以成功处理事件。而 CPU 需要为一个事件的发生调度数个进程,因此会浪费 CPU 资源。

对于惊群现象,处理的思路一般有两种。一种是像 Lighttpd 那样,无视惊群。另一种是像 Nginx 那样,使用全局锁避免惊群。简单起见,本文测试代码采用的是 Lighttpd 的处理方式,即无视惊群。对于这两种思路的细节,由于本人未读过两个开源软件的代码,这里就不多说了。如果大家有兴趣,可以参考网上的一些博文。

4. 总结

epoll 是 I/O 复用模型重要的一个实现,性能优异,应用广泛。像 Linux 平台下的 JVM,NIO 部分就是基于 epoll 实现的。再如大名鼎鼎 Nginx 也是使用了 epoll。由此可以看出 epoll 的重要性,因此我们有很有必要去了解 epoll。本文通过一个测试程序简单演示了一个基于 epoll 的 HTTP Server,总体上也达到了学习 epoll 的目的。大家如果有兴趣,可以下载源码看看。当然,纸上学来终觉浅,还是要自己动手写才行。本文的测试代码是本人现学现卖写的,仅测试使用,写的不好的地方望谅解。

好了,本文到此结束,谢谢阅读!

参考

本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处
作者:coolblog
本文同步发布在我的个人博客:http://www.coolblog.xyz

cc
本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。

目录
相关文章
|
19天前
|
中间件 Java 应用服务中间件
Windows部署web应用服务器Jboss中间件
如何在Windows系统上部署JBoss 7.1作为Web应用服务器,包括配置环境变量、自动部署WAR包、访问JBoss控制台、设置管理员账户以及修改端口和绑定地址等操作。
45 1
|
13天前
|
缓存 NoSQL 数据库
高性能Web服务器架构设计
【8月更文第28天】在当今互联网时代,网站的响应速度直接影响用户体验和业务成功率。因此,构建一个高性能的Web服务器架构至关重要。本文将从硬件配置、软件架构以及网络设置三个方面探讨如何提高Web服务器的性能,并提供一些实际的代码示例。
28 0
|
10天前
|
API C# 开发框架
WPF与Web服务集成大揭秘:手把手教你调用RESTful API,客户端与服务器端优劣对比全解析!
【8月更文挑战第31天】在现代软件开发中,WPF 和 Web 服务各具特色。WPF 以其出色的界面展示能力受到欢迎,而 Web 服务则凭借跨平台和易维护性在互联网应用中占有一席之地。本文探讨了 WPF 如何通过 HttpClient 类调用 RESTful API,并展示了基于 ASP.NET Core 的 Web 服务如何实现同样的功能。通过对比分析,揭示了两者各自的优缺点:WPF 客户端直接处理数据,减轻服务器负担,但需处理网络异常;Web 服务则能利用服务器端功能如缓存和权限验证,但可能增加服务器负载。希望本文能帮助开发者根据具体需求选择合适的技术方案。
34 0
|
10天前
|
Rust 安全 开发者
惊爆!Xamarin 携手机器学习,开启智能应用新纪元,个性化体验与跨平台优势完美融合大揭秘!
【8月更文挑战第31天】随着互联网的发展,Web应用对性能和安全性要求不断提高。Rust凭借卓越的性能、内存安全及丰富生态,成为构建高性能Web服务器的理想选择。本文通过一个简单示例,展示如何使用Rust和Actix-web框架搭建基本Web服务器,从创建项目到运行服务器全程指导,帮助读者领略Rust在Web后端开发中的强大能力。通过实践,读者可以体验到Rust在性能和安全性方面的优势,以及其在Web开发领域的巨大潜力。
21 0
|
10天前
|
Java 数据库 API
JSF与JPA的史诗级联盟:如何编织数据持久化的华丽织锦,重塑Web应用的荣耀
【8月更文挑战第31天】JavaServer Faces (JSF) 和 Java Persistence API (JPA) 分别是构建Java Web应用的用户界面组件框架和持久化标准。结合使用JSF与JPA,能够打造强大的数据驱动Web应用。首先,通过定义实体类(如`User`)和配置`persistence.xml`来设置JPA环境。然后,在JSF中利用Managed Bean(如`UserBean`)管理业务逻辑,通过`EntityManager`执行数据持久化操作。
17 0
|
11天前
|
JavaScript 前端开发 UED
服务器端渲染新浪潮:用Vue.js和Nuxt.js构建高性能Web应用
【8月更文挑战第30天】在现代Web开发中,提升应用性能和SEO友好性是前端开发者面临的挑战。服务器端渲染(SSR)能加快页面加载速度并改善搜索引擎优化。Vue.js结合Nuxt.js提供了一个高效框架来创建SSR应用。通过安装`create-nuxt-app`,可以轻松创建新的Nuxt.js项目,并利用其自动路由功能简化页面管理。Nuxt.js默认采用SSR模式,并支持通过`asyncData`方法预取数据,同时提供了静态站点生成和服务器端渲染的部署选项,显著提升用户体验。
33 0
|
12天前
|
数据可视化 Python
通过python建立一个web服务查看服务器上的文本、图片、视频等文件
通过python建立一个web服务查看服务器上的文本、图片、视频等文件
14 0
|
26天前
|
机器学习/深度学习 编解码 人工智能
阿里云gpu云服务器租用价格:最新收费标准与活动价格及热门实例解析
随着人工智能、大数据和深度学习等领域的快速发展,GPU服务器的需求日益增长。阿里云的GPU服务器凭借强大的计算能力和灵活的资源配置,成为众多用户的首选。很多用户比较关心gpu云服务器的收费标准与活动价格情况,目前计算型gn6v实例云服务器一周价格为2138.27元/1周起,月付价格为3830.00元/1个月起;计算型gn7i实例云服务器一周价格为1793.30元/1周起,月付价格为3213.99元/1个月起;计算型 gn6i实例云服务器一周价格为942.11元/1周起,月付价格为1694.00元/1个月起。本文为大家整理汇总了gpu云服务器的最新收费标准与活动价格情况,以供参考。
阿里云gpu云服务器租用价格:最新收费标准与活动价格及热门实例解析
|
8天前
|
存储 弹性计算 运维
阿里云日常运维-购买服务器
这篇文章是关于如何在阿里云购买和配置云服务器ECS的教程。
35 6
阿里云日常运维-购买服务器
|
2天前
|
网络安全 开发工具 云计算
服务器看代码阿里云
随着云计算技术的发展,阿里云作为国内领先的云计算服务提供商,其服务器受到广大用户青睐。本文主要介绍如何在阿里云服务器上便捷地查看与管理代码,如使用SSH连接服务器并通过命令行工具打开文件,以及利用Git进行版本控制和协作开发,提高代码管理效率。无论个人开发者还是企业团队,都能借助阿里云服务器高效地部署与管理应用程序,提升工作效率及产品质量。
24 10
下一篇
DDNS