目录
一、 背景简介
二、 应用
三、源码
一、背景简介
LengthFieldBasedFrameDecoder 基于长度字段解码器,是一个非常灵活、强大的解码器,他能够根据我们动态配置的参数对接收到的消息进行动态解码,以满足实际的业务需求。当解码具有消息头长度字段表示该消息主体或整个消息的长度的二进制消息时,它是特别有用的。
二、应用
在这里先简单看下LengthFieldBasedFrameDecoder类中的核心字段。这些字段在下面分析各种情况的解码的时候是非常有用的。
//=====================核心属性====================
//表示字节流的数据是大端还是小端,默认是大端
private final ByteOrder byteOrder;
//表示读取每帧数据的最大阈值
private final int maxFrameLength;
//表示长度域左边的偏移量
private final int lengthFieldOffset;
//表示长度域的长度
private final int lengthFieldLength;
//表示长度域左边的偏移量+长度域的长度
private final int lengthFieldEndOffset;
//表示长度域左边的偏移量+长度域的长度后面偏移量、调整量
private final int lengthAdjustment;
//表示跳过多少字节后就能拿到数据域
private final int initialBytesToStrip;
//表示是否开启快速失败机制
private final boolean failFast;
//表示是否开启丢弃模式、正在丢弃数据
private boolean discardingTooLongFrame;
//表示一共丢弃的字节数据
private long tooLongFrameLength;
//表示每一次丢弃的字节数据
private long bytesToDiscard;
下面是DelimiterBasedFrameDecoder的一个简单小测试。
服务端:
public class LengthFieldBasedFrameDecoderTestServer {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ch.pipeline()
.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,0,2,0,2))
.addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
if (msg instanceof ByteBuf) {
ByteBuf packet = (ByteBuf) msg;
System.out.println(packet.toString(Charset.defaultCharset()));
}
}
});
}
});
ChannelFuture f = b.bind(9000).sync();
System.out.println("Started LengthFieldBasedFrameDecoderTestServer...");
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
客户端:
public class LengthFieldBasedFrameDecoderTestClient {
public static void main(String[] args) throws Exception {
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(workerGroup)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ch.pipeline()
.addLast(new ChannelInboundHandlerAdapter() {
public void channelActive(ChannelHandlerContext ctx) {
ByteBuf byteBuf = Unpooled.copiedBuffer("hello world".getBytes());
ctx.writeAndFlush(byteBuf);
}
});
}
});
ChannelFuture f = b.connect("127.0.0.1", 9000).sync();
System.out.println("Started LengthFieldBasedFrameDecoderTestClient...");
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
}
}
}
当然在这里基于这种配置下是没有任何输出的。
然后在接下来我们一一分析,各种配置参数到底在LengthFieldBasedFrameDecoder解码器中扮演着什么角色,起到什么作用,能够满足什么样的协议要求。
【以下示例来自JDK类LengthFieldBasedFrameDecoder DOC文档】
示例①:
2字节长度的长度域,长度域偏移量为0,不跳过消息头。
BEFORE DECODE (14 bytes) AFTER DECODE (14 bytes)
+--------+----------------+ +--------+----------------+
| Length | Actual Content |----->| Length | Actual Content |
| 0x000C | "HELLO, WORLD" | | 0x000C | "HELLO, WORLD" |
+--------+----------------+ +--------+----------------+
说明:
在这个例子中,长度字段的值是12(0x0C)表示“HELLO,WORLD”的长度(不包括长度域,只代表数据域的长度)。默认情况下,解码器假定长度字段表示随后的长度字段中的字节数。因此,它可以与简单的参数组合进行解码。
lengthFieldOffset = 0
lengthFieldLength = 2
lengthAdjustment = 0
initialBytesToStrip = 0 (= do not strip header)
LengthFieldBasedFrameDecoder构造传参如下:
new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,0,2,0,0);
示例②:
2字节长度的长度域,长度域偏移量为0,跳过消息头。
BEFORE DECODE (14 bytes) AFTER DECODE (12 bytes)
+--------+----------------+ +----------------+
| Length | Actual Content |----->| Actual Content |
| 0x000C | "HELLO, WORLD" | |"HELLO, WORLD" |
+--------+----------------+ +----------------+
说明:
在这个例子中,长度字段的值是12(0x0C)表示“HELLO,WORLD”的长度(不包括长度域,只代表数据域的长度)。解码后长度域丢失。
lengthFieldOffset = 0
lengthFieldLength = 2
lengthAdjustment = 0
initialBytesToStrip = 2 (= the length of the Length field)
LengthFieldBasedFrameDecoder构造传参如下:
new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,0,2,0,2);
示例③:
2字节长度的长度域,lengthAdjustment为向左2个字节,不跳过消息头。
BEFORE DECODE (14 bytes) AFTER DECODE (14 bytes)
+--------+----------------+ +--------+----------------+
| Length | Actual Content |----->| Length | Actual Content |
| 0x000E | "HELLO, WORLD" | | 0x000E | "HELLO, WORLD" |
+--------+----------------+ +--------+----------------+
说明:
在大多数情况下,长度字段表示只有消息主体的长度。然而,在一些协议中,长度字段表示整个消息的长度,其中包括消息报头。在这种情况下,我们指定一个非零lengthAdjustment向左偏移,我们指定-2作为lengthAdjustment赔偿。
lengthFieldOffset = 0
lengthFieldLength = 2
lengthAdjustment = -2 (= the length of the Length field)
initialBytesToStrip = 0
LengthFieldBasedFrameDecoder构造传参如下:
new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,0,2,-2,0);
示例④:
3字节长度的长度域,2个字节的消息头长度Header 1,不跳过消息头。
BEFORE DECODE (17 bytes) AFTER DECODE (17 bytes)
+----------+----------+----------------+ +----------+----------+----------------+
| Header 1 | Length | Actual Content |----->| Header 1 | Length | Actual Content |
| 0xCAFE | 0x00000C | "HELLO, WORLD" | | 0xCAFE | 0x00000C | "HELLO, WORLD" |
+----------+----------+----------------+ +----------+----------+----------------+
说明:
该消息是第一个实施的一个简单的变化。一个消息头值被预置到该消息。lengthAdjustment是零。
lengthFieldOffset = 2 (= the length of Header 1)
lengthFieldLength = 3
lengthAdjustment = 0
initialBytesToStrip = 0
LengthFieldBasedFrameDecoder构造传参如下:
new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,2,3,0,0);
示例⑤:
3字节长度的长度域,2个字节的消息头长度Header 1,不跳过消息头。
BEFORE DECODE (17 bytes) AFTER DECODE (17 bytes)
+----------+----------+----------------+ +----------+----------+----------------+
| Length | Header 1 | Actual Content |----->| Length | Header 1 | Actual Content |
| 0x00000C | 0xCAFE | "HELLO, WORLD" | | 0x00000C | 0xCAFE | "HELLO, WORLD" |
+----------+----------+----------------+ +----------+----------+----------------+
说明:
该情况和实例4类似,不过其中在长度域和数据域之间有附加head的情况。你必须指定一个lengthAdjustment,使得解码器计数额外的头到帧的长度计算。
lengthFieldOffset = 0
lengthFieldLength = 3
lengthAdjustment = 2 (= the length of Header 1)
initialBytesToStrip = 0
LengthFieldBasedFrameDecoder构造传参如下:
new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,0,3,2,0);
示例⑥:
1字节长度的Header 1,2个字节的长度域,1字节长度的Header 2,跳过Header 1和长度域。
BEFORE DECODE (16 bytes) AFTER DECODE (13 bytes)
+------+--------+------+----------------+ +------+----------------+
| HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
| 0xCA | 0x000C | 0xFE | "HELLO, WORLD" | | 0xFE | "HELLO, WORLD" |
+------+--------+------+----------------+ +------+----------------+
说明:
这是所有上面的例子的组合。有长度字段之前Header 1和长度字段后附标题Header 2。Header 1影响lengthFieldOffset和Header 2影响lengthAdjustment。我们还指定非零initialBytesToStrip减去长度字段和Header 1。如果你不想要去除预谋头,你可以为initialBytesToSkip指定为0。
lengthFieldOffset = 1 (= the length of HDR1)
lengthFieldLength = 2
lengthAdjustment = 1 (= the length of HDR2)
initialBytesToStrip = 3 (= the length of HDR1 + LEN)
LengthFieldBasedFrameDecoder构造传参如下:
new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,1,2,1,3);
示例⑦:
1字节长度的Header 1,2个字节的长度域,1字节长度的Header 2,跳过Header 1和长度域,指定initialBytesToSkip。
BEFORE DECODE (16 bytes) AFTER DECODE (13 bytes)
+------+--------+------+----------------+ +------+----------------+
| HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
| 0xCA | 0x0010 | 0xFE | "HELLO, WORLD" | | 0xFE | "HELLO, WORLD" |
+------+--------+------+----------------+ +------+----------------+
说明:
让我们再到前面的例子。和前面的例子中,唯一的区别是,长度字段表示整个消息,而不是在消息主体的长度,就像示例③。 此时需要指定initialBytesToSkip向左偏移作为补偿。
lengthFieldOffset = 1
lengthFieldLength = 2
lengthAdjustment = -3 (= the length of HDR1 + LEN, negative)
initialBytesToStrip = 3
LengthFieldBasedFrameDecoder构造传参如下:
new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,1,2,-3,3);
三、源码
netty提供的各种解码器统一都继承了ByteToMessageDecoder,ByteToMessageDecoder负责读取字节流并转换成其他消息,而ByteToMessageDecoder又继承了ChannelInboundHandlerAdapter,而ChannelInboundHandlerAdapter正是我们刚才在测试数据的时候在客户端重写了他的方法channelActive(),使管道生效并且数据写入缓冲区并发送数据。
在客户端代码流程原理并不复杂,我们主要看服务端是怎么解码、获取到数据的。
当客户端通过Channel发送数据的时候,服务端会在ByteToMessageDecoder的channelRead方法接收到。我们以channelRead方法为入口看服务端是怎么进行消息解码的。
在channelRead方法中首先会判断消息msg是不是ByteBuf类型的:
①、如果msg不是ByteBuf类型的,直接调用ctx.fireChannelRead(msg)方法,fireChannelRead方法实际上是调用pipeline管道的下一个Handler去继续处理消息其他的逻辑去了,我们进入到AbstractChannelHandlerContext的fireChannelRead方法,发现只有一个方法invokeChannelRead,寻找下一个绑定的Handler并且调用ChannelRead方法。
@Override
public ChannelHandlerContext fireChannelRead(final Object msg) {
//寻找下一个绑定的Handler并且调用ChannelRead方法
invokeChannelRead(findContextInbound(), msg);
return this;
}
继续进入到invokeChannelRead。
static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
next.invokeChannelRead(m);
} else {
executor.execute(() -> next.invokeChannelRead(m));
}
}
最终都会调用invokeChannelRead,而invokeChannelRead就是将Handler转换成ChannelInboundHandler类型去执行具体的channelRead方法,去进一步的读取消息并处理相关Handler逻辑。
private void invokeChannelRead(Object msg) {
if (invokeHandler()) {
try {
((ChannelInboundHandler) handler()).channelRead(this, msg);
} catch (Throwable t) {
notifyHandlerException(t);
}
} else {
fireChannelRead(msg);
}
}
②、如果msg是ByteBuf类型的,去调用解码逻辑callDecode方法去解析数据。
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
try {
while (in.isReadable()) {
int outSize = out.size();
//将out里的事件执行并且清空out
if (outSize > 0) {
fireChannelRead(ctx, out, outSize);
out.clear();
// 在继续解码之前,请检查是否已删除此处理程序
// 如果已将其删除,则继续在缓冲区上操作是不安全的。
if (ctx.isRemoved()) {
break;
}
outSize = 0;
}
int oldInputLength = in.readableBytes();
// 开始解析数据,如果解析出来数据,那么out的长度一定会改变
decode(ctx, in, out);
// 在继续循环之前,请检查是否已删除此处理程序。
// 如果已将其删除,则继续在缓冲区上操作是不安全的。
if (ctx.isRemoved()) {
break;
}
// 如果没有解析出来数据
if (outSize == out.size()) {
if (oldInputLength == in.readableBytes()) {
break;
} else {
continue;
}
}
if (oldInputLength == in.readableBytes()) {
throw new DecoderException(
StringUtil.simpleClassName(getClass()) +
".decode() did not read anything but decoded a message.");
}
if (isSingleDecode()) {
break;
}
}
} catch (DecoderException e) {
throw e;
} catch (Throwable cause) {
throw new DecoderException(cause);
}
}
在callDecode方法中,使用while循环去解析数据,这是因为一次发送的数据有可能被解析成多个分段,对于循环中的每次解析,首先判断缓冲区中是否还有可读空间,有可读空间执行while循环体,第一次执行循环体的时候out肯定是没有数据的,也就是out的size为0,下面会调用解码逻辑decode(ctx, in, out)去进行数据的第一次解析,我们先不看decode(ctx, in, out)到底执行了什么逻辑,现在假设已经执行了一次decode(ctx, in, out)逻辑,也就是out里面有数据了,就是第一次解析出来的数据,现在out的size为1,现在进入outSize > 0逻辑判断中,可以看到这个if判断逻辑核心就是fireChannelRead(ctx, out, outSize),然后将out清空,size置位0,关于fireChannelRead方法,其实在上面刚才我们也说过了,就是调用下一个Handler处理器去执行下一步处理逻辑,我们再次跟进去。
static void fireChannelRead(ChannelHandlerContext ctx, List<Object> msgs, int numElements) {
if (msgs instanceof CodecOutputList) {
fireChannelRead(ctx, (CodecOutputList) msgs, numElements);
} else {
for (int i = 0; i < numElements; i++) {
ctx.fireChannelRead(msgs.get(i));
}
}
}
继续进入到fireChannelRead,来到AbstractChannelHandlerContext的fireChannelRead。
@Override
public ChannelHandlerContext fireChannelRead(final Object msg) {
//寻找下一个绑定的Handler并且调用ChannelRead方法
invokeChannelRead(findContextInbound(), msg);
return this;
}
继续往里走。
static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
next.invokeChannelRead(m);
} else {
executor.execute(() -> next.invokeChannelRead(m));
}
}
private void invokeChannelRead(Object msg) {
if (invokeHandler()) {
try {
((ChannelInboundHandler) handler()).channelRead(this, msg);
} catch (Throwable t) {
notifyHandlerException(t);
}
} else {
fireChannelRead(msg);
}
}
重点看((ChannelInboundHandler) handler()).channelRead(this, msg),这里将handler转成ChannelInboundHandler类型,执行他的channelRead方法,其实就是我们刚才服务端的重写的channelRead。
也就是说解析完一次数据时候立刻就去执行我们自定义的处理逻辑去了。当然在这里只是一个测试代码,如果真正我们自定义的处理逻辑比较复杂,比如涉及到操作数据库、磁盘IO等,可以考虑放入线程池中进行业务处理,避免造成解析数据缓慢,网络迟钝。
回到我们刚才while循环解析数据的逻辑,进入到decode(ctx, in, out)中。进入到其实现类LengthFieldBasedFrameDecoder中。
这里我们回顾一下LengthFieldBasedFrameDecoder中核心属性。
//=====================核心属性====================
//表示字节流的数据是大端还是小端,默认是大端
private final ByteOrder byteOrder;
//表示读取每帧数据的最大阈值
private final int maxFrameLength;
//表示长度域左边的偏移量
private final int lengthFieldOffset;
//表示长度域的长度
private final int lengthFieldLength;
//表示长度域左边的偏移量+长度域的长度
private final int lengthFieldEndOffset;
//表示长度域左边的偏移量+长度域的长度后面偏移量、调整量
private final int lengthAdjustment;
//表示跳过多少字节后就能拿到数据域
private final int initialBytesToStrip;
//表示是否开启快速失败机制
private final boolean failFast;
//表示是否开启丢弃模式、正在丢弃数据
private boolean discardingTooLongFrame;
//表示一共丢弃的字节数据
private long tooLongFrameLength;
//表示每一次丢弃的字节数据
private long bytesToDiscard;
核心属性都已标明注释,具体作用等下分析源码的时候再详细介绍。
接着进入到decode方法中去。
@Override
protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
Object decoded = decode(ctx, in);
if (decoded != null) {
out.add(decoded);
}
}
decoded就是解析完的对象,如果这个对象不为空,就放到out中去,再看decode方法。
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
//首先判断是否正在处于丢弃模式中,如果正在处于丢弃模式中,直接进行数据丢弃
if (discardingTooLongFrame) {
long bytesToDiscard = this.bytesToDiscard;
int localBytesToDiscard = (int) Math.min(bytesToDiscard, in.readableBytes());
//直接跳过指定的字节数据
in.skipBytes(localBytesToDiscard);
bytesToDiscard -= localBytesToDiscard;
this.bytesToDiscard = bytesToDiscard;
failIfNecessary(false);
}
// 如果当前可读字节还未达到lengthFieldEndOffset大小,说明数据域中并没有可读内容,此时直接返回null
if (in.readableBytes() < lengthFieldEndOffset) {
return null;
}
// 拿到实际的lengthFieldOffset
int actualLengthFieldOffset = in.readerIndex() + lengthFieldOffset;
// 拿到未调整过得数据包长度
long frameLength = getUnadjustedFrameLength(in, actualLengthFieldOffset, lengthFieldLength, byteOrder);
// 如果数据包长度为负数,直接跳过长度域并抛出异常
if (frameLength < 0) {
in.skipBytes(lengthFieldEndOffset);
throw new CorruptedFrameException("negative pre-adjustment length field: " + frameLength);
}
// 调整包的总长度
frameLength += lengthAdjustment + lengthFieldEndOffset;
// 包的总长度还没有长度域长,直接抛出异常
if (frameLength < lengthFieldEndOffset) {
in.skipBytes(lengthFieldEndOffset);
throw new CorruptedFrameException(
"Adjusted frame length (" + frameLength + ") is less " +
"than lengthFieldEndOffset: " + lengthFieldEndOffset);
}
// 数据包长度超出最大包长度,进入丢弃模式,总丢弃数据长度tooLongFrameLength
if (frameLength > maxFrameLength) {
long discard = frameLength - in.readableBytes();
tooLongFrameLength = frameLength;
if (discard < 0) {
// 缓冲区包含更多的字节,然后是frameLength,所以我们现在就可以丢弃所有字节
// 当前readableBytes达到frameLength,直接跳过frameLength个字节
in.skipBytes((int) frameLength);
} else {
// 当前可读字节未达到frameLength,说明后面未读到的字节也需要丢弃,进入丢弃模式
discardingTooLongFrame = true;
// bytesToDiscard表示还需要丢弃多少字节
bytesToDiscard = discard;
// 进入丢弃模式并丢弃到目前为止收到的所有内容.
in.skipBytes(in.readableBytes());
}
failIfNecessary(true);
return null;
}
// frameLength永远不会溢出,因为它小于maxFrameLength
int frameLengthInt = (int) frameLength;
if (in.readableBytes() < frameLengthInt) {
return null;
}
// 如果返回数据包之前跳过的字节都大于frameLengthInt,说明数据异常,跳过现在包中的数据
if (initialBytesToStrip > frameLengthInt) {
in.skipBytes(frameLengthInt);
throw new CorruptedFrameException(
"Adjusted frame length (" + frameLength + ") is less " +
"than initialBytesToStrip: " + initialBytesToStrip);
}
// 正常跳过initialBytesToStrip字节
in.skipBytes(initialBytesToStrip);
// 提取帧数据
int readerIndex = in.readerIndex();
int actualFrameLength = frameLengthInt - initialBytesToStrip;
ByteBuf frame = extractFrame(ctx, in, readerIndex, actualFrameLength);
// 调整读索引
in.readerIndex(readerIndex + actualFrameLength);
return frame;
}
这个方法的处理逻辑就是LengthFieldBasedFrameDecoder解码器的核心解码逻辑。
首先判断是否正在处于丢弃模式中,如果正在处于丢弃模式中,直接进行数据丢弃。bytesToDiscard代表的是每次需要丢弃的字节数据,直到bytesToDiscard为0标明,数据都已经丢弃完成。然后判断如果当前可读字节还未达到lengthFieldEndOffset大小,说明数据域中并没有可读内容,此时直接返回null。接着往下走拿到实际的lengthFieldOffset,然后通过getUnadjustedFrameLength方法计算未调整过得数据包长度frameLength,如果数据包frameLength的长度为负数,直接跳过长度域总长度lengthFieldEndOffset并抛出异常,然后通过frameLength += lengthAdjustment + lengthFieldEndOffset计算调整包的总长度,此时frameLength就是总的数据长度(长度域偏移量+长度域+调整长度+数据域长度)。如果此时包的总长度还没有长度域长,直接抛出异常,如果此时数据包长度超出最大包长度,进入丢弃模式相关逻辑,总丢弃数据长度tooLongFrameLength,如果当前readableBytes达到frameLength,直接跳过frameLength个字节,后面的字节无需丢弃,如果当前可读字节未达到frameLength,说明后面未读到的字节也需要丢弃,进入丢弃模式,并且将丢弃模式开启。最终返回null,这次解析完成。如果以上所有的过程都通过了,继续往下走,此时frameLength永远不会溢出,因为它小于maxFrameLength,如果此时frameLengthInt大于了可读字节,说明数据还不完整,直接返回null,期待下次发送数据,然后如果返回数据包之前跳过的字节都大于frameLengthInt,说明数据异常,跳过现在包中的数据,至此,所有异常情况都已经处理完成,接下来就是正常解码的逻辑,正常跳过initialBytesToStrip字节,然后开始提取帧数据从读索引开始读取实际的数据域长度,获得frame数据,最后返回键就行了。这样一次解析就完成了,接着循环往复解析并执行其他handler逻辑。
至此,服务端利用LengthFieldBasedFrameDecoder解码器解析数据的流程也就分析完了。
个人才疏学浅、信手涂鸦,netty框架更多模块解读相关源码持续更新中,感兴趣的朋友请移步至个人公众号,谢谢支持😜😜......
公众号:wenyixicodedog