Netty源码阅读入门实战(八) - 解码下(下)

简介: Netty源码阅读入门实战(八) - 解码下

基于长度域解码器参数分析

image.png

重要参数

image.png

maxFrameLength (包的最大长度)

防止太大导致内存溢出,超出包的最大长度 Netty 将会做一些特殊处理


lengthFieldOffset (消息体长度)

长度域的偏移量lengthFieldOffset,0表示无偏移

ByteBuf的什么位置开始就是length字段


lengthFieldLength

长度域length字段的长度


lengthAdjustment

有些情况可能会把header也包含到length长度中,或者length字段后面还有一些不包括在length长度内的,可以通过lengthAdjustment调节


initialBytesToStrip

起始截掉的部分,如果传递给后面的Handler的数据不需要消息头了,可以通过这个设置

可以通过消息中的一个表示消息长度的字段值动态分割收到的ByteBuf

基于长度

image.png

这类数据包协议比较常见,前几个字节表示数据包长度(不包括长度域),后面为具体数据

拆完后数据包是一个完整的带有长度域的数据包(之后即可传递到应用层解码器进行解码),

创建一个如下方式的LengthFieldBasedFrameDecoder即可实现这类协议

image.png

6.2 基于长度截断

若应用层解码器不需用到长度字段,那么我们希望 Netty 拆包后,如此

image.png

长度域被截掉,我们只需指定另一个参数 initialBytesToStrip 即可实现

表 Netty 拿到一个完整数据包后向业务解码器传递之前,应该跳过多少字节

image.png

initialBytesToStrip 为4,表获取一个完整数据包后,忽略前面4个字节,应用解码器拿到的就是不带长度域的数据包

6.3 基于偏移长度

image.png

此方式二进制协议更为普遍,前几个固定字节表示协议头,通常包含一些magicNumberprotocol version 之类的meta信息,紧跟着后面的是一个长度域,表示包体有多少字节的数据

只需要基于第一种情况,调整第二个参数既可以实现

image.png

lengthFieldOffset为4,表示跳过4个字节才是长度域。

6.4 基于可调整长度的拆包

有的二进制协议会设计成如下方式

image.png

长度域在前,header在后


长度域在数据包最前面表示无偏移,lengthFieldOffset = 0

长度域的长度为3,即lengthFieldLength = 3

长度域表示的包体的长度略过了header,这里有另外一个参数lengthAdjustment,包体长度调整的大小,长度域的数值表示的长度加上这个修正值表示的就是带header的包,这里是 12+2,header和包体一共占14字节

6.5 基于偏移可调整长度的截断

二进制协议带有两个header

image.png


拆完后,HDR1 丢弃,长度域丢弃,只剩下第二个header和有效包体

这种协议中,一般HDR1可以表示magicNumber,表示应用只接受以该magicNumber开头的二进制数据,RPC 里面用的较多

参数设置

长度域偏移为1,即lengthFieldOffset为1

长度域长度为2,即 lengthFieldLength为2

长度域表示的包体的长度略过HDR2,但拆包时HDR2也被 Netty 当作包体的一部分来拆,HDR2的长度为1,即 lengthAdjustment 为1

拆完后,截掉前面三个字节,即initialBytesToStrip 为 3

image.png

6.6 基于偏移可调整变异长度的截断

前面所有的长度域表示的都是不带header的包体的长度

如果让长度域表示的含义包含整个数据包的长度,如下

image.png

长度域字段值为16, 其字段长度为2,HDR1的长度为1,HDR2的长度为1,包体的长度为12,1+1+2+12=16


参数设置

除长度域表示的含义和上一种情况不一样外,其他都相同,因为 Netty 不了解业务情况,需告诉 Netty ,长度域后再跟多少字节就可形成一个完整数据包,这里显然是13字节,长度域为16,因此减掉3才是真是的拆包所需要的长度,lengthAdjustment为-3

若你的协议基于长度,即可考虑不用字节来实现,而是直接拿来用,或者继承他,简单修改即可

7 基于长度域解码器分析

7.1 构造方法

image.png

public LengthFieldBasedFrameDecoder(ByteOrder byteOrder, int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip, boolean failFast) {
    // 省略参数校验
    this.byteOrder = byteOrder;
    this.maxFrameLength = maxFrameLength;
    this.lengthFieldOffset = lengthFieldOffset;
    this.lengthFieldLength = lengthFieldLength;
    this.lengthAdjustment = lengthAdjustment;
    lengthFieldEndOffset = lengthFieldOffset + lengthFieldLength;
    this.initialBytesToStrip = initialBytesToStrip;
    this.failFast = failFast;
}

把传参数保存在 field即可


byteOrder

字节流表示的数据是大端还是小端,用于长度域的读取

lengthFieldEndOffset

紧跟长度域字段后面的第一个字节的在整个数据包中的偏移量

failFast

为true 表读取到长度域,TA的值的超过maxFrameLength,就抛 TooLongFrameException

为false 表只有当真正读取完长度域的值表示的字节之后,才抛 TooLongFrameException,默认设为true,建议不要修改,否则可能会造成内存溢出

7.2 实现拆包抽象

具体的拆包协议只需要实现

void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) 

in 表目前为止还未拆的数据,拆完之后的包添加到 out这个list中即可实现包向下传递

  • 第一层实现
  • image.png
  • 重载的protected方法decode实现真正的拆包,以下三步走

基于长度域解码器步骤

  • 计算需要抽取的数据包长度
  • 跳过字节逻辑处理
  • 丢弃模式下的处理

1 计算需要抽取的数据包的长度

    protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
        // 拿到实际的未调整过的包长度
        long frameLength = getUnadjustedFrameLength(in, actualLengthFieldOffset, lengthFieldLength, byteOrder);
        if (frameLength < lengthFieldEndOffset) {
            failOnFrameLengthLessThanLengthFieldEndOffset(in, frameLength, lengthFieldEndOffset);
        }
        if (frameLength > maxFrameLength) {
            exceededFrameLength(in, frameLength);
            return null;
        }
    }

拿到长度域的实际字节偏移

image.png

调整包的长度

image.png

如果当前可读字节还未达到长度长度域的偏移,那说明肯定是读不到长度域的,直接不读

image.png

上面有个getUnadjustedFrameLength,若你的长度域代表的值表达的含义不是基本的int,short等基本类型,可重写该方法

image.png

比如,有的奇葩的长度域里面虽然是4个字节,比如 0x1234,但是TA的含义是10进制,即长度就是十进制的1234,那么覆盖这个函数即可实现奇葩长度域拆包

2. 长度校验

  • 整个数据包的长度还没有长度域长,直接抛异常
  • image.png
  • 数据包长度超出最大包长度,进入丢弃模式
  • image.png
  • 当前可读字节已达到frameLength,直接跳过frameLength个字节,丢弃之后,后面有可能就是一个合法的数据包

当前可读字节未达到frameLength,说明后面未读到的字节也需丢弃,进入丢弃模式,先把当前累积的字节全部丢弃

bytesToDiscard 表还需丢弃多少字节

image.png

最后,调用failIfNecessary判断是否需要抛出异常

不需要再丢弃后面的未读字节(bytesToDiscard == 0),重置丢弃状态

如果没有设置快速失败(!failFast),或者设置了快速失败并且是第一次检测到大包错误(firstDetectionOfTooLongFrame),抛出异常,让handler处理

如果设置了快速失败,并且是第一次检测到打包错误,抛出异常,让handler去处理

image.png

前面我们可以知道failFast默认为true,而这里firstDetectionOfTooLongFrametrue,所以,第一次检测到大包肯定会抛出异常

image.png

3 丢弃模式的处理

LengthFieldBasedFrameDecoder.decoder方法入口处还有一段代码

image.png

若当前处在丢弃模式,先计算需要丢弃多少字节,取当前还需可丢弃字节和可读字节的最小值,丢弃后,进入 failIfNecessary,对照着这个函数看,默认情况下是不会继续抛出异常,而如果设置了 failFast为false,那么等丢弃完之后,才会抛出异常

image.png

2 跳过指定字节长度的逻辑处理

在丢弃模式的处理及长度校验都通过后


先验证当前是否已读到足够的字节,若读到了,在下一步抽取一个完整的数据包之前,需根据initialBytesToStrip的设置来跳过某些字节,当然,跳过的字节不能大于数据包的长度,否则抛 CorruptedFrameException 异常

image.png

抽取frame

  • 拿到当前累积数据的读指针,然后拿到待抽取数据包的实际长度进行抽取,抽取之后,移动读指针
  • image.png
  • 抽取的过程即调用了一下 ByteBufretainedSlice API,该API无内存copy的开销
  • image.png
  • 从真正抽取数据包来看看,传入的参数为 int 型,所以自定义协议中,如果你的长度域是8字节,那么前4字节基本没用

小结

  • 如果你使用了Netty,并且二进制协议基于长度,考虑使用LengthFieldBasedFrameDecoder吧,通过调整各种参数,一定会满足你
  • LengthFieldBasedFrameDecoder的拆包包括合法参数校验,异常包处理,以及最后调用 ByteBuf 的retainedSlice来实现无内存copy的拆包

8 解码器总结

8.1 ByteToMessageDecoder 解码步骤

  • 累加字节流
  • 调用子类的decode方法进行解析
  • 将解析到的ByteBuf向下传播

8.2 基于长度解码器步骤

  • 计算需要抽取的数据包长度
  • 跳过字节逻辑处理
  • 丟弃模式下的处理

8.3 两个问题

  • 解码器抽象的解码过程
  • netty里面有哪些拆箱即用的解码器
目录
相关文章
|
7月前
|
缓存 网络协议 算法
Netty的基础入门(上)
Netty的基础入门(上)
241 1
|
1月前
|
消息中间件 编解码 网络协议
Netty从入门到精通:高性能网络编程的进阶之路
【11月更文挑战第17天】Netty是一个基于Java NIO(Non-blocking I/O)的高性能、异步事件驱动的网络应用框架。使用Netty,开发者可以快速、高效地开发可扩展的网络服务器和客户端程序。本文将带您从Netty的背景、业务场景、功能点、解决问题的关键、底层原理实现,到编写一个详细的Java示例,全面了解Netty,帮助您从入门到精通。
122 0
|
4月前
|
编解码 NoSQL Redis
(十一)Netty实战篇:基于Netty框架打造一款高性能的IM即时通讯程序
关于Netty网络框架的内容,前面已经讲了两个章节,但总归来说难以真正掌握,毕竟只是对其中一个个组件进行讲解,很难让诸位将其串起来形成一条线,所以本章中则会结合实战案例,对Netty进行更深层次的学习与掌握,实战案例也并不难,一个非常朴素的IM聊天程序。
106 3
|
4月前
|
前端开发 网络协议
Netty实战巅峰:从零构建高性能IM即时通讯系统,解锁并发通信新境界
【8月更文挑战第3天】Netty是一款高性能、异步事件驱动的网络框架,适用于开发高并发网络应用,如即时通讯(IM)系统。本文将指导你利用Netty从零构建高性能IM程序,介绍Netty基础及服务器/客户端设计。服务器端使用`ServerBootstrap`启动,客户端通过`Bootstrap`连接服务器。示例展示了简单的服务器启动过程。通过深入学习,可进一步实现用户认证等功能,打造出更完善的IM系统。
188 1
|
4月前
|
移动开发 网络协议 算法
(十)Netty进阶篇:漫谈网络粘包、半包问题、解码器与长连接、心跳机制实战
在前面关于《Netty入门篇》的文章中,咱们已经初步对Netty这个著名的网络框架有了认知,本章的目的则是承接上文,再对Netty中的一些进阶知识进行阐述,毕竟前面的内容中,仅阐述了一些Netty的核心组件,想要真正掌握Netty框架,对于它我们应该具备更为全面的认知。
241 2
|
7月前
|
存储 消息中间件 缓存
Netty的基础入门(下)
Netty的基础入门(下)
130 0
|
7月前
|
移动开发 网络协议 Java
通信密码学:探秘Netty中解码器的神奇力量
通信密码学:探秘Netty中解码器的神奇力量
212 0
|
7月前
|
前端开发 网络协议 Java
Netty入门指南:从零开始的异步网络通信
Netty入门指南:从零开始的异步网络通信
187 0
|
存储 缓存 NoSQL
跟着源码学IM(十一):一套基于Netty的分布式高可用IM详细设计与实现(有源码)
本文将要分享的是如何从零实现一套基于Netty框架的分布式高可用IM系统,它将支持长连接网关管理、单聊、群聊、聊天记录查询、离线消息存储、消息推送、心跳、分布式唯一ID、红包、消息同步等功能,并且还支持集群部署。
13521 1
|
7月前
|
消息中间件 Oracle Dubbo
Netty 源码共读(一)如何阅读JDK下sun包的源码
Netty 源码共读(一)如何阅读JDK下sun包的源码
139 1