摘要: 本文将详细讲解如何在Linux环境中使用C语言和Mongoose 7.17库,从零开始构建一个功能完备的静态文件服务器。从基础搭建HTTP服务器,到逐步增强功能(目录列表、MIME类型、自定义缓存、HTTP基本认证和HTTPS支持),再到高级优化(线程与连接配置、日志和错误处理、文件上传简介),配以完整示例代码(包括main.cMakefile),帮助读者循序渐进地掌握相关技术。

1. 基础搭建

本节介绍Mongoose网络库的核心概念,并展示如何创建一个基本的HTTP服务器程序。在该服务器中,我们将配置监听端口和指定的静态文件目录,使其能够提供静态文件访问服务。

1.1 Mongoose 7.17简介

Mongoose 是一款用C/C++编写的嵌入式网络库,自2004年以来广泛应用于各类产品 (Mongoose :: Documentation)。它提供事件驱动的非阻塞API,可以在Windows、Linux、Mac以及各种嵌入式系统上运行。Mongoose内置对多种协议的支持,如TCP、UDP、HTTP、WebSocket、MQTT等,方便开发者快速构建网络功能。对于HTTP服务器而言,Mongoose仅需一个mongoose.cmongoose.h文件,使用非常轻量简单。

Mongoose的核心基于事件驱动模型:它通过事件管理器(mg_mgr)来管理所有连接,通过事件回调函数处理网络事件(如HTTP请求) (Mongoose :: Documentation) (Mongoose :: Documentation)。在HTTP服务器场景下,Mongoose会为每个监听连接和客户端连接触发事件(如新连接、收到HTTP消息等),开发者提供的事件处理函数会被调用来响应事件,从而生成HTTP响应。

主要概念:

  • struct mg_mgr:Mongoose事件管理器,保存当前所有连接对象 (Mongoose :: Documentation)。通常,一个程序创建一个mg_mgr实例,并在主循环中不停调用mg_mgr_poll()来处理事件。
  • struct mg_connection:表示一个网络连接,可以是监听连接或客户端连接 (Mongoose :: Documentation)。每个连接都关联一个事件处理函数。
  • 事件处理函数:应用程序定义的回调函数(例如fn()),当指定事件发生时被调用。对于HTTP服务器,常用事件包括:
    • MG_EV_HTTP_MSG:表示收到一个完整的HTTP请求消息(请求行和消息体均已接收完毕)。
    • MG_EV_ACCEPT:有新的客户端连接已建立(针对于监听连接)。
    • MG_EV_ERROR:发生错误事件(如绑定端口失败等)。
  • HTTP协议处理:使用mg_http_listen()启动HTTP监听时,Mongoose内部会自动设置HTTP协议解析器。在收到数据后,Mongoose首先进行HTTP解析,然后在解析完成时触发MG_EV_HTTP_MSG事件给用户的回调 (Mongoose :: Documentation)。

1.2 创建基本HTTP服务器

下面我们将编写一个最简化的HTTP服务器,它监听一个端口并将某个目录下的文件作为静态资源提供。我们使用Mongoose提供的便捷函数来处理HTTP请求,将请求路由到对应的文件:

  • 监听端口:通过mg_http_listen()函数监听HTTP端口。
  • 事件回调:实现一个事件处理函数,当有HTTP请求时调用mg_http_serve_dir()将静态文件目录中的资源发送给客户端。

代码如下:

#include "mongoose.h"

// 配置静态服务器参数
static const char *s_http_port = "8000";         // HTTP监听端口
static const char *s_web_dir = "./web_root";     // 静态文件根目录

// 事件处理函数
static void handle_event(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {
    if (ev == MG_EV_HTTP_MSG) {
        // 收到一个HTTP请求
        struct mg_http_message *hm = (struct mg_http_message *) ev_data;
        // 配置静态文件服务选项:设置根目录
        struct mg_http_serve_opts opts = {.root_dir = s_web_dir};
        // 将请求映射到静态文件目录并发送响应 ([Mongoose :: Documentation](https://mongoose.ws/documentation/#:~:text=struct%20mg_http_serve_opts%20opts%20%3D%20,%2F%2F%20Serve%20static%20files))
        mg_http_serve_dir(c, hm, &opts);
        // (mg_http_serve_dir会根据请求的URI查找对应文件或目录,并自动回复200/404等)
    }
    // 其他事件暂不处理
}

int main(void) {
    struct mg_mgr mgr;
    mg_mgr_init(&mgr);  // 初始化Mongoose事件管理器

    // 启动HTTP监听
    if (mg_http_listen(&mgr, s_http_port, handle_event, NULL) == NULL) {
        fprintf(stderr, "Error starting server on port %s\n", s_http_port);
        return 1;  // 监听失败,可能端口被占用等
    }

    printf("Server started on HTTP port %s. Serving %s\n", s_http_port, s_web_dir);
    // 进入事件循环,不断轮询事件
    for (;;) {
        mg_mgr_poll(&mgr, 1000);  // 每隔1000毫秒检查一次事件
    }

    mg_mgr_free(&mgr);  // (通常不会运行到这,进程终止时释放资源)
    return 0;
}

上述代码做了以下工作:

  • 定义监听端口(8000)和静态文件目录(./web_root)。请确保运行程序前,在当前目录下创建好web_root文件夹并放入一些测试文件(如index.html)。
  • 实现事件回调函数handle_event():当事件类型为MG_EV_HTTP_MSG时,表示收到HTTP请求,我们使用mg_http_serve_dir()将请求交由Mongoose处理,映射到文件系统。我们通过mg_http_serve_opts结构设置root_dir为静态文件根目录,然后调用mg_http_serve_dir(c, hm, &opts)。 (Mongoose :: Documentation)这一调用会检查请求URI并尝试在root_dir下找到对应的文件:
    • 如果URI对应到某个文件,则读取该文件内容并返回200 OK响应(Content-Type由文件扩展名自动决定)。
    • 如果URI对应到一个目录,则尝试返回目录下的默认文件(如index.html),若不存在则根据设置决定是否返回目录列表。
    • 如果找不到对应的文件或目录,返回404 Not Found。
  • main()函数中,初始化事件管理器后,调用mg_http_listen()开始监听HTTP端口。mg_http_listen的第一个参数是事件管理器,第二个参数可以是形如http://0.0.0.0:8000的URL字符串,但如果只指定端口号如"8000"则默认监听本地所有接口的HTTP协议。我们传入事件回调handle_event。如果返回NULL表示监听失败(例如端口被占用),我们做错误处理退出程序。
  • 进入mg_mgr_poll()循环。这是Mongoose事件循环的核心,每次调用会处理所有当前连接的事件并阻塞指定毫秒数等待新事件。这里设置为1000ms,即每秒醒来检查一次。如果有事件(如新连接、数据收发),mg_mgr_poll会立即处理,不会一直阻塞满1秒。

运行上述程序后,服务器将在端口8000监听。将浏览器指向 http://localhost:8000 即可访问web_root目录下的文件。例如,如果web_root中有index.html,则直接返回该文件内容;若请求某个存在的图片或CSS等,也会返回相应文件。未找到的资源会返回404错误。

1.3 配置监听端口和静态文件目录

在上面的代码中,我们通过代码直接在mg_http_listen()里指定了端口,通过设置mg_http_serve_opts.root_dir指定了静态文件目录。这里还有几点值得注意:

  • 监听地址mg_http_listen()第二个参数既可以是纯端口号字符串(如"8000"表示监听TCP 8000端口),也可以是带协议和IP的URL格式字符串。例如:"http://0.0.0.0:8000" (Mongoose :: Documentation)。默认情况下不需要指定IP和http://前缀,因为我们调用的是mg_http_listen专门用于HTTP协议的监听,它会隐式采用HTTP并监听在所有IP上。
  • 静态目录路径:上例中使用了相对路径./web_root。你也可以使用绝对路径。例如:/home/user/www。确保指定的目录存在且程序有权限读取其中的文件。
  • 默认文档:Mongoose默认将index.html(可通过宏MG_HTTP_INDEX配置)作为目录的默认文件。如果客户端请求一个目录URI(如http://localhost:8000/docs/),且该目录下存在index.html,Mongoose会直接返回该文件内容,而不是列出目录内容。
  • 错误处理:在启动监听时我们检查返回值防止端口占用等错误。同理,在实际开发中,也应考虑捕获MG_EV_ERROR事件。例如,如果mg_http_listen内部出错,会触发MG_EV_ERROR,可以在事件回调中处理(本例未特别处理,默认Mongoose会将错误信息打印到标准错误)。

至此,一个基础的静态文件服务器已经可以工作。接下来我们将在此基础上增加更多实用功能。

2. 功能增强

基础服务器虽然可以提供静态文件,但在实际应用中,我们可能需要更多功能,比如浏览目录、定制MIME类型、控制缓存、增加访问控制,以及提供HTTPS服务等。本节将逐一介绍如何实现这些增强功能,并给出对应的代码示例或改进方案。

2.1 开启目录浏览

目录列表(Directory Listing)功能是指当请求指向一个目录且该目录下没有默认文件(如index.html)时,服务器返回该目录中包含的文件和子目录列表的一个自动生成页面。这样有助于浏览者发现目录中的资源。

在Mongoose中,目录列表功能是一个编译期选项,由宏MG_ENABLE_DIRLIST控制。默认情况下,对于Linux/Unix等平台此宏是启用(值为1)的 (raw.githubusercontent.com)(因此大多数情况下无需手动设置)。当使用mg_http_serve_dir()提供文件服务时,如果请求路径对应文件系统中的一个目录且没有默认首页文件,Mongoose将自动生成一个HTML页面列出该目录下的内容。列表页面包括文件/文件夹名称(作为链接)、大小和修改时间等。

如何确保目录列表有效:

  • 编译选项:确保MG_ENABLE_DIRLIST没有被显式禁用。如果你使用的是官方提供的mongoose.cmongoose.h,默认无需改动。如果曾自定义过mongoose_config.h或编译选项,请检查其中MG_ENABLE_DIRLIST应为1。
  • 运行效果:当目录列表启用且访问目录时,你将在浏览器中看到一个简单的文件列表页面。例如,访问http://localhost:8000/subfolder/会返回类似:
Index of /subfolder/
========================================
../  
file1.txt        1.2 KB    2025-04-27 10:00 
file2.jpg        250 KB    2025-04-20 09:30
...

页面中../表示上级目录,每个文件名是一个可以点击的链接,大小和修改时间以表格形式给出。

如果不想暴露目录列表,可以有几种办法:1) 在目录下放置一个index.html或首页文件,这样Mongoose会优先返回该文件内容;2) 编译时将MG_ENABLE_DIRLIST设置为0禁用目录列表(这样对于无默认文件的目录请求将返回404);3) 在代码中自行捕获目录请求并返回自定义响应(例如返回403 Forbidden)。根据需求选择合适的方法即可。

本教程后续代码均默认保持目录列表功能开启,以方便演示静态资源的组织结构。

2.2 自定义MIME类型映射

MIME类型用于告诉浏览器如何解释收到的文件,例如HTML文件为text/html,PNG图片为image/png等。Mongoose内置了一套常见文件扩展名到MIME类型的映射,例如.html映射为text/html.csstext/css.pngimage/png等等。当使用mg_http_serve_dirmg_http_serve_file时,会根据文件扩展名自动设置Content-Type响应头。

如果你的静态资源包含一些非常规扩展名,或者你希望覆盖默认的MIME映射,可以使用mg_http_serve_opts结构的mime_types字段添加自定义 MIME 类型映射

mime_types字段的格式是一个字符串,包含逗号分隔的映射规则,每条规则形如:扩展名=类型。例如:

opts.mime_types = "md=text/markdown,apk=application/vnd.android.package-archive";

上述设置会将扩展名.md的文件返回Content-Type: text/markdown.apk文件返回Content-Type: application/vnd.android.package-archive

一些特殊规则:

  • 可以使用*作为扩展名来指定未知扩展名的默认MIME类型 (Mongoose :: Documentation)。例如:"*=application/octet-stream"表示对于任何未知类型的文件都使用application/octet-stream(二进制流)类型。又例如:

    opts.mime_types = "*=text/plain,txt=application/octet-stream";
    

    这表示未知扩展默认为纯文本,而覆盖默认的.txt类型为二进制流。

  • 多条映射之间用逗号隔开,字符串中不要有空格。

**示例:**假设我们希望服务器将.json扩展名的文件以application/json类型发送,并将.wasm(WebAssembly模块)文件以application/wasm类型发送。可以这样设置:

struct mg_http_serve_opts opts = { .root_dir = s_web_dir };
opts.mime_types = "json=application/json,wasm=application/wasm";
mg_http_serve_dir(c, hm, &opts);

这样,当客户端请求data.jsonmodule.wasm文件时,会收到正确的MIME类型。在我们的代码中,可以在事件回调里每次请求都设置opts.mime_types,或者更高效地将opts定义为全局静态变量(初始化一次),包含所有自定义映射,以便在每个请求处理中重用。

2.3 实现缓存控制(Cache-Control)

合理的缓存策略能够提高静态资源服务的性能。通过在HTTP响应中加入Cache-Control等头部,可以指示浏览器在一段时间内缓存资源,从而在后续请求中直接使用缓存而不再次向服务器请求。

在Mongoose中,我们可以通过两种方式添加自定义响应头:

  • 使用mg_http_serve_optsextra_headers字段,这会为通过mg_http_serve_dirmg_http_serve_file发送的每个响应附加指定的额外HTTP头。
  • 在手动发送响应时(例如使用mg_http_reply()),直接传入需要的头部字符串。

对于静态文件服务器,全局地添加Cache-Control头是最简单的做法。例如,我们希望浏览器缓存所有静态资源60秒,可以设置:

opts.extra_headers = "Cache-Control: max-age=60\r\n";

这样,无论请求什么文件,响应都会包含:

Cache-Control: max-age=60

浏览器据此会在60秒内不重复请求相同URL的资源。你可以将max-age设置更大(比如一天86400秒)以减少频繁请求。不过在开发调试阶段,可以设置较小的缓存时间甚至不缓存(如Cache-Control: no-cache)以方便观察修改。

**按类型设置缓存:**有时我们希望不同类型的文件有不同的缓存策略,例如CSS、JS可以长时间缓存(配合文件名哈希或版本管理),而HTML文件每次都请求最新。此时,可以在事件回调中根据请求URI或扩展名判断,分别设置不同的头。例如:

const char *uri = hm->uri.ptr;
if (mg_http_match_uri(hm, "*.html")) {
    opts.extra_headers = "Cache-Control: no-cache\r\n";
} else {
    opts.extra_headers = "Cache-Control: max-age=3600\r\n";
}
mg_http_serve_dir(c, hm, &opts);

上面伪代码中,我们对HTML页面不缓存(每次验证是否有更新),其他资源缓存1小时。

需要注意,extra_headers可以包含多个头,以\r\n分隔并在末尾也以\r\n结尾。例如:

opts.extra_headers = "Cache-Control: max-age=3600\r\nX-Server: MyMongoose\r\n";

这样将会在响应中添加两个头:Cache-Control和自定义的X-Server头。

2.4 支持简单身份认证(HTTP Basic Auth)

有时我们希望静态文件服务器仅供特定用户访问,比如作为内部工具或私有文件分发。最简单的办法是使用HTTP Basic Auth(基本身份认证),要求客户端提供用户名和密码才能访问。

**HTTP Basic Auth简介:**Basic Auth是HTTP协议内建的一种认证方式。当服务器返回状态码401和WWW-Authenticate响应头时,浏览器会弹出用户名/密码输入框。输入后浏览器会将“用户名:密码”用Base64编码放入Authorization: Basic xxxx请求头发送。服务器只需校验该头中的凭据是否正确。

在Mongoose中,并没有自动为静态内容提供认证的开关,但我们可以在事件回调中很容易地实现。Mongoose提供了mg_http_creds()函数,可从HTTP请求中提取Basic Auth信息(用户名和密码)。它会检查HTTP头以及Cookie、查询参数等常见位置 (Mongoose :: Documentation):

从HTTP请求中提取凭据,检查顺序如下 (Mongoose :: Documentation):

  1. Authorization头(Basic认证填充用户名和密码;Bearer令牌只填充密码)
  2. access_token Cookie(填充密码)
  3. URL 查询参数?access_token=...(填充密码)
    如果都没有提供,则userpass返回为空字符串。

我们将利用这一函数获取Authorization头中的用户名和密码,并与预设的正确凭据比对。

实现步骤:

  1. **设定用户名/密码:**在服务器代码中定义允许访问的用户名和密码。例如:

    static const char *s_auth_user = "admin";
    static const char *s_auth_pass = "admin123";
    

    这对凭据只是示例,实际应用中应根据需要修改,尽量复杂难猜。

  2. **检查请求头:**在事件处理函数中,在处理静态文件前,先调用mg_http_creds()提取请求中的凭据:

    char user[64], pass[64];
    mg_http_creds(hm, user, sizeof(user), pass, sizeof(pass));
    

    这将把解析出的用户名和密码分别保存到userpass缓冲区。如果请求没有提供认证信息,则两个缓冲区内容会是空字符串。

  3. **验证:**比较提取出的userpass与服务器预设值:

    • 如果匹配,则认证通过,继续正常处理请求(例如调用mg_http_serve_dir)。

    • 如果不匹配,则认证失败。此时需要返回HTTP 401 Unauthorized响应,并要求客户端进行Basic认证。发送401响应的方法是使用mg_http_reply()

      mg_http_reply(c, 401, "WWW-Authenticate: Basic realm=\"Protected Area\"\r\n",
                    "Unauthorized");
      

      这里我们设置了状态码401,并添加WWW-Authenticate头,指定Basic认证的域(realm)。realm可以理解为一段提示信息,会在浏览器的认证对话框中显示,帮助用户区分是哪个服务在要求认证。本例设为"Protected Area"。响应体可以是一些提示文字,这里简单返回"Unauthorized"。

  4. **阻止后续处理:**发送401后,应当结束当前请求处理。对mg_http_reply的调用已经发送并关闭了响应,所以只需在事件回调中return,不要再调用mg_http_serve_dir等函数。

下面将这一逻辑集成到事件处理函数中:

static const char *s_auth_user = "admin";
static const char *s_auth_pass = "admin123";

static void handle_event(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {
    if (ev == MG_EV_HTTP_MSG) {
        struct mg_http_message *hm = (struct mg_http_message *) ev_data;
        // 认证校验
        char user[100], pass[100];
        mg_http_creds(hm, user, sizeof(user), pass, sizeof(pass));
        if (strcmp(user, s_auth_user) != 0 || strcmp(pass, s_auth_pass) != 0) {
            // 未提供认证或用户名密码不匹配
            mg_http_reply(c, 401,
                          "WWW-Authenticate: Basic realm=\"MyServer\"\r\nContent-Type: text/plain\r\n",
                          "Unauthorized");
            return;  // 终止处理,要求客户端提供正确凭据
        }
        // 认证通过,继续处理请求
        struct mg_http_serve_opts opts = {.root_dir = s_web_dir};
        mg_http_serve_dir(c, hm, &opts);
    }
}

**测试 Basic Auth:**编译运行服务器后,用浏览器访问任何资源,浏览器应弹出登录框。输入用户名“admin”、密码“admin123”后即可浏览内容。错误的凭据会导致再次弹出登录框(浏览器会重试请求)。使用命令行工具curl测试示例:

  • 未带认证访问:

    $ curl -i http://localhost:8000
    HTTP/1.1 401 Unauthorized
    Content-Type: text/plain
    WWW-Authenticate: Basic realm="MyServer"
    Content-Length: 12
    
    Unauthorized
    

    可以看到服务器返回了401和认证头。

  • 正确认证访问:

    $ curl -u admin:admin123 http://localhost:8000/index.html
    (返回200和页面内容)
    

    -u user:passcurl发送Basic认证头。

安全注意:Basic Auth的凭据是经Base64编码而非加密传输的。如果不使用HTTPS,网络中的窥探者可以轻易解码出明文密码。因此,在启用Basic Auth的情况下,务必考虑同时启用HTTPS以加密传输,防止凭据泄露(下一节将介绍HTTPS配置)。Basic Auth适用于简单的保护场景,如果需要更强的安全性或更复杂的权限控制,可能需要引入Session、Token机制或者使用更高级的框架。

2.5 支持HTTPS(加载自签名证书)

为了保证传输安全,我们可以让服务器同时通过HTTPS提供服务。HTTPS需要数字证书和私钥。对于测试或内部使用,可以使用自签名证书。这一节将介绍如何生成自签名证书,并在Mongoose服务器中启用TLS/SSL支持。

**生成自签名证书:**使用OpenSSL工具可以快速生成一个自签名的RSA证书与私钥。例如,以下命令生成一个有效期365天的证书server_cert.pem和私钥server_key.pem(密码对话使用-nodes表示不加密私钥):

openssl req -new -x509 -days 365 -nodes -out server_cert.pem -keyout server_key.pem \
    -subj "/CN=localhost"

上述命令使用了/CN=localhost作为证书中的通用名(Common Name),表示证书颁发给localhost域名。生成的server_cert.pemserver_key.pem文件稍后将在程序中使用。

**在Mongoose中启用TLS:**Mongoose 7.x通过mg_tls_init()函数来初始化TLS。启用HTTPS的一般步骤为:

  1. **监听HTTPS端口:**使用mg_http_listen()启动一个HTTPS监听。例如监听8443端口:

    struct mg_connection *lc = mg_http_listen(&mgr, "https://0.0.0.0:8443", handle_event, NULL);
    

    注意这里URL以https://开头。Mongoose会知道这是一个TLS监听,但实际TLS握手仍需我们在接受连接时手动初始化(见下一步)。同时你需要确保编译Mongoose时启用了TLS支持,详见后文编译说明。

  2. **加载证书和私钥:**在事件回调中处理MG_EV_ACCEPT事件。当有新连接建立时(即客户端刚连接上8443端口,但尚未完成TLS握手),Mongoose触发MG_EV_ACCEPT。我们应在此事件中调用mg_tls_init()为该连接初始化TLS上下文。需要准备一个mg_tls_opts结构,设置其中的certkey字段为证书和私钥内容。Mongoose提供了mg_unpacked()帮助函数可以方便地读取文件内容为mg_str结构。

    if (ev == MG_EV_ACCEPT) {
        struct mg_tls_opts tls_opts = {
            .cert = mg_unpacked("certs/server_cert.pem"),
            .key = mg_unpacked("certs/server_key.pem")
        };
        mg_tls_init(c, &tls_opts);
    }
    

    上述代码中,当接受一个新连接c时,将加载certs/server_cert.pemcerts/server_key.pem文件的内容,并调用mg_tls_init()完成该连接的TLS初始化 (Mongoose :: Examples :: SSL/TLS)。mg_tls_init内部会进行TLS握手所需的设置。当握手成功后,连接上的后续HTTP读写都会自动加解密。

  3. **处理HTTPS请求:**一旦TLS握手完成,Mongoose会像处理普通HTTP一样触发MG_EV_HTTP_MSG事件。我们的代码对HTTP请求的处理逻辑(例如静态文件服务、认证等)不需要因为HTTPS做额外改变。同一个事件处理函数可同时处理HTTP和HTTPS请求。我们只需在MG_EV_ACCEPT阶段对HTTPS连接进行TLS初始化,其余部分逻辑通用。

**完整HTTPS集成示例:**假设我们希望服务器同时监听HTTP(8000端口)和HTTPS(8443端口),并使用上一步生成的自签名证书:

// 在main函数中增加HTTPS监听
struct mg_connection *lc_http = mg_http_listen(&mgr, "http://0.0.0.0:8000", handle_event, NULL);
struct mg_connection *lc_https = mg_http_listen(&mgr, "https://0.0.0.0:8443", handle_event, NULL);
if (lc_http == NULL || lc_https == NULL) {
    fprintf(stderr, "Error setting up listeners\n");
    return 1;
}
...
// 在事件回调handle_event中:
if (ev == MG_EV_ACCEPT) {
    // 区分HTTP与HTTPS:检查本地监听端口
    struct mg_tcpip_if *iface = (struct mg_tcpip_if *)fn_data;  // (如果fn_data保存了接口信息)
    // 简单方法:判断连接地址
    uint16_t port = mg_ntohs(c->loc.port);
    if (port == 8443) {
        struct mg_tls_opts tls_opts = {
            .cert = mg_unpacked("certs/server_cert.pem"),
            .key = mg_unpacked("certs/server_key.pem")
        };
        mg_tls_init(c, &tls_opts);
    }
    return;
}

上面代码片段中,我们对MG_EV_ACCEPT事件检查连接的本地端口(c->loc.port需要用mg_ntohs转换为主机字节序)是否为HTTPS的8443端口,如果是则执行TLS初始化。由于我们将HTTP和HTTPS都绑定到了同一个回调函数handle_event,通过这样的判断即可区分。另一种实现是使用不同的事件处理函数或利用fn_data传入标识,在此不再详述。

**编译启用TLS支持:**使用HTTPS需要在编译Mongoose库时启用TLS功能。Mongoose支持多种TLS后端(OpenSSL、mbedTLS、WolfSSL等)。最常用的是OpenSSL。在编译时需要定义宏MG_TLS=MG_TLS_OPENSSL并链接OpenSSL库 (Mongoose :: Examples :: SSL/TLS)。详细的Makefile配置将在后文提供。确保系统已安装OpenSSL开发库(Ubuntu下安装libssl-dev包)。若没有OpenSSL,也可以使用Mongoose内置的mbedTLS实现,只需定义MG_TLS=MG_TLS_BUILTIN,这样无需额外依赖,但要注意内置实现可能不支持某些特定证书格式。

**测试HTTPS:**编译运行程序后,访问https://localhost:8443。由于使用的是自签名证书,浏览器会提醒证书不受信任,需要用户手动信任或添加例外。接受后应可以看到静态页面正常加载,地址栏显示“不安全”或划线的锁,这是正常的(因为证书未经CA签名)。使用curl可以用-k选项忽略证书验证测试:

$ curl -k https://localhost:8443

返回内容应与HTTP相同。此时我们实现了HTTP和HTTPS双栈服务,用户可以根据需要选择。实际部署时,通常会将HTTP重定向到HTTPS以确保全程加密,这可以通过监听HTTP并在MG_EV_HTTP_MSG事件里检查hm->proto(协议)然后返回301跳转实现,或直接只开启HTTPS服务。

3. 高级优化

在实现了主要功能后,我们再来讨论一些优化和注意事项。这些涉及服务器并发、性能和可维护性,包括调整线程和连接处理方式、启用日志和错误跟踪,以及简单的文件上传实现想法等。

3.1 设置合理的线程数与连接数

Mongoose采用事件驱动的异步I/O模型,通常使用单线程就能高效地处理多并发连接。默认情况下,我们在主线程运行mg_mgr_poll()循环来响应所有网络事件。对于大多数静态文件服务场景,这已经足够:一个线程即可同时处理成百上千的连接,因为它在等待网络I/O时不会阻塞其他连接的处理。

线程配置:如果在特定场景下希望利用多核CPU,你可以启用多线程,方式包括:

  • 多实例多线程:启动多个线程,每个线程创建各自的mg_mgr并监听不同的端口或IP。这适用于需要在不同网络接口或端口上提供服务的情况,各线程互不干扰。
  • ReusePort技术:在Linux上,可以开启监听套接字的SO_REUSEPORT选项,让多个线程在同一端口各自listen,从而由内核实现负载均衡。Mongoose目前不直接提供参数开启SO_REUSEPORT,但可以自行在创建监听前用系统调用设置。实现较为复杂,一般不需要。
  • 任务划分:也可以保留一个线程运行Mongoose事件循环,将繁重的非网络计算任务交给工作线程处理。例如在静态服务器中,如果增加了图像处理或其他耗时任务,可以在线程池中处理完再通过Mongoose发送结果。

由于我们的服务器纯粹在执行文件I/O和网络I/O,使用单线程已经非常高效。Linux的文件系统缓存和Mongoose非阻塞I/O相结合,可以让一个线程跑满千兆网卡带宽不是问题。如果开启HTTPS,TLS加解密较耗CPU,必要时可以考虑多线程对称开启多个HTTPS监听实例,但更简单的办法通常是使用更高性能的硬件(或将TLS卸载到反向代理)。所以对于本教程的静态文件服务器,我们不做特别的多线程处理,保持架构简单。

**连接数限制:**Mongoose没有硬编码的最大连接数限制,其能处理的并发连接数主要取决于系统资源(文件描述符数量、内存等)。Linux系统对普通进程的最大文件描述符数(包括网络套接字)有默认限制(ulimit,一般1024),在高并发场景下建议调高此限制。例如:

ulimit -n 10000

将允许进程打开更多socket。与此同时,可以调整Mongoose的发送/接收缓冲区大小(MG_IO_SIZE宏,默认16KB)来平衡内存占用和吞吐。大量并发时也要注意内存占用:每个连接都有其读写缓冲,如果连接非常多而每个都在传大文件,内存需求会上升,需要适当规划。

总之,对于一个静态文件服务器,通常从简单开始:单线程即可。如果实际测试发现CPU成为瓶颈,再考虑多线程扩展。同时监控系统的文件句柄和内存,根据需要调优。

3.2 日志记录与错误处理

日志记录对于服务器调试和运维非常重要。Mongoose内置了日志模块,可以方便地输出日志信息,分级别控制。常用日志级别包括:错误(MG_LL_ERROR)、信息(MG_LL_INFO)、调试(MG_LL_DEBUG)等。默认情况下,Mongoose的日志级别可能设为INFO或更高,只打印主要信息。我们可以通过调用mg_log_set()宏调整日志级别,例如:

mg_log_set(MG_LL_DEBUG);

将日志级别设为DEBUG,这样INFO、ERROR和DEBUG级的信息都会输出。

使用Mongoose日志有两种方式:

  • 使用自带的宏:MG_ERROR(("message", args...)), MG_INFO((...)), MG_DEBUG((...)) 等。双重括号是因为这些宏本质上包装了printf风格函数 (raw.githubusercontent.com)。例如:

    MG_INFO(("Serving directory %s on port %s", s_web_dir, s_http_port));
    

    这些宏会打印文件名、行号等前缀,方便定位。

  • 自己打印:也可以直接使用printf/fprintf打印日志到控制台。不过如果已经使用Mongoose库,建议用其自带日志系统以保持一致,并且可以随时调整日志级别或重定向日志输出。

错误处理方面:

  • 监听错误:上一节已经展示,如果mg_http_listen返回NULL,要及时捕获并退出或重试。

  • 运行时错误:Mongoose会通过MG_EV_ERROR事件报告某些错误情况。比如DNS解析失败、TLS握手失败等。如果我们实现了case MG_EV_ERROR:分支,可以得到ev_data中携带的错误消息字符串。例如:

    if (ev == MG_EV_ERROR) {
        fprintf(stderr, "MG_EV_ERROR: %s\n", (char *) ev_data);
    }
    

    这在调试网络问题时很有帮助。

  • 文件服务错误mg_http_serve_dir在找不到文件时自动返回404,对于权限不足等文件系统错误也会返回相应状态码(403或500),通常不需要我们干预。但如果想自定义错误页面,可以利用mg_http_serve_opts.page404指定404错误时的HTML页面路径。

  • 异常情况:任何异常(如空指针、内存分配失败)Mongoose内部会保护,常见表现是打印错误日志。作为开发者,可以检查日志或在调试模式下运行来发现问题。

**例行日志示例:**为了示范日志的使用,我们可以在事件回调中加入一些日志输出:

if (ev == MG_EV_HTTP_MSG) {
    struct mg_http_message *hm = ev_data;
    MG_INFO(("HTTP request: %.*s %.*s", (int)hm->method.len, hm->method.ptr,
                                          (int)hm->uri.len, hm->uri.ptr));
    ...
}

这样每当收到HTTP请求,都会打印一行日志包含请求的方法和URI,例如:

I 2025-04-27T15:00:00Z main.c:45: HTTP request: GET /index.html

这里I表示INFO级别。日志输出格式可能类似于<级别> <时间> <文件:行>: <消息>。通过阅读日志,我们可以了解服务器的运行状况。出于性能考虑,可以选择在开发阶段开DEBUG日志,在部署阶段将日志调为INFO或WARNING以上。

3.3 文件上传功能简介

虽然我们的服务器主要用于提供静态文件下载,但在某些场景下,可能需要支持简单的文件上传来管理静态资源(例如通过Web界面上传新的图片或文档到静态目录)。实现文件上传要处理HTTP的POST请求,并保存上传的文件内容到服务器磁盘。这里我们简单介绍思路,并提供一个基本实现供参考。

**HTML表单上传 (multipart/form-data):**常用的文件上传方法是通过HTML表单,使用enctype="multipart/form-data". 浏览器会将文件内容连同其它字段一起编码成HTTP请求的body(使用特殊的分隔符boundary)。服务器需要解析这个multipart格式。Mongoose提供了两种方法来处理:

  • 直接解析整个表单: 对于小文件(相对于内存而言),Mongoose会将整个请求body读入内存(存储在hm->body中)。我们可以使用mg_http_next_multipart()函数遍历表单中的各个部分 (Mongoose :: Documentation)。每次调用该函数会填充一个mg_http_part结构,里面包含当前部分的字段名、文件名(如果该部分是文件)和数据内容等 (Mongoose :: Documentation)。我们可以检查part.filename来判断是否是文件字段,然后将part.body写入文件系统。
  • 流式保存大文件: 对于大文件上传,读入内存可能不现实。Mongoose提供了mg_http_upload()辅助函数,可以在接收到multipart数据的同时逐段写入文件,避免一次性存整个文件在内存中。它还支持通过查询参数指定上传偏移,实现断点续传等高级功能。然而使用略复杂,这里不深入展开。

**简单实现示例:**我们为服务器增加一个上传接口/upload用于接受文件。流程:

  1. 提供一个上传表单页面(例如upload.html),其中包含一个<input type="file" name="file1">字段和提交按钮。

  2. 当用户选择文件并提交后,浏览器向POST /upload发送请求,内容为multipart格式。

  3. 服务器handle_eventMG_EV_HTTP_MSG中检测到URI是/upload且方法是POST,则进入上传处理逻辑:

    • 调用mg_http_next_multipart循环提取请求中各部分。找到文件部分后,获取文件名和内容。
    • 构造一个保存路径,例如将文件保存到web_root/uploads/目录下。最好只使用文件名的基础部分,避免目录穿越攻击。
    • 将内容写入服务器磁盘文件。如果文件很大,Mongoose此时已经将整个请求收齐在内存,可以直接一次 fwrite 写出。当然,这对内存要求较高,所以建议主要用于小文件(几MB以内)。
    • 返回一个简单的响应(例如200 OK,带一段HTML提示上传成功)。
  4. 上传完成后,用户可通过静态文件URL访问刚上传的文件(如果我们将上传目录包含在web_root中,则上传的文件立即成为可下载的静态资源)。

下面给出代码片段演示如何处理上传(适用于小文件场景):

if (mg_match(hm->uri, mg_str("/upload"), NULL)) {
    if (mg_vcmp(&hm->method, "POST") == 0) {
        // 准备上传目录
        const char *upload_dir = "./web_root/uploads";
        mkdir(upload_dir, 0755);  // 确保上传目录存在

        // 解析multipart表单
        size_t ofs = 0;
        struct mg_http_part part;
        bool uploaded = false;
        while ((ofs = mg_http_next_multipart(hm->body, ofs, &part)) != 0) {
            if (part.filename.len == 0) {
                // 这不是文件字段,可能是文本字段
                continue;
            }
            // 提取上传的文件名并构造保存路径
            char filename[MG_PATH_MAX];
            mg_snprintf(filename, sizeof(filename), "%s/%.*s",
                        upload_dir, (int)part.filename.len, part.filename.ptr);
            // 简单安全处理:将路径中的'/'替换为下划线,防止子目录
            for (char *p = filename + strlen(upload_dir) + 1; *p; p++) {
                if (*p == '/' || *p == '\\') *p = '_';
            }
            // 将文件内容写入磁盘
            FILE *fp = fopen(filename, "wb");
            if (fp != NULL) {
                fwrite(part.body.ptr, 1, part.body.len, fp);
                fclose(fp);
                MG_INFO(("Saved upload file: %s (%d bytes)", filename, (int)part.body.len));
                uploaded = true;
            } else {
                MG_ERROR(("Cannot save file to %s", filename));
            }
        }
        // 回复结果给客户端
        if (uploaded) {
            mg_http_reply(c, 200, "Content-Type: text/plain\r\n", 
                          "Upload OK\n");
        } else {
            mg_http_reply(c, 500, "Content-Type: text/plain\r\n", 
                          "Upload Failed\n");
        }
    } else {
        // 如果收到非POST对/upload的请求,返回方法不允许
        mg_http_reply(c, 405, "Allow: POST\r\nContent-Type: text/plain\r\n", 
                      "Method Not Allowed\n");
    }
    return;
}

上述代码要点:

  • 使用mg_match(hm->uri, mg_str("/upload"), NULL)检测URI是否为/upload(也可以用strncmp等实现)。
  • 仅在POST方法时处理上传,否则返回405错误。
  • 调用mg_http_next_multipart逐个解析出表单部分。当part.filename.len > 0时,说明这是一个文件字段。通过part.filename.ptr获取文件名(原始字符指针,非空终止,需要注意长度),part.body则是文件内容。
  • 构造服务器保存路径时,我们简单地把上传文件统一存到web_root/uploads目录下,并将任何路径分隔符替换掉,防止用户上传文件名中带../试图跳出目录或覆盖其他文件。这是一种基本的安全措施。
  • 将文件内容写出磁盘。如果文件很大,这里会消耗较多内存(因为part.body包含整个文件内容)。生产环境可考虑使用流式写出(通过mg_http_upload按块写入),但实现起来要处理请求的分片和偏移,这里不展开。
  • 根据保存结果返回简单响应。可以返回一段HTML引导用户,比如给出链接让用户返回主页面或查看刚上传的文件。

**测试上传:**编译后,通过浏览器访问上传表单页面,选择文件并提交。如果实现正确,服务器会在web_root/uploads/下创建文件,并返回“Upload OK”。然后可以尝试通过http://localhost:8000/uploads/filename下载刚上传的文件。

**注意:**出于本教程目的,我们实现了一个基础版本的上传功能。实际应用中应考虑:

  • 权限:结合前面的Basic Auth,最好要求登录后才能上传,否则任何人都能上传可能不安全。可以将上传接口也置于认证保护之下(我们的Basic Auth代码已全局保护了所有请求,因此包括/upload)。
  • 大小限制:防止用户上传过大的文件耗尽服务器资源。可以在读取时检查part.body.len或利用Content-Length头来限制。例如拒绝超过一定大小的上传,并返回413 Payload Too Large状态码。
  • 文件名处理:我们简单地替换了斜杠。还可以进一步过滤特殊字符,或者干脆忽略客户端提供的文件名,用随机文件名存储,然后在应用层记录原始名与存储名的对应关系。
  • 并发处理:当前实现将上传逻辑放在事件回调中执行(实际上属于主事件循环线程执行)。如果上传文件很大且磁盘写入较慢,在这段时间内该线程将无法处理其他连接的事件(因为我们没有在写入时让出事件循环)。这可能影响并发性能。对于更优化的实现,可以将文件写入操作交给后台线程,当前线程继续处理其他连接,然后在写完后通知完成。但这会显著增加复杂度,不在本教程讨论范围内。

4. 示例项目

本节将提供一个完整的示例项目结构以及源码,整合前面所述的各项功能。您可以将此作为模板,根据自己的需求进行裁剪或扩展。

4.1 项目结构

我们规划如下的目录结构:

project-root/
├── src/
│   └── main.c                // C语言源代码,包含服务器主程序
├── mongoose.c                // Mongoose库源文件
├── mongoose.h                // Mongoose库头文件
├── web_root/                 // 静态资源根目录
│   ├── index.html            // 示例首页
│   ├── upload.html           // 上传页面
│   └── uploads/              // 上传的文件保存目录
├── certs/                    // 存放TLS证书的目录
│   ├── server_cert.pem       // 自签名证书文件
│   └── server_key.pem        // 自签名证书私钥
└── Makefile                  // 构建项目的Makefile
  • src/main.c 将包含我们整合的代码逻辑,包括HTTP服务初始化、事件回调实现(处理静态文件请求、目录列表、Basic Auth校验、HTTPS握手、文件上传等)。
  • mongoose.cmongoose.h 可以从Mongoose官方仓库获取对应版本7.17。将这两个文件放在项目目录下,编译时一起链接。
  • web_root 目录作为网站根目录。这里放入了两个示例页面:
    • index.html:简单的欢迎页面,提供网站说明。
    • upload.html:包含文件上传表单。
    • uploads/:这是上传文件将存储的子目录(需事先创建),并且由于位于web_root内,因此上传的文件可以通过HTTP访问。
  • certs 目录存放自签名证书。如果不需要HTTPS,此目录和内容可省略,或者编译时忽略TLS部分代码。
  • Makefile 用于编译整个项目,后文将给出内容。

4.2 示例源码(main.c)

下面是完整的main.c代码。代码涵盖了以下模块:HTTP/HTTPS监听初始化,事件回调实现(包含目录浏览、MIME映射、缓存头、Basic Auth、文件上传处理)、日志初始化等。我们在代码中通过注释标明各部分功能。

#include "mongoose.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <sys/stat.h>   // 为 mkdir() 引入

// 配置:静态资源目录和上传目录
static const char *s_web_dir    = "./web_root";
static const char *s_upload_dir = "./web_root/uploads";

// 简单认证账户(Basic Auth)
static const char *s_auth_user = "admin";
static const char *s_auth_pass = "admin123";

// 静态文件服务选项及缓存策略
static struct mg_http_serve_opts s_http_opts;
static const char *s_extra_headers = "Cache-Control: max-age=60\r\n";

// 事件回调函数:三参签名,适配 Mongoose 7.17
static void handle_event(struct mg_connection *c, int ev, void *ev_data) {
    if (ev == MG_EV_ACCEPT) {
        // 新连接:如果是 HTTPS 端口,则初始化 TLS
        uint16_t port = mg_ntohs(c->loc.port);
        if (port == 8443) {
            struct mg_tls_opts tls_opts = {
                .cert = mg_unpacked("certs/server_cert.pem"),
                .key  = mg_unpacked("certs/server_key.pem")
            };
            mg_tls_init(c, &tls_opts);
        }
        return;
    }

    if (ev == MG_EV_HTTP_MSG) {
        struct mg_http_message *hm = (struct mg_http_message *) ev_data;

        // 1) HTTP Basic Auth 校验
        char user[100], pass[100];
        mg_http_creds(hm, user, sizeof(user), pass, sizeof(pass));
        if (s_auth_user[0] &&
            (strcmp(user, s_auth_user) != 0 || strcmp(pass, s_auth_pass) != 0)) {
            mg_http_reply(c, 401,
                "WWW-Authenticate: Basic realm=\"MongooseSecure\"\r\n"
                "Content-Type: text/plain\r\n",
                "Unauthorized");
            return;
        }

        // 2) 上传接口:/upload
        if (mg_strcmp(hm->uri, mg_str("/upload")) == 0) {
            if (mg_strcmp(hm->method, mg_str("POST")) == 0) {
                mkdir(s_upload_dir, 0755);  // 确保目录存在
                size_t ofs = 0;
                struct mg_http_part part;
                bool ok = false;
                while ((ofs = mg_http_next_multipart(hm->body, ofs, &part)) != 0) {
                    if (part.filename.len == 0) continue;
                    // 构造保存路径
                    char filepath[MG_PATH_MAX];
                    mg_snprintf(filepath, sizeof(filepath), "%s/%.*s",
                                s_upload_dir,
                                (int)part.filename.len,
                                part.filename.buf);
                    // 替换分隔符
                    for (char *p = filepath + strlen(s_upload_dir) + 1; *p; p++) {
                        if (*p == '/' || *p == '\\') *p = '_';
                    }
                    FILE *fp = fopen(filepath, "wb");
                    if (fp) {
                        fwrite(part.body.buf, 1, part.body.len, fp);
                        fclose(fp);
                        ok = true;
                    } else {
                        ok = false;
                        break;
                    }
                }
                if (ok) {
                    mg_http_reply(c, 200,
                                  "Content-Type: text/html\r\n",
                                  "<!DOCTYPE html><html><body>"
                                  "<h3>Upload successful</h3>"
                                  "<p><a href=\"/\">Back to home</a></p>"
                                  "</body></html>");
                } else {
                    mg_http_reply(c, 500,
                                  "Content-Type: text/plain\r\n",
                                  "Upload failed\n");
                }
            } else {
                mg_http_reply(c, 405,
                              "Allow: POST\r\nContent-Type: text/plain\r\n",
                              "Method not allowed\n");
            }
            return;
        }

        // 3) 静态文件服务
        s_http_opts.root_dir    = s_web_dir;
        s_http_opts.mime_types  = "wasm=application/wasm";
        s_http_opts.extra_headers = s_extra_headers;
        mg_http_serve_dir(c, hm, &s_http_opts);
    }
}

int main(void) {
    struct mg_mgr mgr;
    mg_mgr_init(&mgr);
    mg_log_set(MG_LL_INFO);

    if (mg_http_listen(&mgr, "http://0.0.0.0:8000",  handle_event, NULL) == NULL ||
        mg_http_listen(&mgr, "https://0.0.0.0:8443", handle_event, NULL) == NULL) {
        fprintf(stderr, "Failed to create listeners\n");
        return 1;
    }
    printf("Server running on http://0.0.0.0:8000 and https://0.0.0.0:8443\n");

    for (;;) mg_mgr_poll(&mgr, 1000);
    mg_mgr_free(&mgr);
    return 0;
}

代码解读:

  • 全局配置:使用全局静态变量配置端口、目录路径、认证信息等,以便在不同函数中访问。这里定义HTTP端口8000,HTTPS端口8443,静态文件根目录web_root,以及Basic Auth的用户名和密码(admin/admin123)。还定义了mg_http_serve_opts s_http_opts作为文件服务的配置结构,我们将在处理请求时填充它(例如设置mime_types和extra_headers)。
  • MG_EV_ACCEPT 处理:当有新连接建立时,如果该连接是通过HTTPS监听(端口8443)接入,则调用mg_tls_init()进行TLS握手初始化 (Mongoose :: Examples :: SSL/TLS)。这里利用c->loc.port判断端口号是否等于HTTPS端口。mg_unpacked()函数用于读取证书和密钥文件的内容,构造成mg_str注意:为简化示例,我们假定文件路径有效,未对mg_unpacked失败情况做检查。如果证书文件路径不正确,mg_tls_init会失败并可能触发MG_EV_ERROR。
  • MG_EV_HTTP_MSG 处理:此处是HTTP请求主逻辑。按顺序执行了几项操作:
    1. 日志记录请求:使用MG_INFO输出了收到的HTTP方法和URI,以便在控制台日志中记录每个请求。
    2. Basic Auth校验:如果设置了s_auth_user(本例为"admin"),则调用mg_http_creds提取请求中的用户和密码 (Mongoose :: Documentation)。比对失败则回复401 Unauthorized并返回,成功则继续。这样未认证用户无法进入后续的文件访问环节。MG_INFO也记录了失败的用户名以做审计。
    3. 路由判断:利用mg_http_match_uri检查请求URI。如果前缀或完整匹配某些路径,则进入特殊处理。本例中匹配/upload作为文件上传接口。对/upload以外的请求,默认走静态文件处理。
    4. 文件上传处理:当URI为/upload时:
      • 若方法为POST,则通过mg_http_next_multipart循环解析请求体,寻找文件字段。对于每个文件,将其保存到web_root/uploads/目录下(确保提前创建)。用mkdir保证目录存在。保存文件时替换了文件名中的斜杠,防止用户试图上传文件到其他路径。然后发送一个简单的HTML页面响应,提示成功并提供返回主页的链接。
      • 若方法不是POST,则返回405 Method Not Allowed,并在响应头注明允许的方法为POST。
      • 处理完/upload请求后用return结束,不会再经过后续的文件服务逻辑。
    5. 静态文件服务:对于其他所有请求,设置mg_http_serve_opts并调用mg_http_serve_dir。我们将root_dir设为web_root,并演示了如何添加自定义MIME类型(.wasm映射)和额外响应头(Cache-Control)。这里extra_headers用了之前定义的s_extra_headers全局,可以灵活调整。然后mg_http_serve_dir会完成实际的文件查找和发送工作。如果请求路径非法或文件不存在,将自动返回404等,不需要我们处理。
  • 日志与错误:我们在关键操作处使用MG_INFOMG_ERROR做了日志记录,如上传成功/失败时记录文件名和错误。通过在main开始处调用mg_log_set(MG_LL_INFO or MG_LL_DEBUG)可以控制日志输出多寡。由于Mongoose内部也会使用MG_ERROR等记录TLS握手失败等情况,这些日志可以帮助我们发现问题。

4.3 示例网页内容

为了测试服务器功能,我们提供两个简单的HTML文件。

web_root/index.html – 主页,提供欢迎信息和上传页面链接:

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Welcome</title></head>
<body>
  <h1>欢迎来到静态文件服务器</h1>
  <p>这是一个使用Mongoose构建的示例静态文件服务器。</p>
  <ul>
    <li><a href="upload.html">上传文件</a> (需要登录)</li>
    <li>浏览本目录查看已有文件。</li>
  </ul>
  <hr>
  <p style="font-size:small;color:gray;">
    Server running with Mongoose 7.17 on Linux.
  </p>
</body>
</html>

web_root/upload.html – 上传页面,包含文件选择表单:

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Upload File</title></head>
<body>
  <h2>上传文件</h2>
  <form action="/upload" method="POST" enctype="multipart/form-data">
    <p>选择文件: <input type="file" name="file1"></p>
    <p><input type="submit" value="上传"></p>
  </form>
  <p><a href="/">返回首页</a></p>
</body>
</html>

这两个页面都非常简单,但足以配合我们的服务器代码进行功能验证。其中upload.html的表单提交指向/upload(对应服务器的上传处理逻辑),同时使用multipart/form-data编码以支持文件上传字段。

4.4 Makefile示例

下面是Makefile的内容,用于编译上述项目。在Linux环境下使用gcc和Make:

CC      = gcc
CFLAGS  = -Wall -O2 -DMG_TLS=MG_TLS_OPENSSL -I.
LDLIBS  = -lssl -lcrypto

TARGET  = mongoose_server
SRC     = src/main.c mongoose.c

all: $(TARGET)

$(TARGET): $(SRC)
    $(CC) $(CFLAGS) $(SRC) -o $@ $(LDLIBS)

clean:
    rm -f $(TARGET)

说明:

  • -DMG_TLS=MG_TLS_OPENSSL:启用Mongoose的OpenSSL后端TLS支持 (Mongoose :: Examples :: SSL/TLS)。这要求系统有OpenSSL库,并在链接时加入-lssl -lcrypto。在Windows或其他环境可能需要不同设置,但Linux下通常这样即可。
  • 若不需要HTTPS支持,可以去掉-DMG_TLS=...定义以及-lssl -lcrypto链接选项,同时可以不包含certs目录和相关代码(如mg_tls_init部分)。服务器将仅运行HTTP服务。
  • 编译目标为mongoose_server,依赖main.cmongoose.c。假定mongoose.cmongoose.h在项目根目录或指定位置。如果在src/目录下,需要调整路径或SRC变量。

使用make命令即可编译生成可执行文件mongoose_server。确保在编译前已将mongoose.c/.h复制到项目,并安装了OpenSSL开发库(否则会链接失败)。

4.5 运行与测试

编译成功后,运行服务器:

$ ./mongoose_server

如果日志级别为INFO(默认),启动时应该会看到类似输出:

I main.c:XX: Serving directory ./web_root on port 8000
I main.c:YY: HTTP server started on http://0.0.0.0:8000 and https://0.0.0.0:8443

(我们可以在代码中增加一些启动日志,但就算没有,后续日志也会指示服务器活动。)

现在逐项测试功能:

  1. 基本访问:打开浏览器访问 http://localhost:8000。因为开启了Basic Auth,浏览器应弹出登录提示。输入用户名“admin”、密码“admin123”后可见index.html内容。未登录或错误密码将收到“Unauthorized”拒绝。
  2. 目录列表:在index.html页面所在目录(根目录)由于有index.html,不会显示列表。可以尝试创建一个子目录web_root/testdir放一些文件,然后访问 http://localhost:8000/testdir/。如果没有index.html,则应显示文件列表页面,列出testdir中的内容(需先通过认证登录一次,浏览器会缓存凭据用于后续请求)。
  3. 自定义MIME:测试一个非常规扩展类型。如果有.wasm文件,上传或放置到web_root下,通过浏览器请求看看响应头Content-Type是否为application/wasm。其他常见类型可查看Network面板验证Content-Type正确性。
  4. 缓存控制:打开浏览器开发者工具的Network,访问一个文件,看Response Headers中是否有Cache-Control: max-age=60字样。如果再次刷新该资源(且浏览器未禁用缓存),应看到浏览器可能直接使用缓存(状态码变为304或从缓存加载)。可以调整max-age值测试效果。
  5. HTTPS访问:访问 https://localhost:8443。浏览器会提示不信任证书,选择继续浏览(添加例外信任)。之后应跳出同样的Basic Auth登录框,登录后看到内容与HTTP相同,只是地址变成了HTTPS。查看证书信息,可看到颁发给CN=localhost的自签名证书,有效期等于创建时设定。
  6. 文件上传:在首页点击“上传文件”链接进入上传页面。选择一个文件后提交。若上传成功,服务器返回“Upload successful”。此时可以在web_root/uploads/目录中找到该文件,并通过 http://localhost:8000/uploads/文件名 来下载验证(注意:上传的文件也受Basic Auth保护,因为我们保护了整个站点)。

通过以上测试,相信您已经验证了所有功能模块运作正常。

5. 其他注意事项

最后,我们讨论一些部署运行时的安全和性能考虑,以帮助你将这个示例应用到真实环境中。

5.1 安全性建议

  • 始终使用HTTPS传输敏感信息:如果开启了Basic Auth或允许文件上传,强烈建议使用HTTPS协议。未加密的HTTP容易遭受窃听,攻击者可以截获用户名密码或篡改传输内容。为正式部署申请可信的TLS证书(例如Let’s Encrypt免费证书) (Mongoose :: Examples :: SSL/TLS),替换掉自签名证书。
  • 妥善保管证书私钥server_key.pem是私钥,请确保服务器上该文件权限严格(例如chmod 600),不要泄漏。私钥一旦泄漏,需要立即更换证书。
  • 限制管理接口访问:本示例使用Basic Auth保护了所有页面。如果只想保护上传等管理功能,可以将Basic Auth检查仅应用于/upload等路径,而对公开的静态内容不要求认证。不过这意味着任何人都能下载所有文件,需要根据场景决定策略。
  • 防止目录穿越:Mongoose在mg_http_serve_dir实现中已经防范了路径遍历攻击(即请求URL中包含..试图访问根目录之外的文件)。请勿通过修改root_dir或者文件解析逻辑使其绕过这些检查。上传功能中,我们也替换了/避免用户构造非法文件名写出web_root之外路径。
  • 验证上传内容:如果允许用户上传文件,要考虑过滤不安全的文件类型。例如如果服务器目录在Web环境下能执行脚本,需防止上传可执行脚本文件导致的安全风险。在我们的场景中,服务器只是静态提供文件,不执行任何代码,安全风险较小。但依然需要留意不要上传过大的文件占满磁盘。
  • 最小权限运行:将服务器进程以普通用户权限运行(不要用root),并尽量将web_root目录的拥有者权限锁定,只给服务器进程写上传目录的必要权限。这样就算服务器被攻破,损害也限制在有限范围。
  • 更新依赖:关注Mongoose官方更新,及时升级库版本以获得最新的安全修复。对于OpenSSL等底层库也一样,避免使用存在已知漏洞的版本。
  • 审计日志:开启足够的日志以留存关键操作记录,例如登录失败日志、上传文件日志等,便于事后审查。

5.2 性能调优建议

  • 启用内容压缩:网络传输中压缩文本内容(HTML/CSS/JS等)可以显著节省带宽。Mongoose有一个巧妙的机制:如果同目录下存在file.ext.gz,当请求file.ext时,会自动发送压缩文件并添加Content-Encoding: gzip头 (Mongoose :: Examples :: Embedded Filesystem)。因此,你可以预先将一些大的JS/CSS文件gzip压缩一份,让服务器直接发送压缩版,从而提高传输效率而无需实时压缩开销。浏览器收到后会自动解压呈现。
  • 利用浏览器缓存:我们在响应中添加了Cache-Control: max-age=60作为示例。在实际应用中,可以根据文件类型和更新频率调整。例如对于版本固定的资源文件(如打包后的JS/CSS,带有hash文件名),可以设置较长的过期时间(甚至一年),而对经常变动的文件(如HTML)则不缓存或使用ETag/Last-Modified验证新鲜度。合理的缓存策略能大幅减少重复请求,降低服务器负载。
  • 减少不必要的功能:如果不需要的功能,可以在编译时关闭以减小开销。例如如果不需要目录列表,可以-DMG_ENABLE_DIRLIST=0编译,减小代码体积。又比如不需要文件上传,就不要包含相应处理逻辑。
  • 提升I/O效率:Mongoose已经是非阻塞I/O模型,但在文件读取方面,大文件可能会占用CPU进行读写。如果发现服务器瓶颈在磁盘IO,可考虑使用SSD提升读写速度,或者使用操作系统层面的sendfile()等零拷贝技术(Mongoose目前并未直接提供接口,但可以自行扩展)。不过对于典型的静态服务器场景,瓶颈往往先出现在网络带宽和CPU而非磁盘,除非文件特别大而内存又不足以缓存。
  • 横向扩展:当一台服务器资源耗尽时,可以考虑横向扩容,使用多台服务器分担流量。借助负载均衡器或CDN将请求分发到多实例上。这超出了应用本身的范围,但对于大规模部署是常用手段。
  • 监控:使用监控工具观察CPU、内存、带宽利用率。在吞吐达到上限时,你能知道瓶颈在哪(CPU高说明可能加密压缩等耗时,带宽满说明网络打满,内存不足说明缓冲过大等)。针对瓶颈采取相应优化措施。
  • 使用最新协议:Mongoose支持HTTP/1.1和WebSocket。如果需要更高性能,可以考虑升级到HTTP/2或HTTP/3(需使用对应库或者额外实现)。HTTP/2多路复用可以减少握手延迟和提升并发资源加载效率。但目前Mongoose不直接支持HTTP/2,或可通过放置Nginx等前端代理实现。

通过本教程的学习,我们已经从零开始搭建了一个功能齐全的Linux C语言静态文件服务器,涵盖了从基础HTTP服务、目录浏览、MIME类型配置、缓存、Basic Auth认证到HTTPS加密和文件上传等各方面内容。示例代码和项目结构为进一步扩展提供了良好基础。读者可以根据自身需求,对代码进行增删修改,例如整合更多高级特性(如WebSocket通知、REST API等),打造属于自己的定制服务器。

希望本教程有助于您理解Mongoose库的使用方法和HTTP服务器开发要点。祝您学习愉快,开发顺利! (Mongoose :: Documentation) (Mongoose :: Documentation) (Mongoose :: Examples :: SSL/TLS) (Mongoose :: Examples :: Embedded Filesystem)