Flutter MQTT 通信文档(TLS/非 TLS 配置全解)
本文档记录 MQTT 通信层 的设计与关键实现。
提供 启用 TLS(推荐) 与 不启用 TLS(仅测试) 两种配置方式,便于开发者在不同环境下使用。
1. 设计目标
- 作为 远程通信通道,替代HTTP通过 MQTT 与云端服务器交互
- 支持 请求-响应 与 状态推送 两种模式
- 可选择 启用 TLS (传输层加密 + 证书校验)或 不开启 TLS(内网测试)
- 自动重连,保证长连接稳定性
2. 连接配置
2.1 启用 TLS(推荐)
dart
final client = MqttServerClient('your.mqtt.host', 'rv_app_client');
client.port = 8883; // 典型 TLS 端口
client.secure = true; // 启用 TLS
client.securityContext = yourContext; // 指定证书上下文
client.keepAlivePeriod = 60;
client.autoReconnect = true;
client.resubscribeOnAutoReconnect = true;
2.2 不启用 TLS(仅内网/测试)
dart
final client = MqttServerClient('192.168.1.100', 'rv_app_client');
client.port = 1883; // 明文端口
client.secure = false; // 不启用 TLS
client.keepAlivePeriod = 60;
client.autoReconnect = true;
⚠️ 警告 :明文模式下数据裸传,仅适合内网调试;公网环境必须使用 TLS。
3. 主题约定
rv/{deviceId}/{subsystem}/{action}
示例:
- 状态推送:
rv/device123/appliance/status
- 控制命令:
rv/device123/light/control
- 响应消息:
rv/device123/battery/response
4. 请求-响应机制
- 客户端下发请求时带
req_id
- 服务端响应时回填相同
req_id
- 客户端
pending
表完成匹配并支持超时清理
5. 状态推送
- 服务端会主动推送设备状态(
command_type=status_update
) - 客户端在回调中解析并分发到业务层(Provider/EventBus)
6. TLS 证书配置详解
6.1 准备证书
- 公有 CA:直接信任系统根 →
withTrustedRoots:true
- 自签名 CA:需要在客户端内置
ca.pem
并加载 →withTrustedRoots:false
- 双向认证:需额外准备
client.crt
与client.key
6.2 pubspec.yaml
yaml
flutter:
assets:
- assets/certs/ca.pem
# - assets/certs/client.crt
# - assets/certs/client.key
6.3 构建 SecurityContext
dart
import 'dart:io';
import 'package:flutter/services.dart' show rootBundle;
Future<SecurityContext> buildSecurityContext({
String caAsset = 'assets/certs/ca.pem',
String? clientCertAsset,
String? clientKeyAsset,
String? clientKeyPassword,
bool withTrustedRoots = false,
}) async {
final sc = SecurityContext(withTrustedRoots: withTrustedRoots);
final caBytes = await rootBundle.load(caAsset);
sc.setTrustedCertificatesBytes(caBytes.buffer.asUint8List());
if (clientCertAsset != null && clientKeyAsset != null) {
final certBytes = await rootBundle.load(clientCertAsset);
final keyBytes = await rootBundle.load(clientKeyAsset);
sc.useCertificateChainBytes(certBytes.buffer.asUint8List());
sc.usePrivateKeyBytes(keyBytes.buffer.asUint8List(), password: clientKeyPassword);
}
return sc;
}
6.4 连接时启用证书
dart
final sc = await buildSecurityContext(
caAsset: 'assets/certs/ca.pem',
withTrustedRoots: false, // 自签名 CA 必须 false;公有 CA 可 true
);
final client = MqttServerClient('your.mqtt.host', 'rv_app_client')
..port = 8883
..secure = true
..securityContext = sc;
6.5 开发模式跳过验证(⚠️仅调试)
dart
client.onBadCertificate = (X509Certificate cert) {
print('⚠️ 忽略证书校验: $cert');
return true;
};
7. 自动重连策略
autoReconnect = true
+resubscribeOnAutoReconnect = true
_onDisconnected
里增加指数退避 + 抖动的兜底重连
8. 调试与常见问题
- 证书错误 → 检查证书链完整,自签名需内置 CA
- Broken pipe → 调整 keepAlive(30--60s)
- 无限重连 → 确认没有与
autoReconnect
互相冲突的手动循环 - 消息收不到 → 确认订阅了正确主题(
rv/{deviceId}/#
)
9. 与 TCP 的关系
- MQTT:远程公网控制
- TCP:本地热点直连(低延迟)
- 由
ConnectionManager
自动切换
10. 完整代码示例
下面给出一个支持 TLS / 非 TLS 的完整 MqttService
实现,省略 AES/HMAC。
dart
// mqtt_service.dart
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'dart:io';
import 'package:flutter/services.dart' show rootBundle;
import 'package:mqtt_client/mqtt_client.dart';
import 'package:mqtt_client/mqtt_server_client.dart';
typedef OnNotification = void Function(Map<String, dynamic> msg);
typedef OnLog = void Function(String line);
typedef OnError = void Function(Object error, [StackTrace? st]);
Future<SecurityContext> buildSecurityContext({
String caAsset = 'assets/certs/ca.pem',
String? clientCertAsset,
String? clientKeyAsset,
String? clientKeyPassword,
bool withTrustedRoots = false,
}) async {
final sc = SecurityContext(withTrustedRoots: withTrustedRoots);
final caBytes = await rootBundle.load(caAsset);
sc.setTrustedCertificatesBytes(caBytes.buffer.asUint8List());
if (clientCertAsset != null && clientKeyAsset != null) {
final certBytes = await rootBundle.load(clientCertAsset);
final keyBytes = await rootBundle.load(clientKeyAsset);
sc.useCertificateChainBytes(certBytes.buffer.asUint8List());
sc.usePrivateKeyBytes(keyBytes.buffer.asUint8List(), password: clientKeyPassword);
}
return sc;
}
class MqttService {
final String host;
final int port;
final String clientId;
final String? username;
final String? password;
final SecurityContext? securityContext; // 可为 null,表示不启用 TLS
final Set<String> _subscriptions = {};
final Map<String, Completer<Map<String, dynamic>>> _pending = {};
final OnNotification? onNotification;
final OnLog? log;
final OnError? onError;
int keepAliveSeconds;
final Duration minBackoff;
final Duration maxBackoff;
late final MqttServerClient _client;
Timer? _manualReconnectTimer;
int _reconnectAttempt = 0;
final Random _rnd = Random();
bool get isConnected =>
_client.connectionStatus?.state == MqttConnectionState.connected;
MqttService({
required this.host,
this.port = 1883, // 默认非 TLS
required this.clientId,
this.username,
this.password,
this.keepAliveSeconds = 60,
this.onNotification,
this.log,
this.onError,
this.minBackoff = const Duration(seconds: 1),
this.maxBackoff = const Duration(seconds: 30),
this.securityContext, // 传 null = 不启用 TLS
}) {
_client = MqttServerClient(host, clientId)
..port = port
..keepAlivePeriod = keepAliveSeconds
..autoReconnect = true
..resubscribeOnAutoReconnect = true
..secure = securityContext != null
..securityContext = securityContext
..onConnected = _onConnected
..onDisconnected = _onDisconnected
..onAutoReconnect = _onAutoReconnect
..onAutoReconnected = _onAutoReconnected
..logging(on: false);
_client.connectionMessage = MqttConnectMessage()
.withClientIdentifier(clientId)
.startClean()
.keepAliveFor(keepAliveSeconds);
if (username != null || password != null) {
_client.connectionMessage =
_client.connectionMessage!.authenticateAs(username ?? '', password ?? '');
}
}
Future<void> connect() async {
if (isConnected) return;
try {
_info('🔌 [MQTT] connecting to $host:$port (clientId=$clientId)');
await _client.connect();
_client.updates!.listen(_onMessage, onError: (e, st) {
_error('❌ [MQTT] stream error: $e', e, st);
}, onDone: () {
_warn('⚠️ [MQTT] update stream done');
});
} catch (e, st) {
_error('🚫 [MQTT] connect failed: $e', e, st);
_scheduleManualReconnect();
rethrow;
}
}
Future<void> subscribe(String topic, {MqttQos qos = MqttQos.atLeastOnce}) async {
if (!_subscriptions.contains(topic)) {
_subscriptions.add(topic);
}
if (!isConnected) return;
_info('🧭 [MQTT] subscribe $topic, qos=$qos');
_client.subscribe(topic, qos);
}
Future<void> publishJson(
String topic,
Map<String, dynamic> payload, {
MqttQos qos = MqttQos.atLeastOnce,
bool retain = false,
}) async {
final jsonStr = jsonEncode(payload);
final builder = MqttClientPayloadBuilder();
builder.addString(jsonStr);
_client.publishMessage(topic, qos, builder.payload!, retain: retain);
_debug('📤 [MQTT] publish $topic: $jsonStr');
}
Future<Map<String, dynamic>> sendRequest({
required String reqTopic,
required String respTopic,
required Map<String, dynamic> payload,
Duration timeout = const Duration(seconds: 5),
MqttQos qos = MqttQos.atLeastOnce,
}) async {
await subscribe(respTopic, qos: qos);
final String reqId = (payload['req_id'] as String?) ?? _genReqId();
payload['req_id'] = reqId;
final completer = Completer<Map<String, dynamic>>();
_pending[reqId] = completer;
await publishJson(reqTopic, payload, qos: qos, retain: false);
try {
final rsp = await completer.future.timeout(timeout, onTimeout: () {
_pending.remove(reqId);
throw TimeoutException('MQTT request timeout: $reqId');
});
return rsp;
} catch (e) {
rethrow;
}
}
void _onMessage(List<MqttReceivedMessage<MqttMessage>> events) {
for (final e in events) {
final topic = e.topic;
final MqttPublishMessage msg = e.payload as MqttPublishMessage;
final payload =
MqttPublishPayload.bytesToStringAsString(msg.payload.message);
_debug('📥 [MQTT] recv $topic: $payload');
Map<String, dynamic> data;
try {
data = jsonDecode(payload) as Map<String, dynamic>;
} catch (e, st) {
_error('⚠️ [MQTT] json decode failed: $e', e, st);
continue;
}
final reqId = data['req_id'] as String?;
if (reqId != null && _pending.containsKey(reqId)) {
final c = _pending.remove(reqId)!;
if (!c.isCompleted) c.complete(data);
continue;
}
try {
onNotification?.call(data);
} catch (e, st) {
_error('⚠️ [MQTT] onNotification error: $e', e, st);
}
}
}
void _onConnected() {
_info('✅ [MQTT] connected');
_reconnectAttempt = 0;
_manualReconnectTimer?.cancel();
_manualReconnectTimer = null;
for (final t in _subscriptions) {
_client.subscribe(t, MqttQos.atLeastOnce);
}
}
void _onDisconnected() {
_warn('⚠️ [MQTT] disconnected');
_scheduleManualReconnect();
}
void _onAutoReconnect() {
_warn('⏳ [MQTT] auto reconnecting...');
}
void _onAutoReconnected() {
_info('🔁 [MQTT] auto reconnected');
}
void _scheduleManualReconnect() {
if (isConnected) return;
_manualReconnectTimer?.cancel();
final base = minBackoff.inMilliseconds * pow(2, _reconnectAttempt).toInt();
final capped = base.clamp(minBackoff.inMilliseconds, maxBackoff.inMilliseconds);
final jitter = (capped * (0.2 * (_rnd.nextDouble() * 2 - 1))).round();
final delay = Duration(milliseconds: max(0, capped + jitter));
_reconnectAttempt = min(_reconnectAttempt + 1, 10);
_warn('🕰️ [MQTT] schedule reconnect in ${delay.inMilliseconds} ms');
_manualReconnectTimer = Timer(delay, () async {
try {
await connect();
} catch (_) {
_scheduleManualReconnect();
}
});
}
Future<void> dispose() async {
_info('🧹 [MQTT] dispose');
_manualReconnectTimer?.cancel();
_manualReconnectTimer = null;
for (final c in _pending.values) {
if (!c.isCompleted) c.completeError(StateError('MQTT disposed'));
}
_pending.clear();
if (isConnected) {
await _client.disconnect();
} else {
_client.doAutoReconnect(force: false);
}
}
String _genReqId() {
final v = List<int>.generate(8, (_) => _rnd.nextInt(256));
return v.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
}
void _info(String s) => log?.call(s);
void _warn(String s) => log?.call(s);
void _debug(String s) => log?.call(s);
void _error(String msg, Object e, [StackTrace? st]) {
log?.call(msg);
onError?.call(e, st);
}
}
使用示例
启用 TLS(生产推荐)
dart
final sc = await buildSecurityContext(
caAsset: 'assets/certs/ca.pem',
withTrustedRoots: false,
);
final mqtt = MqttService(
host: 'your.mqtt.host',
port: 8883,
clientId: 'rv_app_client_001',
username: 'user',
password: 'pass',
securityContext: sc,
onNotification: (msg) => print('NOTIFY: $msg'),
log: print,
);
await mqtt.connect();
不启用 TLS(仅内网测试)
dart
final mqtt = MqttService(
host: '192.168.1.100',
port: 1883,
clientId: 'rv_app_client_001',
username: 'user',
password: 'pass',
securityContext: null, // ⭐ null 表示不开启 TLS
onNotification: (msg) => print('NOTIFY: $msg'),
log: print,
);
await mqtt.connect();
11. req_id
设计详解
req_id
用于将一次请求与其响应一一对应 ,也是跨日志、跨通道(MQTT/TCP)排障的核心线索。好的 req_id
设计需要兼顾唯一性、可读性、跨端一致性 与实现成本。
11.1 设计目标
- 全局唯一(至少在合理时间窗口内不碰撞)
- 易于检索(日志里一眼能搜到)
- 跨通道复用 (同一业务请求在 MQTT/TCP 间切换仍沿用同一
req_id
) - 无机密信息(不要把用户/密钥放进去)
- 长度适中(建议 < 64 字节,字符集友好)
11.2 推荐格式
xml
<origin>-<ts36>-<seq36>-<randHex>
origin
:来源/通道(如mqtt
/tcp
/app
),便于排查ts36
:DateTime.now().millisecondsSinceEpoch
的 36 进制(更短)seq36
:进程内自增序列的 36 进制(防同毫秒并发冲突)randHex
:若干字节的随机数(16 进制),增强唯一性
示例:
mqtt-l9h3v7-00k-7f9a21c45b
11.3 生成策略(Dart 实现)
移动端/Flutter 推荐使用
Random.secure()
(如可用)或Random()
退化。
dart
class ReqIdGenerator {
final String origin;
int _seq = 0;
final Random _rnd;
ReqIdGenerator(this.origin) : _rnd = _secureRandom();
static Random _secureRandom() {
try {
return Random.secure();
} catch (_) {
return Random(); // Web/少数平台回退
}
}
String next() {
final ts36 = DateTime.now().millisecondsSinceEpoch.toRadixString(36);
final seq36 = (_seq++ & 0x0FFF).toRadixString(36).padLeft(3, '0'); // 0..4095
final rand = List<int>.generate(5, (_) => _rnd.nextInt(256)) // 5B => 10 hex
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join();
return '$origin-$ts36-$seq36-$rand';
}
}
为什么足够稳:
- 毫秒时间戳 + 4096 级并发序列,能覆盖同毫秒并发请求
- 额外 5 字节随机数进一步降低碰撞概率
- 生成成本极低,且字符串对日志友好
11.4 在 MqttService
中使用
将原来的 _genReqId()
替换为注入式生成器:
dart
class MqttService {
// ...
final ReqIdGenerator _req = ReqIdGenerator('mqtt'); // or include deviceId suffix
// ...
Future<Map<String, dynamic>> sendRequest({ /* ... */ }) async {
await subscribe(respTopic, qos: qos);
final String reqId = (payload['req_id'] as String?) ?? _req.next(); // ✅
payload['req_id'] = reqId;
// 其余逻辑不变
// ...
}
}
如果项目同时有 TCP 通道,建议让上层(如
ConnectionManager
)统一生成req_id
,并传给 MQTT/TCP,以便跨通道故障排查。
11.5 生命周期与清理
- 客户端发出请求后放入
_pending[req_id] = Completer()
- 超时(如 5s) :
- 从
_pending
移除,返回超时错误,避免内存泄漏 - 不要复用超时的
req_id
(至少在 60s 内)
- 从
- 响应到达 :
- 命中
_pending
→complete()
,并移除条目
- 命中
11.6 幂等性与重试(服务端配合)
- 服务端应当以
req_id
为幂等键:- 重复的
req_id
视为同一次请求,直接返回同一响应 - 对于"可能重复发送"的客户端,避免副作用被执行多次
- 重复的
- 客户端重试策略:
- 保留同一
req_id
发起重试,或在明确失败 后用新req_id
重发(根据业务)
- 保留同一
11.7 日志与可观测性
- 客户端/服务端日志统一打印:
[MQTT] req_id=... action=...
- 链路跟踪:将
req_id
写入所有关键日志与告警 - 指标:统计超时率、重试次数、平均 RTT(按
req_id
聚合)
11.8 与安全的关系
req_id
不包含任何敏感信息(如用户、密钥、token)- 不依赖时间同步(仅作为组成部分)
- 若有安全审计要求,可在
req_id
加一个 来源前缀 (如app
,gw
,cloud
)帮助定位来源
12. 总结
- 启用 TLS:推荐生产环境;数据加密,证书校验;支持自签名和双向认证。
- 不启用 TLS:仅限内网/测试,数据裸传,不安全。
req_id
保证请求响应对应;自动重连增强稳定性。