关于flutter包mqtt_client中文显示乱码的思考与解决

发布时间 2023-12-07 19:00:36作者: 追光随笔

在使用flutter构建MQTT客户端时,使用的mqtt_client接收中文消息时会显示乱码。下面是对乱码的分析与解决。

分析

经过网络搜索后发现MQTT消息乱码与消息发送和接收的编码、解码不匹配有关,所以查看消息的发送与接收函数,函数如下。


MqttPublishPayload.bytesToStringAsString(recMess.payload.message)// 来自https://www.emqx.com/zh/blog/how-to-use-mqtt-in-dart的示例

// 发布消息

client.published!.listen((MqttPublishMessage message) {

  print('Published topic: topic is ${message.variableHeader!.topicName}, with Qos ${message.header!.qos}');

});

const pubTopic = 'test/topic';

final builder = MqttClientPayloadBuilder();

builder.addString('Hello from mqtt_client');  // 对消息进行编码

print('Subscribing to the $pubTopic topic');

client.subscribe(pubTopic, MqttQos.exactlyOnce);

print('Publishing our topic');

client.publishMessage(pubTopic, MqttQos.exactlyOnce, builder.payload!

// 接收消息

client.onSubscribed = onSubscribed;

const topic = 'topic/test';

print('Subscribing to the $topic topic');

client.subscribe(topic, MqttQos.atMostOnce);

client.updates!.listen((List<MqttReceivedMessage<MqttMessage?>>? c) {

  final recMess = c![0].payload as MqttPublishMessage;

  final pt = MqttPublishPayload.bytesToStringAsString(recMess.payload.message); // 对消息进行解码

  print('Received message: topic is ${c[0].topic}, payload is $pt');

});

/// The subscribed callback

void onSubscribed(String topic) {

  print('Subscription confirmed for topic $topic');

}

在发布消息时使用到了builder.addString('Hello from mqtt_client');

接收消息时使用MqttPublishPayload.bytesToStringAsString(recMess.payload.message)对消息进行解码

进一步查看 addString() 函数与 byteToStringAsString() 函数


// 编码函数位于mqtt_client_payload_builder.dart内

  /// Add a standard Dart string

  MqttClientPayloadBuilder addString(String val) {

    addUTF16String(val);  // 采用UTF16进行编码

    return this;

  }

  /// Add a UTF16 string, note Dart natively encodes strings as UTF16

  MqttClientPayloadBuilder addUTF16String(String val) {

    for (final codeUnit in val.codeUnits) {

      if (codeUnit <= 255 && codeUnit >= 0) {

        _payload!.add(codeUnit);

      } else {

        addHalf(codeUnit);

      }

    }

    return this;

  }


// 解码函数位于mqtt_client_mqtt_publish_payload.dart 内

  /// Converts an array of bytes to a character string.

  static String bytesToStringAsString(typed.Uint8Buffer message) {	// 接收时采用Uint8

    final sb = StringBuffer();

    message.forEach(sb.writeCharCode);

    return sb.toString();

  }

通过对比可知,消息显示乱码就是由于编码和解码的格式不对。

然后我就一直好奇,数据在传输过程中是什么样的格式,所以添加了显示原始消息的代码。


MqttPublishPayload.bytesToStringAsString// 接收订阅的消息

  client.updates!.listen((List<MqttReceivedMessage<MqttMessage?>>? c) { // 监听client的更新流

    final recMess = c![0].payload as MqttPublishMessage; // 获取接收到的消息

    // 将消息的负载转换为字符串

    print("接收到的原始数据: ${recMess.payload.message}");  // 查看原始数据

    final pt = MqttPublishPayload.bytesToStringAsString(recMess.payload.message);

    try{

      final r_base = base64Decode(pt);

      final row_info = utf8.decode(r_base);

      print("收到消息: 解密后消息内容 $row_info"); // 打印收到的消息的主题和负载

    } on Exception {

      print("收到消息: 主题为 ${c[0].topic}, 消息内容 $pt"); // 打印收到的消息的主题和负载

    }

  });

获取到的数据如下

也就是说实际的数据其实是Uint8List格式,但是经过接收函数处理后,被包装成Uint8Buffer格式。

尝试

我本以为这个问题比较简单,只需要把编码方式改为UTF8就行了,而我也在mqtt_client_payload_builder.dart中找到了使用UTF8格式编码的函数,所以对发送函数进行了如下的修改


// 发送函数

  print('发布自己的消息');

  final message = MqttClientPayloadBuilder();

  message.addUTF8String(sendBuffer);  // 使用UTF8进行编码

  client.publishMessage(pubTopic, MqttQos.exactlyOnce, message.payload!);

  await MqttUtilities.asyncSleep(1);

但经过测试发现,解码还是不对劲,还是解不出中文。在MQTTX软件上查看发送的消息,发现MQTTX可以正常解读我发送的中文消息了。

1701839409095.png

思考

既然MQTTX可以正常显示中文了,那说明发送的问题解决了,剩下的就是接收的问题。

所以再次看了看接收的原始数据与解码函数,进入到了StringBuffer类中。


class StringBuffer implements StringSink {

  /// Creates a string buffer containing the provided [content].

  external StringBuffer([Object content = ""]);

  /// Returns the length of the content that has been accumulated so far.

  /// This is a constant-time operation.

  external int get length;

  /// Returns whether the buffer is empty. This is a constant-time operation.

  bool get isEmpty => length == 0;

  /// Returns whether the buffer is not empty. This is a constant-time

  /// operation.

  bool get isNotEmpty => !isEmpty;

  external void write(Object? object);

  external void writeCharCode(int charCode);

  external void writeAll(Iterable<dynamic> objects, [String separator = ""]);

  /// Writes the string representation of [object] followed by a newline.

  ///

  /// Equivalent to `buffer.write(object)` followed by `buffer.write("\n")`.

  ///

  /// The newline is always represented as `"\n"`, and does not use a platform

  /// specific line ending, e.g., `"\r\n"` on Windows.

  ///

  /// Notice that calling `buffer.writeln(null)` will write the `"null"` string

  /// before the newline. Omitting the argument, or explicitly passing an empty

  /// string, is the recommended way to emit just the newline.

  external void writeln([Object? obj = ""]);

  /// Clears the string buffer.

  external void clear();

  /// Returns the contents of buffer as a single string.

  external String toString();

}

本想继续跟踪进入writeCharCode()函数中,但是ide跳转不到函数实现,所以想从原始数据入手。

原始数据是Uint8List 类型,所以我打算调用dart标准库中的utf8.encode(sendBuffer) 将中文进行UTF8编码,然后将编码后的字节数据转换Uint8Buffer格式,再调用MqttPublishPayload.bytesToStringAsString(),看看是不是能还原出中文字符串。


  import 'package:typed_data/typed_data.dart' as typed;

  final test_utf8 = utf8.encode("你好,中国");

  print("test_utf8: $test_utf8");

  final test_u8buff = typed.Uint8Buffer();

  test_u8buff.addAll(test_utf8);

  print("test_u8buff: $test_u8buff");

  

  // 试图通过MqttPublishPayload.bytesToStringAsString解码数据

  print("row information: ${MqttPublishPayload.bytesToStringAsString(test_u8buff)}");

测试结果如下:

1701842241600.png

所以MqttPublishPayload.bytesToStringAsString函数的解码也有问题。


解决

传输后的数据是Uint8List格式,所以调用utf8的解码函数就能获得我们需要的信息


// 接收订阅的消息

  client.updates!.listen((List<MqttReceivedMessage<MqttMessage?>>? c) { // 监听client的更新流

    final recMess = c![0].payload as MqttPublishMessage; // 获取接收到的消息

    // 将消息的负载转换为字符串

    try {

      final pt = Utf8Decoder().convert(recMess.payload.message);

      print("收到消息: 主题为 ${c[0].topic}, 消息内容 $pt"); // 打印收到的消息的主题和负载

    } on FormatException  { // 遇到UTF16编码的数据时调用这个

      final pt = MqttPublishPayload.bytesToStringAsString(recMess.payload.message);

      print("收到消息: 主题为 ${c[0].topic}, 消息内容 $pt"); // 打印收到的消息的主题和负载

    } on Exception catch(e){

      print("出现问题: $e");

    }


或者,换一种思路,既然中文直接传输有问题,那就传输base64编码后的数据。


// 对图片或其他消息进行base64编码

String to_base64(String sendBuffer) {

  final bytes = utf8.encode(sendBuffer);

  String formatBuffer = base64Encode(bytes);

  return formatBuffer;

}

// 解密base64编码

String r_base64(String pt) {

  final r_base = base64Decode(pt);

  final row_info = utf8.decode(r_base);

  return row_info;

}


总结

解决方法有两个

  • 修复中文编码、解码问题

    编码时使用UTF8编码

    解码时使用utf8类中的解码函数


  • 将包含中文的数据转换成不含中文的数据

    使用base64转换