×

Mongoose - 嵌入式 Web 服务器/嵌入式网络库使用分享 - 4(HTTP服务器)

hqy hqy 发表于2025-10-20 18:46:47 浏览4 评论0

抢沙发发表评论

?简介

Mongoose 的一个基本应用就是作为轻量级的 HTTP 服务器。

在此基础上,我们可以逐步添加 web-ui、文件上传下载、websocket 等一系列应用。

这里只是尽可能的介绍作为服务器来说所使用到的各个接口的细节问题。

下文的所有案例都是基于 Linux 环境,对于其他硬件平台后文会单独说明。

?静态服务器

为了逐步了解HTTP的相关接口,我们先从实现一个最简的静态服务器所需要的知识讲起。

?mg_http_listen



struct mg_connection * mg_http_listen(struct mg_mgr *mgr,   const char *url, mg_event_handler_t fn, void *fn_data);

该接口用于创建一个 http 的监听器,也就是服务器。

参数:mgr 指向一个事件管理结构体;

参数:url 要监听的本地IP地址和端口,例如 "http://0.0.0.0:8000";

参数:fn 事件回调处理函数;

参数:fn_data 传递给回调函数的参数;

返回:成功则返回指向已创建的连接结构,失败返回NULL;

这个事件处理的回调函数也是Mongoose的核心基础之一,有必要了解一下:




























enum {  MG_EV_ERROR,      // Error                        char *error_message  MG_EV_OPEN,       // Connection created           NULL  MG_EV_POLL,       // mg_mgr_poll iteration        uint64_t *uptime_millis  MG_EV_RESOLVE,    // Host name is resolved        NULL  MG_EV_CONNECT,    // Connection established       NULL  MG_EV_ACCEPT,     // Connection accepted          NULL  MG_EV_TLS_HS,     // TLS handshake succeeded      NULL  MG_EV_READ,       // Data received from socket    long *bytes_read  MG_EV_WRITE,      // Data written to socket       long *bytes_written  MG_EV_CLOSE,      // Connection closed            NULL  MG_EV_HTTP_HDRS,  // HTTP headers                 struct mg_http_message *  MG_EV_HTTP_MSG,   // Full HTTP request/response   struct mg_http_message *  MG_EV_WS_OPEN,    // Websocket handshake done     struct mg_http_message *  MG_EV_WS_MSG,     // Websocket msg, text or bin   struct mg_ws_message *  MG_EV_WS_CTL,     // Websocket control msg        struct mg_ws_message *  MG_EV_MQTT_CMD,   // MQTT low-level command       struct mg_mqtt_message *  MG_EV_MQTT_MSG,   // MQTT PUBLISH received        struct mg_mqtt_message *  MG_EV_MQTT_OPEN,  // MQTT CONNACK received        int *connack_status_code  MG_EV_SNTP_TIME,  // SNTP time received           uint64_t *epoch_millis  MG_EV_WAKEUP,     // mg_wakeup() data received    struct mg_str *data  MG_EV_USER        // Starting ID for user events};//类型定义typedef void (*mg_event_handler_t)(struct mg_connection *, int ev, void *ev_data);//使用示例static void fn(struct mg_connection *c, int ev, void *ev_data);

每个连接都有一个与之关联的事件处理函数,该函数必须由用户实现。

参数:c 指示哪个连接触发的此事件。因为多个连接可以使用同一个 fn;

参数:ev 事件类型,在 mongoose.h 中定义,具体如上;

参数:ev_data 指向特定于事件的数据,不同的事件具有不同的含义;


例如:对于 HTTP 消息来说,事件类型为 MG_EV_HTTP_MSG,而事件数据 ev_data 需要转化为 (struct mg_http_message *) ev_data 类型(下文讲解)。


然后我们就可以实现一个最小的静态服务器了,代码如下:






















static void fn(struct mg_connection *c, int ev, void *ev_data) {    if(ev == MG_EV_HTTP_MSG)     {        struct mg_http_serve_opts opts = {.root_dir = "./web_root"};        mg_http_serve_dir(c, ev_data, &opts);    }}int main(int argc, char *argv[]){    /* 创建并初始化事件管理结构体 */    struct mg_mgr mgr;    mg_mgr_init(&mgr);    /* 创建一个http监听服务器,添加到指定的事件管理结构中 */    mg_http_listen(&mgr, "http://0.0.0.0:8000", fn, NULL);    /* 循环处理事件 */    for(;;){ mg_mgr_poll(&mgr, 1000); }     /* 关闭连接,释放资源 */    mg_mgr_free(&mgr);    exit(0);}

关于 mg_http_serve_dir() 接口具体有什么用,后文会详细说明,这里只需要知道它主要用于提供静态文件的服务,例如将指定目录(如 ./web_root)下的 HTML、CSS、JS、图片等文件直接通过 HTTP 提供给客户端。


简单测试一下,为了让大家看到效果,我们在 web_root 文件夹下添加一个 hello.txt 文件,内容只有一行 hello:

0


编译后运行,在浏览器上输入网址(我这里是:192.168.11.102:8000),可以看到显示内容如下:

0


点开这个文件能显示里面的内容:

0


这也说明了 mg_http_serve_dir() 接口的作用。

如果请求的路径是目录而非文件,且配置允许,会生成一个动态的目录文件列表页面(类似 Apache 的默认目录浏览)。

如果请求的路径是文件,则直接将文件内容提供给客户端。


试想一下,如果我们在该目录下放一个 html 格式的文件,浏览器在获取到文件时是否就直接解析成了前端页面呢?

实验一下,写个简单的 index.html 文件,内容如下:














<!DOCTYPE html><html lang='zh-CN'>    <head>        <meta charset='UTF-8'>        <meta name='viewport' content='width=device-width, initial-scale=1.0'>        <title>Http Test</title>    </head>    <body>        <input type='text' name='user' placeholder='账号'>        <input type='password' name='pwd' placeholder='密码'>        <button type='button'>登录</button>    </body></html>

浏览器的访问显示如下:

0


这就是通过 Mongoose 实现 web-ui 的基本逻辑,不过这不是该篇要讲的内容。


?动态 RESTful 服务器

我们上面实现的最简静态服务器只能向客户端返回既定的内容,无法实现动态的交互。

那如何实现动态响应呢?显然如果我们能在HTTP事件处理函数中根据不同的请求,来返回不同的响应内容就能实现我们想要的效果。

那现在的问题其实就是如何在事件处理函数中提取出 http 请求报文的各个内容?

我们先来了解一下 struct mg_http_message 结构体:


?struct mg_http_message












struct mg_http_header {  struct mg_str name;   // Header name  struct mg_str value;  // Header value};struct mg_http_message {  struct mg_str method, uri, query, proto;             // Request/response line  struct mg_http_header headers[MG_MAX_HTTP_HEADERS];  // Headers  struct mg_str body;                                  // Body  struct mg_str head;                                  // Request + headers  struct mg_str message;  // Request + headers + body};

该结构体描述了 http 的请求和应答数据。

其中 struct mg_http_header 描述了 http 的报文头,例如 Content-Type: text/html 中 Content-Type 是报文头名称,text/html 是报文头的值。

而 struct mg_http_message 结构体中各个成员的含义可以用官方的一个对比图来很好的说明:

0

这里主要注意一下:

head 成员是包含了 Request + headers;

message 成员是包含了 Request + headers + body;


?代码演示

我们需要一个测试用的客户端软件,这里笔者选用 Apifox 。

这是一款 API 设计、开发、测试一体化的协作平台,它已经整合了 Postman 的功能,并且有在线和离线版本可选,作为测试来说非常方便。

0


这里先做一个基础配置:post 请求,请求路径为 http://192.168.11.102:8000/api/test,消息体采用文本格式,其他均没有配置。

注意:关于 Apifox 软件操作和 HTML 的基础知识笔者不再赘述,有不了解的同学建议先补充一下!


事件处理函数如下所示:




























static void fn(struct mg_connection *c, int ev, void *ev_data) {    if(ev == MG_EV_HTTP_MSG)     {        struct mg_http_message *hm = (struct mg_http_message *) ev_data;        printf("method =>%.*s\n",(int)hm->method.len, hm->method.buf);        printf("uri =>%.*s\n",(int)hm->uri.len, hm->uri.buf);        printf("query =>%.*s\n",(int)hm->query.len, hm->query.buf);        printf("proto =>%.*s\n",(int)hm->proto.len, hm->proto.buf);        printf("headers =>\n");        for(uint8_t i=0; i<MG_MAX_HTTP_HEADERS; i++)        {            if((int)hm->headers[i].name.len > 0){                printf("%.*s:%.*s\n",                  (int)hm->headers[i].name.len,                   hm->headers[i].name.buf,                   (int)hm->headers[i].value.len,                   hm->headers[i].value.buf);            }        }        printf("body =>%.*s\n",(int)hm->body.len, hm->body.buf);        printf("head =>%.*s\n",(int)hm->head.len, hm->head.buf);        printf("message =>%.*s\n",(int)hm->message.len, hm->message.buf);        struct mg_http_serve_opts opts = {.root_dir = "./web_root"};        mg_http_serve_dir(c, ev_data, &opts);    }}

注意:如果我们使用原始的 printf() 打印输出字符串的话,一定要使用 "%.*s" 的格式,否则输出的结果是多个信息粘连在一起的。


我们的目的就是想看看通过 (struct mg_http_message *) ev_data 传递进来的各项数据和客户端实际发出的数据到底是如何对应的?了解这点对于嵌入式的后端开发很重要。


发送请求的具体内容如下:

0


注意:有些同学会疑惑,上文并没有设置任何报文头信息,这里这些报文头哪里来的?其实这是软件默认设置的,点开隐藏选项即可看到:

0


接收打印的具体内容如下:

0
仔细对比一下可以发现,这个对应关系和上文中讲解的 

struct mg_http_message 结构体中的各个成员是完全对应的。

对于这些信息的提取,Mongoose 也给我们内置了一些方便的接口,下面来学习一下。


?mg_http_get_request_len



int mg_http_get_request_len(const unsigned char *buf,                             size_t buf_len);

该接口主要用于获取http请求的长度,就是到报文头末尾的字节数,但不包括正文的长度。

参数:buf 指向请求所在的缓冲区;

参数:buf_len 缓冲区长度;

返回:<0 表示出错,>=0 表示请求长度;


这里引用官方的一个说明示例:

0


?mg_http_get_header



struct mg_str *mg_http_get_header(struct mg_http_message *hm,                                   const char *name);

该接口主要用于在 HTTP 响应信息 hm 中,根据指定的报文头名称 name,去寻找相应的报文头值并返回。找不到则返回 NULL。


?mg_http_get_header_var



struct mg_str mg_http_get_header_var(struct mg_str s,                                      struct mg_str v);

该接口主要用于在已经提取出的报文头值 s 中,再根据 v 去提取指定的键值对的值。

它通常用于解析结构化头部字段(如 multipart/form-data 或 Cookie),因为这些字段可能包含键值对(如 name=value)。

这在文件上传、会话管理或自定义头部扩展等场景下都很有用。


例如 Cookie 这个报文头可能是这样的:Cookie: session_id=abc123; user_id=42(分号分隔的键值对),通过 mg_http_get_header() 只能获取到 session_id=abc123; user_id=42 这整个内容,再通过 mg_http_get_header_var() 就能获取到 abc123 这个内容。


注意:虽然该接口名为  mg_http_get_header_var(),但从它的形参可以看出它并不局限于从报文头获取信息,而是只要由分号分隔的键值对都可以。


代码演示






















static void fn(struct mg_connection *c, int ev, void *ev_data) {    if(ev == MG_EV_HTTP_MSG)     {        struct mg_http_message *hm = (struct mg_http_message *) ev_data;        //提取 Cookie 报文头的全部内容        struct mg_str *s = mg_http_get_header(hm, "Cookie");        printf("Cookie =>%.*s\n",(int)s->len, s->buf);                //从 Cookie 报文头的全部内容中提取会话 id        struct mg_str id = mg_str("");        id = mg_http_get_header_var(*s, mg_str("id"));        printf("Cookie - id =>%.*s\n",(int)id.len, id.buf);        //从消息体中提取 age        struct mg_str age = mg_str("");        age = mg_http_get_header_var(hm->body, mg_str("age"));        printf("age =>%.*s\n",(int)age.len, age.buf);        struct mg_http_serve_opts opts = {.root_dir = "./web_root"};        mg_http_serve_dir(c, ev_data, &opts);    }}

将客户端配置一个 Cookie 的报文头,消息体的文本也改为 age=10;sex=man,具体发送的消息如下:

0


接收打印如下:

0


?mg_http_creds



void mg_http_creds(struct mg_http_message *hm, char *user,                    size_t userlen, char *pass, size_t passlen);

该接口主要用于从请求报文中获取身份验证凭据,并将其存储到 user、userlen 和 pass、passlen 缓冲区中。


凭据的获取按以下顺序查找:

1. 从 Authorization 这个报文头的信息中获取。
  • 如果是 Basic Auth 形式的,例如 Authorization: Basic am9objpzZWNyZXQ=  # 解码后为 "john:secret",那么信息提取后将会填充 user 和 pass 两个缓冲区;
  • 如果是 Bearer Auth 形式的,例如 Authorization: Bearer abc123xyz456,那么信息提取后仅填充 pass 缓冲区(将 token 当作密码看待);
2. 从 Cookie 中的 access_token 获取,当作密码填充到  pass 缓冲区。
3. 从 URL 的查询参数 ?access_token=... 获取(如果有的话),当作密码填充到  pass 缓冲区。

如果都没找到,user 和 pass 两个缓冲区将设置为空。


?代码演示

服务器代码很简单,就是将获取到的用户名和密码打印出来:













static void fn(struct mg_connection *c, int ev, void *ev_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));        printf("user:%s, pass:%s\n",user,pass);        struct mg_http_serve_opts opts = {.root_dir = "./web_root"};        mg_http_serve_dir(c, ev_data, &opts);    }}

?方法1 - 通过 Authorization:Basic Auth 获取

0


?方法2 - 通过 Authorization:Bearer Auth 获取

0
0


?方法3 - 通过 Cookie 中的 access_token 获取

0
0


?方法4 - 通过 URL 的查询参数 ?access_token=... 获取

0
0


注意:上述传输身份凭证的方法只是举例说明该接口如何使用,实际使用中身份凭证的传递方式以及加解密方式多种多样,明文传递是最不安全的做法,但这不是该篇要讲解的内容!


?mg_http_next_multipart









//该结构体用于描述 HTTP 分段消息单个部分的结构。struct mg_http_part {  struct mg_str name;      //表单字段名(如 <input name="user"> 中的 "user")  struct mg_str filename;  //文件名(如果是文件上传,如 <input type="file" name="file">)  struct mg_str body;      //分块的实际内容(表单值或文件数据)};size_t mg_http_next_multipart(struct mg_str body,                     size_t offset, struct mg_http_part *part);

我们知道 http 在传输 multipart/form-data 表单数据时会采用 boundary 分隔符来分隔表单中的各个内容,并且还会有额外的属性描述信息。对于接收者而言并不像纯文本那样所见即所得,所以对于表单数据的提取有一定的复杂性。


注意:采用分隔符和分块传输有本质区别,官方的相关描述多处都会出现 chunk 字样,读者要注意区分它的含义。


该接口主要用于在正文中遍历解析出各个由boundary分隔的内容块

参数:body 消息体;

参数:offset 偏移量,初始偏移应为0,每次调用该接口需要传递新的偏移量;

参数:part指向获取到的内容块

返回:>0 表示到下一个内容块的偏移量,在下次调用该接口时需要传递这个值,=0 表示没有更多的内容块,遍历完成;


官方的一个对比图能帮助读者更好的理解这个对应关系:

0


?代码演示





















static void fn(struct mg_connection *c, int ev, void *ev_data) {    if(ev == MG_EV_HTTP_MSG)     {        struct mg_http_message *hm = (struct mg_http_message *) ev_data;        //打印接收到的整个报文        printf("message =>%.*s\n",(int)hm->message.len, hm->message.buf);        size_t pos = 0;        struct mg_http_part part;        while((pos = mg_http_next_multipart(hm->body, pos, &part)) != 0)         {            printf("name:%.*s, filename:%.*s, body:%.*s\n",                 (int)part.name.len, part.name.buf,                 (int)part.filename.len, part.filename.buf,                 (int)part.body.len, part.body.buf);        }        struct mg_http_serve_opts opts = {.root_dir = "./web_root"};        mg_http_serve_dir(c, ev_data, &opts);    }}

我们通过表单随意发送几组数据:

0


接收结果如下:

0


?mg_http_get_var



int mg_http_get_var(const struct mg_str *var,                     const char *name, char *buf, int len);

该接口主要用于从 key=value&key2=value2... 格式的字符串中提取指定 name 对应的 value,存入到 buf 指向的缓冲区

返回:<=0 表示错误,>0 表示提取的变量值的长度;

适用场景:

  • 解析 URL 查询参数(如 ?name=John&age=20);
  • 解析 POST 表单数据(Content-Type: application/x-www-form-urlencoded);
  • 解析自定义相同格式的数据;


?代码演示
















static void fn(struct mg_connection *c, int ev, void *ev_data) {    if(ev == MG_EV_HTTP_MSG)     {        struct mg_http_message *hm = (struct mg_http_message *) ev_data;        //打印出查询参数        printf("query =>%.*s\n",(int)hm->query.len, hm->query.buf);        char buf[20]={0};        //提取查询参数中,name 变量的值        int ret = mg_http_get_var(&(hm->query), "name", buf, sizeof(hm->query));        printf("ret:%d, buf:%s\n",ret,buf);        struct mg_http_serve_opts opts = {.root_dir = "./web_root"};        mg_http_serve_dir(c, ev_data, &opts);    }}

结果如下:

0
0


?mg_http_var


struct mg_str mg_http_var(struct mg_str buf, struct mg_str name);

该接口和 mg_http_get_var() 的作用是类型的,区别在于:

  • mg_http_get_var:将变量值 拷贝到用户提供的缓冲区。
  • mg_http_var:直接返回 原始字符串中的变量值子串(无拷贝,更高效)。


?接口路径获取处理

在上述的所有案例中,我们的所有案例都是从同一个路径访问的,为了案例的简洁,我们都没有对客户端的访问路径做处理。

同时对客户端的请求也没有做出应答,如果读者做相同的实验就会发现在客户端上的响应总是显示 404 之类的错误。

下面我们模拟通过web页面来设置服务器的IP地址,实现一个相对完整的小案例。

?mg_http_reply



void mg_http_reply(struct mg_connection *c, int status_code,                 const char *headers, const char *body_fmt, ...);

该接口主要用于发送简单的格式化(printf 语义)后的HTTP响应。

参数:c 指向当前使用的连接结构体;

参数:status_code htttp 状态码;

参数:headers附加的响应头信息,如果不是NULL,则必须以\r\n结尾

参数:body_fmt 格式化的响应正文;


我们大致看一下这个接口的具体实现:

0

可以看到默认情况下只添加了基本的状态信息和 Content-Length(内容长度)这一个响应头信息,因为这些都是可以确定的。所以如果需要其他的响应头信息需要自己添加。


?代码演示






































































#include <stdio.h>#include <string.h>#include <stdint.h>#include "mongoose.h"//编译: gcc example_https.c mongoose.c#define MIN(a, b) ((a) < (b) ? (a) : (b))static void fn(struct mg_connection *c, int ev, void *ev_data) {    if(ev == MG_EV_HTTP_MSG)     {        struct mg_http_message *hm = (struct mg_http_message *) ev_data;        struct mg_http_part part;        size_t pos = 0;
        //路径判断                  if(mg_match(hm->uri, mg_str("/api/test"), NULL))         {            char type[10+1]={0};            char address[20+1]={0};            char netmask[20+1]={0};            char gateway[20+1]={0};            char dns[20+1]={0};            int steperr = 0;            //从表单获取数据            while ((pos = mg_http_next_multipart(hm->body, pos, &part)) != 0            {                if(strncmp("TYPE", part.name.buf, part.name.len)==0){                    strncpy(type, part.body.buf, MIN(part.body.len, sizeof(type)-1));                }else if(strncmp("ADDRESS", part.name.buf, part.name.len)==0){                    strncpy(address, part.body.buf, MIN(part.body.len, sizeof(address)-1));                }else if(strncmp("NETMASK", part.name.buf, part.name.len)==0){                    strncpy(netmask, part.body.buf, MIN(part.body.len, sizeof(netmask)-1));                }else if(strncmp("GATEWAY", part.name.buf, part.name.len)==0){                    strncpy(gateway, part.body.buf, MIN(part.body.len, sizeof(gateway)-1));                }else if(strncmp("DNS", part.name.buf, part.name.len)==0){                    strncpy(dns, part.body.buf, MIN(part.body.len, sizeof(dns)-1));                }            }            printf("type:%s, address:%s, netmask:%s, gateway:%s, dns:%s\n", type, address, netmask, gateway, dns);            //数据校验和网络设置            //略                        //返回应答            if(steperr == 0){                mg_http_reply(c, 200"Content-Type: application/json\r\n""{\"state\":\"ok\"}\n");            }else{                mg_http_reply(c, 200"Content-Type: application/json\r\n""{\"state\":\"err\"}\n");            }           }         else        {            struct mg_http_serve_opts opts = {.root_dir = "./web_root"};            mg_http_serve_dir(c, ev_data, &opts);        }    }}int main(int argc, char *argv[]){    /* 创建并初始化事件管理结构体 */    struct mg_mgr mgr;    mg_mgr_init(&mgr);    /* 创建一个http监听服务器,添加到指定的事件管理结构中 */    mg_http_listen(&mgr, "http://0.0.0.0:8000", fn, NULL);    /* 循环处理事件 */    for(;;){ mg_mgr_poll(&mgr, 1000); }     /* 关闭连接,释放资源 */    mg_mgr_free(&mgr);    exit(0);}

我们通过表单形式去发送IP地址的相关信息,得到的响应和接收信息如下:

0

0




打赏

本文链接:https://kinber.cn/post/5726.html 转载需授权!

分享到:


推荐本站淘宝优惠价购买喜欢的宝贝:

image.png

 您阅读本篇文章共花了: 

群贤毕至

访客