我发现这个问题经常发生,所以我故意比平时更广泛一些
发生此问题的原因是 TCP 是基于流的,而不是基于数据包的。
这基本上会发生:
- [client]想要发送10k字节的数据
- [client] 将数据发送到TCP层
- [客户端] TCP 层分割数据包,它知道最大数据包大小为 1500(这是默认的 MTU)几乎全部网络使用)
- [client] 客户端向服务器发送数据包,其中包含 40 字节作为标头,1460 字节作为数据
- [服务器] Netty收到第一个数据包,直接调用你的函数,第一个数据包包含1460字节数据
- [服务器] 当您的函数需要处理剩余数据时(初始数据 - 1260)
所以解决这个问题有多种方法
在消息前面添加长度:
虽然这通常是解决数据包的最简单方法,但在同时处理小型和大型消息时,它也是效率最低的方法。这也需要更改协议。
基本思想是在发送数据包之前添加长度,这样您就可以正确拆分消息
优点
- 无需循环数据来过滤字符或阻止禁止字符
- 如果您的网络中有中继系统,则它们不必对消息边界进行任何硬解析
缺点
How?
如果您使用标准整数字段,这非常简单,因为 Netty 已为此构建了类:
- LengthFieldBasedFrameDecoder http://netty.io/4.0/api/io/netty/handler/codec/LengthFieldBasedFrameDecoder.html
- LengthFieldPrepender http://netty.io/4.0/api/io/netty/handler/codec/LengthFieldPrepender.html
这在您的管道中以以下方式使用
// int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip
pipeline.addLast(new LengthFieldBasedFrameDecoder(1024 * 4, 0, 2, 0, 2));
// int lengthFieldLength, int lengthAdjustment
pipeline.addLast(new LengthFieldPrepender(2, 0));
这基本上构成了如下所示的数据包:
您发送:
DATA: 12B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 48 65 6c 6c 6f 20 57 6f 72 6c 64 21 |Hello World! |
+--------+-------------------------------------------------+----------------+
LengthFieldPrepender http://netty.io/4.0/api/io/netty/handler/codec/LengthFieldPrepender.html将其转换为:
DATA: 14B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 0c 48 65 6c 6c 6f 20 57 6f 72 6c 64 21 |..Hello World! |
+--------+-------------------------------------------------+----------------+
然后当你收到消息时,LengthFieldBasedFrameDecoder http://netty.io/4.0/api/io/netty/handler/codec/LengthFieldBasedFrameDecoder.html将其解码为:
DATA: 12B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 48 65 6c 6c 6f 20 57 6f 72 6c 64 21 |Hello World! |
+--------+-------------------------------------------------+----------------+
按简单分隔符拆分消息
某些协议采用不同的方法,它们不是按固定长度进行拆分,而是按分隔符进行拆分。一种快速查看方法是 Java 中的字符串以 a 结尾"
,文本文件中的行以换行符结尾,自然文本中的段落以双换行符结尾。
优点
- 如果您知道某个数据不包含字符,则相对容易生成,例如 JSON 通常不包含空格,因此用空格分隔消息很容易。
- 易于通过脚本语言实现,因为不需要状态
缺点
- 与框架字符冲突可能会使消息大小膨胀
- 长度事先未知,因此要么在代码中设置硬编码限制,要么继续读取直到内存不足或数据结束
- 即使您对数据包不感兴趣,也需要阅读每个字符
How?
从 Netty 发送消息时,您需要手动将分隔符添加到消息本身,接收时您可以使用DelimiterBasedFrameDecoder https://netty.io/4.0/api/io/netty/handler/codec/DelimiterBasedFrameDecoder.html将传入流解码为消息。
管道示例:
这在您的管道中以以下方式使用
// int maxFrameLength, ByteBuf... delimiters
pipeline.addLast(1024 * 4, DelimiterBasedFrameDecoder(Delimiters.lineDelimiter()));
发送消息时,需要手动添加分隔符:
DATA: 14B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 48 65 6c 6c 6f 20 57 6f 72 6c 64 21 0d 0a |Hello World!.. |
+--------+-------------------------------------------------+----------------+
收到消息时,DelimiterBasedFrameDecoder https://netty.io/4.0/api/io/netty/handler/codec/DelimiterBasedFrameDecoder.html为您将消息转换为帧:
DATA: 12B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 48 65 6c 6c 6f 20 57 6f 72 6c 64 21 |Hello World! |
+--------+-------------------------------------------------+----------------+
根据复杂的业务分隔符进行拆分
并不是所有的框架都是容易的,有些解决方案如果避免的话实际上是最好的,但有时,你确实需要做一些肮脏的工作。
优点
缺点
- 通常你必须检查每个字节
- 代码可能很难理解
- 快速解决方案可能会因其认为格式错误的输入而产生奇怪的错误
这分为两类:
基于现有解码器
通过这些解决方案,您基本上可以使用其他框架中的现有解码器来解析数据包,并检测其处理中的故障。
示例为GSON https://github.com/google/gson and ReplayingDecoder https://netty.io/4.0/api/io/netty/handler/codec/ReplayingDecoder.html:
public class GSONDecoder
extends ReplayingDecoder<Void> {
Gson gson = new GsonBuilder().create();
protected void decode(ChannelHandlerContext ctx, ByteBuf buf, List<Object> out)
throws Exception {
out.add(gson.fromJson(new ByteBufInputStream(buf, false), Object.class));
}
}
模式检测
如果您要使用模式检测方法,您需要了解您的协议。让我们为 JSON 制作一个模式检测解码器。
根据JSON的结构,我们做以下假设:
- JSON 基于匹配对
{
and }
, and [
and ]
- 匹配对
{
and }
之间应该被忽略"
-
"
当前面加上 a 时应被忽略\
- A
\
如果前面加上一个则应被忽略\
,从左到右解析时
基于这些属性,让我们制作一个ByteToMessageDecoder https://netty.io/4.0/api/io/netty/handler/codec/ByteToMessageDecoder.html基于这些假设:
public static class JSONDecoder extends ByteToMessageDecoder {
// Notice, this class is designed for JSON without a charset definition at the start, adding this is hard as we basicly have to call differend
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
in.markReaderIndex();
int fromIndex = in.readerIndex();
int unclosedCurlyBracketsSeen = 0;
boolean inQuotedSection = false;
boolean nonWhitespaceSeen = false;
boolean slashSeen = false;
while (in.isReadable()) {
boolean newSlashSeenState = false;
byte character = in.readByte();
if (character == '{' && !inQuotedSection) {
unclosedCurlyBracketsSeen++;
}
if (character == '}' && !inQuotedSection) {
unclosedCurlyBracketsSeen--;
}
if (character == '[' && !inQuotedSection) {
unclosedCurlyBracketsSeen++;
}
if (character == ']' && !inQuotedSection) {
unclosedCurlyBracketsSeen--;
}
if (character == '"' && !slashSeen) {
inQuotedSection = !inQuotedSection;
}
if (character == '\\' && !slashSeen) {
newSlashSeenState = true;
}
if (!Character.isWhitespace(character)) {
nonWhitespaceSeen = true;
}
slashSeen = newSlashSeenState;
if(unclosedCurlyBracketsSeen == 0 && nonWhitespaceSeen) {
int targetIndex = in.readerIndex();
out.add(in.slice(fromIndex, targetIndex - fromIndex).retain());
return;
}
}
// End of stream reached, but our JSON is not complete, reset our progress!
in.resetReaderIndex();
}
}
接收消息时,它是这样工作的:
DATA: 35B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 7b 22 68 69 21 22 2c 22 53 74 72 69 6e 67 3a 20 |{"hi!","String: |
|00000010| 5c 22 48 69 5c 22 22 7d 20 20 7b 22 73 6c 61 73 |\"Hi\""} {"slas|
|00000020| 68 22 3a |h": |
+--------+-------------------------------------------------+----------------+
DATA: 34B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 22 5c 5c 22 7d 7b 22 4e 65 73 74 65 64 3a 22 3a |"\\"}{"Nested:":|
|00000010| 7b 22 64 65 65 70 65 72 22 3a 7b 22 6f 6b 22 7d |{"deeper":{"ok"}|
|00000020| 7d 7d |}} |
+--------+-------------------------------------------------+----------------+
正如你所看到的,我们收到了 2 条消息,其中 1 条甚至在 2 个“虚拟 TCP”数据包之间被分段,这由我们的“JSON 解码器”转换为以下 ByteBuf 数据包:
DATA: 24B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 7b 22 68 69 21 22 2c 22 53 74 72 69 6e 67 3a 20 |{"hi!","String: |
|00000010| 5c 22 48 69 5c 22 22 7d |\"Hi\""} |
+--------+-------------------------------------------------+----------------+
DATA: 16B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 20 20 7b 22 73 6c 61 73 68 22 3a 22 5c 5c 22 7d | {"slash":"\\"}|
+--------+-------------------------------------------------+----------------+
DATA: 29B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 7b 22 4e 65 73 74 65 64 3a 22 3a 7b 22 64 65 65 |{"Nested:":{"dee|
|00000010| 70 65 72 22 3a 7b 22 6f 6b 22 7d 7d 7d |per":{"ok"}}} |
+--------+-------------------------------------------------+----------------+