物联网实战--入门篇之(七)嵌入式-MQTT
目录
一、MQTT简介
二、MQTT使用方法
三、MQTT驱动设计
四、代码解析
五、使用过程
六、总结
一、MQTT简介
MQTT因为其轻量、高效和稳定的特点,特别适合作为物联网系统的数据传输协议,已经成为物联网事实上的通信标准了。关于协议的具体内容看看这篇文章和官方文档MQTT协议详解(完整版)-CSDN博客,在这里我们主要讲解使用方法。
作为嵌入式设备,设备资源比较紧张,我们这里选用开源库paho mqtt,开源地址在这儿GitHub - eclipse/paho.mqtt.embedded-c: Paho MQTT C client library for embedded systems. Paho is an Eclipse IoT project (https://iot.eclipse.org/)
我们项目里已经都整理好了,直接用就行了,具体如下图所示,从映射文件可以看出,mqtt开源库大概占用2KB的 ROM,已经很轻量化了。这个开源库的核心作用就是可以帮我们根据协议要求组合要发送的数据,或者拆解接收到的数据,而应用层不用去太关心协议本身的内容。



二、MQTT使用方法
MQTT是以服务器为中心,客户端对为对象,话题为关系纽带的一种通讯协议,在这个体系里,净化器设备是客户端,用户手机也是客户端,手机订阅净化器发布的话题,服务器就会把净化器发布的消息推送给手机;同样的道理,手机根据设备订阅的话题来发布消息,就可以对净化器设备进行控制了。
下图是净化器项目的话题,其中11223344是设备的序列号,对于所有净化器的数据手机都能收的到,手机针对某个净化器的数据也只有某个净化器能接收,其它序列号的设备收不到。这里面的核心逻辑都是服务器根据话题来区分运行的。

三、MQTT驱动设计
MQTT的驱动应该算是比较难的,首先要确定它的地位和作用,如下图所示,drv_mqtt是作为设备端mqtt的核心,整合了底层的开源库、物理层的收发接口和应用层的参数配置功能,以及自身的连接、收发、订阅/取消订阅等功能。

下面进入代码进行解析,从头文件开始,MQTTPacket.h主要包含了mqtt开源库的功能文件,这个应该没什么问题,下面的ringbuffer.h需要强调下,它是RT-Thread的功能,叫环形缓冲区,就是数据按顺序环形保存,取出的时候按照先进先出的原则,MQTT开源库需要按顺序取出数据解析,有这个ringbuffer作为缓存媒介在操作上非常便捷,这也是使用RT-Thread的另一个重要原因了。

接下来是宏定义的内容,没什么特殊情况默认即可,有需要改变的在user_opt.h中重定义即可,具体的内容都有注释,就不赘述了。

订阅话题是个重要组成部分,在这里定义了话题的三个状态,空闲、订阅和取消订阅,取消订阅一般用不到,特殊情况下会有一些临时话题,为了缓解资源,可以取消订阅。结构体里的base_msg_id主要是为了标记 订阅/取消订阅 时返回的话题,这样程序才能区分。

最后是最重要的客户端连接信息了,具体都有注释,其中用户名、密码和客户端ID都是指针,在应用层定义这些信息需要用全局变量或者静态变量,才能保证信息的完整性;同样的,收发函数也是采用回调的方式,在应用程根据不同的物理接口进行注册,这里我们采用的自然是esp8266的收发函数了。



四、代码解析
先从初始化开始,主要就是对用户名、密码和客户端ID进行赋值。
/*
================================================================================
描述 : 初始化指定MQTT连接
输入 :
输出 :
================================================================================
*/
void drv_mqtt_init(u8 index, char *usr_name, char *passwd, char *client_id)
{if(index<MQTT_CONN_NUM){MqttClientStruct *pClient=&g_sMqttWork.client_list[index];MQTTPacket_connectData connect_init = MQTTPacket_connectData_initializer;if((pClient->rb=rt_ringbuffer_create(MQTT_RING_BUFF_SIZE))!=NULL ) {memcpy(&pClient->condata, &connect_init, sizeof(connect_init));//复制连接初始化信息pClient->condata.keepAliveInterval=MQTT_KEEP_TIME; pClient->condata.username.cstring=usr_name;//用户名pClient->condata.password.cstring=passwd;//密码pClient->condata.clientID.cstring=client_id;//客户ID pClient->is_enable=true;} }
}
接下来就是连接和订阅了,在这里就可以很清晰的看到mqtt开源库的作用了,就是组合连接、订阅和取消订阅的报文。MQTT里也有保活功能,这是协议层的,如果指定时间内没有没有收到数据,那么会自己发个ping请求包来保持连接。
/*
================================================================================
描述 : 连接和订阅
输入 :
输出 :
================================================================================
*/
void drv_mqtt_connect(void)
{static u32 last_sec_time=0;static u8 make_buff[80]={0};const int make_size=sizeof(make_buff);int make_len; u32 now_sec_time=drv_get_sec_counter();if(now_sec_time-last_sec_time>=2){static u8 conn_ptr=0;if(conn_ptr>=MQTT_CONN_NUM)conn_ptr=0;MqttClientStruct *pClient=&g_sMqttWork.client_list[conn_ptr];if(pClient->is_enable){if(pClient->is_connected==false){memset(make_buff, 0, make_size);make_len=MQTTSerialize_connect(make_buff, make_size, &pClient->condata);//组合连接请求包 if(pClient->mqtt_send!=NULL){
// printf("client=%d, mqtt send connect! make_len=%d\n",conn_ptr, make_len); pClient->mqtt_send(make_buff, make_len);//发送} }else{//订阅话题for(u8 i=0; i<MQTT_SUB_NUM; i++){SubPackStruct *pSub=&pClient->sub_list[i];if(strlen(pSub->sub_topic)>0 && pSub->curr_state!=pSub->dst_state){if(pSub->dst_state==TopicStateSub)//需要订阅{MQTTString topicString = MQTTString_initializer;int req_qos=1; topicString.cstring=pSub->sub_topic;memset(make_buff, 0, make_size);make_len = MQTTSerialize_subscribe(make_buff, make_size, 0, pSub->base_msg_id, 1, &topicString, &req_qos);//组合订阅报文if(pClient->mqtt_send!=NULL){printf("sub topic=%s\n", pSub->sub_topic);pClient->mqtt_send(make_buff, make_len);//发送} }else if(pSub->dst_state==TopicStateUnSub)//需要取消订阅{MQTTString topicString = MQTTString_initializer; topicString.cstring=pSub->sub_topic;memset(make_buff, 0, make_size);make_len = MQTTSerialize_unsubscribe(make_buff, make_size, 0, pSub->base_msg_id, 1, &topicString);//组合取消订阅报文if(pClient->mqtt_send!=NULL){printf("unsub topic=%s\n", pSub->sub_topic);pClient->mqtt_send(make_buff, make_len);//发送} }break;//每次只订阅一个,避免堵塞}}//超时检测u32 det_time=now_sec_time-pClient->keep_time;if(det_time>=MQTT_KEEP_TIME){printf("mqtt sock_id=%d timeout, close!\n", conn_ptr);drv_mqtt_close(pClient);//超时关闭 }else if(det_time>=MQTT_KEEP_TIME-10){//发送ping请求,保活memset(make_buff, 0, make_size);make_len=MQTTSerialize_pingreq(make_buff, make_size);//组合ping包 if(pClient->mqtt_send!=NULL){
// printf("sock=%d, mqtt send ping req! make_len=%d\n",conn_ptr,make_len); pClient->mqtt_send(make_buff, make_len);//发送} } }}conn_ptr++;last_sec_time=drv_get_sec_counter();}
}
接收部分的逻辑是MQTTPacket_read函数调用回调函数pClient->mqtt_recv获取环形缓冲区内的数据并按照协议解析,最后根据解析结果执行相应动作,消息类型如下图所示,常用的是连接回复、收到发布数据、订阅回复、取消订阅回复、ping回复和断开连接。

/*
================================================================================
描述 : 接收检查
输入 :
输出 :
================================================================================
*/
void drv_mqtt_recv_check(void)
{static u8 make_buff[MQTT_SUB_BUFF_SIZE];const int make_size=sizeof(make_buff);int rc;u8 dup;int qos;u8 retained;u16 msgid;int payloadlen_in;u8 *payload_in; MQTTString receivedTopic; for(u8 i=0; i<MQTT_CONN_NUM; i++){MqttClientStruct *pClient=&g_sMqttWork.client_list[i];if(pClient->is_enable==true)//启用{rc=MQTTPacket_read(make_buff, make_size, pClient->mqtt_recv);switch(rc){case CONNACK://连接回复{printf("mqtt_id=%d CONNACK!\n", i);u8 sessionPresent, connack_rc;if (MQTTDeserialize_connack(&sessionPresent, &connack_rc, make_buff, make_size) != 1 || connack_rc != 0)//解析收到的回复报文{drv_mqtt_close(pClient);printf("mqtt sock_id=%d Unable to connect, return code %d\n",i, connack_rc); }else{pClient->is_connected=true;pClient->keep_time=drv_get_sec_counter();//更新时间printf("mqtt sock_id=%d connect ok!\n", i);} break;} case PUBREC:case PUBACK: //发布回复{
// debug("sock_id=%d PUBACK!\n", i);break;} case PUBLISH://收到发布的消息{pClient->keep_time=drv_get_sec_counter();//更新时间printf("sock_id=%d PUBLISH!\n", i);rc = MQTTDeserialize_publish(&dup, &qos, &retained, &msgid, &receivedTopic, &payload_in, &payloadlen_in, make_buff, make_size); char *pTopic=receivedTopic.lenstring.data;if(g_sMqttWork.mqtt_recv_parse!=NULL){char topic[30]={0};int len=(char*)payload_in-pTopic;//topic 长度if(len>sizeof(topic)){len=sizeof(topic)-1;}memcpy(topic, pTopic, len);g_sMqttWork.mqtt_recv_parse(i, topic, payload_in, payloadlen_in);//应用层数据解析}break;} case SUBACK://订阅回复{
// debug("sock_id=%d SUBACK!\n", i);
// printf_hex("sub buff=", make_buff, 30);int count, requestedQoSs[1];MQTTDeserialize_suback(&msgid, 1, &count, requestedQoSs, make_buff, make_size);
// debug("$$$ msgid=0x%04X\n", msgid);for(u8 k=0; k<MQTT_SUB_NUM; k++){SubPackStruct *pSub=&pClient->sub_list[k];if(pSub->base_msg_id==msgid){printf("topic=%s sub ok!\n", pSub->sub_topic);pSub->curr_state=TopicStateSub;
// pSub->subed_time=drv_get_sec_counter();}}break;} case UNSUBACK://取消订阅回复{
// debug("sock_id=%d UNSUBACK!\n", i);MQTTDeserialize_unsuback(&msgid, make_buff, make_size);
// debug("$$$ msgid=0x%04X\n", msgid); for(u8 k=0; k<MQTT_SUB_NUM; k++){SubPackStruct *pSub=&pClient->sub_list[k];if(pSub->base_msg_id==msgid){printf("topic=%s unsub ok!\n", pSub->sub_topic);pSub->curr_state=TopicStateUnSub;
// pSub->subed_time=drv_get_sec_counter();}} break;}case PINGRESP://ping回复{pClient->keep_time=drv_get_sec_counter();//更新时间
// debug("sock_id=%d PINGRESP!\n", i);break;} case DISCONNECT://断开连接{printf("mqtt_id=%d DISCONNECT!\n", i);drv_mqtt_close(pClient); break;} }} }
}
剩下的就是一些简单的功能了,比如设置话题、发布消息,关闭连接等等,较为简单。
/*
================================================================================
描述 : 设置话题信息
输入 :
输出 :
================================================================================
*/
void drv_mqtt_set_topic_info(u8 client_id, u8 sub_id, char *topic, u32 base_msg_id, u8 dst_state)
{if(client_id<MQTT_CONN_NUM) { MqttClientStruct *pClient=&g_sMqttWork.client_list[client_id];if(sub_id<MQTT_SUB_NUM){SubPackStruct *pSub=&pClient->sub_list[sub_id];if(strlen(topic)<sizeof(pSub->sub_topic)){pSub->curr_state=TopicStateIdel; pSub->dst_state=dst_state;pSub->base_msg_id=base_msg_id;strcpy(pSub->sub_topic, topic); }}}
}/*
================================================================================
描述 : 设置话题订阅状态
输入 :
输出 :
================================================================================
*/
void drv_mqtt_set_topic_state(u8 client_id, u8 sub_id, u8 dst_state)
{if(client_id<MQTT_CONN_NUM) { MqttClientStruct *pClient=&g_sMqttWork.client_list[client_id];if(sub_id<MQTT_SUB_NUM){SubPackStruct *pSub=&pClient->sub_list[sub_id];pSub->dst_state=dst_state;}}
}
/*
================================================================================
描述 : MQTT发布数据
输入 :
输出 :
================================================================================
*/
void drv_mqtt_publish(u8 index, u8 *msg_buff, u16 msg_len, char *topic)
{static u8 make_buff[MQTT_PUB_BUFF_SIZE]={0};static const int make_size=sizeof(make_buff); u16 make_len=0; if(index<MQTT_CONN_NUM){MqttClientStruct *pClient=&g_sMqttWork.client_list[index];if(pClient->is_connected==true)//已经连接{ pClient->msg_id++;MQTTString topicString = MQTTString_initializer;topicString.cstring=topic; make_len = MQTTSerialize_publish(make_buff, make_size, 0, 1,0, pClient->msg_id, topicString, msg_buff, msg_len);//组合发布报文if(pClient->mqtt_send!=NULL && make_len>0){int ret=pClient->mqtt_send(make_buff, make_len);//发送} } }
}
/*
================================================================================
描述 : 关闭连接
输入 :
输出 :
================================================================================
*/
void drv_mqtt_close(MqttClientStruct *pClient)
{pClient->is_connected=false;for(u8 i=0; i<MQTT_SUB_NUM; i++){SubPackStruct *pSub=&pClient->sub_list[i];pSub->curr_state=TopicStateIdel;
// pSub->subed_time=0;}pClient->msg_id=0;pClient->keep_time=0;
}
五、使用过程
应用层的使用主要就是根据要求配置信息,首先物理通讯接口先设置,这里使用esp8266的连接3作为网络链路,同时注册接收函数把数据缓存进ringbuffer;然后就是MQTT用户名、密码、客户端ID的设置了;接下来有三个回调函数注册,两个是物理层的MQTT收发,还有一个是应用层的数据解析,这里已经来到了最后的净化器项目本身了,由此可以看出,要想代码好维护,写代码之前就要分层设计,这样出问题了才好分级排查,再后期自己阅读时逻辑也更走得通;最后一步就是话题订阅了,这样才能收到用户的控制数据,每个设备订阅话题都不一样,最后都带上了自己序列号,这样用户端才能针对性控制设备。



下面代码是净化器应用层的数据解析。
/*
================================================================================
描述 : 设备解析服务器下发的数据
输入 :
输出 :
================================================================================
*/
void app_air_recv_parse(u8 *buff, u16 len)
{u8 head[2]={0xAA, 0x55};u8 *pData=memstr(buff, len, head, 2);if(pData!=NULL){u16 total_len=pData[2]<<8 | pData[3];u16 crcValue=pData[total_len]<<8 | pData[total_len+1];if(crcValue==drv_crc16(pData, total_len)){pData+=4;u32 device_sn=pData[0]<<24|pData[1]<<16|pData[2]<<8|pData[3];pData+=4;if(device_sn!=g_sAirWork.device_sn)//识别码确认return;u8 cmd_type=pData[0];pData++;switch(cmd_type){case AIR_CMD_HEART://心跳包{break;}case AIR_CMD_DATA://数据包{break;}case AIR_CMD_SET_SPEED://设置风速{u8 speed=pData[0];pData+=1;app_motor_set_speed(speed);break;} case AIR_CMD_SET_SWITCH://设置开关{u8 state=pData[0];pData+=1;g_sAirWork.switch_state=state;if(state>0){app_motor_set_speed(100);//启动风扇}else{app_motor_set_speed(0);//停止风扇}app_air_send_status();break;}}}}
}
六、总结
MQTT协议本身较为繁琐,现在应用阶段暂时不用太深入,先学会使用就行,用熟了再去查阅文档,这样理解起来更透彻。mqtt的驱动设计相较于其他驱动文件更为复杂,因为它所牵涉的内容更广,有开源库、网络链路、应用层参数配置等等,完整的工程在第二篇文章里有的下载,自行查阅。
本项目的交流QQ群:701889554
写于2024-4-1
相关文章:
物联网实战--入门篇之(七)嵌入式-MQTT
目录 一、MQTT简介 二、MQTT使用方法 三、MQTT驱动设计 四、代码解析 五、使用过程 六、总结 一、MQTT简介 MQTT因为其轻量、高效和稳定的特点,特别适合作为物联网系统的数据传输协议,已经成为物联网事实上的通信标准了。关于协议的具体内容看看这…...
跑模型——labelme的json文件转成yolo使用的txt文件(语义分割)
前言 将labelme多边形标注的json文件转换成yolo使用的txt文件 import os import json import numpy as np from tqdm import tqdm#实现函数 def json2txt(path_json, path_txt): # 可修改生成格式with open(path_json, r) as path_json:jsonx json.load(path_json)with open…...
一个项目仿京东商场代码
git clone http://git.itcast.cn/heimaqianduan/erabbit-uni-app-vue3-ts.git...
计算机网络——WEB服务器编程实验
实验目的 1. 处理一个 http 请求 2. 接收并解析 http 请求 3. 从服务器文件系统中获得被请求的文件 4. 创建一个包括被请求的文件的 http 响应信息 5. 直接发送该信息到客户端 具体内容 一、C 程序来实现 web 服务器功能。 二、用 HTML 语言编写两个 HTML文件,并…...
蓝桥杯算法题:最大比例
题目描述: X星球的某个大奖赛设了 M 级奖励。 每个级别的奖金是一个正整数。 并且,相邻的两个级别间的比例是个固定值。 也就是说:所有级别的奖金数构成了一个等比数列。 比如:16,24,36,54,其等比值为:3/2。…...
【堡垒机】堡垒机的介绍
目前,常用的堡垒机有收费和开源两类。 收费的有行云管家、纽盾堡垒机; 开源的有jumpserver; 这几种各有各的优缺点,如何选择,大家可以根据实际场景来判断 什么是堡垒机 堡垒机,即在一个特定的网络环境下&…...
通过 ffmpeg命令行 调节视频播放速度
1. 仅调整视频速率 视频调速原理:修改视频的pts,dts # 可能会丢帧 ffmpeg -i input.mkv -an -filter:v "setpts0.5*PTS" output.mkv # 可用-r参数指定输出视频FPS以防止丢帧 ffmpeg -i input.mkv -an -r 60 -filter:v "setpts2.0*PTS&q…...
SQLite数据库在Linux系统上的使用
SQLite是一个轻量级的数据库解决方案,它是一个嵌入式的数据库管理系统。SQLite的特点是无需独立的服务器进程,可以直接嵌入到使用它的应用程序中。由于其配置简单、支持跨平台、服务器零管理,以及不需要复杂的设置和操作,SQLite非…...
Spring中依赖注入的方法有几种,分别是什么?
依赖注入的目的: 都是为了减少对象之间的紧密耦合 1. 构造函数注入:通过在类的构造函数中接受依赖对象作为参数,Spring在创建对象时将依赖注入。 2. Setter方法注入:在类中提供setter方法,Spring通过调用这些setter方法…...
【面试精讲】MyBatis设计模式及源码分析,MyBatis设计模式实现原理
【面试精讲】MyBatis设计模式及源码分析,MyBatis设计模式实现原理 目录 本文导读 一、MyBatis中运用的设计模式详解 1. 工厂模式(Factory Pattern) 2. 单例模式(Singleton Pattern) 3. 建造者模式(Bu…...
Acrel-1000DP光伏监控系统在尚雷仕(湖北)健康科技有限公司5.98MW分布式光伏10KV并网系统的应用
摘 要:分布式光伏发电特指在用户场地附近建设,运行方式多为自发自用,余电上网,部分项目采用全额上网模式。分布式光伏全额上网的优点是可以充分利用分布式光伏发电系统的发电量,提高分布式光伏发电系统的利用率。发展分…...
电脑远程控制esp32上的LED
1、思路整理 首先esp32需要连接上wifi 然后创建udp socket 接受udp数据 最后解析数据,控制LED 2、micropython代码实现 import network from socket import * from machine import Pin p2Pin(2,Pin.OUT)def do_connect(): #连接wifi wlan network.WLAN(network.…...
ARXML处理 - C#的解析代码(一)
目的 本文介绍通过AUTOSAR组织提供的xsd文件,自动生成对应的C#解析代码的框架。 自动生成方法:Microsoft SDKs\Windows\v7.0A\bin\xsd.exe 命令:xsd.exe AUTOSAR_4-0-3.xsd /c /l:CS /n:AUTOSAR4 AUTOSAR_4-0-3.xsd 是需要生成代码的xsd文…...
OJ 栓奶牛【C】【Python】【二分算法】
题目 算法思路 要求的距离在最近木桩与最远木桩相隔距离到零之间,所以是二分法 先取一个中间值,看按照这个中间值可以栓多少奶牛,再与输入奶牛数比较,如果大于等于,则增大距离,注意这里等于也是增大距离…...
Spring6-单元测试:JUnit
1. 概念 在进行单元测试时,特别是针对使用了Spring框架的应用程序,我们通常需要与Spring容器交互以获取被测试对象及其依赖。传统做法是在每个测试方法中手动创建Spring容器并从中获取所需的Bean。以下面的两行常见代码为例: ApplicationCo…...
ubuntu系统安装k8s1.28精简步骤
目录 一、规划二、环境准备2.1 配置apt仓库配置系统基本软件仓库配置k8s软件仓库安装常用软件包 2.2 修改静态ip、ntp时间同步、主机名、hosts文件、主机免密2.3 内核配置2.4 关闭防火墙、selinux、swap2.5 安装软件安装docker安装containerd安装k8s软件包 三、安装配置k8s3.1 …...
探讨Java和Go语言的缺点
文章目录 Java的缺点Go语言的缺点 通常我们都会讨论Java和GO的优点,如果讨论缺点往往能让人们更清楚优点的重要性,Java和Go的缺点或许往往就是对方优点所在 Java的缺点 冗长的代码:相较于一些现代编程语言,Java 的语法相对冗长&am…...
短剧在线搜索PHP网站源码
源码简介 短剧在线搜索PHP网站源码,自带本地数据库500数据,共有6000短剧视频,与短剧猫一样。 搭建环境 PHP 7.3 Mysql 5.6 安装教程 1.上传源码到网站目录中 2.修改【admin.php】中, $username ‘后台登录账号’; $passwor…...
Python map遍历
在Python中,map 函数是一个内置函数,它将指定的函数应用于给定序列(如列表、元组等)的每个项,并返回一个迭代器,该迭代器包含所有项经过指定函数处理后的结果。 ### map 函数的基本用法 map 函数的语法如…...
数据结构—红黑树
红黑树介绍 红黑树(Red Black Tree)是一种自平衡二叉查找树。由于其自平衡的特性,保证了最坏情形下在 O(logn) 时间复杂度内完成查找、增加、删除等操作,性能表现稳定。 在 JDK 中,TreeMap、TreeSet 以及 JDK1.8 的 …...
智慧工地云平台源码,基于微服务架构+Java+Spring Cloud +UniApp +MySql
智慧工地管理云平台系统,智慧工地全套源码,java版智慧工地源码,支持PC端、大屏端、移动端。 智慧工地聚焦建筑行业的市场需求,提供“平台网络终端”的整体解决方案,提供劳务管理、视频管理、智能监测、绿色施工、安全管…...
条件运算符
C中的三目运算符(也称条件运算符,英文:ternary operator)是一种简洁的条件选择语句,语法如下: 条件表达式 ? 表达式1 : 表达式2• 如果“条件表达式”为true,则整个表达式的结果为“表达式1”…...
MySQL中【正则表达式】用法
MySQL 中正则表达式通过 REGEXP 或 RLIKE 操作符实现(两者等价),用于在 WHERE 子句中进行复杂的字符串模式匹配。以下是核心用法和示例: 一、基础语法 SELECT column_name FROM table_name WHERE column_name REGEXP pattern; …...
使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台
🎯 使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台 📌 项目背景 随着大语言模型(LLM)的广泛应用,开发者常面临多个挑战: 各大模型(OpenAI、Claude、Gemini、Ollama)接口风格不统一;缺乏一个统一平台进行模型调用与测试;本地模型 Ollama 的集成与前…...
今日学习:Spring线程池|并发修改异常|链路丢失|登录续期|VIP过期策略|数值类缓存
文章目录 优雅版线程池ThreadPoolTaskExecutor和ThreadPoolTaskExecutor的装饰器并发修改异常并发修改异常简介实现机制设计原因及意义 使用线程池造成的链路丢失问题线程池导致的链路丢失问题发生原因 常见解决方法更好的解决方法设计精妙之处 登录续期登录续期常见实现方式特…...
使用SSE解决获取状态不一致问题
使用SSE解决获取状态不一致问题 1. 问题描述2. SSE介绍2.1 SSE 的工作原理2.2 SSE 的事件格式规范2.3 SSE与其他技术对比2.4 SSE 的优缺点 3. 实战代码 1. 问题描述 目前做的一个功能是上传多个文件,这个上传文件是整体功能的一部分,文件在上传的过程中…...
相关类相关的可视化图像总结
目录 一、散点图 二、气泡图 三、相关图 四、热力图 五、二维密度图 六、多模态二维密度图 七、雷达图 八、桑基图 九、总结 一、散点图 特点 通过点的位置展示两个连续变量之间的关系,可直观判断线性相关、非线性相关或无相关关系,点的分布密…...
【版本控制】GitHub Desktop 入门教程与开源协作全流程解析
目录 0 引言1 GitHub Desktop 入门教程1.1 安装与基础配置1.2 核心功能使用指南仓库管理日常开发流程分支管理 2 GitHub 开源协作流程详解2.1 Fork & Pull Request 模型2.2 完整协作流程步骤步骤 1: Fork(创建个人副本)步骤 2: Clone(克隆…...
aurora与pcie的数据高速传输
设备:zynq7100; 开发环境:window; vivado版本:2021.1; 引言 之前在前面两章已经介绍了aurora读写DDR,xdma读写ddr实验。这次我们做一个大工程,pc通过pcie传输给fpga,fpga再通过aur…...
[C++错误经验]case语句跳过变量初始化
标题:[C错误经验]case语句跳过变量初始化 水墨不写bug 文章目录 一、错误信息复现二、错误分析三、解决方法 一、错误信息复现 write.cc:80:14: error: jump to case label80 | case 2:| ^ write.cc:76:20: note: crosses initialization…...
