说明
概要介绍
具体请参考Netty官网API中LengthFieldBasedFrameDecoder的说明:https://netty.io/4.1/api/index.html
io.netty.handler.codec.LengthFieldBasedFrameDecoder是一个ByteToMessageDecoder的实现类,它基于长度字段对收到的ByteBuf进行拆分。
在构建LengthFieldBasedFrameDecoder实例时,调用者可以输入下面参数,控制拆包的行为:
- ByteOrder:长度字段的字节序,有BIG_ENDIAN和LITTLE_ENDIAN两种情况。默认值是BIG_ENDIAN。当长度字段大于1个字节的时候有用。
- maxFrameLength:帧(frame)的最大字节数。
- lengthFieldOffset:长度字段的偏移字节数。是相对于帧的开始。
- lengthFieldLength:长度字段的字节数。只支持1个字节、2个字节、3个字节、4个字节、8个字节。LengthFieldBasedFrameDecoder的下面这个函数对长度字段的字节数做了校验:
protected long getUnadjustedFrameLength(ByteBuf buf, int offset, int length, ByteOrder order) {
buf = buf.order(order);
long frameLength;
switch (length) {
case 1:
frameLength = buf.getUnsignedByte(offset);
break;
case 2:
frameLength = buf.getUnsignedShort(offset);
break;
case 3:
frameLength = buf.getUnsignedMedium(offset);
break;
case 4:
frameLength = buf.getUnsignedInt(offset);
break;
case 8:
frameLength = buf.getLong(offset);
break;
default:
throw new DecoderException(
"unsupported lengthFieldLength: " + lengthFieldLength + " (expected: 1, 2, 3, 4, or 8)");
}
return frameLength;
}
- lengthAdjustment:默认值为0。增加到长度字段的值的补偿值。该补偿值=该帧中长度字段后面的字节数 - 长度字段的值。
- initialBytesToStrip:默认值为0。解包后的帧开头多少字节应该被去掉。
- failFast:默认值为true。当值为true的时候,解码器如果看到帧的长度大于maxFrameLength,就抛出TooLongFrameException异常,而不管帧后面的数据有没有实际读取。当值为false的时候,只有当解码器实际读取的字节数超过maxFrameLength时,才会抛出TooLongFrameException异常。
几种组合情况的示意性说明
开始是占2个字节的长度字段,长度字段的值表示实际内容的字节数,不去掉头部字段
lengthFieldOffset = 0
lengthFieldLength = 2
lengthAdjustment = 0
initialBytesToStrip = 0
解码前 (14 字节) 解码后 (14 字节)
+--------+----------------+ +--------+----------------+
|长度 | 实际内容 |----> |长度| 实际内容 |
| 0x000C | "HELLO, NETTY" | | 0x000C | "HELLO, NETTY" |
+--------+----------------+ +--------+----------------+
开始是占2个字节的长度字段,长度字段的值表示实际内容的字节数,去掉头部字段
lengthFieldOffset = 0
lengthFieldLength = 2
lengthAdjustment = 0
initialBytesToStrip = 2(长度字段的值)
解码前 (14 字节) 解码后 (12 字节)
+--------+----------------+ +----------------+
|长度 | 实际内容 |----> |长度| 实际内容 |
| 0x000C | "HELLO, NETTY" | | "HELLO, NETTY" |
+--------+----------------+ +----------------+
开始是占2个字节的长度字段,长度字段的值表示整个报文的字节数,不去掉头部字段
lengthFieldOffset = 0
lengthFieldLength = 2
lengthAdjustment = -2(长度字段的字节数)
initialBytesToStrip = 0
解码前 (14 字节) 解码后 (14 字节)
+--------+----------------+ +--------+----------------+
|长度 | 实际内容 |----> |长度| 实际内容 |
| 0x000E | "HELLO, NETTY" | | 0x000E | "HELLO, NETTY" |
+--------+----------------+ +--------+----------------+
开始是占2个字节头部字段,然后是占3个字节的长度字段,长度字段的值表示实际内容的字节数,不去掉头部字段
lengthFieldOffset = 2(头部字段占了2个字节)
lengthFieldLength = 3
lengthAdjustment = 0
initialBytesToStrip = 0
解码前 (17 字节) 解码后 (17 字节)
+----------+----------+----------------+ +----------+----------+----------------+
| 头部字段 | 长度 | 实际内容 |----->| 头部字段 | 长度 | 实际内容 |
| 0x6801 | 0x00000C | "HELLO, NETTY" | | 0x6801 | 0x00000C | "HELLO, NETTY" |
+----------+----------+----------------+ +----------+----------+----------------+
开始是占3个字节的长度字段,然后是占2个字节的头部,长度字段的值表示实际内容的字节数,不去掉头部字段
lengthFieldOffset = 0
lengthFieldLength = 3
lengthAdjustment = 2(头部字段占用2个字节)
initialBytesToStrip = 0
解码前 (17 字节) 解码后 (17 字节)
+----------+----------+----------------+ +----------+----------+----------------+
| 长度 | 头部字段 | 实际内容 |----->| 长度 | 头部字段 | 实际内容 |
| 0x00000C | 0x6801 | "HELLO, NETTY" | | 0x00000C | 0x6801 | "HELLO, NETTY" |
+----------+----------+----------------+ +----------+----------+----------------+
开始是占1个字节的头部1,然后是占2个字节的长度字段,然后是占1个字节的头部2,长度字段的值表示实际内容的字节数,去掉头部1和长度字段
lengthFieldOffset = 1(头部1占1个字节)
lengthFieldLength = 2
lengthAdjustment = 1(头部2占1个字节)
initialBytesToStrip = 3(头部1+长度字段合起来占3个字节)
解码前 (16 字节) 解码后 (13 字节)
+------+--------+------+----------------+ +------+----------------+
| 头部1 | 长度 |头部2 | 实际内容 |----->|头部2 | 实际内容 |
| 0x68 | 0x000C | 0x01 | "HELLO, WORLD" | | 0x01 | "HELLO, WORLD" |
+------+--------+------+----------------+ +------+----------------+
开始是占1个字节的头部1,然后是占2个字节的长度字段,然后是占1个字节的头部2,长度字段的值表示整个报文的字节数,去掉头部1和长度字段
lengthFieldOffset = 1(头部1占1个字节)
lengthFieldLength = 2
lengthAdjustment = -3(头部1 和 长度字段合起来占3个字节)
initialBytesToStrip = 3(头部1+长度字段合起来占3个字节)
解码前 (16 字节) 解码后 (13 字节)
+------+--------+------+----------------+ +------+----------------+
| 头部1| 长度 |头部2 | 实际内容 |----->|头部2 | 实际内容 |
| 0x68 | 0x0010 | 0x01 | "HELLO, WORLD" | | 0x01 | "HELLO, WORLD" |
+------+--------+------+----------------+ +------+----------------+
代码示例
示例:用LengthFieldBasedFrameDecoder基于长度字段将整个帧解析下来,传递给后端的处理业务逻辑的ChannelHandler
本示例验证场景:
从客户端发送一个12个字节的业务报文给服务端。服务端用LengthFieldBasedFrameDecoder将整个报文收取下来,传递给后续的处理业务逻辑的ChannelHandler。
发送的报文各字节介绍:
第1个字节:头部字段1
第2个字节:头部字段2
第3个字节:长度字段,值为6,指明实际内容的长度
第4-9个字节:共6个字节,代表实际内容
第10-11字节:共2个字节,尾部字段1
第12个字节:尾部字段2
服务端代码片段
package com.thb.power.server;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
/**
* 服务端的主函数
* @author thb
*
*/
public class MainStation {
private static final Logger logger = LogManager.getLogger();
static final int PORT = Integer.parseInt(System.getProperty("port", "22335"));
public static void main(String[] args) throws Exception {
logger.traceEntry();
// 配置服务器
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new MainStationInitializer());
// 启动服务端
ChannelFuture f = b.bind(PORT).sync();
// 等待直到server socket关闭
f.channel().closeFuture().sync();
} finally {
// 关闭所有event loops以便终止所有的线程
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
logger.traceExit();
}
}
package com.thb.power.server;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.thb.power.server.register.ServerRegisterRequestHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
public class MainStationInitializer extends ChannelInitializer<SocketChannel> {
private static final Logger logger = LogManager.getLogger();
@Override
public void initChannel(SocketChannel ch) throws Exception {
logger.traceEntry();
ChannelPipeline p = ch.pipeline();
p.addLast(new LoggingHandler(LogLevel.INFO));
p.addLast(new LengthFieldBasedFrameDecoder(12, 2, 1, 3, 0));
p.addLast(new ServerRegisterRequestHandler());
logger.traceExit();
}
}
package com.thb.power.server.register;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
public class ServerRegisterRequestHandler extends ChannelInboundHandlerAdapter {
private static final Logger logger = LogManager.getLogger();
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
logger.traceEntry();
ByteBuf m = (ByteBuf)msg;
logger.info("readableBytes: " + m.readableBytes());
logger.traceExit();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
运行服务端
运行客户端,并向服务端发送总长度为12个字节的报文
查看服务端的输出
从输出可以看出,LengthFieldBasedFrameDecoder后面的ServerRegisterRequestHandler收到了12个字节的数据,解析成功。