概述
MQTTS 即 MQTT over TLS:在明文 MQTT 之上增加 TLS 加密层(默认端口 8883),保证消息在传输中机密、完整、防篡改,并可通过数字证书(网络世界的「身份证」,证明服务器确实是它自己,不是冒牌的)验证服务器身份。本教程使用的官方示例还演示了更严格的双向认证(mTLS)——服务器验证设备(客户端证书+私钥),设备也验证服务器(CA 证书),常用于 AWS IoT 等要求设备级认证的云平台。证书文件随固件打包进 ROMFS 文件系统分区(固件里划出的一块只读存储区,证书等文件随固件一起烧进开发板),运行时加载。
用大白话讲:MQTTS 就是「加密的 MQTT」——像把要说的话先装进加密信封再寄出去:MQTT 管「说什么」(发消息),TLS 管「怎么安全地说」(加密),别人就算截获了信封也看不懂内容。它还要双向验明正身:设备出示自己的「身份证」(客户端证书)给服务器看,服务器也出示「身份证」(CA 证书)给设备验——谁都不能冒充谁,这就是 mTLS 双向认证。
本教程基于安信可官方 SDK(Ai-Thinker-Open/Ai-Thinker-WB2,版本
release_bl_iot_sdk_1.6.40)的官方示例applications/protocols/mqtt/ssl编写,代码可在本地 SDK 中直接找到(官方 README 为中文版,本节提炼其要点)。
打开终端,进入官方 mqtt/ssl 示例工程目录:
cd ~/Ai-Thinker-WB2/applications/protocols/mqtt/ssl
说明:
cd是「进入目录」的命令,进入官方示例工程;后面所有make命令都要在这个目录下执行。
工程结构:
ssl/
├── cert/ ← 证书目录:rootcert.pem(CA)、ccert.crt(客户端证书)、ckey.key(客户端私钥)
├── ssl/
│ ├── demo.c ← MQTT 客户端:证书加载 + mTLS 连接配置
│ ├── fs.c ← ROMFS 文件系统读写(vfs 封装)
│ ├── main.c ← Wi-Fi 联网框架(GOT_IP 后调用 mqtt_start())
└── proj_config.mk ← 已开启 CONFIG_SYS_USER_VFS_ROMFS_ENABLE=1
打开 ssl/main.c,修改 SSID/密码(官方默认 specter,需改成自己的路由器):
#define ROUTER_SSID "your ssid"
#define ROUTER_PWD "your password"
打开 ssl/demo.c,修改 MQTT 服务器地址与账号:
axk_mqtt_client_config_t mqtt_cfg = {
.uri = "mqtts://hostname.com:8883", // ★ 改成你自己的 MQTTS 服务器
...
.username = "123",
.password = "12345678",
.client_id = "11111111",
...
};
⚠️ 官方默认
hostname.com是占位符,必须替换为真实服务器。公共测试建议用 EMQX 公共 Broker:mqtts://broker.emqx.io:8883(单向认证,无需客户端证书);AWS IoT 等云平台则是 mTLS 双向认证(申请设备证书)。
本示例需要三份 PEM 格式(一种文本格式的证书文件,以 -----BEGIN CERTIFICATE----- 开头)文件(ssl/cert/ 目录下官方已放好占位证书):
| 文件 | 作用 | 用途 |
|---|---|---|
rootcert.pem |
CA 根证书(.cert_pem) |
设备验证服务器身份(必填),防止连上冒牌服务器 |
ccert.crt |
客户端证书(.client_cert_pem) |
服务器验证设备(mTLS 双向认证),证明「我就是这台设备」 |
ckey.key |
客户端私钥(.client_key_pem) |
与客户端证书配对(mTLS 双向认证),私钥要保密不能泄露 |
- 从你的云平台(AWS IoT:控制台 → 安全 → 策略/证书)下载/生成对应证书与私钥
- 单向认证(如 EMQX 公共 Broker)可只填
cert_pem,去掉客户端证书字段 - 证书文件在编译时被构建系统打包进固件的 ROMFS 分区,运行时从
/romfs/路径读取
💡 证书 vs 账号密码:TLS 证书验证的是"设备/服务器是谁"(身份),MQTT
.username/.password验证的是"允许做什么"(权限)——两者叠加才是完整的设备接入安全方案。
打开 ssl/demo.c,本步完整代码已移至文末,见:
📜 完整代码 — 位于本页「完整代码」章节,默认折叠,点击展开,与官方示例(
applications/protocols/mqtt/ssl/ssl/demo.c)完全一致。
与 MQTT 通信(tcp 版)的差异:
| 差异点 | 说明 |
|---|---|
.uri = "mqtts://hostname.com:8883" |
协议 mqtt:// → mqtts://,端口 1883 → 8883;服务器靠它知道要走加密连接,端口必须与服务器 TLS 端口一致 |
load_romfs_file() + aos_open/read |
从 ROMFS 读取证书(而非硬编码在代码里),以后换证书不用重新编译代码 |
.cert_pem + .cert_len=0 |
CA 证书;长度传 0 由库自动计算(PEM 以 \0 结尾),手算字节数极易出错 |
.client_cert_pem / .client_key_pem |
客户端证书 + 私钥(双向认证),缺了服务器不认这台设备 |
.username / .password / .client_id |
MQTT 账号密码与客户端 ID;证书验「是谁」,账号验「能做什么」 |
MQTT_EVENT_ERROR 更详细 |
打印 axk_tls_cert_verify_flags(证书校验标志)、connect_return_code;证书出错时靠它定位原因 |
💡 单向认证(最常见,如公共 Broker)只需
.uri+.cert_pem(CA),不需要客户端证书/私钥。双向认证(mTLS,如 AWS IoT)才需要三件套:CA + 客户端证书 + 私钥,服务器通过客户端证书识别设备。
在工程目录执行编译:
make -j8
说明:
make是「编译」命令,把代码变成开发板能运行的固件(烧进开发板的程序);-j8表示用 8 个 CPU 核并行编译,更快。
编译成功后生成固件 build_out/ssl.bin——证书已被构建系统打包进 ROMFS 区域,随固件一起烧录(proj_config.mk 已开启 CONFIG_ENABLE_VFS_ROMFS=1 与 CONFIG_SYS_USER_VFS_ROMFS_ENABLE=1)。
烧录(make flash 与普通工程相同,ROMFS 分区随固件一并写入):
make flash p=/dev/ttyUSB0 b=921600
说明:
make flash是「烧录」命令,把编译好的固件下载进开发板;p=后面是串口设备号(Linux 下常为/dev/ttyUSB0,Windows 下是COM3之类,以你电脑实际为准),b=921600是烧录波特率(串口传数据的速度),保持默认即可。
⏳ 烧录过程中按提示长按开发板 EN 键进入下载模式。若使用官方 BLDevCube 图形烧录工具,需额外导入
partition_cfg_*.toml分区表,并确认 ROMFS 分区已勾选烧录。
烧录完成后开发板自动重启运行,串口(波特率 921600,串口传数据的「语速」,两边必须一致)打印:
[APP] [EVT] GOT IP 5594
load_romfs_file success ca_len:1100, cli_len:800, key_len:900
[MQTT] axk_mqtt_client_init OK, starting client...
MQTT_EVENT_CONNECTED
sent subscribe successful, msg_id=1
sent publish successful, msg_id=2
MQTT_EVENT_SUBSCRIBED, msg_id=1
sent publish successful, msg_id=3
MQTT_EVENT_PUBLISHED, msg_id=2
MQTT_EVENT_PUBLISHED, msg_id=3
出现 MQTT_EVENT_CONNECTED 且无 MQTT_EVENT_ERROR(无证书校验失败),即 TLS 握手成功。如果一直停在 axk_mqtt_client_init OK 之后没有 CONNECTED,先确认开发板已联网(前面先看到 GOT IP)、电脑能连上同一个 MQTTS 服务器(8883 端口可达),再检查证书与账号配置,见文末 FAQ。
电脑端验证:MQTTX 新建连接时启用 TLS(端口 8883,开启证书校验并导入同一 CA 根证书),订阅 test/echo 主题,能收到开发板发布的 data_3、data 消息——且这些消息全程加密传输。
💡 抓包对比:明文 MQTT(1883)用 Wireshark 可直接看到消息内容;MQTTS(8883)只能看到 TLS 加密报文——这就是加密的意义。若只想验证加密链路,用
openssl s_client -connect host:8883亦可看到握手过程。
代码执行流程
例程从启动到运行的完整流程如下(图中的循环箭头表示反复执行):
本文 API 汇总
axk_mqtt_client_init(&config)
创建 MQTT 客户端(配置 uri/证书/事件回调),返回句柄。
参数:
config:axk_mqtt_client_config_t结构体指针,.uri(mqtts://host:8883)、证书字段与.event_handle必填
返回值:成功返回客户端句柄;失败返回 NULL
axk_mqtt_client_start(client)
启动客户端并异步建立 TLS 连接。
参数:
client:axk_mqtt_client_init返回的客户端句柄
返回值:成功返回 0;失败返回负值错误码
axk_mqtt_client_subscribe(client, topic, qos)
订阅主题。
参数:
client:客户端句柄topic:主题字符串(官方示例"test/echo")qos:QoS 等级(0/1/2)
返回值:成功返回消息 ID(msg_id);失败返回负值
axk_mqtt_client_publish(client, topic, data, len, qos, retain)
发布消息到主题。
参数:
client:客户端句柄topic:主题字符串data:消息数据指针len:消息长度qos:QoS 等级retain:是否保留消息
返回值:成功返回消息 ID(msg_id);失败返回负值
axk_mqtt_client_config_t
客户端配置结构体(TLS 新增字段)。
参数:
.uri:mqtts://host:8883(TLS 用mqtts://协议).cert_pem/.cert_len:CA 证书;长度传0由库按 PEM 文本自动计算.client_cert_pem:客户端证书(双向认证需要).client_key_pem:客户端私钥(双向认证需要).username/.password:MQTT 账号密码.client_id:客户端 ID
返回值:无(结构体,非函数)
aos_open(path, 0)
打开 ROMFS 文件(证书打包在 ROMFS 分区)。
参数:
path:文件路径,如"/romfs/rootcert.pem"flags:打开标志,只读传0
返回值:成功返回文件描述符;失败返回负值
aos_lseek(fd, off, SEEK_END/SET)
定位文件(先取文件大小再回到开头)。
参数:
fd:文件描述符off:偏移量whence:SEEK_END(文件末尾)/SEEK_SET(文件开头)
返回值:成功返回新偏移位置;失败返回负值
MQTT_EVENT_ERROR
MQTT_EVENT_ERROR 事件扩展字段(排错关键)。
参数:
error_type:MQTT_ERROR_TYPE_TCP_TRANSPORT(TCP 传输层)/CONNECTION_REFUSED(连接被拒)axk_tls_cert_verify_flags:证书校验失败标志connect_return_code:Broker 返回的连接码
返回值:无(事件字段)
📌 证书长度传 0:库按 PEM 文本(
\0结尾)自动计算长度,避免手算字节数出错(README 明确指出这是mbedtls_x509_crt_parse失败的常见原因)。证书缓冲区必须在客户端生命周期内保持有效(示例用malloc且不 free,注意堆开销 3×2048 字节)。
完整代码
以下为 ssl/demo.c 完整源码,与官方示例(applications/protocols/mqtt/ssl/ssl/demo.c)完全一致:
📜 点击展开 ssl/demo.c 完整代码
#include "blog.h"
#include <FreeRTOS.h>
#include <mqtt_client.h>
#include <stdio.h>
#include <task.h>
#include <vfs.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#define ROOTCERT_PATH "/romfs/rootcert.pem"
#define CLI_CERT_PATH "/romfs/ccert.crt"
#define CLI_KEY_PATH "/romfs/ckey.key"
#define PUB_TOPIC "test/echo"
#define SUB_TOPIC "test/echo"
static int load_romfs_file(int index, char *out_buf, size_t *out_len)
{
int fd = -1;
switch (index) {
case 0:
fd = aos_open(ROOTCERT_PATH, 0);
break;
case 1:
fd = aos_open(CLI_CERT_PATH, 0);
break;
case 2:
fd = aos_open(CLI_KEY_PATH, 0);
break;
default:
return -1;
}
if (fd < 0) {
blog_error("aos_open path[%d] failed", index);
return -1;
}
ssize_t size = aos_lseek(fd, 0, SEEK_END);
if (size < 0) {
blog_error("aos_lseek SEEK_END failed");
aos_close(fd);
return -1;
}
aos_lseek(fd, 0, SEEK_SET);
if (aos_read(fd, out_buf, size) != size) {
aos_close(fd);
return -1;
}
out_buf[size] = '\0';
aos_close(fd);
*out_len = size;
return 0;
}
static void log_error_if_nonzero(const char *message, int error_code)
{
if (error_code != 0)
{
blog_error("Last error %s: 0x%x", message, error_code);
}
}
static axk_err_t event_cb(axk_mqtt_event_handle_t event)
{
int32_t event_id;
axk_mqtt_client_handle_t client = event->client;
event_id = event->event_id;
blog_debug("Event dispatched, event_id=%d", event_id);
int msg_id;
switch ((axk_mqtt_event_id_t)event_id)
{
case MQTT_EVENT_CONNECTED:
blog_info("MQTT_EVENT_CONNECTED");
msg_id = axk_mqtt_client_subscribe(client, SUB_TOPIC, 0);
blog_info("sent subscribe successful, msg_id=%d", msg_id);
msg_id = axk_mqtt_client_publish(client, PUB_TOPIC, "data_3", 0, 1, 0);
blog_info("sent publish successful, msg_id=%d", msg_id);
break;
case MQTT_EVENT_DISCONNECTED:
blog_info("MQTT_EVENT_DISCONNECTED");
break;
case MQTT_EVENT_SUBSCRIBED:
blog_info("MQTT_EVENT_SUBSCRIBED, msg_id=%d", event->msg_id);
msg_id = axk_mqtt_client_publish(client, PUB_TOPIC, "data", 0, 0, 0);
blog_info("sent publish successful, msg_id=%d", msg_id);
break;
case MQTT_EVENT_UNSUBSCRIBED:
blog_info("MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_PUBLISHED:
blog_info("MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_DATA:
blog_info("MQTT_EVENT_DATA");
printf("TOPIC=%.*s\r\n", event->topic_len, event->topic);
printf("DATA=%.*s\r\n", event->data_len, event->data);
break;
case MQTT_EVENT_ERROR:
blog_info("MQTT_EVENT_ERROR");
if (event->error_handle) {
blog_info("error_type=%d, sock_errno=%d, tls_err=%d, tls_stack_err=%d, cert_verify=0x%x",
event->error_handle->error_type,
event->error_handle->axk_transport_sock_errno,
event->error_handle->axk_tls_last_axk_err,
event->error_handle->axk_tls_stack_err,
event->error_handle->axk_tls_cert_verify_flags);
if (event->error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) {
log_error_if_nonzero("reported from axk-tls", event->error_handle->axk_tls_last_axk_err);
log_error_if_nonzero("reported from tls stack", event->error_handle->axk_tls_stack_err);
log_error_if_nonzero("captured as transport's socket errno", event->error_handle->axk_transport_sock_errno);
blog_info("Last errno string (%s)", strerror(event->error_handle->axk_transport_sock_errno));
} else if (event->error_handle->error_type == MQTT_ERROR_TYPE_CONNECTION_REFUSED) {
blog_info("Connection refused, return_code=%d", event->error_handle->connect_return_code);
}
} else {
blog_info("error_handle is NULL");
}
break;
default:
blog_info("Other event id:%d", event->event_id);
break;
}
return AXK_OK;
}
void mqtt_start(void)
{
char *ca_buf = malloc( sizeof(char)*2048);
char *cli_buf = malloc( sizeof(char)*2048);
char *key_buf = malloc( sizeof(char)*2048);
memset(ca_buf, 0, sizeof(char)*2048);
memset(cli_buf, 0, sizeof(char)*2048);
memset(key_buf, 0, sizeof(char)*2048);
size_t ca_len = 0, cli_len = 0, key_len = 0;
if (load_romfs_file(0, ca_buf, &ca_len) != 0 ||
load_romfs_file(1, cli_buf, &cli_len) != 0 ||
load_romfs_file(2, key_buf, &key_len) != 0) {
blog_error("Failed to load certificates from MEDIA");
return;
}
blog_error("load_romfs_file success ca_len:%d, cli_len:%d, key_len:%d\r\n", ca_len, cli_len, key_len);
axk_mqtt_client_config_t mqtt_cfg = {
.uri = "mqtts://hostname.com:8883",
.cert_pem = ca_buf,
.cert_len = 0,
.client_cert_pem = cli_buf, // 双向认证:客户端证书
.client_cert_len = 0,
.client_key_pem = key_buf, // 双向认证:客户端私钥
.client_key_len = 0,
.username = "123",
.password = "12345678",
.client_id = "11111111",
.event_handle = event_cb,
};
axk_mqtt_client_handle_t client = axk_mqtt_client_init(&mqtt_cfg);
if (client == NULL) {
blog_error("[MQTT] axk_mqtt_client_init returned NULL! Check config/URI.");
return;
}
blog_info("[MQTT] axk_mqtt_client_init OK, starting client...");
axk_mqtt_client_start(client);
// 注意:ca_buf / cli_buf / key_buf 需要在 client 生命周期内保持有效
//(示例中未 free,实际可做 static 或在 deinit 时释放)
}常见问题与踩坑提示
⚠️ 串口打印 aos_open path[0] failed
原因:ROMFS 中找不到证书文件——CONFIG_SYS_USER_VFS_ROMFS_ENABLE=1 未开启,或证书未打包进固件/未烧录 ROMFS 分区
解决:检查 proj_config.mk 中 CONFIG_ENABLE_VFS_ROMFS:=1;确认 cert/ 目录三个文件存在并重新编译;用图形烧录工具时确认 ROMFS 分区一并烧录
⚠️ MQTT_EVENT_ERROR,cert_verify 非 0(证书校验失败)
原因:CA 证书与服务器不匹配、证书过期、主机名与证书 CN/SAN 不符、或单向/双向认证配置颠倒
解决:看 error_type 与 cert_verify 标志位;换用与服务器同一签发链的 CA 根证书;公共 Broker 用其官方公布的 CA;AWS IoT 用亚马逊根 CA(Amazon Root CA 1)
⚠️ mbedtls_x509_crt_parse 失败
原因:PEM 文件未以 \0 结尾、缓冲区过小(官方 2048 字节,证书更大时截断)、或长度字段未设 0
解决:把 .cert_len 等长度字段设为 0;证书较大时把 malloc(2048) 调大(如 4096)
⚠️ 服务器拒绝连接(Connection refused)
原因:服务器不支持双向认证(公共 Broker 一般只收客户端证书?不,是不收客户端证书)、账号密码错误、或端口非 8883
解决:连接公共 Broker(如 broker.emqx.io:8883)时去掉 .client_cert_pem/.client_key_pem 改为单向认证;核对 .username/.password/.client_id;确认服务器 8883 端口可达
⚠️ 与 tcp 版相同的其它问题
说明:GOT_IP 后再启动客户端、主题/QoS 不匹配等
解决:参照 MQTT 通信 的踩坑提示处理
⚠️ 一直打印 Connecting,始终没有 GOT IP(连不上路由器)
原因:SSID/密码填错、路由器是 5GHz、或信号太弱
解决:核对 ssl/main.c 里 ROUTER_SSID/ROUTER_PWD 与路由器完全一致;确认路由器是 2.4GHz(开发板不支持 5GHz);把开发板靠近路由器再试
⚠️ 串口找不到设备 / 打不开
原因:USB 转串口驱动未装、权限不足,或数据线只能充电不能传数据
解决:Linux 用 lsusb/dmesg 查看设备,权限不足可 sudo chmod 666 /dev/ttyUSB0;Windows 装驱动后到设备管理器查 COM 口;换一根能传数据的线
⚠️ 烧录一直等待 / 失败
原因:未进入下载模式、波特率不对、或串口号填错
解决:烧录时按提示长按 EN 键进入下载模式;确认 p=/dev/ttyUSB0 换成你实际的串口;用 BLDevCube 图形工具时确认 ROMFS 分区已勾选;换 USB 口或数据线重试
运行自检
串口出现 MQTT_EVENT_CONNECTED 且无 cert_verify 错误,MQTTX(开启 TLS)能收到 test/echo 消息,即 MQTTS 验证通过。
遇到问题?
如有其他问题,请到统一的提问与讨论区:Ai-Thinker Discussions

