基于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 国际许可协议进行许可。

目录
相关文章
|
2月前
|
Java PHP
PHP作为广受青睐的服务器端脚本语言,在Web开发中占据重要地位。理解其垃圾回收机制有助于开发高效稳定的PHP应用。
【10月更文挑战第1天】PHP作为广受青睐的服务器端脚本语言,在Web开发中占据重要地位。其垃圾回收机制包括引用计数与循环垃圾回收,对提升应用性能和稳定性至关重要。本文通过具体案例分析,详细探讨PHP垃圾回收机制的工作原理,特别是如何解决循环引用问题。在PHP 8中,垃圾回收机制得到进一步优化,提高了效率和准确性。理解这些机制有助于开发高效稳定的PHP应用。
47 3
|
4月前
|
JavaScript 搜索推荐 前端开发
从零搭建到部署:Angular与Angular Universal手把手教你实现服务器端渲染(SSR),全面解析及实战指南助你提升Web应用性能与SEO优化效果
【8月更文挑战第31天】服务器端渲染(SSR)是现代Web开发的关键技术,能显著提升SEO效果及首屏加载速度,改善用户体验。Angular Universal作为官方SSR解决方案,允许在服务器端生成静态HTML文件。本文通过具体示例详细介绍如何使用Angular Universal实现SSR,并分享最佳实践。首先需安装Node.js和npm。
83 1
|
4月前
|
API C# 开发框架
WPF与Web服务集成大揭秘:手把手教你调用RESTful API,客户端与服务器端优劣对比全解析!
【8月更文挑战第31天】在现代软件开发中,WPF 和 Web 服务各具特色。WPF 以其出色的界面展示能力受到欢迎,而 Web 服务则凭借跨平台和易维护性在互联网应用中占有一席之地。本文探讨了 WPF 如何通过 HttpClient 类调用 RESTful API,并展示了基于 ASP.NET Core 的 Web 服务如何实现同样的功能。通过对比分析,揭示了两者各自的优缺点:WPF 客户端直接处理数据,减轻服务器负担,但需处理网络异常;Web 服务则能利用服务器端功能如缓存和权限验证,但可能增加服务器负载。希望本文能帮助开发者根据具体需求选择合适的技术方案。
169 0
|
4月前
|
Rust 安全 开发者
惊爆!Xamarin 携手机器学习,开启智能应用新纪元,个性化体验与跨平台优势完美融合大揭秘!
【8月更文挑战第31天】随着互联网的发展,Web应用对性能和安全性要求不断提高。Rust凭借卓越的性能、内存安全及丰富生态,成为构建高性能Web服务器的理想选择。本文通过一个简单示例,展示如何使用Rust和Actix-web框架搭建基本Web服务器,从创建项目到运行服务器全程指导,帮助读者领略Rust在Web后端开发中的强大能力。通过实践,读者可以体验到Rust在性能和安全性方面的优势,以及其在Web开发领域的巨大潜力。
45 0
|
4月前
|
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`执行数据持久化操作。
58 0
|
4月前
|
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`方法预取数据,同时提供了静态站点生成和服务器端渲染的部署选项,显著提升用户体验。
90 0
|
4月前
|
数据可视化 Python
通过python建立一个web服务查看服务器上的文本、图片、视频等文件
通过python建立一个web服务查看服务器上的文本、图片、视频等文件
76 0
|
2月前
|
XML JSON API
ServiceStack:不仅仅是一个高性能Web API和微服务框架,更是一站式解决方案——深入解析其多协议支持及简便开发流程,带您体验前所未有的.NET开发效率革命
【10月更文挑战第9天】ServiceStack 是一个高性能的 Web API 和微服务框架,支持 JSON、XML、CSV 等多种数据格式。它简化了 .NET 应用的开发流程,提供了直观的 RESTful 服务构建方式。ServiceStack 支持高并发请求和复杂业务逻辑,安装简单,通过 NuGet 包管理器即可快速集成。示例代码展示了如何创建一个返回当前日期的简单服务,包括定义请求和响应 DTO、实现服务逻辑、配置路由和宿主。ServiceStack 还支持 WebSocket、SignalR 等实时通信协议,具备自动验证、自动过滤器等丰富功能,适合快速搭建高性能、可扩展的服务端应用。
117 3
|
27天前
|
设计模式 前端开发 数据库
Python Web开发:Django框架下的全栈开发实战
【10月更文挑战第27天】本文介绍了Django框架在Python Web开发中的应用,涵盖了Django与Flask等框架的比较、项目结构、模型、视图、模板和URL配置等内容,并展示了实际代码示例,帮助读者快速掌握Django全栈开发的核心技术。
134 45
|
8天前
|
开发框架 JavaScript 前端开发
TypeScript 是一种静态类型的编程语言,它扩展了 JavaScript,为 Web 开发带来了强大的类型系统、组件化开发支持、与主流框架的无缝集成、大型项目管理能力和提升开发体验等多方面优势
TypeScript 是一种静态类型的编程语言,它扩展了 JavaScript,为 Web 开发带来了强大的类型系统、组件化开发支持、与主流框架的无缝集成、大型项目管理能力和提升开发体验等多方面优势。通过明确的类型定义,TypeScript 能够在编码阶段发现潜在错误,提高代码质量;支持组件的清晰定义与复用,增强代码的可维护性;与 React、Vue 等框架结合,提供更佳的开发体验;适用于大型项目,优化代码结构和性能。随着 Web 技术的发展,TypeScript 的应用前景广阔,将继续引领 Web 开发的新趋势。
23 2