一次接收回调,为什么不是一条消息?
聊天客户端收到网络数据时,可能只有半条消息,也可能一次来了好几条。这个 Flutter App 的 Dart 协议层使用 XMPP,消息以 XML 传输,接收端需要先把字节解码,再提取完整元素。
问题出在“这一块数据”的边界与“这一条消息”的边界没有必然关系。把每次回调直接当完整 XML,正常网络上的分片就可能变成解析错误。
通常怎样从流里取出完整消息?
TCP 规范 RFC 9293 描述的是有序字节流,接收边界不保证对应发送端的一次写入。于是接收端需要连续解码,并让解析器保留尚未完成的部分。
项目当前的做法是先解码,将字符串追加到缓冲区,从中找完整 <message>;提取一条后留下剩余内容,继续寻找。示意消息是:
<message id="m1"><body>你好</body></message>
它可能在“你”和“好”之间分两次到达,也可能与下一条消息一起到达。缓冲能处理这种情况,但当前这条缓冲路径主要负责聊天 message,握手和其他响应仍有各自的监听,没有统一进入同一个完整元素解析入口。
找到第一个结束标签,为什么还不能截?
多端转发会把消息嵌套。XEP-0280 的消息转发结构,省略地址等属性后可以写成:
<message>
<received xmlns="urn:xmpp:carbons:2">
<forwarded xmlns="urn:xmpp:forward:0">
<message xmlns="jabber:client">
<body>你好</body>
</message>
</forwarded>
</received>
</message>
第一个 </message> 只结束内层,外面的 forwarded 和 received 尚未关闭。项目用深度计数处理:遇到 message 开始就加一,结束就减一,归零时才提取。
另一种选择是使用理解完整 XML 语法的流式解析。当前深度计数仍在扫描标签字符串,注释、CDATA 和自闭合等输入还需要规则,不能把这段扫描当成完整词法解析器。
标签或一个汉字本身被拆开,怎么办?
缓冲必须能保留潜在的起点。例如第一次只收到 <mes,下一次才收到 sage...。当前实现找不到完整 <message 就清空,按这条路径推演,第一段会丢掉,后面无法重新拼回。这是尚需补测试的源码风险。
字节解码更早就可能出问题。现在每块数据分别调用 UTF-8 解码,一个多字节字符拆在两个回调里时,还没进入 XML 缓冲便可能失败。Dart 的 Utf8Decoder 提供流转换与分块转换,用来保留尚未解完的字节。
允许非法输入时使用替代字符,并不能还原原来的字节。后续需要先连续解码,再处理 XML 边界;仅修字符串缓冲,补不回解码阶段已丢失的信息。
缓冲超长时,丢掉尾部还是断开?
需要根据协议和恢复要求选择。保留尾部能控制长度,但尾部可能已没有完整起点;清空则直接放弃未完成消息。若要终止流并报告错误,也需要让上层知道发生了什么。
当前入口超过 2 * 1024 * 1024 时保留尾部,解析器遇到未完成消息超过 16384 时清空。比较使用 Dart 的 String.length,单位是 UTF-16 code unit,不是网络 UTF-8 字节。
因此单纯调大数字,不能解决丢弃后的恢复问题。验证时可以把一条合法消息的字节在每个位置分别切成两块,对照一次性输入的结果,再加入连续分块、消息相连、嵌套转发和中文,检查两层缓冲在什么位置留下或丢掉了数据。