netty ChannelInboundHandlerAdapter 将帧裁剪为 ~1500 字节

2024-03-12

我已经实现了一个服务器应用程序,它使用 netty 框架通过 ChannelInblundHandlerAdapter 读取传入的字节。

如标题所示,我的问题是,我不定期地从客户端获取内容,我认为这些内容在 ~1.500 字节后被剪切。例如:在这种情况下,我应该收到一个大的 JSON 数组。因为它被剪切了,所以我无法解析它。

我尝试在使用消息之前使用管道中的附加 ByteToMessageDecoder 通道对消息进行解码。但这并不能解决问题。我在 JSON 中没有分隔符,我可以检查并将两个(或更多)部分再次粘在一起。

这是我的管道配置:

        ServerBootstrap b = new ServerBootstrap();
        b.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    public void initChannel(SocketChannel ch) {
                        ch.pipeline().addLast(new IdleStateHandler(45,0,0));
                        ch.pipeline().addLast(new MyByteToMessageDecoder());
                        ch.pipeline().addLast(new GatewayCommunicationHandler());
                    }
                })
                .option(ChannelOption.SO_BACKLOG, 128)
                .option(ChannelOption.SO_RCVBUF, 8192)
                .childOption(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(8192))
                .childOption(ChannelOption.SO_KEEPALIVE, true);

        initRestServer();

        // Bind and start to accept incoming connections.
        ChannelFuture f = b.bind(Config.gatewayPort).sync();
        f.channel().closeFuture().sync();

这就是我的 ByteToMessageDecoder:(我知道它一团糟,但我不知道在我的情况下如何处理它)

public class MyByteToMessageDecoder extends ByteToMessageDecoder {

@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
    byte[] receivedBytes = new byte[in.readableBytes()];
    in.getBytes(in.readerIndex(), receivedBytes);


    if (receivedBytes[in.readableBytes()-1] != (byte) 0) {
        out.add(receivedBytes);
        return;
    }

    int lenForOutBytes = 0;
    for (Object o : out) {
        byte[] bytes = (byte[]) o;
        lenForOutBytes += bytes.length;
    }

    byte[] outBytes = new byte[lenForOutBytes];

    for (Object o : out) {
        byte[] bytes = (byte[]) o;

        if (out.size() == 1) {
            outBytes = (byte[]) out.get(0);
        }
        else {
            int i = 0;

            for (int j = 0; j < bytes.length; j++) {
                outBytes[i + j] = bytes[j];
            }
            i += bytes.length;
        }
    }

    ctx.fireChannelRead(outBytes);
    in.resetReaderIndex();
}
...

还有其他人有这样的问题吗?

感谢您的回复

Br Joe


我发现这个问题经常发生,所以我故意比平时更广泛一些

发生此问题的原因是 TCP 是基于流的,而不是基于数据包的。

这基本上会发生:

  1. [client]想要发送10k字节的数据
  2. [client] 将数据发送到TCP层
  3. [客户端] TCP 层分割数据包,它知道最大数据包大小为 1500(这是默认的 MTU)几乎全部网络使用)
  4. [client] 客户端向服务器发送数据包,其中包含 40 字节作为标头,1460 字节作为数据
  5. [服务器] Netty收到第一个数据包,直接调用你的函数,第一个数据包包含1460字节数据
  6. [服务器] 当您的函数需要处理剩余数据时(初始数据 - 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的结构,我们做以下假设:

  1. JSON 基于匹配对{ and }, and [ and ]
  2. 匹配对{ and }之间应该被忽略"
  3. "当前面加上 a 时应被忽略\
  4. 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"}}}   |
+--------+-------------------------------------------------+----------------+
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

netty ChannelInboundHandlerAdapter 将帧裁剪为 ~1500 字节 的相关文章

随机推荐

  • 如何将vim(通过tmux)绑定到Cmd键

    我通常使用 macvim 并且使用 macs 命令键有许多键绑定 我正在尝试切换到 tmux 但这些绑定都不起作用 有关如何修改我的 tmux config 或 vimrc 以恢复这些绑定的任何提示 你是否有机会使用
  • 如何在单击时切换选中/未选中状态时更改复选框标签的颜色

    当我选中或取消选中时 我试图更改复选框标签的字体颜色和背景颜色 我在这个网站上找到了一个 javascript 解决方案 但无法使代码正常工作 这是我到目前为止所尝试过的 现在它正在将 突出显示 类附加到父 div 我只想更改标签 谢谢你的
  • 如何在 ASP.NET 5 中将实体框架 6 与 MySQL 结合使用?

    我有一个使用 ASP NET MVC 4 Entity Framework 6 和 MySQL 的现有网站 我正在尝试将其升级到 ASP NET 5 但希望继续使用实体框架 6 因为实体框架缺少一些功能并且尚不支持 MySQL 如何在 AS
  • 如何在条形图上方注释geom_bar?

    我正在尝试使用 ggplot2 做一个简单的绘图 library ggplot2 ggplot diamonds aes x cut y depth geom bar stat identity color blue facet wrap
  • R - 如何将数据转换为块形式以进行弗里德曼测试?

    在此输入图像描述 https i stack imgur com N0cvs png我有一些与治疗前后血液中化学物质水平有关的数据 有 4 个治疗组 ABCD 有人告诉我可以运行弗里德曼测试来立即比较所有这些变量 我尝试过的代码是 atta
  • 如何限制极坐标的显示宽度,以便以清晰的方式打印宽数据帧?

    考虑下面的例子 pd set option display width 50 pl DataFrame data np random randint 0 20 size 10 42 columns list abcdefghijklmnop
  • EF 中的 CurrentDateTime()

    我使用这段代码来获取服务器日期 但我真的不明白 CreateDateTime 不是一个 sql 函数 那么它是什么 DateTime ServerDate Entities CreateQuery
  • Python matplotlib 减小颜色条标签的大小

    我需要你的帮助 我有一个绘图代码如下 fig plt figure ax1 fig add subplot 111 imax1 ax1 imshow data interpolation nearest origin lower cmap
  • Log4J - 类似 SiftingAppender 的功能

    我在一个使用的项目中工作Log4J http logging apache org log4j 1 2 index html 要求之一是为每个线程创建一个单独的日志文件 这本身就是一个奇怪的问题 通过动态创建一个新的 FileAppende
  • 在 Matlab 图中重叠两个轴

    我正在寻找一种方法来覆盖 x y 时间序列 比如用 plot 创建的 在 contourf 生成的显示之上 在 y 轴上具有不同的缩放比例 似乎在两个 x y 图的情况下执行此操作的典型方法是使用内置函数 plotyy 它甚至可以由 plo
  • 实体框架代码优先一对一必需-必需关系

    使用 Entity Framework Code First 4 3 1 时 可以创建具有多重性的 1 对 1 关系 也就是说 关系的每一端都有一个实体 可以将一对一关系配置为需要 需要 or 必需 可选 然而 当我在两者之间切换时 我没有
  • Spark - 它如何在节点周围分发数据?

    Spark如何将数据分发给worker 工作人员是从数据源读取数据 还是驱动程序读取数据并将其发送给工作人员 当一个工作人员需要另一个工作人员中的数据时 他们是否直接进行通信 Thanks 如果您使用分布式输入法 例如SparkContex
  • 如果对于屏幕来说太长,则使水平单选按钮换行

    所以我有以下单选按钮 我想让它们像这样显示 然而 出现这种情况 我怎样才能让它像上面那样显示 我可以在 Eclipse 中的 GUI 编辑器中移动它 但它会从 RadioGroup 中删除 RadioButton 在组内 它忽略所有其他布局
  • 对象初始值设定项和构造函数有什么区别?

    两者之间有什么区别 何时使用 对象初始值设定项 而不是 构造函数 反之亦然 我正在使用 C 如果这很重要的话 另外 对象初始值设定项方法是否特定于 C 或 NET 对象初始值设定项是 C 3 中添加的内容 目的是在使用对象时简化对象的构造
  • 强制抽象类属性由具体类实现

    考虑这个抽象类和实现它的类 from abc import ABC class FooBase ABC foo str bar str baz int def init self self bar bar self baz baz clas
  • 无论如何,在一个 neo4j 实例上有多个数据库?

    从关系数据库的思维方式来看 每个 Neo4j 实例只有一个图形数据库似乎很奇怪 我们的想法是从根开始创建多个子图吗 Thanks 根 节点的概念正在消失 这存在很多问题 其中大部分与节点密度有关 我相信您问题的核心在于数据库设计 以及拥有多
  • Eclipse 类似于:“无法确定 [项目名称]/[文件路径]/[文件名称] 的 URI”

    主要问题 我在 Eclipse Luna 上遇到以下错误 有一天 您上班并尝试启动 Eclipse 并提高工作效率 但是工作台一打开 您就会看到所有文件选项卡都出现错误 如下所示 无法确定 my project path to file f
  • 与 object.prop 相比,使用 `in` 有什么好处?

    我们都看到该功能检测到执行以下操作 var touch function return ontouchstart in window 但我想知道使用它是否还有其他好处in类似这样的操作符 这可以节省一些字节 var touch functi
  • 获取 JavaScript 对象的第一个键名称[重复]

    这个问题在这里已经有答案了 假设我们有以下 JavaScript 对象 ahash one 1 2 3 two 4 5 6 是否有一个函数可以返回给定对象的第一个键名称 从上面的例子我想得到one作为该功能的结果 在 JavaScript
  • netty ChannelInboundHandlerAdapter 将帧裁剪为 ~1500 字节

    我已经实现了一个服务器应用程序 它使用 netty 框架通过 ChannelInblundHandlerAdapter 读取传入的字节 如标题所示 我的问题是 我不定期地从客户端获取内容 我认为这些内容在 1 500 字节后被剪切 例如 在