在使用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可以正常解读我发送的中文消息了。
思考
既然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)}");
测试结果如下:
所以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转换