文章目录
ByteBuf
byteBuf的引用与释放
Netty的halo world
netty的pipeline与handler
netty的编解码器
StringEncoder&StringDecoder
LineBasedFrameDecoder
FixedLengthFrameDecoder
LengthFieldBaseFrameDecoder
官方示例
/*
【示例1】长度字节偏移量为0,长度字节个数为2,不剥离头部。
解析:
lengthField的值为12(0x0c),它代表后面实际内容的长度有12字节。
默认情况下,该解码器认为此长度字段的字节值代表跟在该长度字段字节后面的字节数量。
因此,它可以通过以下参数的组合来进行解码:
lengthFieldOffset = 0 长度字节所处偏移量
lengthFieldLength = 2 长度字节的个数
lengthAdjustment = 0(暂未用到,默认给0)
initialBytesToStrip = 0 (= do not strip header)
解码前 (14 bytes) 解码后 (14 bytes)
+--------+----------------+ +--------+----------------+
| Length | Actual Content |----->| Length | Actual Content |
| 0x000C | "HELLO, WORLD" | | 0x000C | "HELLO, WORLD" |
+--------+----------------+ +--------+----------------+
*/
/*
【示例2】长度字节偏移量为0,长度字节个数为2,剥离头部。
解析:
因为,我们可以调用ByteBuf#readableBytes()来获取内容的长度,
所以你可能想要剥离长度字段,通过指定initialBytesToStrip参数来实现。
在这个示例中,我们指定了2,这个2跟长度字段的长度是相等的,目的就是去掉头部的2个字节。
lengthFieldOffset = 0 长度字节所处偏移量
lengthFieldLength = 2 长度字节的个数
lengthAdjustment = 0 (暂未用到,默认给0)
initialBytesToStrip = 2 (长度字段的长度)
解码前 (14 bytes) 解码后 (12 bytes)
+--------+----------------+ +----------------+
| Length | Actual Content |----->| Actual Content |
| 0x000C | "HELLO, WORLD" | | "HELLO, WORLD" |
+--------+----------------+ +----------------+
*/
/*
【示例3】长度字节偏移量为0,长度字节个数为2,不剥离头,长度字段的字节值代表的是整个消息的长度
解析:
在大部分情况下,长度字段的字节值仅代表消息体的长度,就像前面的示例一样。
然而,在一些协议中,长度字段的字节值代表整个消息的长度,包括消息头。
在这样的例子中,我们可以通过指定lengthAdjustment参数来作调整,
因为这个长度字段的字节值都比实际内容的长度要大2,我们可以指定lengthAdjustment参数为-2来作为补偿。
(可以这么去理解:通过长度字段的偏移值和长度值段的长度,来得到长度字段的字节值。
但是获取到的这个长度字段的字节值在默认情况下,它代表实际内容的长度,
但是显然,这里的长度字段的字节值并不是实际内容的长度,
因此,需要对获取到的这个长度字段的字节值作1个调整,
因此,长度字段的字节值减去2刚刚好就是实际内容的长度,
因此,要设置lengthAdjustment为-2)
lengthFieldOffset = 0
lengthFieldLength = 2
lengthAdjustment = -2
initialBytesToStrip = 0
解码前 (14 bytes) 解码后 (14 bytes)
+--------+----------------+ +--------+----------------+
| Length | Actual Content |----->| Length | Actual Content |
| 0x000E | "HELLO, WORLD" | | 0x000E | "HELLO, WORLD" |
+--------+----------------+ +--------+----------------+
*/
/*
【示例4】长度字节偏移量为2,长度字节个数为3,不剥离头
解析:
下面的消息是示例1的变种,消息的最前面还附加了2个字节长度的消息头。
lengthAdjustment仍然设置为0,因为该解码器在帧长计算中都会把附加数据的长度考虑进去。
lengthFieldOffset = 2
lengthFieldLength = 3
lengthAdjustment = 0
initialBytesToStrip = 0
解码前 (17 bytes) 解码后 (17 bytes)
+----------+----------+----------------+ +----------+----------+----------------+
| Header 1 | Length | Actual Content |----->| Header 1 | Length | Actual Content |
| 0xCAFE | 0x00000C | "HELLO, WORLD" | | 0xCAFE | 0x00000C | "HELLO, WORLD" |
+----------+----------+----------------+ +----------+----------+----------------+
*/
/*
【示例5】头部有5个字节,其中开始的3个字节为长度字段,不剥离头部
解析:
在这个示例中,长度字段和实际内容的中间夹杂了个消息头。
因此,必须为lengthAdjustment参数指定1个正数,以便于该解码器在帧长计算中把这个中间夹的消息头也考虑进去。
lengthFieldOffset = 0
lengthFieldLength = 3
lengthAdjustment = 2 (长度字段和实际内容的中间夹杂的字节长度)
initialBytesToStrip = 0
解码前 (17 bytes) 解码后 (17 bytes)
+----------+----------+----------------+ +----------+----------+----------------+
| Length | Header 1 | Actual Content |----->| Length | Header 1 | Actual Content |
| 0x00000C | 0xCAFE | "HELLO, WORLD" | | 0x00000C | 0xCAFE | "HELLO, WORLD" |
+----------+----------+----------------+ +----------+----------+----------------+
*/
/*
【示例6】长度字节偏移量为1,长度字节个数为2。长度字段前后各1个字节,一共4个字节组成消息的头部。剥离长度字段前面的1个字节和长度字段字节。
解析:
这是上面示例的1个综合示例。
在长度字段前面有1个附加的1个字节(hdr1),长度字段后面与消息体中间也有1个额外的1个字节(hdr2)。
前面附加的这个字节影响的是lengthFieldOffset-长度字段偏移量参数,长度字段后面与消息体中间额外的字节影响的是lengthAdjustment-长度调整参数。
我们在这里指定了initialBytesToStrip为3,从帧数据中,剥离掉附加的1个字节和长度字段的2个字节。
lengthFieldOffset = 1(hdr1的长度)
lengthFieldLength = 2
lengthAdjustment = 1(hdr2的长度)
initialBytesToStrip = 3(hdr1 + len的长度)
解码前 (16 bytes) 解码后 (13 bytes)
+------+--------+------+----------------+ +------+----------------+
| HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
| 0xCA | 0x000C | 0xFE | "HELLO, WORLD" | | 0xFE | "HELLO, WORLD" |
+------+--------+------+----------------+ +------+----------------+
*/
/*
【示例7】长度字节偏移量为1,长度字段字节个数为2。长度字段前后各1个字节,一共4个字节组成消息的头部。剥离头部的1个字节和长度字段的2个字节。其中长度字段的字节值代表的是整个消息的长度。
解析:
让我们看1个与上面案例的变体,唯一的不同点在于长度字段的字节值代表的是整个消息的长度而不是消息体的长度,就像示例3一样。
我们必须把hdr1的长度和长度字段的字节长度考虑进去,来设置lengthAdjustment(调整长度)。
请注意:我们不需要考虑hdr2的长度,因为长度字段早就包含了整个头部的长度。
lengthFieldOffset = 1
lengthFieldLength = 2
lengthAdjustment = -3(hdr1 + 长度字段的长度,要减去它们,所以是负数)
initialBytesToStrip = 3
解码前 (16 bytes) (16 bytes) 解码后 (13 bytes)
+------+--------+------+----------------+ +------+----------------+
| HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
| 0xCA | 0x0010 | 0xFE | "HELLO, WORLD" | | 0xFE | "HELLO, WORLD" |
+------+--------+------+----------------+ +------+----------------+
*/
真实案例
netty服务器代码
NettyServer
@Slf4j
public class NettyServer {
public void run() {
NioEventLoopGroup boss = new NioEventLoopGroup(1);
NioEventLoopGroup worker = new NioEventLoopGroup(16);
ServerBootstrap serverBootstrap = new ServerBootstrap()
.group(boss, worker)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler(LogLevel.INFO));
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 3, 2, -3, 7));
ch.pipeline().addLast(new DiscardTailTwoByteHandler());
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 1, 2, -3, 3));
ch.pipeline().addLast(new SecondInBoundHandler());
}
});
try {
ChannelFuture channelFuture = serverBootstrap.bind(new InetSocketAddress(8080)).sync();
log.info("=======服务器启动成功=======");
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
public static void main(String[] args) {
new DiscardServer().run();
}
}
DiscardTailTwoByteHandler
@Slf4j
public class DiscardTailTwoByteHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("DiscardTailTwoByteHandler#channelRead");
if (msg instanceof ByteBuf) {
log.info("DiscardTailTwoByteHandler#channelRead-byteBuf");
ByteBuf byteBuf = (ByteBuf) msg;
try {
ByteBuf newByteBuf = byteBuf.readRetainedSlice(byteBuf.readableBytes() - 2);
ctx.fireChannelRead(newByteBuf);
} finally {
log.info("DiscardTailTwoByteHandler#channelRead-release");
byteBuf.release();
}
} else {
ctx.fireChannelRead(msg);
}
}
}
SecondInBoundHandler
@Slf4j
public class SecondInBoundHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
log.info("收到消息: {}", ByteBufUtil.hexDump(byteBuf));
log.info("收到消息2: {}", new String(ByteBufUtil.getBytes(byteBuf)));
}
}
NettyClient
@Slf4j
public class NettyClient {
public static void main(String[] args) throws InterruptedException {
Channel channel = new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
}
})
.connect("127.0.0.1", 8080)
.sync()
.channel();
log.info("=======客户端连接服务器成功=======");
Scanner sc = new Scanner(System.in);
while (true) {
System.out.print("输入:");
String line = sc.nextLine();
if ("end".equals(line)) {
channel.close().sync();
break;
}
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer();
if ("1".equals(line)) {
// 16进制数据
byte[] bytes = new byte[]{
(byte) 0x46,(byte) 0x4d,(byte) 0x53,(byte) 0x00,(byte) 0x1c,(byte) 0xFF,(byte) 0xFF,(byte) 0x82,(byte) 0x00,(byte) 0x07,(byte) 0xa1,(byte) 0x10,(byte) 0xe0,(byte) 0xa1,(byte) 0x82,(byte) 0x00,(byte) 0x07,(byte) 0xa1,(byte) 0x10,(byte) 0xe0,(byte) 0xa1,(byte) 0x82,(byte) 0x00,(byte) 0x07,(byte) 0xa1,(byte) 0x10,(byte) 0xe0,(byte) 0xa1,(byte) 0x23,(byte) 0xa5,};
byteBuf.writeBytes(bytes);
channel.writeAndFlush(byteBuf);
System.out.println("=======发送成功=======");
}
}
}
}
测试
可以看到,最后的业务处理器,在客户端发送了1次消息后,收到了3次方法调用
[2023-12-09 15:09:05] [INFO ] [main] c.z.t.server.DiscardServer [41] - =======服务器启动成功=======
[2023-12-09 15:09:17] [INFO ] [nioEventLoopGroup-3-1] i.n.h.logging.LoggingHandler [148] - [id: 0x89cd300a, L:/127.0.0.1:8080 - R:/127.0.0.1:52484] REGISTERED
[2023-12-09 15:09:17] [INFO ] [nioEventLoopGroup-3-1] i.n.h.logging.LoggingHandler [148] - [id: 0x89cd300a, L:/127.0.0.1:8080 - R:/127.0.0.1:52484] ACTIVE
[2023-12-09 15:09:19] [INFO ] [nioEventLoopGroup-3-1] i.n.h.logging.LoggingHandler [148] - [id: 0x89cd300a, L:/127.0.0.1:8080 - R:/127.0.0.1:52484] READ: 30B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 46 4d 53 00 1c ff ff 82 00 07 a1 10 e0 a1 82 00 |FMS.............|
|00000010| 07 a1 10 e0 a1 82 00 07 a1 10 e0 a1 23 a5 |............#. |
+--------+-------------------------------------------------+----------------+
[2023-12-09 15:09:19] [INFO ] [nioEventLoopGroup-3-1] c.z.t.s.DiscardTailTwoByteHandler [13] - DiscardTailTwoByteHandler#channelRead
[2023-12-09 15:09:19] [INFO ] [nioEventLoopGroup-3-1] c.z.t.s.DiscardTailTwoByteHandler [15] - DiscardTailTwoByteHandler#channelRead-byteBuf
[2023-12-09 15:09:19] [INFO ] [nioEventLoopGroup-3-1] c.z.t.s.SecondInBoundHandler [17] - 收到消息: a110e0a1
[2023-12-09 15:09:19] [INFO ] [nioEventLoopGroup-3-1] c.z.t.s.SecondInBoundHandler [17] - 收到消息: a110e0a1
[2023-12-09 15:09:19] [INFO ] [nioEventLoopGroup-3-1] c.z.t.s.SecondInBoundHandler [17] - 收到消息: a110e0a1
[2023-12-09 15:09:19] [INFO ] [nioEventLoopGroup-3-1] c.z.t.s.DiscardTailTwoByteHandler [21] - DiscardTailTwoByteHandler#channelRead-release
[2023-12-09 15:09:19] [INFO ] [nioEventLoopGroup-3-1] i.n.h.logging.LoggingHandler [148] - [id: 0x89cd300a, L:/127.0.0.1:8080 - R:/127.0.0.1:52484] READ COMPLETE
ByteToMessageFrameDecoder
LengthFieldBasedFrameDecoder源码分析
下面以LengthFieldBasedFrameDecoder源码注释中的最后1个示例,调试代码,来看下netty是如何使用LengthFieldBasedFrameDecoder解码器实现解码的过程
NettyServer代码
@Slf4j
public class NettyServer10 {
public static void main(String[] args) {
NioEventLoopGroup boss = new NioEventLoopGroup(1);
NioEventLoopGroup worker = new NioEventLoopGroup(16);
ServerBootstrap serverBootstrap = new ServerBootstrap()
.group(boss, worker)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 1, 2, -3, 4));
ch.pipeline().addLast(new SecondInBoundHandler());
}
});
try {
ChannelFuture channelFuture = serverBootstrap.bind(new InetSocketAddress(8080)).sync();
log.info("=======服务器启动成功=======");
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
NettyClient代码
输入1,发送头部,输入2,发送消息。将1次消息拆分成2个步骤发送,观察基于长度字段解码器的处理过程。
@Slf4j
public class NettyClient10 {
public static void main(String[] args) throws Exception {
Channel channel = new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
}
})
.connect("127.0.0.1", 8080)
.sync()
.channel();
log.info("=======客户端连接服务器成功=======");
Scanner sc = new Scanner(System.in);
/*
lengthFieldOffset = 1
lengthFieldLength = 2
lengthAdjustment = -3(hdr1 + 长度字段的长度,要减去它们,所以是负数)
initialBytesToStrip = 3
解码前 (16 bytes) (16 bytes) 解码后 (13 bytes)
+------+--------+------+----------------+ +------+----------------+
| HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
| 0xCA | 0x0010 | 0xFE | "HELLO, WORLD" | | 0xFE | "HELLO, WORLD" |
+------+--------+------+----------------+ +------+----------------+
*/
while (true) {
System.out.print("输入:");
String line = sc.nextLine();
if (line == null || line.length() == 0) {
continue;
}
if ("end".equals(line)) {
channel.close().sync();
break;
}
// 输入内容
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer();
if ("1".equals(line)) {
// 长度字段字节数据计算(内容的长度 + hdr1的长度 + length的长度 + hdr2的长度)
ByteBuf contentLengthByteBuf = ByteBufAllocator.DEFAULT.buffer().writeShort("hello, world".getBytes().length + 4);
// 发送头部(HDR1 + Length + HDR2)
byteBuf.writeBytes(new byte[]{
(byte) 0xCA,
contentLengthByteBuf.getByte(0),
contentLengthByteBuf.getByte(1),
(byte) 0xFE,
});
contentLengthByteBuf.release();
channel.writeAndFlush(byteBuf);
} else if ("2".equals(line)) {
byteBuf.writeBytes("hello, world".getBytes());
channel.writeAndFlush(byteBuf);
}
System.out.println("=======发送成功=======");
}
}
}
DelimiterBasedFrameDecoder
与LineBasedFrameDecoder的区别在于,它可以指定特定的字符作为消息的分隔符。它会根据分隔符拆分消息,然后将拆分得到的消息传给后面的处理器处理。
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(Integer.MAX_VALUE, Unpooled.wrappedBuffer("halo".getBytes())));
Handler线程安全问题(@Sharable)
当客户端连接上服务器后,服务端可以给channel的pipeline上添加各种不同的handler,这些handler可以是共享的,也可以是直接创建新的。这取决于handler是否存在状态变量,也即会不会出现线程安全问题。一般情况下,在handler存在状态变量的情况下,是不应该被共享的,但是我们可以借助AttributeKey将状态变量绑定到channel上,将此handler做成共享的
我们可以使用@Sharable注解来标识某个handler是可以被共享的,也就是可以全局使用同一个handler。但是如果1个handler被添加到了多个SocketChannel的pipeline中,那么这个handler必须使用@Sharable注解,否则会报错