前言
授人以鱼不如授人以渔,这篇文章详细介绍了,对于一个从来没有听说过mongoose的小菜鸟如何快速了解和上手mongoose
其他一些开源库可以借助类似的方法进行学习
提前需要准备的工具
1.官网文档 Mongoose :: Documentation
官网提供了很多例子讲解,本文主要针对HTTP server/client和webSocket Server/client进行讲解。
2.下载源码 GitHub - cesanta/mongoose: Embedded Web Server
下载最新版源码,本文主要通过源码中例子进行快速了解mongoose如何使用,以官网提供的文档进行辅助工具
3.阅读源码的工具(这里推荐Source Insight)
4.开发工具(IDE,本文使用VS2017,请根据自己的开发环境进行选择)
通过source insight查看源码
对于如何使用source insight,安装下面截图的步骤进行基本不会有太大的问题,这里把工具也分享给大家:source insight4.0破解版
打开的整体页码:
mongoose简介
根据官网文档给出的描述大致总结下:
1.mongoose是一个用于C/C++的网络库,它为TCP、UDP、HTTP、WebSocket、MQTT实现了事件驱动的非阻塞API。Mongoose使嵌入式编程快速、健壮、简单。
2.mongoose可在windows、Linux、Mac和许多嵌入式架构上运行。它可以在现有的操作系统和TCP/IP堆栈(如FreeRTOS和lwIP)上运行,也可以在裸机上运行,利用Mongoose内置的TCP/IP堆栈和网络驱动程序。
如何使用mongoose
这部分官网提供了说明,根据2-minute integration guide这部分的说明,我们可以猜测下:
为了将Mongoose集成到现有的C/C++应用程序或固件中,请使用以下步骤:
1.将mongoose.c和mongoose.h复制到源代码中
使用起来很简单,只需要把mongoose.c和mongoose.h加入到源代码中就可以顺畅的使用了,不需要使用cmake和vs编译,神仙体验。
分析http-server
再来说下,为什么建议去看源码中的例子,而不是别人博客中的案例。版本问题,不同的版本可能使用的方法会有一些不同,不同的版本支持功能源有会有一些差异。
如何看源码:主-谓-宾,动词最重要,也就是说函数最重要(函数就是一个动作)
重要的函数先摘出来:
signal(SIGINT, signal_handler); mg_log_set(s_debug_level); mg_mgr_init(&mgr); mg_http_listen(&mgr, s_listening_address, cb, &mgr) mg_casecmp(s_enable_hexdump, "yes") MG_INFO(("Mongoose version : v%s", MG_VERSION)); mg_mgr_poll(&mgr, 1000); mg_mgr_free(&mgr);
我们看下函数原型:
1===================
_ACRTIMP _crt_signal_t __cdecl signal(_In_ int _Signal, _In_opt_ _crt_signal_t _Function);
可以猜测应该是为某一个信号增加一个控制方式: signal(SIGINT, signal_handler);
为SIGINT这个信号做一个处理动作,SIGINT是ctrl+C时触发
我们看源码中signal_handler的信号捕捉函数:
typedef void (__CRTDECL* _crt_signal_t)(int);
2===================
mg_log_set(s_debug_level);
设置日志等级,如果源码中没有给出注释,可以查看官网中的文档
这里应该是把日志设置成了INFO和ERROR等级
3=======================
mg_mgr_init(&mgr);
函数原型:
void mg_mgr_init(struct mg_mgr *mgr)
根据文档描述可以知道,他是为事件管理器mg_mgr进行初始化的,初始化工作做了什么:
1.将活动连接列表设置为NULL
2.设置默认的DNS server为IPv4和IPv6
3.设置默认的DNS查找超时
什么是mg_mgr呢?
事件管理结构,包含活动连接的列表以及一些内务管理信息
4===============================
mg_http_listen(&mgr, s_listening_address, cb, &mgr)
函数原型:
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
5===============================
mg_casecmp(s_enable_hexdump, "yes")
函数原型:
int mg_casecmp(const char *s1, const char *s2);
不分大小写的比较两个以NULL结束的字符串
参数:s1,s2指向这两个字符串的指针
返回值:如果返回0则说明这两个字符串相等,如果大于0则s1>s2,如果小于0则s1<s2
6===============================
MG_INFO(("Mongoose version : v%s", MG_VERSION));
函数原型:
#define MG_INFO(args) MG_LOG(MG_LL_INFO, args)
写日志
7==============================
mg_mgr_poll(&mgr, 1000);
函数原型:
void mg_mgr_poll(struct mg_mgr *mgr, int ms);
执行轮询迭代,对于在监听链表中的每一个连接
1.查看是否有传入数据,如果有,将其读入到读缓冲区中,并发送 MG_EV_READ事件
2.查看是否有数据在写缓冲区,并且写入,发送MG_EV_WRITE事件
3.如果连接正在侦听,则接受传入连接(如果有),并向其发送MG_EV_accept事件
4.发送MG_EV_POLL事件
参数:
mgr:一个事件管理器
ms:超时时间(毫秒)
8===============================
mg_mgr_free(&mgr);
函数原型:
void mg_mgr_free(struct mg_mgr *mgr);
关闭所有的连接,释放所有的资源
结合上面的描述我们做个总结:
日志部分我们这里不做探究,直接跳过。
1.我们需要准备一个事件管理器,有两个步骤:
struct mg_mgr mgr;
mg_mgr_init(&mgr);
2.然后对这个事件管理器设置监听事件并且指定IP和PORT:
准备一个回调函数 ==> 回调函数内部我们之后探究
准备指定的IP和端口
mg_http_listen();
3.调用消息循环
mg_mgr_poll(); // 指定阻塞的时长
4.停止循环和释放资源
mg_mgr_free()
HTTP SERVER - V1.0
// Copyright (c) 2020 Cesanta Software Limited // All rights reserved #include <signal.h> #include "mongoose.h" static const char *s_listening_address = "http://0.0.0.0:8000"; static void cb(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { if (ev == MG_EV_HTTP_MSG) { printf("有数据到达\n"); } (void)fn_data; } int main(int argc, char *argv[]) { struct mg_mgr mgr; struct mg_connection *c; mg_mgr_init(&mgr); if ((c = mg_http_listen(&mgr, s_listening_address, cb, &mgr)) == NULL) { exit(EXIT_FAILURE); } // Start infinite event loop while (1) mg_mgr_poll(&mgr, 1000); mg_mgr_free(&mgr); return 0; }
客户端用postman进行测试就可以了,这里不自己写客户端代码了
HTTP SERVER - v2.0
现在我们对回调函数好好分析下:
函数原型,mongoose是通过函数指针来实现回调的,其原理跟信号捕捉函数一样。
typedef void (*mg_event_handler_t)(struct mg_connection *, int ev, void *ev_data, void *fn_data);
官方文档给出了结构体中各个成员的解释我们经常会用到的:POST请求和GET请求
// 区别是POST请求还是GET请求 if (strstr(hm->method.ptr, "POST")) { printf("这是POST请求\n"); mg_http_reply(c, 200, "Content-Type: application/json\r\n", "{%s:%s}", "status", "okk"); } else if(strstr(hm->method.ptr, "GET")) { printf("这是GET请求\n"); mg_http_reply(c, 200, "Content-Type: application/json\r\n", "{%s:%s}", "status", "okk"); } else { mg_http_reply(c, 500, NULL, "{%s:%s}", "status", "已经收到GET请求"); }
有时候会根据uri进行不同的处理: /hello
if (mg_http_match_uri(hm, "/hello")) { if (strstr(hm->method.ptr, "POST")) { printf("这是POST请求\n"); mg_http_reply(c, 200, "Content-Type: application/json\r\n", "{%s:%s}", "status", "okk"); } else if (strstr(hm->method.ptr, "GET")) { printf("这是GET请求\n"); mg_http_reply(c, 200, "Content-Type: application/json\r\n", "{%s:%s}", "status", "okk"); } else { mg_http_reply(c, 500, NULL, "{%s:%s}", "status", "已经收到GET请求"); } } else if (mg_http_match_uri(hm, "/app")) { printf("------------------\n"); }
mongoose给出了几种不同的方式发送消息:
int mg_printf(struct mg_connection *, const char *fmt, ...); void mg_http_printf_chunk(struct mg_connection *c, const char *fmt, ...);
官网给出了很多函数,这些都是关于HTTP,官网有详细的介绍。
这里就不每一个都给大家解释了
HTTP-CLIENT
在mongoose的HTTP客户端中内部自动帮我们发送连接,因为数据可能比较复杂。在建立连接的时候会发送一个MG_EV_CONNECT事件,我们在回调函数中对该事件进行处理。
客户端代码与服务端基本相同,唯一不同的是连接和监听连接不同,其他基本相同
#include <stdio.h> #include <string.h> #include "mongoose.h" static const char *s_url = "http://127.0.0.1:8080/hello"; void cb(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { const char *post_data = "aaaa"; // 连接事件 if (MG_EV_CONNECT == ev) { struct mg_str host = mg_url_host(s_url); // 解析出主机 // 发送请求 mg_printf(c, "%s %s HTTP/1.1\r\n" "Host:%.*s\r\n" "\r\n" "%.*s", post_data == NULL ? "GET":"POST", mg_url_uri(s_url), (int)host.len, host.ptr, post_data == NULL ? 0:(int)strlen(post_data), post_data); } // 接受到回应 if (MG_EV_HTTP_MSG == ev) { struct mg_str host = mg_url_host(s_url); // 解析出主机 struct mg_http_message *hm = (struct mg_http_message *)ev_data; printf("%s", hm->message); mg_printf(c, "%s %s HTTP/1.1\r\n" "Host:%.*s\r\n" "\r\n" "%.*s", post_data == NULL ? "GET" : "POST", mg_url_uri(s_url), (int)host.len, host.ptr, post_data == NULL ? 0 : (int)strlen(post_data), post_data); } } int main(void) { // 1.创建一个 struct mg_mgr mgr; mg_mgr_init(&mgr); // 2.连接HTTP服务器 mg_http_connect(&mgr, "127.0.0.1:8080", cb, NULL); while (1) { // 消息循环 mg_mgr_poll(&mgr, 1000); } // 释放资源 mg_mgr_free(&mgr); return 0; }
WEBSOCKET-SERVER
可能很多朋友对webSocket协议不是很了解,这里简单说下:
本质上与HTTP协议类型,webSocket是一种全双工通信的协议,是基于HTTP协议的不足提出的一种新的通信协议。
http是一种单向的应用层协议,它采用了请求响应模型,通信请求只能由客户端发起,服务端对请求做出应答处理。这样的弊端显然是很大的,只要服务器状态连续变化,客户端就必须实时响应,这样显然很麻烦,同时轮询的效率低,非常的浪费资源。
webSocket是一种全面双工通讯的网络技术,任意一方都可以建立连接将数据推向另一方,webSocket只需要建立一次连接就可以一直保持。
mongoose对webSocket的处理
mongoose的处理很简单,还是通过HTTP进行处理的,只不过在回调函数中会发送一个MG_EV_WS_MSG的事件,通过mg_ws_upgrade()将数据转换成ws数据进行处理,当调用mg_ws_upgrade会发送一个MG_EV_WS_MSG事件。
#include "mongoose.h" static const char *s_listen_on = "ws://localhost:8080"; static void fn(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { //if (ev == MG_EV_OPEN) { // // c->is_hexdumping = 1; //} //else if (ev == MG_EV_HTTP_MSG) { struct mg_http_message *hm = (struct mg_http_message *) ev_data; if (mg_http_match_uri(hm, "/websocket")) { // Upgrade to websocket. From now on, a connection is a full-duplex // Websocket connection, which will receive MG_EV_WS_MSG events. mg_ws_upgrade(c, hm, NULL); } } else if (ev == MG_EV_WS_MSG) { // Got websocket frame. Received data is wm->data. Echo it back! struct mg_ws_message *wm = (struct mg_ws_message *) ev_data; printf("%s", wm->data); mg_ws_send(c, wm->data.ptr, wm->data.len, WEBSOCKET_OP_TEXT); } (void)fn_data; } int main(void) { struct mg_mgr mgr; // Event manager mg_mgr_init(&mgr); // Initialise event manager mg_http_listen(&mgr, s_listen_on, fn, NULL); // Create HTTP listener while (1) { mg_mgr_poll(&mgr, 1000); // Infinite event loop } mg_mgr_free(&mgr); return 0; }