组件
本博客是根据黑马程序员Netty实战 学习时所做的笔记
1、EventLoop 事件循环对象 EventLoop
EventLoop 本质是一个单线程执行器 (同时维护了一个 Selector ),里面有 run 方法处理一个或多个 Channel 上源源不断的 io 事件
它的继承关系如下
继承自 j.u.c.ScheduledExecutorService 因此包含了线程池中所有的方法
继承自 netty 自己的 OrderedEventExecutor
提供了 boolean inEventLoop(Thread thread) 方法判断一个线程是否属于此 EventLoop
提供了 EventLoopGroup parent() 方法来看看自己属于哪个 EventLoopGroup
事件循环组 EventLoopGroup
EventLoopGroup 是一组 EventLoop,Channel 一般会调用 EventLoopGroup 的 register 方法来绑定其中一个 EventLoop,后续这个 Channel 上的 io 事件都由此 EventLoop 来处理(保证了 io 事件处理时的线程安全)
继承自 netty 自己的 EventExecutorGroup
实现了 Iterable 接口提供遍历 EventLoop 的能力
另有 next 方法获取集合中下一个 EventLoop
处理普通与定时任务 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class TestEventLoop { public static void main (String[] args) { EventLoopGroup group = new NioEventLoopGroup (2 ); System.out.println(group.next()); System.out.println(group.next()); group.next().execute(()->{ System.out.println(Thread.currentThread().getName() + " hello" ); }); group.next().scheduleAtFixedRate(()->{ System.out.println(Thread.currentThread().getName() + " hello2" ); }, 0 , 1 , TimeUnit.SECONDS); group.shutdownGracefully(); } }
输出结果如下
1 2 3 4 5 6 io.netty.channel.nio.NioEventLoop@7bb11784 io.netty.channel.nio.NioEventLoop@33a10788 nioEventLoopGroup-2 -1 hello nioEventLoopGroup-2 -2 hello2 nioEventLoopGroup-2 -2 hello2 nioEventLoopGroup-2 -2 hello2
关闭 EventLoopGroup
优雅关闭 shutdownGracefully
方法。该方法会首先切换 EventLoopGroup
到关闭状态从而拒绝新的任务的加入,然后在任务队列的任务都处理完成后,停止线程的运行。从而确保整体应用是在正常有序的状态下退出的
处理IO任务 服务器代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class MyServer { public static void main (String[] args) { new ServerBootstrap () .group(new NioEventLoopGroup ()) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer <SocketChannel>() { @Override protected void initChannel (SocketChannel socketChannel) throws Exception { socketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter () { @Override public void channelRead (ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf) msg; System.out.println(Thread.currentThread().getName() + " " + buf.toString(StandardCharsets.UTF_8)); } }); } }) .bind(8080 ); } }
客户端代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class MyClient { public static void main (String[] args) throws IOException, InterruptedException { Channel channel = new Bootstrap () .group(new NioEventLoopGroup ()) .channel(NioSocketChannel.class) .handler(new ChannelInitializer <SocketChannel>() { @Override protected void initChannel (SocketChannel socketChannel) throws Exception { socketChannel.pipeline().addLast(new StringEncoder ()); } }) .connect(new InetSocketAddress ("localhost" , 8080 )) .sync() .channel(); System.out.println(channel); System.in.read(); } }
分工 Bootstrap的group()方法可以传入两个EventLoopGroup参数 ,分别负责处理不同的事件
1 2 3 4 5 6 7 8 9 public class MyServer { public static void main (String[] args) { new ServerBootstrap () .group(new NioEventLoopGroup (1 ), new NioEventLoopGroup (2 )) ... } }
多个客户端分别发送 hello
结果
1 2 3 4 5 nioEventLoopGroup-3 -1 hello1 nioEventLoopGroup-3 -2 hello2 nioEventLoopGroup-3 -1 hello3 nioEventLoopGroup-3 -2 hello4 nioEventLoopGroup-3 -2 hello4
可以看出,一个EventLoop可以负责多个 Channel,且EventLoop一旦与Channel绑定,则一直负责 处理该Channel中的事件
增加自定义EventLoopGroup 当有的任务需要较长的时间处理时,可以使用非NioEventLoopGroup ,避免同一个NioEventLoop中的其他Channel在较长的时间内都无法得到处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class MyServer { public static void main (String[] args) { EventLoopGroup group = new DefaultEventLoopGroup (); new ServerBootstrap () .group(new NioEventLoopGroup (1 ), new NioEventLoopGroup (2 )) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer <SocketChannel>() { @Override protected void initChannel (SocketChannel socketChannel) throws Exception { socketChannel.pipeline().addLast("nioHandler" ,new ChannelInboundHandlerAdapter () { @Override public void channelRead (ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf) msg; System.out.println(Thread.currentThread().getName() + " " + buf.toString(StandardCharsets.UTF_8)); ctx.fireChannelRead(msg); } }) .addLast(group, "myHandler" , new ChannelInboundHandlerAdapter () { @Override public void channelRead (ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf) msg; System.out.println(Thread.currentThread().getName() + " " + buf.toString(StandardCharsets.UTF_8)); } }); } }) .bind(8080 ); } }
启动四个客户端发送数据
1 2 3 4 5 6 7 8 nioEventLoopGroup-4 -1 hello1 defaultEventLoopGroup-2 -1 hello1 nioEventLoopGroup-4 -2 hello2 defaultEventLoopGroup-2 -2 hello2 nioEventLoopGroup-4 -1 hello3 defaultEventLoopGroup-2 -3 hello3 nioEventLoopGroup-4 -2 hello4 defaultEventLoopGroup-2 -4 hello4
可以看出,客户端与服务器之间的事件,被nioEventLoopGroup和defaultEventLoopGroup分别处理
切换的实现 不同的EventLoopGroup切换的实现原理如下
由上面的图可以看出,当handler中绑定的Group不同时,需要切换Group来执行不同的任务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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(new Runnable () { public void run () { next.invokeChannelRead(m); } }); } }
如果两个 handler 绑定的是同一个EventLoopGroup ,那么就直接调用
否则,把要调用的代码封装为一个任务对象,由下一个 handler 的 EventLoopGroup 来调用
2、Channel Channel 的常用方法
close() 可以用来关闭Channel
closeFuture() 用来处理 Channel 的关闭
sync 方法作用是同步等待 Channel 关闭
而 addListener 方法是异步等待 Channel 关闭
pipeline() 方法用于添加处理器
write() 方法将数据写入
因为缓冲机制,数据被写入到 Channel 中以后,不会立即被发送
只有当缓冲满了或者调用了flush()方法后 ,才会将数据通过 Channel 发送出去
writeAndFlush() 方法将数据写入并立即发送(刷出)
ChannelFuture 连接问题 拆分客户端代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class MyClient { public static void main (String[] args) throws IOException, InterruptedException { ChannelFuture channelFuture = new Bootstrap () .group(new NioEventLoopGroup ()) .channel(NioSocketChannel.class) .handler(new ChannelInitializer <SocketChannel>() { @Override protected void initChannel (SocketChannel socketChannel) throws Exception { socketChannel.pipeline().addLast(new StringEncoder ()); } }) .connect(new InetSocketAddress ("localhost" , 8080 )); channelFuture.sync(); Channel channel = channelFuture.channel(); channel.writeAndFlush("hello world" ); System.in.read(); } }
如果我们去掉channelFuture.sync()
方法,会服务器无法收到hello world
这是因为建立连接(connect)的过程是 异步非阻塞 的,若不通过sync()
方法阻塞主线程,等待连接真正建立,这时通过 channelFuture.channel() 拿到的 Channel 对象,并不是真正与服务器建立好连接的 Channel ,也就没法将信息正确的传输给服务器端
所以需要通过channelFuture.sync()
方法,阻塞主线程,同步处理结果 ,等待连接真正建立好以后,再去获得 Channel 传递数据。使用该方法,获取 Channel 和发送数据的线程 都是主线程
下面还有一种方法,用于 异步 获取建立连接后的 Channel 和发送数据,使得执行这些操作的线程是 NIO 线程(去执行connect操作的线程)
addListener方法
通过这种方法可以在NIO线程中获取 Channel 并发送数据 ,而不是在主线程中执行这些操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class MyClient { public static void main (String[] args) throws IOException, InterruptedException { ChannelFuture channelFuture = new Bootstrap () .group(new NioEventLoopGroup ()) .channel(NioSocketChannel.class) .handler(new ChannelInitializer <SocketChannel>() { @Override protected void initChannel (SocketChannel socketChannel) throws Exception { socketChannel.pipeline().addLast(new StringEncoder ()); } }) .connect(new InetSocketAddress ("localhost" , 8080 )); channelFuture.addListener(new ChannelFutureListener () { @Override public void operationComplete (ChannelFuture channelFuture) throws Exception { Channel channel = channelFuture.channel(); channel.writeAndFlush("hello world" ); } }); System.in.read(); } }
处理关闭 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public class ReadClient { public static void main (String[] args) throws InterruptedException { NioEventLoopGroup group = new NioEventLoopGroup (); ChannelFuture channelFuture = new Bootstrap () .group(group) .channel(NioSocketChannel.class) .handler(new ChannelInitializer <SocketChannel>() { @Override protected void initChannel (SocketChannel socketChannel) throws Exception { socketChannel.pipeline().addLast(new StringEncoder ()); } }) .connect(new InetSocketAddress ("localhost" , 8080 )); channelFuture.sync(); Channel channel = channelFuture.channel(); Scanner scanner = new Scanner (System.in); new Thread (()->{ while (true ) { String msg = scanner.next(); if ("q" .equals(msg)) { channel.close(); break ; } channel.writeAndFlush(msg); } }, "inputThread" ).start(); ChannelFuture closeFuture = channel.closeFuture(); System.out.println("waiting close..." ); closeFuture.sync(); System.out.println("关闭之后执行一些额外操作..." ); group.shutdownGracefully(); } }
关闭channel
当我们要关闭channel时,可以调用channel.close()方法进行关闭。但是该方法也是一个异步方法 。真正的关闭操作并不是在调用该方法的线程中执行的,而是在NIO线程中执行真正的关闭操作
如果我们想在channel真正关闭以后 ,执行一些额外的操作,可以选择以下两种方法来实现
3、Future与Promise 概念 netty 中的 Future 与 jdk 中的 Future 同名 ,但是是两个接口
netty 的 Future 继承自 jdk 的 Future,而 Promise 又对 netty Future 进行了扩展
jdk Future 只能同步等待任务结束(或成功、或失败)才能得到结果
netty Future 可以同步等待任务结束得到结果,也可以异步方式得到结果,但都是要等任务结束
netty Promise 不仅有 netty Future 的功能,而且脱离了任务独立存在,只作为两个线程间传递结果的容器
功能/名称
jdk Future
netty Future
Promise
cancel
取消任务
-
-
isCanceled
任务是否取消
-
-
isDone
任务是否完成,不能区分成功失败
-
-
get
获取任务结果,阻塞等待
-
-
getNow
-
获取任务结果,非阻塞,还未产生结果时返回 null
-
await
-
等待任务结束,如果任务失败,不会抛异常 ,而是通过 isSuccess 判断
-
sync
-
等待任务结束,如果任务失败,抛出异常
-
isSuccess
-
判断任务是否成功
-
cause
-
获取失败信息,非阻塞,如果没有失败,返回null
-
addLinstener
-
添加回调,异步接收结果
-
setSuccess
-
-
设置成功结果
setFailure
-
-
设置失败结果
JDK Future 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class JdkFuture { public static void main (String[] args) throws ExecutionException, InterruptedException { ThreadFactory factory = new ThreadFactory () { @Override public Thread newThread (Runnable r) { return new Thread (r, "JdkFuture" ); } }; ThreadPoolExecutor executor = new ThreadPoolExecutor (5 , 10 ,10 , TimeUnit.SECONDS, new ArrayBlockingQueue <>(10 ), factory); Future<Integer> future = executor.submit(new Callable <Integer>() { @Override public Integer call () throws Exception { TimeUnit.SECONDS.sleep(1 ); return 50 ; } }); System.out.println(future.get()); } }
Netty Future 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class NettyFuture { public static void main (String[] args) throws ExecutionException, InterruptedException { NioEventLoopGroup group = new NioEventLoopGroup (); EventLoop eventLoop = group.next(); Future<Integer> future = eventLoop.submit(new Callable <Integer>() { @Override public Integer call () throws Exception { return 50 ; } }); System.out.println(Thread.currentThread().getName() + " 获取结果" ); System.out.println("getNow " + future.getNow()); System.out.println("get " + future.get()); future.addListener(new GenericFutureListener <Future<? super Integer>>() { @Override public void operationComplete (Future<? super Integer> future) throws Exception { System.out.println(Thread.currentThread().getName() + " 获取结果" ); System.out.println("getNow " + future.getNow()); } }); } }
运行结果
1 2 3 4 5 main 获取结果 getNow null get 50 nioEventLoopGroup-2-1 获取结果 getNow 50
Netty中的Future对象,可以通过EventLoop的sumbit()方法得到
可以通过Future对象的get方法 ,阻塞地获取返回结果
也可以通过getNow方法 ,获取结果,若还没有结果,则返回null,该方法是非阻塞的
还可以通过future.addListener方法 ,在Callable方法执行的线程中,异步获取返回结果
Netty Promise Promise相当于一个容器,可以用于存放各个线程中的结果,然后让其他线程去获取该结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class NettyPromise { public static void main (String[] args) throws ExecutionException, InterruptedException { NioEventLoopGroup group = new NioEventLoopGroup (); EventLoop eventLoop = group.next(); DefaultPromise<Integer> promise = new DefaultPromise <>(eventLoop); new Thread (()->{ try { TimeUnit.SECONDS.sleep(1 ); } catch (InterruptedException e) { e.printStackTrace(); } promise.setSuccess(50 ); }).start(); System.out.println(Thread.currentThread().getName() + " " + promise.get()); } }
4、Handler与Pipeline Pipeline 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 public class PipeLineServer { public static void main (String[] args) { new ServerBootstrap () .group(new NioEventLoopGroup ()) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer <SocketChannel>() { @Override protected void initChannel (SocketChannel socketChannel) throws Exception { socketChannel.pipeline().addLast("handler1" ,new ChannelInboundHandlerAdapter () { @Override public void channelRead (ChannelHandlerContext ctx, Object msg) throws Exception { System.out.println(Thread.currentThread().getName() + " Inbound handler 1" ); super .channelRead(ctx, msg); } }); socketChannel.pipeline().addLast("handler2" , new ChannelInboundHandlerAdapter () { @Override public void channelRead (ChannelHandlerContext ctx, Object msg) throws Exception { System.out.println(Thread.currentThread().getName() + " Inbound handler 2" ); socketChannel.writeAndFlush(ctx.alloc().buffer().writeBytes("Server..." .getBytes(StandardCharsets.UTF_8))); super .channelRead(ctx, msg); } }); socketChannel.pipeline().addLast("handler3" ,new ChannelOutboundHandlerAdapter (){ @Override public void write (ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { System.out.println(Thread.currentThread().getName() + " Outbound handler 1" ); super .write(ctx, msg, promise); } }); socketChannel.pipeline().addLast("handler4" ,new ChannelOutboundHandlerAdapter (){ @Override public void write (ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { System.out.println(Thread.currentThread().getName() + " Outbound handler 2" ); super .write(ctx, msg, promise); } }); } }) .bind(8080 ); } }
运行结果如下
1 2 3 4 nioEventLoopGroup-2 -2 Inbound handler 1 nioEventLoopGroup-2 -2 Inbound handler 2 nioEventLoopGroup-2 -2 Outbound handler 2 nioEventLoopGroup-2 -2 Outbound handler 1
通过channel.pipeline().addLast(name, handler)添加handler时,记得给handler取名字 。这样可以调用pipeline的addAfter、addBefore等方法更灵活地向pipeline中添加handler
handler需要放入通道的pipeline中,才能根据放入顺序来使用handler
pipeline是结构是一个带有head与tail指针的双向链表,其中的节点为handler
要通过ctx.fireChannelRead(msg)等方法,将当前 handler 的处理结果传递给下一个handler
当有 入站 (Inbound)操作时,会从 head开始向后 调用handler,直到handler不是处理Inbound操作为止
当有 出站 (Outbound)操作时,会从 tail开始向前 调用handler,直到handler不是处理Outbound操作为止
具体结构如下
调用顺序如下
OutboundHandler socketChannel.writeAndFlush() 当handler中调用该方法进行写操作时,会触发Outbound操作,此时是从tail向前寻找OutboundHandler
ctx.writeAndFlush() 当handler中调用该方法进行写操作时,会触发Outbound操作,此时是从当前 handler 向前寻找OutboundHandler
EmbeddedChannel EmbeddedChannel可以用于测试各个handler,通过其构造函数按顺序传入需要测试handler,然后调用对应的Inbound和Outbound方法即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public class TestEmbeddedChannel { public static void main (String[] args) { ChannelInboundHandlerAdapter h1 = new ChannelInboundHandlerAdapter () { @Override public void channelRead (ChannelHandlerContext ctx, Object msg) throws Exception { System.out.println("1" ); super .channelRead(ctx, msg); } }; ChannelInboundHandlerAdapter h2 = new ChannelInboundHandlerAdapter () { @Override public void channelRead (ChannelHandlerContext ctx, Object msg) throws Exception { System.out.println("2" ); super .channelRead(ctx, msg); } }; ChannelOutboundHandlerAdapter h3 = new ChannelOutboundHandlerAdapter () { @Override public void write (ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { System.out.println("3" ); super .write(ctx, msg, promise); } }; ChannelOutboundHandlerAdapter h4 = new ChannelOutboundHandlerAdapter () { @Override public void write (ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { System.out.println("4" ); super .write(ctx, msg, promise); } }; EmbeddedChannel channel = new EmbeddedChannel (h1, h2, h3, h4); channel.writeInbound(ByteBufAllocator.DEFAULT.buffer().writeBytes("hello" .getBytes(StandardCharsets.UTF_8))); channel.writeOutbound(ByteBufAllocator.DEFAULT.buffer().writeBytes("hello" .getBytes(StandardCharsets.UTF_8))); } }
5、ByteBuf 调试工具方法
1 2 3 4 5 6 7 8 9 10 11 private static void log (ByteBuf buffer) { int length = buffer.readableBytes(); int rows = length / 16 + (length % 15 == 0 ? 0 : 1 ) + 4 ; StringBuilder buf = new StringBuilder (rows * 80 * 2 ) .append("read index:" ).append(buffer.readerIndex()) .append(" write index:" ).append(buffer.writerIndex()) .append(" capacity:" ).append(buffer.capacity()) .append(NEWLINE); appendPrettyHexDump(buf, buffer); System.out.println(buf.toString()); }
该方法可以帮助我们更为详细地查看ByteBuf中的内容
创建 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class ByteBufStudy { public static void main (String[] args) { ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(16 ); ByteBufUtil.log(buffer); StringBuilder sb = new StringBuilder (); for (int i = 0 ; i < 20 ; i++) { sb.append("a" ); } buffer.writeBytes(sb.toString().getBytes(StandardCharsets.UTF_8)); ByteBufUtil.log(buffer); } }
运行结果
1 2 3 4 5 6 7 8 9 read index:0 write index:0 capacity:16 read index:0 write index:20 capacity:64 +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000 | 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 |aaaaaaaaaaaaaaaa| |00000010 | 61 61 61 61 |aaaa | +--------+-------------------------------------------------+----------------+
ByteBuf 通过ByteBufAllocator选择allocator并调用对应的buffer()方法来创建的 ,默认使用 直接内存 作为ByteBuf,容量为256个字节,可以指定初始容量的大小
当 ByteBuf 的容量无法容纳所有数据时,ByteBuf会进行扩容操作
如果在handler中创建ByteBuf,建议使用ChannelHandlerContext ctx.alloc().buffer()来创建
直接内存与堆内存 通过该方法创建的ByteBuf,使用的是基于直接内存 的ByteBuf
1 ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(16 );
可以使用下面的代码来创建池化 基于堆 的 ByteBuf
1 ByteBuf buffer = ByteBufAllocator.DEFAULT.heapBuffer(16 );
也可以使用下面的代码来创建池化基于直接内存 的 ByteBuf
1 ByteBuf buffer = ByteBufAllocator.DEFAULT.directBuffer(16 );
直接内存创建和销毁的代价昂贵,但读写性能高(少一次内存复制),适合配合池化功能一起用
直接内存对 GC 压力小,因为这部分内存不受 JVM 垃圾回收的管理,但也要注意及时主动释放
验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class ByteBufStudy { public static void main (String[] args) { ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(16 ); System.out.println(buffer.getClass()); buffer = ByteBufAllocator.DEFAULT.heapBuffer(16 ); System.out.println(buffer.getClass()); buffer = ByteBufAllocator.DEFAULT.directBuffer(16 ); System.out.println(buffer.getClass()); } } class io .netty.buffer.PooledUnsafeDirectByteBuf class io .netty.buffer.PooledUnsafeHeapByteBuf class io .netty.buffer.PooledUnsafeDirectByteBuf
池化与非池化 池化的最大意义在于可以重用 ByteBuf,优点有
没有池化,则每次都得创建新的 ByteBuf 实例,这个操作对直接内存代价昂贵,就算是堆内存,也会增加 GC 压力
有了池化,则可以重用池中 ByteBuf 实例,并且采用了与 jemalloc 类似的内存分配算法提升分配效率
高并发时,池化功能更节约内存,减少内存溢出的可能
池化功能是否开启,可以通过下面的系统环境变量来设置
1 -Dio.netty.allocator.type={unpooled|pooled}
4.1 以后,非 Android 平台默认启用池化实现 ,Android 平台启用非池化实现
4.1 之前,池化功能还不成熟,默认是非池化实现
组成 ByteBuf主要有以下几个组成部分
最大容量与当前容量
在构造 ByteBuf 时,可传入两个参数,分别代表初始容量和最大容量,若未传入第二个参数(最大容量),最大容量默认为Integer.MAX_VALUE
当ByteBuf容量无法容纳所有数据时,会进行扩容操作,若超出最大容量 ,会抛出java.lang.IndexOutOfBoundsException
异常
读写操作不同于 ByteBuffer 只用 position 进行控制,ByteBuf分别由读指针和写指针两个指针控制。进行读写操作时,无需进行模式的切换
读指针与写指针之间的空间称为可读部分
写入 常用方法如下
方法签名
含义
备注
writeBoolean(boolean value)
写入 boolean 值
用一字节 01|00 代表 true|false
writeByte(int value)
写入 byte 值
writeShort(int value)
写入 short 值
writeInt(int value)
写入 int 值
Big Endian(大端写入),即 0x250,写入后 00 00 02 50
writeIntLE(int value)
写入 int 值
Little Endian(小端写入),即 0x250,写入后 50 02 00 00
writeLong(long value)
写入 long 值
writeChar(int value)
写入 char 值
writeFloat(float value)
写入 float 值
writeDouble(double value)
写入 double 值
writeBytes(ByteBuf src)
写入 netty 的 ByteBuf
writeBytes(byte[] src)
写入 byte[]
writeBytes(ByteBuffer src)
写入 nio 的 ByteBuffer
int writeCharSequence(CharSequence sequence, Charset charset)
写入字符串
CharSequence为字符串类的父类,第二个参数为对应的字符集
注意
这些方法的未指明返回值的,其返回值都是 ByteBuf,意味着可以链式调用来写入不同的数据
网络传输中,默认习惯是 Big Endian ,使用 writeInt(int value)
使用方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class ByteBufStudy { public static void main(String[] args) { // 创建ByteBuf ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(16, 20); ByteBufUtil.log(buffer); // 向buffer中写入数据 buffer.writeBytes(new byte[]{1, 2, 3, 4}); ByteBufUtil.log(buffer); buffer.writeInt(5); ByteBufUtil.log(buffer); buffer.writeIntLE(6); ByteBufUtil.log(buffer); buffer.writeLong(7); ByteBufUtil.log(buffer); } }
运行结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 read index:0 write index:0 capacity:16 read index:0 write index:4 capacity:16 +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 01 02 03 04 |.... | +--------+-------------------------------------------------+----------------+ read index:0 write index:8 capacity:16 +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 01 02 03 04 00 00 00 05 |........ | +--------+-------------------------------------------------+----------------+ read index:0 write index:12 capacity:16 +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 01 02 03 04 00 00 00 05 06 00 00 00 |............ | +--------+-------------------------------------------------+----------------+ read index:0 write index:20 capacity:20 +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 01 02 03 04 00 00 00 05 06 00 00 00 00 00 00 00 |................| |00000010| 00 00 00 07 |.... | +--------+-------------------------------------------------+----------------+
还有一类方法是 set 开头 的一系列方法,也可以写入数据,但不会改变写指针位置
扩容 当ByteBuf中的容量无法容纳写入的数据时,会进行扩容操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 buffer.writeLong(7); ByteBufUtil.log(buffer); // 扩容前 read index:0 write index:12 capacity:16 ... // 扩容后 read index:0 write index:20 capacity:20 +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 01 02 03 04 00 00 00 05 06 00 00 00 00 00 00 00 |................| |00000010| 00 00 00 07 |.... | +--------+-------------------------------------------------+----------------+
扩容规则
如何写入后数据大小未超过 512 字节,则选择下一个 16 的整数倍进行扩容
例如写入后大小为 12 字节,则扩容后 capacity 是 16 字节
如果写入后数据大小超过 512 字节,则选择下一个 2^n
例如写入后大小为 513 字节,则扩容后 capacity 是 210=1024 字节(29=512 已经不够了)
扩容不能超过 maxCapacity,否则会抛出java.lang.IndexOutOfBoundsException
异常
1 2 Exception in thread "main" java.lang.IndexOutOfBoundsException: writerIndex(20 ) + minWritableBytes(8 ) exceeds maxCapacity (20 ) : PooledUnsafeDirectByteBuf(ridx: 0 , widx: 20 , cap: 20 /20 ) ...
读取 读取主要是通过一系列read方法进行读取,读取时会根据读取数据的字节数移动读指针
如果需要 重复读取 ,需要调用buffer.markReaderIndex()
对读指针进行标记,并通过buffer.resetReaderIndex()
将读指针恢复到mark标记的位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public class ByteBufStudy { public static void main (String[] args) { ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(16 , 20 ); buffer.writeBytes(new byte []{1 , 2 , 3 , 4 }); buffer.writeInt(5 ); System.out.println(buffer.readByte()); System.out.println(buffer.readByte()); System.out.println(buffer.readByte()); System.out.println(buffer.readByte()); ByteBufUtil.log(buffer); buffer.markReaderIndex(); System.out.println(buffer.readInt()); ByteBufUtil.log(buffer); buffer.resetReaderIndex(); ByteBufUtil.log(buffer); } } read index:4 write index:8 capacity:16 +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000 | 00 00 00 05 |.... | +--------+-------------------------------------------------+----------------+ 5 read index:8 write index:8 capacity:16 read index:4 write index:8 capacity:16 +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000 | 00 00 00 05 |.... | +--------+-------------------------------------------------+----------------+
还有以 get 开头的一系列方法,这些方法不会改变读指针的位置
释放 由于 Netty 中有堆外内存(直接内存)的 ByteBuf 实现,堆外内存最好是手动来释放 ,而不是等 GC 垃圾回收。
UnpooledHeapByteBuf 使用的是 JVM 内存,只需等 GC 回收内存即可
UnpooledDirectByteBuf 使用的就是直接内存了,需要特殊的方法来回收内存
PooledByteBuf 和它的子类使用了池化机制,需要更复杂的规则来回收内存
Netty 这里采用了引用计数法来控制回收内存,每个 ByteBuf 都实现了 ReferenceCounted 接口
每个 ByteBuf 对象的初始计数为 1
调用 release 方法计数减 1,如果计数为 0,ByteBuf 内存被回收
调用 retain 方法计数加 1,表示调用者没用完之前,其它 handler 即使调用了 release 也不会造成回收
当计数为 0 时,底层内存会被回收,这时即使 ByteBuf 对象还在,其各个方法均无法正常使用
释放规则 因为 pipeline 的存在,一般需要将 ByteBuf 传递给下一个 ChannelHandler,如果在每个 ChannelHandler 中都去调用 release ,就失去了传递性(如果在这个 ChannelHandler 内这个 ByteBuf 已完成了它的使命,那么便无须再传递)
基本规则是,谁是最后使用者,谁负责 release
当ByteBuf被传到了pipeline的head与tail时,ByteBuf会被其中的方法彻底释放,但前提是ByteBuf被传递到了head与tail中
TailConext中释放ByteBuf的源码
1 2 3 4 5 6 7 8 protected void onUnhandledInboundMessage (Object msg) { try { logger.debug("Discarded inbound message {} that reached at the tail of the pipeline. Please check your pipeline configuration." , msg); } finally { ReferenceCountUtil.release(msg); } }
判断传过来的是否为ByteBuf,是的话才需要释放
1 2 3 public static boolean release (Object msg) { return msg instanceof ReferenceCounted ? ((ReferenceCounted)msg).release() : false ; }
切片 ByteBuf 切片是【零拷贝】的体现之一,对原始 ByteBuf 进行切片成多个 ByteBuf,切片后的 ByteBuf 并没有发生内存复制,还是使用原始 ByteBuf 的内存 ,切片后的 ByteBuf 维护独立的 read,write 指针
得到分片后的buffer后,要调用其 retain 方法,使其内部的引用计数加一。避免原ByteBuf释放,导致切片buffer无法使用修改原ByteBuf中的值,也会影响切片后得到的ByteBuf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class TestSlice { public static void main(String[] args) { // 创建ByteBuf ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(16, 20); // 向buffer中写入数据 buffer.writeBytes(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}); // 将buffer分成两部分 ByteBuf slice1 = buffer.slice(0, 5); ByteBuf slice2 = buffer.slice(5, 5); // 需要让分片的buffer引用计数加一 // 避免原Buffer释放导致分片buffer无法使用 slice1.retain(); slice2.retain(); ByteBufUtil.log(slice1); ByteBufUtil.log(slice2); // 更改原始buffer中的值 System.out.println("===========修改原buffer中的值==========="); buffer.setByte(0,5); System.out.println("===========打印slice1==========="); ByteBufUtil.log(slice1); } }
运行结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 read index:0 write index:5 capacity:5 +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 01 02 03 04 05 |..... | +--------+-------------------------------------------------+----------------+ read index:0 write index:5 capacity:5 +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 06 07 08 09 0a |..... | +--------+-------------------------------------------------+----------------+ ===========修改原buffer中的值=========== ===========打印slice1=========== read index:0 write index:5 capacity:5 +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 05 02 03 04 05 |..... | +--------+-------------------------------------------------+----------------+
优势
池化思想 - 可以重用池中 ByteBuf 实例,更节约内存,减少内存溢出的可能
读写指针分离 ,不需要像 ByteBuffer 一样切换读写模式
可以自动扩容
支持链式调用,使用更流畅
很多地方体现零拷贝,例如
slice、duplicate、CompositeByteBuf
四、应用