广州网站建设粤icp/武汉网站推广优化
【Mongoose笔记】MQTT 服务器
简介
Mongoose 笔记系列用于记录学习 Mongoose 的一些内容。
Mongoose 是一个 C/C++ 的网络库。它为 TCP、UDP、HTTP、WebSocket、MQTT 实现了事件驱动的、非阻塞的 API。
项目地址:
https://github.com/cesanta/mongoose
学习
下面通过学习 Mongoose 项目代码中的 mqtt-server 示例程序 ,来学习如何使用 Mongoose 实现一个简单的 MQTT 服务器。使用树莓派平台进行开发验证。
mqtt-server 的示例程序不长,代码如下:
// Copyright (c) 2020 Cesanta Software Limited
// All rights reserved
//
// Example MQTT server. Usage:
// 1. Start this server, type `make`
// 2. Install mosquitto MQTT client
// 3. In one terminal, run: mosquitto_sub -h localhost -t foo -t bar
// 4. In another, run: mosquitto_pub -h localhost -t foo -m hi#include "mongoose.h"static const char *s_listen_on = "mqtt://0.0.0.0:1883";// A list of subscription, held in memory
struct sub {struct sub *next;struct mg_connection *c;struct mg_str topic;uint8_t qos;
};
static struct sub *s_subs = NULL;// Handle interrupts, like Ctrl-C
static int s_signo;
static void signal_handler(int signo) {s_signo = signo;
}static size_t mg_mqtt_next_topic(struct mg_mqtt_message *msg,struct mg_str *topic, uint8_t *qos,size_t pos) {unsigned char *buf = (unsigned char *) msg->dgram.ptr + pos;size_t new_pos;if (pos >= msg->dgram.len) return 0;topic->len = (size_t) (((unsigned) buf[0]) << 8 | buf[1]);topic->ptr = (char *) buf + 2;new_pos = pos + 2 + topic->len + (qos == NULL ? 0 : 1);if ((size_t) new_pos > msg->dgram.len) return 0;if (qos != NULL) *qos = buf[2 + topic->len];return new_pos;
}size_t mg_mqtt_next_sub(struct mg_mqtt_message *msg, struct mg_str *topic,uint8_t *qos, size_t pos) {uint8_t tmp;return mg_mqtt_next_topic(msg, topic, qos == NULL ? &tmp : qos, pos);
}size_t mg_mqtt_next_unsub(struct mg_mqtt_message *msg, struct mg_str *topic,size_t pos) {return mg_mqtt_next_topic(msg, topic, NULL, pos);
}// Event handler function
static void fn(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {if (ev == MG_EV_MQTT_CMD) {struct mg_mqtt_message *mm = (struct mg_mqtt_message *) ev_data;MG_DEBUG(("cmd %d qos %d", mm->cmd, mm->qos));switch (mm->cmd) {case MQTT_CMD_CONNECT: {// Client connectsif (mm->dgram.len < 9) {mg_error(c, "Malformed MQTT frame");} else if (mm->dgram.ptr[8] != 4) {mg_error(c, "Unsupported MQTT version %d", mm->dgram.ptr[8]);} else {uint8_t response[] = {0, 0};mg_mqtt_send_header(c, MQTT_CMD_CONNACK, 0, sizeof(response));mg_send(c, response, sizeof(response));}break;}case MQTT_CMD_SUBSCRIBE: {// Client subscribessize_t pos = 4; // Initial topic offset, where ID endsuint8_t qos, resp[256];struct mg_str topic;int num_topics = 0;while ((pos = mg_mqtt_next_sub(mm, &topic, &qos, pos)) > 0) {struct sub *sub = calloc(1, sizeof(*sub));sub->c = c;sub->topic = mg_strdup(topic);sub->qos = qos;LIST_ADD_HEAD(struct sub, &s_subs, sub);MG_INFO(("SUB %p [%.*s]", c->fd, (int) sub->topic.len, sub->topic.ptr));// Change '+' to '*' for topic matching using mg_matchfor (size_t i = 0; i < sub->topic.len; i++) {if (sub->topic.ptr[i] == '+') ((char *) sub->topic.ptr)[i] = '*';}resp[num_topics++] = qos;}mg_mqtt_send_header(c, MQTT_CMD_SUBACK, 0, num_topics + 2);uint16_t id = mg_htons(mm->id);mg_send(c, &id, 2);mg_send(c, resp, num_topics);break;}case MQTT_CMD_PUBLISH: {// Client published message. Push to all subscribed channelsMG_INFO(("PUB %p [%.*s] -> [%.*s]", c->fd, (int) mm->data.len,mm->data.ptr, (int) mm->topic.len, mm->topic.ptr));for (struct sub *sub = s_subs; sub != NULL; sub = sub->next) {if (mg_match(mm->topic, sub->topic, NULL)) {mg_mqtt_pub(sub->c, mm->topic, mm->data, 1, false);}}break;}case MQTT_CMD_PINGREQ: {// The server must send a PINGRESP packet in response to a PINGREQ packet [MQTT-3.12.4-1]MG_INFO(("PINGREQ %p -> PINGRESP", c->fd));mg_mqtt_send_header(c, MQTT_CMD_PINGRESP, 0, 0);break;}}} else if (ev == MG_EV_ACCEPT) {// c->is_hexdumping = 1;} else if (ev == MG_EV_CLOSE) {// Client disconnects. Remove from the subscription listfor (struct sub *next, *sub = s_subs; sub != NULL; sub = next) {next = sub->next;if (c != sub->c) continue;MG_INFO(("UNSUB %p [%.*s]", c->fd, (int) sub->topic.len, sub->topic.ptr));LIST_DELETE(struct sub, &s_subs, sub);}}(void) fn_data;
}int main(void) {struct mg_mgr mgr; // Event managersignal(SIGINT, signal_handler); // Setup signal handlers - exist eventsignal(SIGTERM, signal_handler); // manager loop on SIGINT and SIGTERMmg_mgr_init(&mgr); // Initialise event managerMG_INFO(("Starting on %s", s_listen_on)); // Inform that we're startingmg_mqtt_listen(&mgr, s_listen_on, fn, NULL); // Create MQTT listenerwhile (s_signo == 0) mg_mgr_poll(&mgr, 1000); // Event loop, 1s timeoutmg_mgr_free(&mgr); // Cleanupreturn 0;
}
下面从main
函数开始分析代码。
首先是变量定义。struct mg_mgr
是用于保存所有活动连接的事件管理器。
struct mg_mgr mgr; // Event manager
设置 signal
函数捕获 SIGINT
信号和 SIGTERM
信号。
signal(SIGINT, signal_handler); // Setup signal handlers - exist eventsignal(SIGTERM, signal_handler); // manager loop on SIGINT and SIGTERM
下面是对应的信号处理函数,当 SIGINT
信号和 SIGTERM
信号到达时,修改 s_signo
的值,使其值不为 0,然后会让主事件循环退出。当用户通过 Ctrl-C
结束进程是会发送 SIGINT
信号,通过 kill
命令不带参数时会发送 SIGTERM
信号。当通过以上两种操作时,都能让主事件循环正常退出。
// Handle interrupts, like Ctrl-C
static int s_signo;
static void signal_handler(int signo) {s_signo = signo;
}
初始化一个事件管理器,也就是将最开始定义的struct mg_mgr
变量 mgr
中的数据进行初始化。
mg_mgr_init(&mgr); // Initialise event manager
打印出接下来要监听的本地IP地址和端口s_listen_on
。
MG_INFO(("Starting on %s", s_listen_on)); // Inform that we're starting
s_listen_on
是一个全局变量,默认值为mqtt://0.0.0.0:1883
。
static const char *s_listen_on = "mqtt://0.0.0.0:1883";
使用mg_mqtt_listen
创建一个 MQTT 监听器。s_listen_on
是指定要侦听的本地IP地址和端口,fn
是事件处理函数。
mg_mqtt_listen(&mgr, s_listen_on, fn, NULL); // Create MQTT listener
进行事件循环,mg_mgr_poll
遍历所有连接,接受新连接,发送和接收数据,关闭连接,并为各个事件调用事件处理函数。
while (s_signo == 0) mg_mgr_poll(&mgr, 1000); // Event loop, 1s timeout
当 s_signo
不为 0 时,也就是接收到了退出信号,则结束无限循环,调用 mg_mgr_free
关闭所有连接,释放所有资源。
mg_mgr_free(&mgr); // Cleanup
分析完main
函数后,我们看下事件处理函数fn
的代码。
判断是否接收到 MG_EV_MQTT_CMD
事件,表示收到 MQTT 命令。
if (ev == MG_EV_MQTT_CMD) {
将函数参数ev_data
转换为 struct mg_mqtt_message
,这个结构体用于表示 MQTT 消息。
struct mg_mqtt_message *mm = (struct mg_mqtt_message *) ev_data;
打印收到的命令cmd
和服务质量 qos
。
MG_DEBUG(("cmd %d qos %d", mm->cmd, mm->qos));
使用switch
判断收到的命令cmd
是什么。
switch (mm->cmd) {
如果收到的是MQTT_CMD_CONNECT
命令,表示 MQTT 客户端连接到服务器。MQTT 客户端到服务端的网络连接建立后,客户端发送给服务端的第一个报文必须是 CONNECT 报文。
case MQTT_CMD_CONNECT: {// Client connects
判断 MQTT 帧长度是否正确,如果长度小于 9,表示是 MQTT 帧格式不正确。
if (mm->dgram.len < 9) {mg_error(c, "Malformed MQTT frame");
判断 MQTT 帧头第 8 Byte的数据是否等于 4,这是一个协议级别字节(Protocol Level byte)。对于 3.1.1 版协议,协议级别字段的值是 4(0x04)。如果不等于 4,表示这是一个不支持的 MQTT 版本。
} else if (mm->dgram.ptr[8] != 4) {mg_error(c, "Unsupported MQTT version %d", mm->dgram.ptr[8]);}
如果 MQTT 的帧正常,则回复 MQTT 客户端。MQTT_CMD_CONNACK
确认连接请求,服务端发送 CONNACK 报文响应从客户端收到的 CONNECT 报文。服务端发送给客户端的第一个报文必须是 CONNACK。调用mg_mqtt_send_header
发送 MQTT 命令头,固定报头(Fixed header)部分,剩余长度字段为 2。调用mg_send
发送可变报头(Variable header)部分,共 2 个 Byte,分别为连接确认标志和连接返回码。连接返回码的值为 0x00 表示连接已接受 。
} else {uint8_t response[] = {0, 0};mg_mqtt_send_header(c, MQTT_CMD_CONNACK, 0, sizeof(response));mg_send(c, response, sizeof(response));}break;}
如果收到的是MQTT_CMD_SUBSCRIBE
命令,表示客户端订阅主题。MQTT 客户端向服务端发送 SUBSCRIBE 报文用于创建一个或多个订阅。
case MQTT_CMD_SUBSCRIBE: {// Client subscribes
首先定义了一些变量。pos
用于指向下一个主题过滤器(Topic Filter)在数据报文中的偏移,初始化为 4 是因为 SUBSCRIBE 报文的固定报头(Fixed header)和可变报头(Variable header)一共占 4 个字节,所以第一个主题过滤器在报文的偏移为 4。qos
和topic
用于从下面的函数mg_mqtt_next_sub
中获取服务质量(quality of service)和主题。resp
用于后续记录每个主题的服务质量,num_topics
用于后续记录主题数量。
size_t pos = 4; // Initial topic offset, where ID endsuint8_t qos, resp[256];struct mg_str topic;int num_topics = 0;
通过函数mg_mqtt_next_sub
遍历所有的主题。这个函数是在示例程序中实现的。
while ((pos = mg_mqtt_next_sub(mm, &topic, &qos, pos)) > 0) {
接下来为每个请求的主题创建一个struct sub
订阅描述符。
struct sub *sub = calloc(1, sizeof(*sub));sub->c = c;sub->topic = mg_strdup(topic);sub->qos = qos;
然后将创建的订阅描述符sub
添加到订阅列表s_subs
中。LIST_ADD_HEAD
是一个链表管理宏,用于将sub
加入到s_subs
中。
LIST_ADD_HEAD(struct sub, &s_subs, sub);
将所添加的主题打印出来。
MG_INFO(("SUB %p [%.*s]", c->fd, (int) sub->topic.len, sub->topic.ptr));
将主题中的+
改为*
,这是为了后续可以使用mg_match
进行主题匹配。
// Change '+' to '*' for topic matching using mg_matchfor (size_t i = 0; i < sub->topic.len; i++) {if (sub->topic.ptr[i] == '+') ((char *) sub->topic.ptr)[i] = '*';}
记录当前主题的服务质量(quality of service)。num_topics
记录了主题数量,resp
记录了每个主题的服务质量,用于下面回复消息。
resp[num_topics++] = qos;}
在遍历完了所有主题后,开始回复消息给客户端。服务端发送 SUBACK 报文给客户端,用于确认它已收到并且正在处理 SUBSCRIBE 报文。
使用mg_mqtt_send_header
发送 MQTT 命令头,也就是固定报头(Fixed header)部分,报文类型为 SUBACK。然后可变报头为 2 Byte的报文标识符,有效载荷(Payload)部分包含一个返回码(Return Code)列表,每个返回码对应等待确认的 SUBSCRIBE 报文中的一个主题过滤器(Topic Filter),所以命令头后续的数据长度为num_topics + 2
,报文标识符使用id
,mg_htons
用于将uint16_t
类型的值转换为网络字节序,返回码(Return Code)部分为resp
,长度为num_topics
。
mg_mqtt_send_header(c, MQTT_CMD_SUBACK, 0, num_topics + 2);uint16_t id = mg_htons(mm->id);mg_send(c, &id, 2);mg_send(c, resp, num_topics);break;}
接下来看下上面使用的mg_mqtt_next_sub
函数是如何实现的。
在函数mg_mqtt_next_sub
里面又调用了mg_mqtt_next_topic
函数。
size_t mg_mqtt_next_sub(struct mg_mqtt_message *msg, struct mg_str *topic,uint8_t *qos, size_t pos) {uint8_t tmp;return mg_mqtt_next_topic(msg, topic, qos == NULL ? &tmp : qos, pos);
}
接下来看下mg_mqtt_next_topic
函数是如何实现的。
buf
是指向下一个主题过滤器(Topic Filter)的位置,其中dgram.ptr
表示数据报文,pos
是指向下一个主题的偏移。如果pos
大于等于数据报文的长度,表示已没有下一个主题了,返回 0。主题过滤器部分,前两个字节表示主题名的长度,然后是主题名,主题名后的一个字节是服务质量要求(Requested QoS)。最后返回下一个题过滤器的偏移。
static size_t mg_mqtt_next_topic(struct mg_mqtt_message *msg,struct mg_str *topic, uint8_t *qos,size_t pos) {unsigned char *buf = (unsigned char *) msg->dgram.ptr + pos;size_t new_pos;if (pos >= msg->dgram.len) return 0;topic->len = (size_t) (((unsigned) buf[0]) << 8 | buf[1]);topic->ptr = (char *) buf + 2;new_pos = pos + 2 + topic->len + (qos == NULL ? 0 : 1);if ((size_t) new_pos > msg->dgram.len) return 0;if (qos != NULL) *qos = buf[2 + topic->len];return new_pos;
}
接下来回到事件处理函数中,来看下一个判断的 MQTT 命令。
如果收到的是MQTT_CMD_PUBLISH
命令,表示有客户端发布消息。PUBLISH 控制报文是指从 MQTT 客户端向服务端或者服务端向客户端传输一个应用消息。下面需要将消息推送到所有订阅频道。
case MQTT_CMD_PUBLISH: {// Client published message. Push to all subscribed channels
将发布的消息和主题打印出来。
MG_INFO(("PUB %p [%.*s] -> [%.*s]", c->fd, (int) mm->data.len,mm->data.ptr, (int) mm->topic.len, mm->topic.ptr));
遍历整个订阅列表s_subs
,通过mg_match
比较主题名称。如果主题匹配,则通过函数mg_mqtt_pub
发布消息,将消息发送到订阅主题的连接。
for (struct sub *sub = s_subs; sub != NULL; sub = sub->next) {if (mg_match(mm->topic, sub->topic, NULL)) {mg_mqtt_pub(sub->c, mm->topic, mm->data, 1, false);}}break;}
收到MQTT_CMD_PINGREQ
表示有客户端发送心跳请求。客户端发送 PINGREQ 报文给服务端的。用于: 1. 在没有任何其它控制报文从客户端发给服务的时,告知服务端客户端还活着。 2. 请求服务端发送 响应确认它还活着。 3. 使用网络以确认网络连接没有断开。
case MQTT_CMD_PINGREQ: {
服务端必须发送 PINGRESP 报文响应客户端的 PINGREQ 报文。使用mg_mqtt_send_header
发送 MQTT 命令头,也就是固定报头部分,报文类型为 PINGRESP。
// The server must send a PINGRESP packet in response to a PINGREQ packet [MQTT-3.12.4-1]MG_INFO(("PINGREQ %p -> PINGRESP", c->fd));mg_mqtt_send_header(c, MQTT_CMD_PINGRESP, 0, 0);break;}
到这里结束MG_EV_MQTT_CMD
事件处理的部分,接下来看其他的事件处理。
判断是否接收到 MG_EV_ACCEPT
事件,这表示已接受连接。
} else if (ev == MG_EV_ACCEPT) {// c->is_hexdumping = 1;}
判断是否接收到 MG_EV_CLOSE
事件,表示客户端连接已关闭。
当客户端断开连接时,遍历整个订阅列表s_subs
,将该客户端的所有订阅删除。其中LIST_DELETE
是一个链表管理宏,用于将sub
从s_subs
中删除。
} else if (ev == MG_EV_CLOSE) {// Client disconnects. Remove from the subscription listfor (struct sub *next, *sub = s_subs; sub != NULL; sub = next) {next = sub->next;if (c != sub->c) continue;MG_INFO(("UNSUB %p [%.*s]", c->fd, (int) sub->topic.len, sub->topic.ptr));LIST_DELETE(struct sub, &s_subs, sub);}}
mqtt-server 的示例程序代码就都解析完了,下面实际运行一下 mqtt-server 程序。
打开示例程序,编译并运行:
pi@raspberrypi:~ $ cd Desktop/study/mongoose/examples/mqtt-server/
pi@raspberrypi:~/Desktop/study/mongoose/examples/mqtt-server $ make
cc ../../mongoose.c -I../.. -W -Wall -DMG_ENABLE_LINES=1 -o example main.c
./example
10e0a1 2 main.c:131:main Starting on mqtt://0.0.0.0:1883
这个时候我们的 MQTT 服务器就运行起来了,这个时候还需要一个 MQTT 客户端,我们使用 Mongoose 的 mqtt-client 示例程序,并将代码中的 URL 变量s_url
修改:
static const char *s_url = "mqtt://localhost:1883";
保存后编译运行程序:
pi@raspberrypi:~/Desktop/study/mongoose/examples/mqtt-client $ make clean all
rm -rf example *.o *.dSYM *.gcov *.gcno *.gcda *.obj *.exe *.ilk *.pdb
cc ../../mongoose.c -I../.. -W -Wall -o example main.c
./example
12305b 2 main.c:29:fn CREATED
12305d 2 main.c:44:fn CONNECTED to mqtt://localhost:1883
12305d 2 main.c:46:fn SUBSCRIBED to mg/+/test
12305d 2 main.c:50:fn PUBLISHED hello -> mg/clnt/test
12305d 2 main.c:55:fn RECEIVED hello <- mg/clnt/test
12305e 2 main.c:58:fn CLOSED
可以看到 mqtt-client 示例程序完成了 MQTT 客户端创建,连接,订阅主题mg/+/test
,向主题mg/clnt/test
发布数据hello
,收到所订阅主题mg/clnt/test
的数据hello
,最后关闭连接。
然后我们来看下 MQTT 服务器这边的日志信息:
pi@raspberrypi:~/Desktop/study/mongoose/examples/mqtt-server $ make
cc ../../mongoose.c -I../.. -W -Wall -DMG_ENABLE_LINES=1 -o example main.c
./example
10e0a1 2 main.c:131:main Starting on mqtt://0.0.0.0:1883
12305d 2 main.c:87:fn SUB 0x5 [mg/+/test]
12305d 2 main.c:103:fn PUB 0x5 [hello] -> [mg/clnt/test]
12305e 2 main.c:119:fn UNSUB 0x5 [mg/*/test]
可以看到 MQTT 客户端订阅主题mg/+/test
,然后向所有订阅mg/clnt/test
主题的客户端发布数据hello
,最后断开连接的时候取消订阅。
【参考资料】
examples/mqtt-server
Documentation
examples/mqtt-client
MQTT协议中文版
MQTT Version 3.1.1
本文链接:https://blog.csdn.net/u012028275/article/details/129116209
相关文章:

【Mongoose笔记】MQTT 服务器
【Mongoose笔记】MQTT 服务器 简介 Mongoose 笔记系列用于记录学习 Mongoose 的一些内容。 Mongoose 是一个 C/C 的网络库。它为 TCP、UDP、HTTP、WebSocket、MQTT 实现了事件驱动的、非阻塞的 API。 项目地址: https://github.com/cesanta/mongoose学习 下面…...

数据结构概述
逻辑结构 顺序存储 随机访问是可以通过下标取到任意一个元素,即数组的起始位置下标 链式存储 链式存储是不连续的,比如A只保留了当前的指针,那么怎么访问到B和C呢 每个元素不仅存储自己的值还使用额外的空间存储指针指向下一个元素的地址&a…...

【前端】Vue3+Vant4项目:旅游App-项目总结与预览(已开源)
文章目录项目预览首页Home日历:日期选择开始搜索位置选择上搜索框热门精选-房屋详情1热门精选-房屋详情2其他页面项目笔记项目代码项目数据项目预览 启动项目: npm run dev在浏览器中F12: 首页Home 热门精选滑动到底部后会自动加载新数据&a…...

51单片机蜂鸣器的使用
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录前言一、有源蜂鸣器和无源蜂鸣器的区别二、代码编写总结前言 本文旨在介绍如何使用51单片机驱动蜂鸣器。 一、有源蜂鸣器和无源蜂鸣器的区别 有源蜂鸣器是一种电子…...

算法练习-链表(二)
算法练习-链表(二) 文章目录算法练习-链表(二)1. 奇偶链表1.1 题目1.2 题解2. K 个一组翻转链表2.1 题目2.2 题解3. 剑指 Offer 22. 链表中倒数第k个节点3.1 题目3.2 题解3.2.1 解法13.2.2 解法24. 删除链表的倒数第 N 个结点4.1 …...

LabVIEW使用实时跟踪查看器调试多核应用程序
LabVIEW使用实时跟踪查看器调试多核应用程序随着多核CPU的推出,开发人员现在可以在LabVIEW的帮助下充分利用这项新技术的功能。并行编程在为多核CPU开发应用程序时提出了新的挑战,例如同步多个线程对共享内存的并发访问以及处理器关联。LabVIEW可自动处理…...

【go语言grpc之client端源码分析二】
go语言grpc之server端源码分析二DialContextparseTargetAndFindResolvergetResolvernewCCResolverWrapperccResolverWrapper.UpdateStatecc.maybeApplyDefaultServiceConfigccBalancerWrapper.updateClientConnState上一篇文章分析了ClientConn的主要结构体成员,然后…...

centos7安装RabbitMQ
1、查看本机基本信息 查看Linux发行版本 uname -a # Linux VM-0-8-centos 3.10.0-1160.11.1.el7.x86_64 #1 SMP Fri Dec 18 16:34:56 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux cat /etc/redhat-release # CentOS Linux release 7.9.2009 (Core)2、创建创建工作目录 mkdir /…...

node基于springboot 口腔卫生防护口腔牙科诊所管理系统
目录 1 绪论 1 1.1课题背景 1 1.2课题研究现状 1 1.3初步设计方法与实施方案 2 1.4本文研究内容 2 2 系统开发环境 4 2.1 JAVA简介 4 2.2MyEclipse环境配置 4 2.3 B/S结构简介 4 2.4MySQL数据库 5 2.5 SPRINGBOOT框架 5 3 系统分析 6 3.1系统可行性分析 6 3.1.1经济可行性 6 3.…...

Linux常用命令之find命令详解
简介 find命令主要用于:用来在指定目录下查找文件。任何位于参数之前的字符串都将被视为欲查找的目录名。 如果使用该命令时,不设置任何参数,则find命令将在当前目录下查找子目录与文件。并且将查找到的子目录和文件全部进行显示。 是我们在…...

CMake 入门学习4 软件包管理
CMake 入门学习4 软件包管理一、Linux下的软件包管理1. 检索已安装的软件包2. 让自己编译软件支持pkg-config搜索3. 在CMakeLists查找已安装的软件包二、适合Windows下的包管理工具1. vcpkg2. Conan(1) 安装Conan(2) 配置Conan(3) 创建工程(4) 安装依赖库(5) 使用依赖库三、CMa…...

【数据库数据乱码错误】存进去的数据乱码(???)
目录 1.当我新增一条数据的时候,成功后查看数据库中的数据时,竟然变成???乱码格式了: 2.那么问题有3处需要注意: 第一:settings配置 第二:POM文件 第三:…...

rewrite中的if、break、last
目录 rewrite 作用: 依赖: 打开重定向日志: if 判断: location {} 本身有反复匹配执行特征 在 location 中加入 break 和 last (不一样) 加了break后,立刻停止向下 且 跳出。 加了last…...

JavaSE-线程池(5)- 建议使用的方式
JavaSE-线程池(5)- 建议使用的方式 虽然JDK Executors 工具类提供了默认的创建线程池的方法,但一般建议自定义线程池参数,下面是阿里巴巴开发手册给出的理由: 另外Spring也提供了线程池的实现,比如 Thread…...

城市轨道交通供电系统研究(Matlab代码实现)
👨🎓个人主页:研学社的博客💥💥💞💞欢迎来到本博客❤️❤️💥💥🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密…...

什么是 RESTful 风格?
一、什么是 REST ? REST即表述性状态传递(英文:Representational State Transfer,简称REST)是Roy Thomas Fielding博士在2000年他的博士论文中提出来的一种软件架构风格。它是一种针对网络应用的设计和开发方式&#…...

从业6年,对敏捷和自动化测试的一点心得
不久前,参加Thoughtworks组织的一场自动化测试的分享,同事由于出差国外不能参加,特意嘱托我提问两个问题: 在互联网这个将“敏捷”与“持续集成”进行积极实践的环境里,“敏捷测试”与“自动化测试”成了一个大家经常…...

ThreeJS 之界面控制
文章目录参考描述界面自适应问题resize 事件修改画布大小修改视锥体的宽高比全屏显示dblclick 事件检测全屏显示状态进入全屏显示状态退出全屏显示状态尾声参考 项目描述ThreeJS官方文档哔哩哔哩老陈打码搜索引擎BingMDN 文档document.mozFullScreenElementMDN 文档Element.re…...

【查找算法】解析学习四大常用的计算机查找算法 | C++
第二十二章 四大查找算法 目录 第二十二章 四大查找算法 ●前言 ●查找算法 ●一、顺序查找法 1.什么是顺序查找法? 2.案例实现 ●二、二分查找法 1.什么是二分查找法? 2.案例实现 ●三、插值查找法 1.什么是插值查找法? 2…...

Android实例仿真之一
目录 零 开局三问 第一问:为什么要有这一章? 第二问:Android算不算是一个嵌入式系统? 第三问:用什么方法来分析Android这个大系统? 一 讨论Android的流行 二 深入浅出Android 零 开局三问 在正式开始…...

软考高级-信息系统管理师之重要工具和技术的口语化表示(最新版)
重要工具和技术的口语化表示 本文主要介绍重要工具和技术的口语化解释 1、 模板、表格和标准:就是用之前的项目的模版、表格、标准,结合本项目进行了修改,在编制一些计划、方案的时候就可以采用这个工具和技术。可以拿来就用的,节约时间、提高质量的。 2、 产品分析:通过一…...

基于springboot+vue的个人健康信息服务平台
基于springbootvue的个人健康信息服务平台 ✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ 🍅文末获取项目下载方式🍅 一、项目背…...

SpringBoot2.x实战专题——SpringBoot2 多配置文件【开发环境、测试环境、生产环境】
SpringBoot2.x实战专题——SpringBoot2 多配置文件【开发环境、测试环境、生产环境】 目录SpringBoot2.x实战专题——SpringBoot2 多配置文件【开发环境、测试环境、生产环境】一、创建一个SpringBoot项目二、修改pom.xml中SpringBoot的版本三、配置文件3.1 application-dev.ym…...

测试2:编写测试用例的方法
2.编写测试用例的方法 7种 测试常用的方法:code review 代码静态分析、CI/CD CI–持续集成–开发成员经常集成它们的工作,尽快发现集成错误 CD–持续部署–将集成后的代码部署到更贴近真实运行的环境 2.1 测试用例的描述: 用例编号 用例…...

docker安装配置镜像加速器-拉取创建Mysql容器示例
List item docker 常见命令大全docker安装docker拉取创建Mysql容器docker 安装 1、安装链接:https://blog.csdn.net/BThinker/article/details/123358697 ; 2、安装完成需要配置docker镜像加速器 3、docker 镜像加速器推荐使用阿里云的: 编…...

WSL1和WSL2相互转换以及安装路径迁移相关问题
目录 1.从WSL 1如何切换到WSL 2? 2.从WSL 2如何切换回WSL 1? 3.WSL1转换为WSL2后,WSL1里面安装的程序和库需要重装吗? 4.WSL2转换为WSL1后,WSL2里面安装的程序和库需要重装吗? 5.如何备份WSL2…...

系统分析*
文章目录系统分析分析的任务结构化方法OO的方法的任务常用的详细调查方法有哪些?系统分析的建模TFD业务流程图DFDDD数据流图用例模型(重点用例图)用例图的内容:用例之间的关系:对象模型(类图)时…...

【redis】持久化:RDB和AOF
redis的持久化指将数据写入可靠内存中,如ssd。Redis提供了4种持久化策略 RDB:Redis Database,周期性的将某个时间点的数据集快照持久化AOF:Append Only File,每次redis服务接收到写操作(修改内存的操作),都…...

2023Python接口自动化测试实战教程,附视频实战讲解
这两天一直在找直接用python做接口自动化的方法,在网上也搜了一些博客参考,今天自己动手试了一下。 一、整体结构 上图是项目的目录结构,下面主要介绍下每个目录的作用。 Common:公共方法:主要放置公共的操作的类,比如数据库sql…...

【原创】java+swing+sqlserver药品管理系统设计与实现
之前数据库都是用的mysql,今天我们使用sqlserver在配合swing来开发一个药品管理系统。方便医院工作人员进行药品的管理,基础功能基本都是一些增删改查操作。 功能分析: 药品管理系统主要提供给管理员和员工使用,功能如下&#x…...