Dart处理Tcp中的粘包问题-utf8
在tcp通信中,粘包是极其常见的事情,
下面我分享一个再实际开发的时候遇见的粘包问题,虽然它的实际触发场景不是tcp通信,但能应用于一些类似的粘包情况
由于我对计算机网路还没有过系统的学习,根据我看过的文献说一下我自己的理解
- 1.发送端没有对数据进行很好的分割
- 2.Tcp接收到的数据都存在缓冲区里面,由应用程序主动读取,而大部分的情况是,tcp收到的数据存在缓冲区的速度过快,导致一组完整数据的头部,粘到了前一组完整数据的尾部。或者在读取时,由于缓冲区的固定大小缘故,某一次读取的数据占满了缓冲区,可尾部的数据并不是完整的。
- tcp通信收发数据都是char的数组,在缓冲区的上限情况下,完全有可能发生粘包的情况
举个例子,’梦魇’对应的utf8编码为[230, 162, 166, 233, 173, 135]
使用
1 | utf8.decode([230, 162, 166, 233, 173, 135]); |
即可获取’梦魇’二字,此时我们删掉最后一个数
1 | utf8.decode([230, 162, 166, 233, 173]); |
那么这次转换一定会报错
1 | Unfinished UTF-8 octet sequence (at offset 5) |
所以如果这一串编码在tcp中发生了粘包情况时,在一次数据接收并进行解码的过程中,如果尾部的数据只有一部分,那么对应这部分的解码会失败,从而下一次数据的开头也有着不规则的编码。
我的触发场景
来自于我上一篇Flutter开发的完整终端模拟器之后的维护,由于ptmx创建了一对虚拟终端对,所以我们的读写都是基于ptm设备,控制台的运行程序将自身的输出写进了pts,我们就能从ptm端拿到程序的输出。
大家平时开发的经验就知道,如果我们打开我们的终端模拟器,输入find /命令后,终端的输出不是简单的程序能避免从ptm读取数据不会粘包的,一瞬间可以说成千上万的输出到终端。
由于dart端是循环从ptm中读出程序的输出,它的速度是远远赶不上类似于find命令输出的速度的,最后就导致随便一两个命令就打断了我的运行调试,就是utf8解码时遇到了不规则序列,
我们也可以将allowMalformed参数写成true,utf8解码就不再丢错,但你的输出中就会有大量的’�’字符
所以本篇是解决tcp中utf8字符持续接收解码的场景
解决思路
根据编码的协议来进行拆包,拼包操作。我们只要将尾部不规则的编码拆出来存进缓存,并且在下一次数据到来的时候拼接到头部即可
所以关键难点就在于如何拆除不规则序列的尾部
查看编码规则
来自百度百科的UTF-8的相关内容
我们不需要关心它的完整编码规则,我们只需要找出它的规律
根据上图我们得出
任何字符转换成utf8序列后
- 第一个字节中1开头个数就代表了这一整组完整序列的字节数,包括这一个字节
- 剩余字节对应的二进制位总是以10开头
- 第一个范围Unicode对应编码的字节首位为0
得出检查算法🧐
我们选择从一次数据的尾部往前遍历
假定我们收到的数据放进名为units的列表
当Unicode范围在0000~007F的时候
由上面表格可知这个范围的Unicode值占用一个字节,并且是以首位为0
一个dart判定二进制是什么开头的小经验
不要使用int.toRadixString,因为当这个数对应二进制的首位不是1的时候,会被系统省略掉,所以我们要借助好语言中提供的移位,按位与,按位或等运算。
所以当
1 | units.last & 128 == 0 |
通过与128做按位与运算,具体如下:
1 | 1 0 0 0 0 0 0 0 |
只要某个字节的首位不是0,那么与128做按位与运算得到的结果就是0
这种情况就代表这组序列的最后一个字节是完整的序列,不需要做处理
数据的最后一个字节是11开头
这是我自己的一种技巧吧
由于发现是字节对应的位11开头的话,就说明这个字节1开头的个数代表这整组序列的字节个数,如果最后一个字节有着代表个数的作用,那么它一定不属于这次数据,直接拆下来,扔进缓存,拼在下次的开头。
1 | else if (units.last & 192 == 192) { |
192对应11000000
数据的最后一个字节为10开头
为10开头就会导致两种情况的存在
- 这组序列完整
- 不完整
haha废话了🤣
我们只需要从后往前遍历,记录下以10开头字节的个数,当遍历到11开头的字节时,对比11开头的字节中1的个数,是否是前面10开头的个数加1即可
例如某次数据
从后往前遍历发现5个10开头的字节,倒数第6个字节有6个1,那么尾部的这组序列就是完整,如果是7,就代表它还差一个字节的数据。
说了大半天
完整代码
1 | import 'dart:convert'; |
以上代码是用于将原生Pointer<Uint8>类型转为String并且能够应对大量字符传输过来时的粘包情况。
同步到我的个人博客
结语
之后的博文还是准备继续深入Flutter对标准终端模拟器的开发讲解,应该在考试后了。
文章主要分享主要是思想,可能听说过我的人都知道,我给的代码都是祖传的,使用没问题我就很少碰(没时间呀🤪 )
最近自己项目还有学校的事情忙得不开交,时刻记着学习呀。
小弟我各部分基础都还不扎实,只想借掘金分享自己的学习,帮助有同样需求的人,如有任何错误还恳请各位前辈不吝赐教,不能光想着职责我呀。
Dart处理Tcp中的粘包问题-utf8
http://blog.nightmare.press/2020/02/22/Dart处理Tcp中的粘包问题-utf8/