mongoose介绍
1 介绍1.1 Author1.2 License1.3 version1.4 About1.5 link
2 用户指南使用步骤连接和事件管理器发送和接收缓冲区事件处理函数事件连接标志打开和关闭连接最佳实践架构图构建选项自定义构建使用JSON内置 TCP/IP堆栈内置 TLS1.3堆栈内置 OTA 固件更新
3 代码分析【7.5版本】mgr 管理句柄结构mgr 管理句柄初始化mg_connection 结构创建 socket 监听初始化 sockaddr_in创建 socket 句柄 fd, 并 bind, 设置 socket 句柄为非堵塞poll — Infinite event loop 轮询mg_connection 网络连接, 队列处理并发实现 select数据 buffer, MG_IO_SIZE 设置 mg_iobuf 块大小, 最大存储能力如何平衡 大文件传输 与 读写效率 ?
知识点补充堵塞、非堵塞区别 以及 select、poll、epoll 对比堵塞、非堵塞区别select、poll、epoll 对比
select 介绍select 函数 【获取 fd 状态集合】函数参数timeout: 阻塞、非阻塞、等待 【fd 状态查询是否等待?】缺点【调用 select 一次,fd集合(0 ~ (nfds- 1))要全拷贝一次】
FD_ISSET 函数 判断描述符 fd 是否在给定的描述符集 fdset 中fcntl 文件描述符设置非阻塞 【读写非堵塞】
epoll 介绍红黑树 & 多级时间轮
阻塞I/O & 非阻塞I/O & I/O多路复用 & 信号驱动I/O & 异步I/O(1) 阻塞式 I/O 模型(blocking I/O)(2)非阻塞式 I/O 模型(non-blocking I/O)(3)I/O 多路复用模型(I/O multiplexing)(4)信号驱动式 I/O 模型(signal-driven I/O)(5)异步 I/O 模型(即AIO,全称asynchronous I/O)总结
参考
1 介绍
1.1 Author
…
1.2 License
GPLv2 or mongoose commercial license
1.3 version
7.5
1.4 About
Mongoose 是一个用于 C/C++ 的网络库。它实现了 TCP、UDP、HTTP、WebSocket、MQTT 的事件驱动非阻塞 API。它被设计用于连接设备并将它们上线。自 2004 年上市以来,被大量开源和商业产品使用 – 它甚至在国际空间站上运行!Mongoose 使嵌入式网络编程变得快速、稳健且简单。特性包括:
跨平台:在 Linux/UNIX、MacOS、Windows、Android、FreeRTOS 等系统上工作。支持的嵌入式架构:ESP32、NRF52、STM32、NXP 等。内置协议:纯 TCP/UDP、HTTP、MQTT、Websocket。SSL/TLS 支持:mbedTLS、OpenSSL 或自定义(通过 API)。异步 DNS 解析器。极小的静态和运行时占用空间。源代码符合 ISO C 和 ISO C++ 标准。与任何具有套接字 API 的网络堆栈兼容,如 LwIP 或 FreeRTOS-Plus-TCP。非常容易集成:只需将 mongoose.c 和 mongoose.h 文件复制到你的构建树中。详细的文档和教程。
1.5 link
https://github.com/cesanta/mongoose
https://mongoose.ws/documentation/
2 用户指南
使用步骤
步骤 1. 按照Build Tools设置您的开发环境
步骤 2. 将mongoose.c 和mongoose.h复制 到您的源树
步骤 3. 将以下代码片段添加到您的main.c文件中:
#include "mongoose.h"
// Connection event handler function
static void fn(struct mg_connection *c, int ev, void *ev_data) {
if (ev == MG_EV_HTTP_MSG) { // New HTTP request received
struct mg_http_message *hm = (struct mg_http_message *) ev_data; // Parsed HTTP request
if (mg_match(hm->uri, mg_str("/api/hello"), NULL)) { // REST API call?
mg_http_reply(c, 200, "", "{%m:%d}
", MG_ESC("status"), 1); // Yes. Respond JSON
} else {
struct mg_http_serve_opts opts = {.root_dir = "."}; // For all other URLs,
mg_http_serve_dir(c, hm, &opts); // Serve static files
}
}
}
int main() {
struct mg_mgr mgr; // Mongoose event manager. Holds all connections
mg_mgr_init(&mgr); // Initialise event manager
mg_http_listen(&mgr, "http://0.0.0.0:8000", fn, NULL); // Setup listener
for (;;) {
mg_mgr_poll(&mgr, 1000); // Infinite event loop
}
return 0;
}
123456789101112131415161718192021222324
步骤 4.重建并运行。将浏览器指向http://localhost:8000。
注意:如果您要为某些嵌入式系统构建,请mongoose_config.h 在该文件中创建并添加额外的构建标志。有关详细信息,请参阅构建选项。
连接和事件管理器
Mongoose 有两个基本数据结构:
struct mg_mgr – 保存所有活动连接的事件管理器struct mg_connection – 单个连接描述符
连接可以是侦听、出站或入站。出站连接由调用创建 mg_connect()。侦听连接由调用创建 mg_listen()。入站连接是由侦听连接接受的连接。每个连接都由一个结构描述 struct mg_connection ,该结构具有多个字段。所有字段都按设计向应用程序公开,以使应用程序能够完全了解 Mongoose 的内部结构。
考虑启动 HTTP 服务器的代码片段:
struct mg_mgr mgr; // Event manager
mg_mgr_init(&mgr); // Init manager
mg_http_listen(&mgr, "http://0.0.0.0:8000", fn, NULL); // Setup HTTP listener
mg_http_listen(&mgr, "https://0.0.0.0:8443", fn, NULL); // Setup HTTPS listener
for (;;) { // Infinite event loop
mg_mgr_poll(&mgr, 1000); // Process all connections
}
1234567
mg_mgr_poll()遍历所有连接,接受新连接,发送和接收数据,关闭连接,并调用相应事件的事件处理函数。
每个连接都有两个事件处理程序函数:c->fn 和 c->pfn。 c->fn 是用户指定的事件处理程序函数。c->pfn 是隐式设置的特定于协议的处理程序函数。例如, mg_http_listen()设置c->pfn为 Mongoose 的 HTTP 事件处理程序。在用户特定的处理程序之前调用特定于协议的处理程序。它解析传入的数据并可能调用特定于协议的事件,例如 MG_EV_HTTP_MSG。在上面的代码片段中,fn()每个事件(例如传入的 HTTP 请求)都会调用用户指定的函数。
注意:由于 Mongoose 的核心没有针对并发访问的保护,因此请确保所有mg_*API 函数都从同一个线程或 RTOS 任务调用。
发送和接收缓冲区
每个连接都有一个发送和接收缓冲区:
struct mg_connection::send – 要发送给对等方的数据struct mg_connection::recv – 从对等点接收到的数据
当数据到达时,Mongoose 将接收到的数据附加到recv并触发 MG_EV_READ事件。用户可以通过调用输出函数之一(如mg_send())mg_printf()或协议特定函数(如 ) 来发回数据mg_ws_send。输出函数将数据附加到send缓冲区。当 Mongoose 成功将数据写入套接字时,它会丢弃结构中的数据mg_connection::send 并发送MG_EV_WRITE事件。
事件处理函数
每个连接都有一个与之关联的事件处理程序函数,该函数必须由用户实现。事件处理程序是 Mongoose 的关键元素,因为它定义了连接的行为。请参阅下面的事件处理程序函数示例:
// Event handler function defines connection behavior
static void fn(struct mg_connection *c, int ev, void *ev_data) {
if (ev == MG_EV_READ) {
mg_send(c, c->recv.buf, c->recv.len); // Implement echo server
c->recv.len = 0; // Delete received data
}
}
1234567
struct mg_connection *c
– 接收此事件的连接int ev
– 事件编号,在 mongoose.h 中定义。例如,当数据到达入站连接时,ev 将 MG_EV_READvoid *ev_data
– 指向事件特定数据,对于不同的事件,它具有不同的含义。例如,对于某个MG_EV_READ事件, ev_data是long *指向从远程对等方接收并保存到 IO 缓冲区的字节数c->recv。 的确切含义ev_data针对每个事件进行了描述。协议特定事件通常具有ev_data 指向保存协议特定信息的结构的指针c->fn_data, void *
– 连接的用户定义指针,它是应用程序特定数据的占位符。此指针在或调用fn_data期间设置。监听连接将的值复制到新接受的连接,因此所有接受的连接最初共享相同的指针。可以随时通过设置更新/替换任何连接的该指针 *_listen()*_connect()c->fn_datafn_datac->fn_data = new_value
;
事件
以下是 Mongoose 触发的事件列表,原样取自 mongoose.h
。对于每个事件,注释描述了 ev_data
传递给事件处理程序的指针的含义:
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
};
1234567891011121314151617181920212223
连接标志
struct mg_connection
具有带有连接标志的位字段。标志是二进制的:它们可以是 0 或 1。某些标志由 Mongoose 设置,并且不得由应用程序代码更改。例如,该is_udp标志告诉应用程序该连接是否为 UDP。某些标志可以由应用程序更改,例如,is_draining如果该标志由应用程序设置,则告诉 Mongoose 将剩余数据发送给对等方,并在发送完所有内容后关闭连接。
注意:用户可更改的标志为:is_hexdumping,is_draining,is_closing。
这是按mongoose.h原样获取的:
struct mg_connection {
...
unsigned is_listening : 1; // Listening connection
unsigned is_client : 1; // Outbound (client) connection
unsigned is_accepted : 1; // Accepted (server) connection
unsigned is_resolving : 1; // Non-blocking DNS resolv is in progress
unsigned is_connecting : 1; // Non-blocking connect is in progress
unsigned is_tls : 1; // TLS-enabled connection
unsigned is_tls_hs : 1; // TLS handshake is in progress
unsigned is_udp : 1; // UDP connection
unsigned is_websocket : 1; // WebSocket connection
unsigned is_hexdumping : 1; // Hexdump in/out traffic
unsigned is_draining : 1; // Send remaining data, then close and free
unsigned is_closing : 1; // Close and free the connection immediately
unsigned is_full : 1; // Stop reads, until cleared
unsigned is_resp : 1; // Response is still being generated
unsigned is_readable : 1; // Connection is ready to read
unsigned is_writable : 1; // Connection is ready to write
};
12345678910111213141516171819
打开和关闭连接
为了打开监听(服务器)连接,请调用相应协议的相应函数。例如,要打开 HTTP 和 MQTT 服务器,
mg_http_listen(&mgr, "http://0.0.0.0:80", http_event_handler_fn, NULL);
mg_mqtt_listen(&mgr, "mqtt://0.0.0.0:1883", mqtt_event_handler_fn, NULL);
12
为了打开自定义协议的监听连接,请使用普通 TCP(或 UDP,如果您愿意)监听器:
mg_listen(&mgr, "tcp://0.0.0.0:1234", tcp_event_handler_fn, NULL);
1
为了打开客户端连接,请使用 mg_*connect() 相应协议的函数:
mg_connect()用于普通 TCP/UDP 或自定义协议mg_http_connect()对于 HTTPmg_ws_connect()对于 Websocketmg_mqtt_connect()对于 MQTTmg_sntp_connect()用于 SNTP
查看 SMTP 客户端、SSDP 客户端等的示例。
没有专门的函数来关闭连接。您应该使用连接标志告诉 Mongoose 关闭连接。在您的事件处理程序函数中,使用以下两个标志之一:
c->is_draining = 1; – 发送发送缓冲区中所有剩余的数据(“耗尽”连接),然后正确关闭连接。c->is_closing = 1; – 立即关闭,在下一个轮询周期中移除连接,不管是否有缓冲数据或是否干净的 TCP 关闭。
例如,这个简单的 TCP 回显服务器在回显第一条消息后立即关闭连接:
static void fn(struct mg_connection *c, int ev, void *ev_data) {
if (ev == MG_EV_READ) {
mg_send(c, c->recv.buf, c->recv.len); // Send received data back
c->recv.len = 0; // Clean receive buffer
c->is_draining = 1; // Close this connection when the response is sent
}
}
1234567
最佳实践
调试日志。要增加调试详细程度,请调用mg_log_set():
mg_log_set(MG_LL_DEBUG);
mg_mgr_init(&mgr);
12
默认情况下MG_INFO(), 日志MG_DEBUG()记录宏使用,即它们使用标准 C流。这在传统操作系统上可以正常工作。在嵌入式环境中,为了查看调试输出,可以使用两种方法:IO 重定向或 Mongoose 日志重定向。IO 重定向已经可以通过嵌入式 SDK 实现 – 例如 ESP32 SDK 重定向到 UART0。否则,可以手动实现 IO 重定向,有关更多详细信息,请参阅 指南 。另一种方法是重定向 Mongoose 日志:putchar()stdoutprintf()
void log_fn(char ch, void *param) {
output_a_single_character_to_UART(ch);
}
...
mg_log_set_fn(log_fn, param); // Use our custom log function
12345
如果您需要对连接执行任何类型的初始化,请通过捕获MG_EV_OPEN事件来完成。该事件在连接分配并添加到事件管理器后立即发送,但在执行其他任何操作之前:
static void fn(struct mg_connection *c, int ev, void *ev_data) {
if (ev == MG_EV_OPEN) {
... // Do your initialisation
}
1234
如果需要保留一些特定于连接的数据,则有两种选择:
使用c->fn_data指针。该指针作为最后一个参数传递给事件处理程序:
static void fn(struct mg_connection *c, int ev, void *ev_data) {
if (ev == MG_EV_OPEN) {
c->fn_data = malloc(123); // Change our fn_data
} else if (ev == MG_EV_CLOSE) {
free(fn_data); // Don't forget to free!
}
...
}
// Every accepted connection inherits a NULL pointer as c->fn_data,
// but then we change it in its connection event handler to something else
mg_http_listen(&mgr, "http://localhost:1234", fn, NULL);
123456789101112
使用c->data缓冲区,它可以保存一定数量的特定于连接的数据而无需额外的内存分配:
static void fn(struct mg_connection *c, int ev, void *ev_data) {
if (ev == MG_EV_WS_OPEN) {
c->data[0] = 'W'; // Established websocket connection, store something
...
1234
使用mg_http_reply()函数创建 HTTP 响应。该函数正确设置Content-Length标头,这很重要。当然,您可以手动创建响应,例如使用mg_printf()函数,但请务必设置Content-Length标头:
mg_printf(c, "HTTP/1.1 200 OK
Content-Length: %d
%s", 2, "hi");
1
或者,使用分块传输编码:
mg_printf(c, "HTTP/1.1 200 OK
Transfer-Encoding: chunked
");
mg_http_printf_chunk(c, "%s", "foo");
mg_http_printf_chunk(c, "%s", "bar");
mg_http_printf_chunk(c, ""); // Don't forget the last empty chunk
1234
注意:如果您没有使用 mg_http_reply()或mg_http_*_chunk(),请确保 c->is_resp = 0;在事件处理程序完成写入其响应时进行设置。
发送和接收缓冲区以及接受的连接数可以无限增长。如果您需要对它们进行限制,您可以在相应的事件处理程序中执行此操作:
static inline numconns(struct mg_mgr *mgr) {
int n = 0;
for (struct mg_connection *t = mgr->conns; t != NULL; t = t->next) n++;
return n;
}
static void fn(struct mg_connection *c, int ev, void *ev_data) {
if (ev == MG_EV_ACCEPT) {
if (numconns(c->mgr) > LIMIT) {
MG_ERROR(("Too many connections"));
c->is_closing = 1;
}
} else if (ev == MG_EV_READ) {
if (c->recv.len > LIMIT) {
MG_ERROR(("Msg too large"));
c->is_draining = 1;
}
}
...
}
1234567891011121314151617181920
if (c->send.len > LIMIT) {
MG_ERROR(("Stalled"));
} else {
// send
}
12345
Mongoose 在提供大文件时内部使用此技术来调整流量;除非您动态发送数据,否则不需要这样做。
在嵌入式环境中,确保服务任务具有足够的堆栈:为简单的 RESTful 服务提供 2k,或为复杂的动态/静态服务提供 4-8k。在某些环境中,也需要调整堆大小。默认情况下,IO 缓冲区分配大小MG_IO_SIZE为 2048:将其更改为 512 以减少运行时每个连接的内存消耗。
如果使用 TLS,则 MG_MAX_RECV_SIZE 限制必须容纳最大的记录(16424 字节),并且的值 MG_IO_SIZE 会影响接收大内容时的解密性能。
另一方面,在数据积累速度快于数据消耗速度的情况下,例如在 TLS 大文件传输中,您可能需要将 MG_IO_SIZE 其放大以加快速度。
架构图
Mongoose 库可以在现有的 TCP/IP 堆栈上工作 – 例如在 Windows、Mac、Linux、Zephyr RTOS、Azure RTOS、lwIP 等上。它还通过驱动程序实现了自己的 TCP/IP 堆栈 – 因此在嵌入式环境中,尤其是裸机环境中,它可以独立使用,不需要任何额外的软件来实现网络。
TLS 也是如此。Mongoose 可以使用 OpenSSL 或 mbedTLS 等第三方库,但它也实现了自己的 TLS 1.3 堆栈。因此,Mongoose 库可以成为提供整个网络功能的一站式解决方案,包括通过 TLS 进行安全通信。
构建选项
Mongoose 源代码包含两个文件:
mongoose.h – API 定义mongoose.c – 实现
因此,要将 Mongoose 集成到应用程序中,只需将这两个文件复制到应用程序的源代码树中。mongoose.c和mongoose.h文件实际上是合并的 – 非合并源代码可以在 https://github.com/cesanta/mongoose/tree/master/src找到
Mongoose 有 3 种影响构建的构建常量(预处理器定义):目标架构/操作系统、目标网络堆栈和可调参数。要在构建期间设置选项,请使用-D OPTION编译器标志:
$ cc app.c mongoose.c # Use defaults!
$ cc app.c mongoose.c -D MG_ENABLE_IPV6=1 # Build with IPv6 enabled
$ cc app.c mongoose.c -D MG_ARCH=MG_ARCH_RTX # Set architecture
$ cc app.c mongoose.c -D MG_ENABLE_SSI=0 -D MG_IO_SIZE=8192 # Multiple options
1234
所支持的体系结构列表在 arch.h头文件中定义。通常,无需明确指定体系结构。体系结构是在构建过程中猜测的,因此通常不需要设置它。
姓名 | 描述 |
---|---|
MG_ARCH_UNIX | 所有类UNIX系统,如Linux、MacOS、FreeBSD等 |
MG_ARCH_WIN32 | Windows系统 |
MG_ARCH_ESP32 | 乐鑫的ESP32 |
MG_ARCH_ESP8266 | Espressif的 ESP8266 |
MG_ARCH_FREERTOS | 所有带有FreeRTOS内核的系统(在ARM上,另请参阅MG_ARCH_CMSIS_RTOS2) |
MG_ARCH_AZURERTOS | Microsoft Azure RTOS |
MG_ARCH_CMSIS_RTOS1 | CMSIS-RTOS API v1(Keil RTX) |
MG_ARCH_CMSIS_RTOS2 | CMSIS-RTOS API v2(Keil RTX5、FreeRTOS) |
MG_ARCH_ZEPHYR | Zephyr实时操作系统 |
MG_ARCH_TIRTOS | 实时操作系统 |
MG_ARCH_RP2040 | RP2040 SDK |
MG_ARCH_NEWLIB | 裸ARM GCC |
MG_ARCH_ARMCC | Keil MDK使用ARM C编译器v6(armclang)或v5(C99模式下的armcc) |
MG_ARCH_CUSTOM | 定制架构,将在下一节讨论 |
网络堆栈常量如下所示。请注意,如果未指定网络堆栈,则假定目标架构支持标准 BSD 套接字 API。
姓名 | 默认 | 描述 |
---|---|---|
MG_ENABLE_LWIP | 0 | lwIP 网络堆栈 |
MG_ENABLE_FREERTOS_TCP | 0 | Amazon FreeRTOS-Plus-TCP 网络堆栈 |
MG_ENABLE_RL | 0 | Keil MDK 网络堆栈 |
MG_ENABLE_TCPIP | 0 | 内置 Mongoose 网络堆栈 |
另一类构建常量 及其默认值在src/config.h中定义。这些是可调参数,用于包含/排除特定功能或更改相关参数。
以下是构建常量及其默认值的列表:
名称 | 默认值 | 描述 |
---|---|---|
MG_ENABLE_SOCKET | 1 | 使用 BSD 套接字低级 API |
MG_TLS | MG_TLS_NONE | 启用 TLS 支持(已禁用) |
MG_ENABLE_IPV6 | 0 | 启用 IPv6 |
MG_ENABLE_MD5 | 0 | 使用本地 MD5 实现 |
MG_ENABLE_SSI | 1 | 通过 mg_http_serve_dir() 启用服务 SSI 文件 |
MG_ENABLE_CUSTOM_RANDOM | 0 | 提供自定义 RNG 函数 mg_random() |
MG_ENABLE_CUSTOM_TLS | 0 | 启用自定义 TLS 库 |
MG_ENABLE_CUSTOM_MILLIS | 0 | 启用自定义 mg_millis() 函数 |
MG_ENABLE_PACKED_FS | 0 | 启用嵌入式文件系统支持 |
MG_ENABLE_FATFS | 0 | 启用嵌入式 FAT 文件系统支持 |
MG_ENABLE_LINES | 未定义 | 如果定义,日志中显示源文件名 |
MG_IO_SIZE | 2048 | 发送/接收 IO 缓冲区增长的粒度 |
MG_MAX_RECV_SIZE | (310241024) | 最大接收缓冲区大小 |
MG_MAX_HTTP_HEADERS | 40 | HTTP 头的最大数量 |
MG_HTTP_INDEX | “index.html” | HTML 目录的索引文件 |
MG_FATFS_ROOT | “/” | FAT 文件系统根目录 |
注意:该MG_IO_SIZE常量还设置了最大 UDP 消息大小, 详情请参阅issues/907MG_IO_SIZE 。如果应用程序使用较大的 UDP 消息,请相应增加限制。
自定义构建
对于现有架构选项未涵盖的情况(例如,使用某些专有 RTOS 和网络堆栈的嵌入式架构),应使用定制构建。
为了在这样的系统上构建,请创建一个名为的文件mongoose_config.h,其中包含与您的平台相关的定义和包含,例如:
#include <stdbool.h>
#include <stdarg.h>
#define MG_ARCH MG_ARCH_CUSTOM
#define MG_ENABLE_POSIX_FS 0
#define MG_IO_SIZE 256
123456
使用JSON
Mongoose 库通常用于实现 RESTful 服务,这些服务使用 JSON 格式进行数据交换。因此 Mongoose 提供了解析 JSON 字符串和轻松创建 JSON 字符串的函数。
例如,以下事件处理函数处理对/api/sumURI 的 POST 请求。POST 正文应为包含两个数字的 JSON 数组,例如。以下是生成此类请求的[123.38, -2.72]示例命令:curl
curl localhost:8000/api/sum -d '[123.38, -2.72]'
1
处理程序返回这两个数字的总和。该mg_json_get_num() 函数用于从 JSON 字符串中提取值,并mg_http_reply() 打印回 JSON 字符串:
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;
if (mg_match(hm->uri, mg_str("/api/sum"), NULL)) {
double num1 = 0.0, num2 = 0.0;
mg_json_get_num(hm->body, "$[0]", &num1); // Extract first number
mg_json_get_num(hm->body, "$[1]", &num2); // Extract second number
mg_http_reply(c, 200, "Content-Type: application/json
",
"{%m:%g}
", MG_ESC("result"), num1 + num2);
} else {
...
}
}
12345678910111213
还有一组函数可以通过 RPC方法简化服务器端处理。
内置 TCP/IP堆栈
Mongoose 适用于任何提供 BSD 套接字 API 的系统。换句话说,它适用于任何兼容 BSD 的 TCP/IP 堆栈。这包括 UNIX、Mac、Windows 系统以及一些嵌入式 TCP/IP 堆栈,如 lwIP。但是,Mongoose 提供了自己的 TCP/IP 堆栈,可以通过将构建选项设置为 来激活MG_ENABLE_TCPIP。1可以通过设置编译器标志或通过 来完成mongoose_config.h:
通过编译器标志(例如:gcc):添加-D MG_ENABLE_TCPIP=1到构建标志
通过mongoose_config.h:添加以下行:
#define MG_ENABLE_TCPIP 1 // Enable built-in TCP/IP stack
1
Mongoose 的 TCP/IP 堆栈提供了 驱动程序 API ,可轻松创建驱动程序。有许多内置驱动程序可用,例如 STM32 F2/F4/F7、STM32 H5/H7、SAME54、TM4C、W5500。您可以在src/drivers/ 目录中查看它们的实现。每个驱动程序都由其各自的构建选项激活,例如 STM32H5 驱动程序需要MG_ENABLE_DRIVER_STM32H设置为1:
通过编译器标志:添加-DMG_ENABLE_DRIVER_STM32H=1到构建标志通过mongoose_config.h:添加以下行:
#define MG_ENABLE_DRIVER_STM32H 1 // Enable STM32H network driver
1
内置 TLS1.3堆栈
Mongoose 实现了内置的 TLS 1.3 堆栈。可以通过以下方式之一启用它:
通过编译器标志:添加-DMG_TLS=MG_TLS_BUILTIN到构建标志通过mongoose_config.h:添加以下行:
#define MG_TLS MG_TLS_BUILTIN // Enable built-in TLS 1.3 stack
1
内置 OTA 固件更新
除了网络功能(Web UI、远程控制等)之外,开发人员还需要开发无线固件更新。因此,Mongoose 提供了用于固件更新的内置 API。它很简单,可以在 src/ota.h文件中检查。
默认设备仪表板示例 实现了固件更新的示例 Web UI,可用作生产实施的框架。您可以在https://mongoose.ws/device-dashboard/在线查看 UI – 以 admin/admin 身份登录并单击“固件更新”侧栏链接。它看起来会像这样:
本质上,更新过程需要 3 个函数:ota_begin()、ota_write()和ota_end()。实现由构建选项驱动MG_OTA。默认情况下,它设置为#define MG_OTA MG_OTA_NONE,这将激活src/ota_dummy.c,这是一个不执行任何操作的存根实现。通过设置#define MG_OTA MG_OTA_CUSTOM ,mongoose_config.h您可以创建自己的一组mg_ota_*函数。
具有内置闪存的设备可以设置#define MG_OTA MG_OTA_FLASH,这需要mg_flash_*来自src/device.h的 API 。
这些函数实现了mg_flash_*API,包括mg_flash_load()和mg_flash_save()。这些是实用函数,用于在没有文件系统的情况下将任意数据持久地加载/保存在闪存的最后一个扇区中。请参阅 Nucleo-H563 裸机示例, 其中包含设备 Web UI 仪表板、固件更新和闪存上设备配置的持久存储。注意:上面的屏幕截图取自该示例。
有关使用此功能的更多信息,请关注固件更新教程
3 代码分析【7.5版本】
mgr 管理句柄结构
6.14 版本,不做分析
struct mg_mgr {
struct mg_connection *active_connections;
#if MG_ENABLE_HEXDUMP
const char *hexdump_file; /*调试己转储文件路径*/
#endif
#if MG_ENABLE_BROADCAST
sock_t ctl[2]; /*UDP-mg_broadcast()的套接字对*/
#endif
void *user_data; /*用户数据-mg_mgr_init函数中用户初始化数据?何用?*/
int num_ifaces; /*mg_ifaces数组元素个数*/
int num_calls; /*当前mongoose中待处理的连接数*/
struct mg_iface **ifaces; /*网络接口*/
const char *nameserver; /*要使用的DNS服务器*/
};
1234567891011121314
7.5 版本
struct mg_mgr {
struct mg_connection *conns; // List of active connections
struct mg_dns dns4; // DNS for IPv4
struct mg_dns dns6; // DNS for IPv6
int dnstimeout; // DNS resolve timeout in milliseconds
unsigned long nextid; // Next connection ID
void *userdata; // Arbitrary user data pointer
#if MG_ARCH == MG_ARCH_FREERTOS_TCP
SocketSet_t ss; // NOTE(lsm): referenced from socket struct
#endif
};
1234567891011
nextid 是一个无符号的 long 类型,用于记录创建的 socket 链接,默认从 1 开始。
mgr 管理句柄初始化
void mg_mgr_init(struct mg_mgr *mgr) {
memset(mgr, 0, sizeof(*mgr));
#if defined(_WIN32) && MG_ENABLE_WINSOCK
// clang-format off
{ WSADATA data; WSAStartup(MAKEWORD(2, 2), &data); }
// clang-format on
#elif MG_ARCH == MG_ARCH_FREERTOS_TCP
mgr->ss = FreeRTOS_CreateSocketSet();
#elif defined(__unix) || defined(__unix__) || defined(__APPLE__)
// Ignore SIGPIPE signal, so if client cancels the request, it
// won't kill the whole process.
signal(SIGPIPE, SIG_IGN);
#endif
mgr->dnstimeout = 3000;
mgr->dns4.url = "udp://8.8.8.8:53";
mgr->dns6.url = "udp://[2001:4860:4860::8888]:53";
}
1234567891011121314151617
mg_connection 结构
struct mg_connection {
struct mg_connection *next; // Linkage in struct mg_mgr :: connections
struct mg_mgr *mgr; // Our container
struct mg_addr peer; // Remote address. For listeners, local address
void *fd; // Connected socket, or LWIP data
unsigned long id; // Auto-incrementing unique connection ID
struct mg_iobuf recv; // Incoming data
struct mg_iobuf send; // Outgoing data
mg_event_handler_t fn; // User-specified event handler function
void *fn_data; // User-specified function parameter
mg_event_handler_t pfn; // Protocol-specific handler function
void *pfn_data; // Protocol-specific function parameter
char label[50]; // Arbitrary label
void *tls; // TLS specific data
unsigned is_listening : 1; // Listening connection
unsigned is_client : 1; // Outbound (client) connection
unsigned is_accepted : 1; // Accepted (server) connection
unsigned is_resolving : 1; // Non-blocking DNS resolution is in progress
unsigned is_connecting : 1; // Non-blocking connect is in progress
unsigned is_tls : 1; // TLS-enabled connection
unsigned is_tls_hs : 1; // TLS handshake is in progress
unsigned is_udp : 1; // UDP connection
unsigned is_websocket : 1; // WebSocket connection
unsigned is_hexdumping : 1; // Hexdump in/out traffic
unsigned is_draining : 1; // Send remaining data, then close and free
unsigned is_closing : 1; // Close and free the connection immediately
unsigned is_readable : 1; // Connection is ready to read
unsigned is_writable : 1; // Connection is ready to write
};
1234567891011121314151617181920212223242526272829
包含 next, mgr, peer, fd, id, recv, send, fn, pfn, 和以位域声明的几个成员变量(eg: is_listening, is_client, is_accepted, is_closing 等)。
两个重要回调函数: fn 和 pfn;
fn 回调函数:用户自定义的回调函数,所有的用户逻辑处理都将从这个回调函数开始;
pfn 回调函数:协议相关的回调函数,比如用于HTTP协议,则该pfn回调函数需要实现与HTTP协议相关的所有内部逻辑处理,比如HTTP请求头、行、实体。
创建 socket 监听
mongoose 提供了 mg_listen, mg_http_listen, mg_mqtt_listen 三个函数用于创建 socket 监听,其中 mg_listen 函数用于创建 TCP/UDP socket 监听,mg_http_listen 和 mg_mqtt_listen 分别用于创建 HTTP 和 MQTT 的 socket。参数 url 参数用于指定监听的地址和端口。
// TCP / UDP
struct mg_connection *mg_listen(struct mg_mgr *mgr, const char *url,
mg_event_handler_t fn, void *fn_data) {
struct mg_connection *c = NULL;
bool is_udp = strncmp(url, "udp:", 4) == 0;
struct mg_addr addr;
SOCKET fd = mg_open_listener(url, &addr);
if (fd == INVALID_SOCKET) {
LOG(LL_ERROR, ("Failed: %s, errno %d", url, MG_SOCK_ERRNO));
} else if ((c = alloc_conn(mgr, 0, fd)) == NULL) {
LOG(LL_ERROR, ("OOM %s", url));
closesocket(fd);
} else {
memcpy(&c->peer, &addr, sizeof(struct mg_addr));
c->fd = S2PTR(fd);
c->is_listening = 1;
c->is_udp = is_udp;
LIST_ADD_HEAD(struct mg_connection, &mgr->conns, c);
c->fn = fn;
c->fn_data = fn_data;
mg_call(c, MG_EV_OPEN, NULL);
LOG(LL_DEBUG,
("%lu accepting on %s (port %u)", c->id, url, mg_ntohs(c->peer.port)));
}
return c;
}
// HTTP
struct mg_connection *mg_http_listen(struct mg_mgr *mgr, const char *url,
mg_event_handler_t fn, void *fn_data) {
struct mg_connection *c = mg_listen(mgr, url, fn, fn_data);
if (c != NULL) c->pfn = http_cb;
return c;
}
// MQTT
struct mg_connection *mg_mqtt_listen(struct mg_mgr *mgr, const char *url,
mg_event_handler_t fn, void *fn_data) {
struct mg_connection *c = mg_listen(mgr, url, fn, fn_data);
if (c != NULL) c->pfn = mqtt_cb, c->pfn_data = mgr;
return c;
}
123456789101112131415161718192021222324252627282930313233343536373839404142
ip 和 port 解析函数 mg_open_listener;addr->port = mg_htons(mg_url_port(url));
初始化 sockaddr_in
union usa {
struct sockaddr sa;
struct sockaddr_in sin;
#if MG_ENABLE_IPV6
struct sockaddr_in6 sin6;
#endif
};
其中
struct sockaddr_in {
__kernel_sa_family_t sin_family; /* Address family */
__be16 sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
/* Pad to size of `struct sockaddr'. */
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)];
};
123456789101112131415161718
创建 socket 句柄 fd, 并 bind, 设置 socket 句柄为非堵塞
// listen 最多支持 128 个连接请求。
#ifndef MG_SOCK_LISTEN_BACKLOG_SIZE
#define MG_SOCK_LISTEN_BACKLOG_SIZE 128
#endif
// 非堵塞
mg_set_non_blocking_mode
// 创建 socket 句柄 fd, 并 bind
static SOCKET mg_open_listener(const char *url, struct mg_addr *addr) {
SOCKET fd = INVALID_SOCKET;
...
if ((fd = socket(af, type, proto)) != INVALID_SOCKET &&
#if (!defined(_WIN32) || !defined(SO_EXCLUSIVEADDRUSE)) &&
(!defined(LWIP_SOCKET) || (defined(LWIP_SOCKET) && SO_REUSE == 1))
!setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char *) &on, sizeof(on)) &&
#endif
#if defined(_WIN32) && defined(SO_EXCLUSIVEADDRUSE) && !defined(WINCE)
// "Using SO_REUSEADDR and SO_EXCLUSIVEADDRUSE"
//! setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (char *) &on, sizeof(on))
//! &&
!setsockopt(fd, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, (char *) &on,
sizeof(on)) &&
#endif
bind(fd, &usa.sa, slen) == 0 &&
// NOTE(lsm): FreeRTOS uses backlog value as a connection limit
(type == SOCK_DGRAM || listen(fd, MG_SOCK_LISTEN_BACKLOG_SIZE) == 0)) {
// In case port was set to 0, get the real port number
if (getsockname(fd, &usa.sa, &slen) == 0) {
addr->port = usa.sin.sin_port;
#if MG_ENABLE_IPV6
if (addr->is_ip6) addr->port = usa.sin6.sin6_port;
#endif
}
mg_set_non_blocking_mode(fd);
} else if (fd != INVALID_SOCKET) {
s_err = MG_SOCK_ERRNO;
closesocket(fd);
fd = INVALID_SOCKET;
}
}
if (fd == INVALID_SOCKET) {
if (s_err == 0) s_err = MG_SOCK_ERRNO;
LOG(LL_ERROR, ("Failed to listen on %s, errno %d", url, s_err));
}
return fd;
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
poll – Infinite event loop 轮询
void mg_mgr_poll(struct mg_mgr *mgr, int ms) {
struct mg_connection *c, *tmp;
unsigned long now;
mg_iotest(mgr, ms);
now = mg_millis();
mg_timer_poll(now);
for (c = mgr->conns; c != NULL; c = tmp) {
tmp = c->next;
mg_call(c, MG_EV_POLL, &now);
LOG(LL_VERBOSE_DEBUG,
("%lu %c%c %c%c%c%c%c", c->id, c->is_readable ? 'r' : '-',
c->is_writable ? 'w' : '-', c->is_tls ? 'T' : 't',
c->is_connecting ? 'C' : 'c', c->is_tls_hs ? 'H' : 'h',
c->is_resolving ? 'R' : 'r', c->is_closing ? 'C' : 'c'));
if (c->is_resolving || c->is_closing) {
// Do nothing
} else if (c->is_listening && c->is_udp == 0) {
if (c->is_readable) accept_conn(mgr, c);
} else if (c->is_connecting) {
if (c->is_readable || c->is_writable) connect_conn(c);
} else if (c->is_tls_hs) {
if ((c->is_readable || c->is_writable)) mg_tls_handshake(c);
} else {
if (c->is_readable) read_conn(c);
if (c->is_writable) write_conn(c);
while (c->is_tls && read_conn(c) > 0) (void) 0; // Read buffered TLS data
}
if (c->is_draining && c->send.len == 0) c->is_closing = 1;
if (c->is_closing) close_conn(c);
}
}
12345678910111213141516171819202122232425262728293031323334
mg_mgr_poll
:管理和处理连接的 I/O 操作,轮询连接状态,接受新连接,建立连接,处理 TLS 握手,读取和写入数据,关闭需要关闭的连接。
mg_iotest
:检查连接的 I/O 状态,具体实现取决于使用的系统架构(FreeRTOS 或 POSIX)。
mg_timer_poll
:处理定时器事件,确保定时器回调函数在定时器到期时被调用,并管理定时器的状态。
mg_connection 网络连接, 队列处理
// Linked list management macros
// 初始化时,第一个 mg_connection 加到 mgr 中
#define LIST_ADD_HEAD(type_, head_, elem_)
do {
(elem_)->next = (*head_);
*(head_) = (elem_);
} while (0)
// 初始化后,新的 mg_connection 加到 队尾
#define LIST_ADD_TAIL(type_, head_, elem_)
do {
type_ **h = head_;
while (*h != NULL) h = &(*h)->next;
*h = (elem_);
} while (0)
// 找到 mg_connection, 删除
#define LIST_DELETE(type_, head_, elem_)
do {
type_ **h = head_;
while (*h != (elem_)) h = &(*h)->next;
*h = (elem_)->next;
} while (0)
1234567891011121314151617181920212223
struct mg_connection *c = NULL;
LIST_ADD_HEAD(struct mg_connection, &mgr->conns, c);
12
并发实现 select
采用多路 I/O 复用,以达到同时监听、处理多个连接的目的。mongoose 使用了 select (类似还有 poll 和 epoll)。
选择 select 可能注意原因是,select 跨平台。
数据 buffer, MG_IO_SIZE 设置 mg_iobuf 块大小, 最大存储能力
根据需求自己修改宏,不过带来一个问题,个别情况才传输大文件,一般不传输大文件,块大小设置太大,会导致内存浪费,读写慢。
struct mg_iobuf {
unsigned char *buf; // Pointer to stored data
size_t size; // Total size available
size_t len; // Current number of bytes
};
12345
// MG_IO_SIZE 发送/接收 IO 缓冲区增长的粒度,默认 2K。
#define MG_IO_SIZE 2048
// MG_MAX_RECV_SIZE 最大接收缓冲区大小, 默认 3M。
#define MG_MAX_RECV_BUF_SIZE (3 * 1024 * 1024)
12345
如何平衡 大文件传输 与 读写效率 ?
封装成 C++ 类?
知识点补充
堵塞、非堵塞区别 以及 select、poll、epoll 对比
C语言练手项目–C 语言编写聊天室
https://blog.csdn.net/qq_38880380/article/details/84979553
堵塞、非堵塞区别
select、poll、epoll 对比
select | poll | epoll | |
---|---|---|---|
操作方式 | 遍历 | 遍历 | 回调 |
底层实现 | 数组 | 链表 | 红黑树 |
IO效率 | 每次调用都进行线性遍历,时间复杂度为O(n) | 每次调用都进行线性遍历,时间复杂度为O(n) | 事件通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到rdllist里面。时间复杂度O(1) |
最大连接数 | 1024(x86)或2048(x64) | 无上限 | 无上限 |
fd拷贝 | 每次调用select,都需要把fd集合从用户态拷贝到内核态 | 每次调用poll,都需要把fd集合从用户态拷贝到内核态 | 调用epoll_ctl时拷贝进内核并保存,之后每次epoll_wait不拷贝 |
epoll 是 Linux 下多路复用 IO 接口 select/poll 的增强版本。其实现和使用方式与 select/poll 有很多不同,epoll 通过一组函数来完成有关任务,而不是一个函数。
epoll 之所以高效,是因为 epoll 将用户关心的文件描述符放到内核里的一个事件表中,而不是像 select/poll 每次调用都需要重复传入文件描述符集或事件集。比如当一个事件发生(比如说读事件),epoll 无须遍历整个被侦听的描述符集,只要遍历那些被内核 IO 事件异步唤醒而加入就绪队列的描述符集合就行了。
与select相比,epoll分清了频繁调用和不频繁调用的操作
epoll 有两种工作方式,LT(level triggered):水平触发和 ET(edge-triggered):边沿触发。LT 是 select/poll 使用的触发方式,比较低效;而 ET 是 epoll 的高速工作方式(本项目使用 epoll 的 ET 方式)。
select 介绍
select() 是一个在 Unix 和类 Unix 操作系统中广泛使用的系统调用,它提供了一种机制,允许程序同时监视多个文件描述符(file descriptors),以确定它们是否处于可读、可写或异常状态。这个函数是处理 I/O 多路复用的基础,特别适用于需要同时处理多个网络连接或 I/O 资源的应用程序。
select 函数 【获取 fd 状态集合】
函数
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
1
参数
nfds: 这是要监视的文件描述符集合中最大文件描述符的值加1。例如,如果最大的文件描述符是1024,那么nfds应该设置为1025。readfds: 指向读文件描述符集合的指针
,select() 会检查这些文件描述符是否有数据可读。writefds: 指向写文件描述符集合的指针
,select() 会检查这些文件描述符是否可写(即写入时不会阻塞)。exceptfds: 指向异常条件文件描述符集合的指针,通常用于检查带外数据(out-of-band data)。timeout: 指向 struct timeval 结构的指针,指定 select() 等待的时间。如果设置为 NULL,则 select() 无限期等待。
timeout: 阻塞、非阻塞、等待 【fd 状态查询是否等待?】
timeout的取值决定了select的状态:
(1)timeout 传入 NULL,则select为阻塞状态,即需要等到监视文件描述符集合中某个文件描述符发生变化才会返回;
———-相当于无穷大的时间,一直等
(2)timeout置为0秒、0微秒,则select为非阻塞状态,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;
(3)timeout置为大于0的值,即等待的超时时间,select在timeout时间内阻塞,超时时间之内有事件到来就返回,否则在超时后不管怎样一定返回,返回值同上述。
———–select函数在使用的时候,我们一般都会设置timeout时间,比如为 1s,此时的select函数最多阻塞timeout时长的时间。
缺点【调用 select 一次,fd集合(0 ~ (nfds- 1))要全拷贝一次】
每次调用 select ,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大。同时每次调用 select 都需要在内核遍历传递进来的所有 fd ,这个开销在 fd 很多时也很大。【它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长
。】每次在 select() 函数返回后,都要通过遍历文件描述符来获取已经就绪的 socket 。select 支持的文件描述符数量太小了,默认是 1024 。
FD_ISSET 函数 判断描述符 fd 是否在给定的描述符集 fdset 中
FD_ISSET(int fd,fd_set *fdset) /*Is bit fd in fdset on? */
1
判断描述符 fd 是否在给定的描述符集 fdset 中,通常配合 select 函数使用,由于 select 函数成功返回时会将未准备好的描述符位清零。通常我们使用 FD_ISSET 是为了检查在select 函数返回后,某个描述符是否准备好,以便进行接下来的处理操作。
当描述符fd在描述符集fdset中返回非零值,否则,返回零。
fcntl 文件描述符设置非阻塞 【读写非堵塞】
默认的连接是阻塞方式的,可以使用fcntl函数进行设置非阻塞模式。
要将文件描述符设置为非阻塞:
// fd is my file descriptor
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
123
当文件处于非阻塞模式:
调用read()时,它将立即返回任何可用的字节。假设已从套接字另一端的服务器到达 100 个字节,并调用read(fd, buf, 150)。 Read 将立即返回值 100,这意味着它会读取您要求的 150 个字节中的 100 个。假设您尝试通过调用read(fd, buf+100, 50)来读取剩余数据,但最后 50 个字节仍未到达。 read()将返回-1 并将全局错误变量 errno 设置为 EAGAIN 或 EWOULDBLOCK。这是系统告诉你数据尚未准备好的方式。
调用write()时,假设您要使用套接字将 40,000 个字节发送到远程服务器。系统一次只能发送这么多字节。通用系统一次可以发送大约 23,000 个字节。在非阻塞模式下,write(fd, buf, 40000)将返回它能够立即发送的字节数,或大约 23,000。如果你再次调用write(),它将返回-1 并将 errno 设置为 EAGAIN 或 EWOULDBLOCK。这是系统告诉你它仍然忙于发送最后一块数据的方式,并且尚未准备好发送更多数据。
// unix
#include<unistd.h>
#include<fcntl.h>
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd ,struct flock* lock);
123456
cmd参数说明:
参数 | 含义 |
---|---|
F_GETFL | 获取文件状态标志 |
F_SETFL | 设置文件状态标志 |
F_GETFD | 获取文件描述符标志 |
F_SETFD | 设置文件描述符标志 |
F_GETLK | 获取文件锁 |
F_SETLK | 设置文件锁 |
F_DUPFD | F_DUPFD |
F_GETOWN | 取当前接受SIGIO和SIGURG信号的进程ID和进程组ID.正的arg指定一个进程ID,负的arg表示等于arg绝对值的一个进程中ID |
F_SETOWN | 设置当前接受SIGIO和SIGURG信号的进程ID和进程组ID |
状态标志:
标志 | 含义 |
---|---|
O_RDONLY | 只读打开 |
O_WRONLY | 只写打开 |
O_RDWR | 为读、写打开 |
O_APPEND | 每次写时追加 |
O_NONBLOCK | 非阻塞模式 |
O_SYNC | 等待写完成(数据和属性) |
O_DSYNC | 等待写完成(数据) |
O_RSYNC | 同步读、写 |
O_FSYNC | 等待写完成(进FreeBSD和Mac OS X) |
O_ASYNC | 异步I/O(进FreeBSD和Mac OS X) |
epoll 介绍
红黑树 & 多级时间轮
定时器的实现方案:红黑树和多级时间轮
阻塞I/O & 非阻塞I/O & I/O多路复用 & 信号驱动I/O & 异步I/O
详见连接: 高性能网络编程 – 解读5种I/O模型
(1) 阻塞式 I/O 模型(blocking I/O)
优点:程序简单,在阻塞等待数据期间进程/线程挂起,基本不会占用 CPU 资源。缺点:每个连接需要独立的进程/线程单独处理,当并发请求量大时为了维护程序,内存、线程切换开销较大,这种模型在实际生产中很少使用。
(2)非阻塞式 I/O 模型(non-blocking I/O)
优点:不会阻塞在内核的等待数据过程,每次发起的 I/O 请求可以立即返回,不用阻塞等待,实时性较好。缺点:轮询将会不断地询问内核,这将占用大量的 CPU 时间,系统资源利用率较低,所以一般 Web 服务器不使用这种 I/O 模型。
(3)I/O 多路复用模型(I/O multiplexing)
在 I/O 复用模型中,会用到 Select 或 Poll 函数或 Epoll 函数(Linux 2.6 以后的内核开始支持),这两个函数也会使进程阻塞,但是和阻塞 I/O 有所不同。
这两个函数可以同时阻塞多个 I/O 操作,而且可以同时对多个读操作,多个写操作的 I/O 函数进行检测,直到有数据可读或可写时,才真正调用 I/O 操作函数。
优点:可以基于一个阻塞对象,同时在多个描述符上等待就绪,而不是使用多个线程(每个文件描述符一个线程),这样可以大大节省系统资源。缺点:当连接数较少时效率相比多线程+阻塞 I/O 模型效率较低,可能延迟更大,因为单个连接处理需要 2 次系统调用,占用时间会有增加。
Nginx这样的高性能互联网反向代理服务器大获成功的关键就是得益于Epoll。
(4)信号驱动式 I/O 模型(signal-driven I/O)
在信号驱动式 I/O 模型中,应用程序使用套接口进行信号驱动 I/O,并安装一个信号处理函数,进程继续运行并不阻塞。
当数据准备好时,进程会收到一个 SIGIO 信号,可以在信号处理函数中调用 I/O 操作函数处理数据。
优点:线程并没有在等待数据时被阻塞,可以提高资源的利用率。缺点:信号 I/O 在大量 IO 操作时可能会因为信号队列溢出导致没法通知。
信号驱动 I/O 尽管对于处理 UDP 套接字来说有用,即这种信号通知意味着到达一个数据报,或者返回一个异步错误。
但是,对于 TCP 而言,信号驱动的 I/O 方式近乎无用,因为导致这种通知的条件为数众多,每一个来进行判别会消耗很大资源,与前几种方式相比优势尽失。
(5)异步 I/O 模型(即AIO,全称asynchronous I/O)
由 POSIX 规范定义,应用程序告知内核启动某个操作,并让内核在整个操作(包括将数据从内核拷贝到应用程序的缓冲区)完成后通知应用程序。
这种模型与信号驱动模型的主要区别在于:信号驱动 I/O 是由内核通知应用程序何时启动一个 I/O 操作,而异步 I/O 模型是由内核通知应用程序 I/O 操作何时完成。
优点:异步 I/O 能够充分利用 DMA 特性,让 I/O 操作与计算重叠。缺点:要实现真正的异步 I/O,操作系统需要做大量的工作。目前
Windows 下通过 IOCP 实现了真正的异步 I/O。
而在 Linux 系统下,Linux 2.6才引入,目前 AIO 并不完善,因此在 Linux 下实现高并发网络编程时都是以 IO 复用模型模式为主。
总结
参考
1、github–mongoose
2、wiki–mongoose
3、mongoose web服务器源码剖析
4、C语言练手项目–C 语言编写聊天室
5、深入理解Linux内核select多路复用原理
6、3. 深入了解select、poll、epoll之间的区别
7、高性能网络编程 – 解读5种I/O模型
8、I/O的五种模型和select与epoll工作原理
9、彻底理解 IO 多路复用实现机制
10、定时器的实现方案:红黑树和多级时间轮
11、BIO/NIO/多路复用/Selector/select/poll/epoll
12、从哈希表到红黑树:探讨 epoll 是如何管理事件的?
13、种一颗小小的行为树 – 开发笔记
14、目前最详细的红黑树原理分析(大量图片+过程推导!!!)
15、算法导论-第13章-红黑树
16、思维训练之红黑树
17、Linux网络编程之网络IO与select
18、I/O多路复用 —— fcntl()、select()的实现
19、select函数的阻塞和非阻塞态理解(实践总结)
20、网络,第 7 部分:非阻塞 I O,select()和 epoll