Mongoose:C 语言嵌入式 Web 服务器

作者:Michael J. Hammel

Web 服务在开发领域非常流行,但对于小型系统解决方案来说,像 JBoss 这样的全功能应用服务器就显得过于笨重了。在许多情况下,简单的 RESTful 接口就足够了,无需复杂的容器。此外,与依赖外部解释器(如 Python 或 PHP)的脚本集合相比,嵌入式单文件实现可能更受欢迎。

Mongoose 是一个 MIT 许可的嵌入式 Web 服务器,包含在一个单独的 C 模块库中,可以嵌入到程序中以提供基本的 Web 服务。其轻量级方法隐藏了全线程系统的强大功能,该系统能够使用标准和安全的 HTTP 协议通过多个端口提供静态和动态内容。它支持 CGI 和 SSI、访问控制列表和摘要认证。使用示例服务器实现中的代码支持文件传输。

除了嵌入式 C 库之外,Mongoose 包还包含一个前端,该前端将模块转换为全功能的服务器,能够从用户指定的文档根目录提供文件。即用型服务器支持来自命令行以及文本配置文件的所有可配置选项。虽然此配置提供了一种快速启动 Mongoose 的方法,但 Mongoose 的强大之处在于编写自定义前端到 Mongoose 库,并为特定的 REST 风格的 URI 提供回调。

在本文中,我将介绍 Mongoose 库 API,展示如何配置它以用于多种用途,并提供一个示例实现,该示例实现提供使用摘要认证的静态页面。本文的目标读者是具有 C 编程知识的开发人员。虽然不是必需的,但熟悉 HTML 和 CSS 也很有用。

基于 Mongoose 的服务器的结构

基于 Mongoose 的服务器提供了一个 main() 函数,该函数解析命令行参数,初始化 Mongoose 库,然后在循环中等待退出事件。命令行参数是服务器特定的,但通常表示要通过 mg_set_option() 函数传递给 Mongoose 库的值。Mongoose 初始化需要创建初始 Mongoose 上下文,设置库选项,并为授权、错误和 URI 处理建立回调函数。然后,main 函数永远循环,而 Mongoose 主线程处理配置网络端口上的传入连接。前端代码负责处理信号以支持关闭操作,包括通知 Mongoose 库优雅地停止。

Mongoose 库是线程化的,并且非常易于使用。初始上下文启动主线程,该线程等待传入连接。新连接排队,并启动工作线程来处理它们。连接的处理发生在工作线程中。在这里,分析连接请求以确定处理将如何进行。为了性能,工作线程根据需要启动,并在连接关闭后保留,以处理任何其他排队的传入请求,而无需启动新线程的开销(图 1)。

Mongoose: an Embeddable Web Server in C

图 1. 绿色框是用户函数,其余所有都在 Mongoose 库中实现。

在工作线程中,分析传入的请求以确定下一步应该发生什么。Mongoose 支持各种 HTTP 请求,例如 PUT、POST 和 DELETE。但是,对于简单的 REST 服务,Mongoose 支持的最重要功能是 URI、错误处理和身份验证的回调。

Hello, World:初始化和回调

Mongoose 初始化在从 main() 函数调用的单个用户定义的函数中处理。此初始化函数执行三个强制性操作:启动 Mongoose 初始上下文 (mg_start)、在该上下文中设置选项 (mg_set_option) 并指定 URI 回调 (mg_set_uri_callback)

void mongooseMgrInit()
{
  struct mg_context *ctx;
  ctx = mg_start();
  mg_set_option(ctx, "ports", port);
  mg_set_uri_callback(ctx, "/*", &uriHandler, NULL);
}

Mongoose API 文档很稀疏,可用选项并不明显。幸运的是,默认前端的手册页记录了命令行选项,这些选项又直接映射到可以使用 mg_set_option() 设置的大多数可用选项。默认服务器的 -A 选项用于编辑摘要认证文件,Mongoose 库不支持此选项。默认服务器通过命令行支持大多数(但不是全部)可用的 Mongoose 库选项。mongoose.c 中的 known_options 数组定义了 Mongoose 库直接支持的选项列表。

mg_set_option() 选项的所有参数都是字符串。Mongoose 会根据需要将它们转换为适当的格式。例如,端口选项的端口号必须指定为一个或多个逗号分隔的字符串,SSL 端口用附加到端口号的字母 s 标识。某些选项可以在编译时禁用。要禁用 SSL 选项,请定义 NO_SSL。要禁用 CGI 选项,请定义 NO_CGI。

注意

mg_set_option() 的参数区分大小写。有关 Mongoose 库选项的快速参考,请参阅 Mongoose 网站手册中显示的命令行选项,使用相同的大小写,但删除破折号前缀。

使用 mg_set_uri_callback() 设置的回调是处理特定 URI 请求的函数。星号用作路径通配符。在这个简单的示例中,有一个处理程序处理所有从 Web 服务器根路径开始的 URI 请求。

为了完成 Mongoose 等效于“Hello, World”程序的示例,所需要的只是一个将页面打印回请求 Web 浏览器的函数

void uriHandler()
{
  mg_printf(conn,
    "HTTP/1.1 200 OK\r\n"
    "Content-Type: text/html\r\n\r\n"
    "<html>\r\n"
    "<body>\r\n"
    "Hello, World!\r\n"
    "</body>\r\n"
    "</html>\r\n"
    );
}

前两行是 HTTP 标头。对于如此简单的示例,这些不是必需的,但如果您确实包含它们,请记住在 Content-Type 标头和页面内容的开头之间包含一个空行。

Mongoose 库中的两个函数用于将结果发送回浏览器:mg_printf() 和 mg_write()。前者为将数据发送回客户端提供 printf() 语义,后者对可以发送的数据量没有限制。如果服务器需要知道客户端在返回所有数据之前关闭了连接,或者服务器需要发送超过 MAX_REQUEST_SIZE (8Kb) 的数据,则应使用 mg_write()。请注意,API 文档说 mg_printf() 的最大大小为 16Kb,但 Mongoose 库源代码默认将 MAX_REQUEST_SIZE 设置为 8Kb。在单个回调中可以进行多次 mg_printf() 或 mg_write() 调用;但是,一旦回调返回,工作线程就会关闭连接。

身份验证和授权

在 Web 世界中,身份验证是指验证传入的请求是否来自已知实体。所需要的只是实体使用系统保留的令牌标识自己。在摘要认证的情况下,这意味着用户名、密码和 realm(领域)。realm 是一个符号名称,允许相同的用户名/密码对服务器 URI 命名空间的不同区域具有不同的含义。在实践中,用户只需要记住用户名/密码组合。realm 由服务器管理。摘要认证具有与服务器和客户端如何通信相关的其他复杂性,但从 Mongoose 用户的角度来看,这不是必需的知识。总之,身份验证用于识别用户。

授权是指验证经过身份验证的实体是否具有执行其尝试执行的操作的权限。尽管人们可能具有服务器的正确登录名,但他们可能没有权限查看网站的某些区域。对特定服务器功能的访问由授权处理。

Mongoose 提供对摘要认证的内置支持。如果配置了,包含用户名、密码和 realm 的文件将存储在运行时服务器可访问的位置。服务器根据来自浏览器的 HTTP 身份验证标头检查此文件以进行身份验证。可以配置全局身份验证文件以及每个 URI 的身份验证文件。Mongoose 用户可以使用 Apache 的 htdigest 程序生成这些文件。文件的位置在使用 mg_set_option() 函数进行 Mongoose 初始化期间设置。如果未指定,realm 默认为“mydomain.com”。摘要认证不是必需的。

清单 1. 摘要认证文件的完全限定路径和关联的 realm 在选项中设置,并且指定回调以执行服务器特定的授权。

#define DOCROOT   "/docs"
#define HTPASSWD  "/.htpasswd"
#define port      "8083"
void mongooseMgrInit()
{
  struct mg_context *ctx;
  char *ptr = NULL;
  char *documentRoot[PATH_MAX];
  char htpath[PATH_MAX];

  ptr = getcwd(NULL, 0);
  memset(documentRoot, 0, PATH_MAX];
  strcpy(documentRoot, ptr);
  documentRoot = strcat(documentRoot, DOCROOT);

  memset(htpath, 0, PATH_MAX);
  strcpy(htpath, documentRoot);
  strcat(htpath, "/.htpasswd");

  ctx = mg_start();
  mg_set_option(ctx, "ports", port);
  mg_set_uri_callback(ctx, "/*", &uriHandler, NULL);

  mg_set_option(ctx, "auth_gpass", htpath);
  mg_set_option(ctx, "auth_realm",
                "mongoose-example.com");
  mg_set_auth_callback(ctx, "/*", &authorize, NULL);
}

Theauth_gpass选项设置全局身份验证文件的位置。此文件用于验证对任何 URI 的请求。此选项的参数是文件的路径。要为特定 URI 设置身份验证,请使用protect选项。此选项的参数是逗号分隔的 URI=PATH 对的集合,其中 URI 相对于 Web 服务器并且包含通配符,PATH 是要用于该 URI 的身份验证文件的路径。路径应该是完全限定的或相对于启动基于 Mongoose 的服务器的目录。

如果为请求的 URI 设置了身份验证选项,Mongoose 将告诉客户端浏览器打开登录对话框。Mongoose 库在将控制权传递给适当的回调(如果有)之前,处理来自用户的登录信息。一旦浏览器用户通过身份验证,注销的唯一方法是再次请求身份验证。事实证明,这种形式的身份验证需要 cookie 来实现注销过程并强制进行另一次登录。或者,cookie 可以用于实现包含 HTML 表单的页面,以便在摘要认证之外进行登录和注销。如果需要注销或服务器端表单进行登录,则可能不应使用 Mongoose 选项设置摘要认证。摘要认证仍然可以手动使用,但 Mongoose 不会为此目的公开 API 函数。

除了身份验证之外,还可以使用使用 mg_set_auth_callback() 函数注册的回调来实现授权。注册的函数在每个 URI 回调之前调用,以允许服务器代码确定是否应授权传入的请求访问请求的 URI。如果授权被授予,此函数在提供的 mg_connection 上调用 mg_authorize()。如果未执行此操作,Mongoose 会假定未授予授权,并且不会为请求的 URI 调用配置的回调

static
void authorize(
       struct mg_connection *conn,
       const struct mg_request_info *ri,
       void *data)
{
    const char  *cookie, *domain;
    cookie = mg_get_header(conn, "Cookie");
    uri = ri->uri;

    if ( (strcmp(ri->uri, "/") == 0) ||
         (strncmp(ri->uri, "/images", 7) == 0)
       )
    {
        mg_authorize(conn);
    }
    else if (strncmp(ri->uri, "/logout", 7) == 0)
    {
        ... Verify login cookie ...
        ... redirect to front page ...
    }
    else if (cookie != NULL &&
             strstr(cookie, "UUID=") != NULL)
    {
        ... Get value from the cookie, if any ...
        if ( ...cookie okay ... )
            mg_authorize(conn);
        else
            ... redirect to /logout ...
    }
}

请注意 authorize() 函数的参数。第一个参数是连接信息。第二个是指向从传入 HTTP 请求中提取的请求信息的指针。第三个参数指向使用 mg_set_auth_callback() 注册回调时提供的数据。当调用 URI 回调函数时,使用相同的参数。

在此示例授权函数中,对文档根目录中的首页或图像目录的任何请求都会自动授权。例如,这允许 CSS 中引用的图像由浏览器检索,而无需由该函数检查或为图像注册 URI 回调。如果未为 URI 注册回调,Mongoose 会尝试提供在文档根目录下指定 URI 找到的文件。

如果 URI 是注销页面,则检查登录 cookie,如果找到,则用户将被重定向到登录页面,在该页面中删除该 cookie。如果未找到 cookie,服务器可以无论如何重定向到首页或执行其他一些适当的操作。

下一个测试查找特定的 cookie,在本例中名为“UUID”。如果找到此 cookie 并且具有正确的值,则请求被授权。否则,用户将被重定向到注销页面,该页面反过来清除登录 cookie 并再次显示登录页面。

mg_request_info 结构在 mongoose.h 中定义,并由工作线程填充从 HTTP 请求中收集的信息。这包括请求方法(POST、PUT、GET 等)、规范化 URI、查询字符串、post 数据以及请求来源的 IP 地址等信息。它还包括一个保存 HTTP 标头集的数组,这就是检索 cookie 的方式。

Cookies

mg_get_header() 函数用于从 mg_request_info 的 mg_header 数组中检索命名的标头。Cookie 在文档内容开始之前在标头中设置

  mg_printf(conn,
    "HTTP/1.1 200 OK\r\n"
    "Set-Cookie: UUID=MGOOSE;\r\n"
    "Set-Cookie: LOGIN=;\r\n"
    "Content-Type: text/html\r\n\r\n"
    ...

此代码将设置 UUID cookie 并清除浏览器上的 LOGIN cookie。检索特定 cookie 需要拉取 cookie 标头并解析它以查找所需的 cookie 名称/值对

static
char *getCookieParam(const char *cookie, char *param)
{
    char *start = NULL;
    char *end  = NULL;
    char *value = NULL;
    int length;

    if ( (cookie!=NULL) &&
         ((start=strstr(cookie, param)) != NULL) )
    {
        if ( (end=strstr(start, "; ")) != NULL )
            length = end-start;
        else
            length = strlen(start);
        value = malloc(length+1);
        memset(value, 0, length+1);
        strncpy(value, start, length);
    }
    return value;
}

此函数将检索 cookie 的名称及其值(如果有),格式为 NAME=VALUE。但是,返回的字符串必须由调用者释放。

提供静态文件

当 Mongoose 遇到没有注册回调的 URI 时,它会尝试打开指定的文件并将其发送回客户端。静态 HTML 文件可以写入并存储在使用root选项配置的文档根目录中。要允许检索文档根目录下的目录列表,请将dir_list选项设置为yes。此选项默认为no。目录列表设置是全局配置,因此要么不允许目录列表,要么可以看到所有目录列表。

注意

允许 yes/no 或 true/false 值的选项针对“1”、“yes”、“true”或“ja”的不区分大小写的字符串值进行测试。任何其他值都被解释为 no 或 false。

清单 2. 因为 dir_list 选项与 true 值不匹配,所以它将禁用目录列表。

void mongooseMgrInit()
{
  ...
  mg_set_option(ctx, "dir", documentRoot);
  mg_set_option(ctx, "dir_list", "0");
  ...
}
服务器日志记录

Mongoose 提供访问和错误日志记录。文件在服务器重新启动时追加。Mongoose 提供了两个可以从 API 访问的函数,这些函数在网站的 Mongoose API 页面中记录:mg_set_error_callback() 和 mg_set_log_callback。这些回调的配置与 URI 回调略有不同

void mongooseMgrInit()
{
  ...
  mg_set_error_callback(ctx, 404, show404, NULL)
  mg_set_log_callback(ctx, logger)
  ...
}

错误回调为 0 到 1000 的错误代码设置回调。这些代码映射到 HTTP 错误代码,例如当请求的 URI 不存在时为 404。当调用此回调函数时,该函数可以打印自定义错误页面。每当 Mongoose 服务器库想要记录某些内容时,都会调用日志回调。

使用 mongoose 实现的示例服务器的源代码可以在 ftp.linuxjournal.com/pub/lj/listings/issue192/10680.tgz 找到。它包含一个带有图像和 CSS 的单页。

总结

Mongoose 项目是稳定的,并且被许多开发人员使用;但是,它的 Google 论坛上充斥着垃圾邮件。不要让这种不便阻止您使用这个设计良好且实现完善的 Web 服务器库。

这篇 Mongoose 简介涵盖了创建轻量级嵌入式 Web 服务器的基础知识,但未涵盖 Mongoose 功能的全部广度,例如 CGI 或 SSL。该库的易用性应该清楚地表明,这些扩展功能几乎不需要额外的 Mongoose 知识,并且使开发人员可以自由构建自定义 Web 服务器。

Michael J. Hammel 是科罗拉多州科罗拉多斯普林斯 Colorado Engineering, Inc. (CEI) 的首席软件工程师,拥有 20 多年的软件开发和管理经验。他为众多在线和印刷杂志撰写了 100 多篇文章,并且是关于 The GIMP(首屈一指的开源图形编辑软件包)的三本书的作者。

加载 Disqus 评论