Flutter專案整合mqtt的過程記錄

103style發表於2019-11-01

轉載請以連結形式標明出處: 本文出自:103style的部落格

目錄

  • 遇到的相關報錯資訊
  • 環境
  • 整合過程
  • 證照驗證

遇到的相關報錯資訊

Unhandled Exception: FileSystemException: Cannot open file, path = '...' 
(OS Error: No such file or directory, errno = 2)

TlsException: Failure trusting builtin roots

SocketException: OS Error: Connection reset by peer, errno = 104
複製程式碼

環境

flutter doctor -v

>flutter doctor -v
[√] Flutter (Channel stable, v1.9.1+hotfix.5, on Microsoft Windows [Version 10.0.17134.1006], locale zh-CN)
    • Flutter version 1.9.1+hotfix.5 at D:\flutter
    • Framework revision 1aedbb1835 (2 weeks ago), 2019-10-17 08:37:27 -0700
    • Engine revision b863200c37
    • Dart version 2.5.0
[√] Android toolchain - develop for Android devices (Android SDK version 28.0.3)
    • Android SDK at D:\Android\sdk
    • Android NDK location not configured (optional; useful for native profiling support)
    • Platform android-29, build-tools 28.0.3
    • Java binary at: D:\Android\AndroidStudio\jre\bin\java
    • Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b03)
    • All Android licenses accepted.
[√] Android Studio (version 3.5)
    • Android Studio at D:\Android\AndroidStudio
    • Flutter plugin version 40.2.2
    • Dart plugin version 191.8593
    • Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b03)
複製程式碼

整合過程

首先來到 flutter package 這個 flutter 相關的庫網站,然後搜了下 mqtt,找到 mqtt_client 這個庫。 上面有提供以下沒有安全認證的使用示例。 原示例地址:pub.flutter-io.cn/packages/mq…

import 'dart:async';
import 'dart:io';
import 'package:mqtt_client/mqtt_client.dart';
///伺服器地址是 test.mosquitto.org , 埠預設是1883
///自定義埠可以呼叫 MqttClient.withPort(伺服器地址, 身份標識, 埠號);
final MqttClient client = MqttClient('test.mosquitto.org', '');

Future<int> main() async {
  client.logging(on: false);///是否開啟日誌
  client.keepAlivePeriod = 20;///設定超時時間
  client.onDisconnected = onDisconnected;//設定斷開連線的回撥
  client.onConnected = onConnected;//設定連線成功的回撥
  client.onSubscribed = onSubscribed;//訂閱的回撥
  client.pongCallback = pong;//ping的回撥
  try {
    await client.connect(); ///開始連線
  } on Exception catch (e) {
    print('EXAMPLE::client exception - $e');
    client.disconnect();
  }
  ///檢查連線結果
  if (client.connectionStatus.state == MqttConnectionState.connected) {
    print('EXAMPLE::Mosquitto client connected');
  } else {
    print('EXAMPLE::ERROR Mosquitto client connection failed - disconnecting, status is ${client.connectionStatus}');
    client.disconnect();
    exit(-1);
  }

  ///訂閱一個topic: 服務端定義的事件   當伺服器傳送了這個訊息,就會在 client.updates.listen 中收到
  const String topic = 'test/lol';
  client.subscribe(topic, MqttQos.atMostOnce);

  ///監聽伺服器發來的資訊
  client.updates.listen((List<MqttReceivedMessage<MqttMessage>> c) {
    final MqttPublishMessage recMess = c[0].payload;
    ///伺服器返回的資料資訊
    final String pt = MqttPublishPayload.bytesToStringAsString(recMess.payload.message);
    print( 'EXAMPLE::Change notification:: topic is <${c[0].topic}>, payload is <-- $pt -->');
  });
  ///設定public監聽,當我們呼叫 publishMessage 時,會告訴你是都發布成功
  client.published.listen((MqttPublishMessage message) {
    print('EXAMPLE::Published notification:: topic is ${message.variableHeader.topicName}, with Qos ${message.header.qos}');
  });

  ///傳送訊息給伺服器的示例
  const String pubTopic = 'Dart/Mqtt_client/testtopic';
  final MqttClientPayloadBuilder builder = MqttClientPayloadBuilder();
  builder.addString('Hello from mqtt_client');///這裡傳 請求資訊的json字串
  client.publishMessage(pubTopic, MqttQos.exactlyOnce, builder.payload);

  ///解除訂閱
  client.unsubscribe(topic);
  
  ///斷開連線
  client.disconnect();
  return 0;
}
void onSubscribed(String topic) {
  print('EXAMPLE::Subscription confirmed for topic $topic');
}
void onDisconnected() {
  print('EXAMPLE::OnDisconnected client callback - Client disconnection');
  if (client.connectionStatus.returnCode == MqttConnectReturnCode.solicited) {
    print('EXAMPLE::OnDisconnected callback is solicited, this is correct');
  }
  exit(-1);
}
void onConnected() {
  print('EXAMPLE::OnConnected client callback - Client connection was sucessful');
}
void pong() {
  print('EXAMPLE::Ping response client callback invoked');
}
複製程式碼

然後我就按這個示例跑了以下,提供的這個測試伺服器是可以連線成功的。


證照驗證

但是我這邊伺服器做了證照驗證,需要配置證照,然後就找到 mqtt_client 這個庫的github地址.

然後在 issue 107 中發現 作者有提供配置證照的示例。 示例地址: github.com/shamblett/m…

作者在 /example/pem 這個目錄下提供了一個證照的檔案, 然後通過 flutter 提供的 context.setTrustedCertificates(filepath) 設定證照。主要邏輯如下:

import 'dart:async';
import 'dart:io';
import 'package:path/path.dart' as path;
import 'package:mqtt_client/mqtt_client.dart';

Future<int> main() async {
  ... 
  client.secure = true;
  final String currDir =
      '${path.current}${path.separator}example${path.separator}';
  final SecurityContext context = SecurityContext.defaultContext;
  context.setTrustedCertificates(currDir + path.join('pem', 'roots.pem'));
  client.securityContext = context;
  client.setProtocolV311();

  await client.connect();
  ...
  return 0;
}
複製程式碼

然後跑起來就發現了第一個問題:

Unhandled Exception: FileSystemException: Cannot open file, path = '...' 
(OS Error: No such file or directory, errno = 2)
複製程式碼

然後我就在 issue 107 下問了這個庫的作者,issue 那裡可以看到我們的對話,庫的作者最後說時 flutter 的 不支援 //crt/crt/cilent.crt 這種路徑的訪問。

我也嘗試了 通過配置 assets 來訪問,但是也沒有相應獲取路徑的方法。

然後我就來到 flutter 的 github 地址那提了這個 issue:flutter/issues/43472,然而到目前 2019/11/01 16:30 為止,flutter 開發人員並沒有提供相關的解決方案。


然後,最後我就想,即然讀不了工程裡面的檔案,我就先寫到手機檔案系統中去,然後再獲取這個檔案的路徑。 參考官方的 檔案讀寫教程. 如下:

/// 獲取證照的本地路徑
Future<String> _getLocalFile(String filename,
    {bool deleteExist: false}) async {
  String dir = (await getApplicationDocumentsDirectory()).path;
  log('dir = $dir');
  File file = new File('$dir/$filename');
  bool exist = await file.exists();
  log('exist = $exist');
  if (deleteExist) {
    if (exist) {
      file.deleteSync();
    } else {
      exist = false;
    }
  }
  if (!exist) {
    log("MqttUtils: start write cert in local");
    await file.writeAsString(mqtt_cert);///mqtt_cert 為證照裡面對應的內容
  }
  return file.path;
}
複製程式碼

然後修改連線的程式碼為:

_client.secure = true;
final SecurityContext context = SecurityContext.defaultContext;
try {
  String crtPath = await _getLocalFile("cert", deleteExist: deleteExist);
  context.setTrustedCertificates(crtPath);
} on Exception catch (e) {
  //出現異常 嘗試刪除本地證照然後重新寫入證照
  log("setTrustedCertificates error : " + e.toString());
  log("setTrustedCertificates error and return -1");
  return -1;
}
_client.securityContext = context;
_client.setProtocolV311();
複製程式碼

然後發現一個問題,因為服務端做了雙向驗證,我這個檔案要寫哪些內容? 搜尋了一圈並沒發現什麼合適的資料。

然後就直接把 ca.crtca.keyclient.crtclient.keyclient.csrserver.crtserver.keyserver.csr 這八個檔案的內容全部copy進去了,大致如下:

String mqtt_cert  = '''
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
-----BEGIN ENCRYPTED PRIVATE KEY-----
...
-----END ENCRYPTED PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE REQUEST-----
...
-----END CERTIFICATE REQUEST-----
-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE REQUEST-----
...
-----END CERTIFICATE REQUEST-----
-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----
''';
複製程式碼

然後執行程式,發現連線成功,NICE!。 經過最後多次嘗試,發現只需複製 ca.crt 中的內容即可。

證照內容不對的話會報以下錯誤:

TlsException: Failure trusting builtin roots
複製程式碼

然後好好的用了幾天,昨天下午連線的時候突然又連線不上了!!! 報錯提示:

SocketException: OS Error: Connection reset by peer, errno = 104
複製程式碼

然後搜尋了一圈,又去問 mqtt_client 庫的作者... mqtt_client/issues/131. 然後他也沒遇到過。

最後通過 Wireshark 抓包發現報錯資訊 TLSv1.2 Handshake failure, 然後通過服務端哪些檢視報錯資訊,然後搜尋一圈發現可能是 docker 1.6.3版本 官方映象的問題,也可能是昨天下午服務端同事改了配置重啟之後導致的,感覺應該是後者....

抓包資訊


最後提供一個Demo: github.com/103style/mq…

以上

如果覺得不錯的話,請幫忙點個讚唄。


掃描下面的二維碼,關注我的公眾號 Android1024, 點關注,不迷路。

Android1024

相關文章