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
2
  1 0 0 0 0 0 0 0
& x x x x x x x x

只要某个字节的首位不是0,那么与128做按位与运算得到的结果就是0

这种情况就代表这组序列的最后一个字节是完整的序列,不需要做处理

数据的最后一个字节是11开头

这是我自己的一种技巧吧

由于发现是字节对应的位11开头的话,就说明这个字节1开头的个数代表这整组序列的字节个数,如果最后一个字节有着代表个数的作用,那么它一定不属于这次数据,直接拆下来,扔进缓存,拼在下次的开头。

1
2
3
else if (units.last & 192 == 192) {
unitsCache = units.sublist(len - 1, len);
units.removeRange(len - 1, len);

192对应11000000

数据的最后一个字节为10开头

为10开头就会导致两种情况的存在

  • 这组序列完整
  • 不完整

    haha废话了🤣

我们只需要从后往前遍历,记录下以10开头字节的个数,当遍历到11开头的字节时,对比11开头的字节中1的个数,是否是前面10开头的个数加1即可

例如某次数据
从后往前遍历发现5个10开头的字节,倒数第6个字节有6个1,那么尾部的这组序列就是完整,如果是7,就代表它还差一个字节的数据。

说了大半天

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import 'dart:convert';
import 'dart:ffi';

mixin CustomUtf {
static List<int> unitsCache = <int>[]; //这个缓存是为了解决拿到的最后字符不完整
static String cStringtoString(Pointer<Uint8> str) {
if (str == null) {
return null;
}
int len = 0;
while (str.elementAt(++len).value != 0) {}
List<int> units = List<int>(len);
for (int i = 0; i < len; ++i) {
units[i] = str.elementAt(i).value;
}
units = unitsCache + units;
// print('len=====$len');
len = len + unitsCache.length;
unitsCache.clear();
//只有当为0开头经过二进制转换才小于7位,如果读取到的最后一个字符为0开头,
//说明这整个UTF8字符占用1个字节,不存在后面还有其他字节没有读取到的情况
if (units.last & 128 == 0) {
// print('===>${units.last}');
try {
return utf8.decode(units, allowMalformed: false);
} catch (e) {
print(units);
print(e);
}
} else if (units.last & 192 == 192) {
print('结尾数');
unitsCache = units.sublist(len - 1, len);
units.removeRange(len - 1, len);
} else {
// print('发现需要拼包的序列');
// print(units.last);
// print(units);
// print('拆包中');
int number = 0;
while (true) {
//等于2说明移位后为10
final int cur = units[len - 1 - number];
// print('当前指向的数===>$cur');
if (cur >> 6 == 2) {
// print('经过一个10');
//经过一次10开头的便记录一次
} else if (cur.toRadixString(2).startsWith('1' * (number + 2))) {
//此时该字节以number+2个字节开始,说明该次读取不完整
//因为此时该字节开始的1应该为number+1个(10开始的个数加上此时这个字节)
unitsCache = units.sublist(len - number, len);
units.removeRange(len - number, len);
break;
} else if (cur.toRadixString(2).startsWith('1' * (number+1))) {
{
break;
}
}
number++;
}
}
try {
return utf8.decode(units, allowMalformed: false);
} catch (e) {
print('===>$units');
print(e);
}
return null;
}
}

以上代码是用于将原生Pointer<Uint8>类型转为String并且能够应对大量字符传输过来时的粘包情况。

同步到我的个人博客

Dart处理Tcp中的粘包问题(utf8)

结语

之后的博文还是准备继续深入Flutter对标准终端模拟器的开发讲解,应该在考试后了。

  • 文章主要分享主要是思想,可能听说过我的人都知道,我给的代码都是祖传的,使用没问题我就很少碰(没时间呀🤪 )

  • 最近自己项目还有学校的事情忙得不开交,时刻记着学习呀。

  • 小弟我各部分基础都还不扎实,只想借掘金分享自己的学习,帮助有同样需求的人,如有任何错误还恳请各位前辈不吝赐教,不能光想着职责我呀。

作者

梦魇兽

发布于

2020-02-22

更新于

2023-03-11

许可协议

评论