1. Netty概述
-
Netty是什么:
Netty is an asynchronous event-driven network application framework for rapid
development of maintainable high performance protocol servers & clients.也就是说Netty是一个异步的,基于事件驱动的网络应用框架,用于快速开发可维护,高性能的网络服务器和客户端;但是Netty的异步是基于多路复用模拟出来的异步,并没有实现真正意义上的异步IO;
-
Netty的地位:Netty 在 Java 网络应用框架中的地位就好比Spring 框架在 JavaEE 开发中的地位;很多有网络通信需求的框架都用到了Netty;
-
Netty的优势:如果使用传统的NIO进行程序的编写,那么工作量是非常大的,而且还容易产生很多的bug,除此之外,我们还需要自己构建相应的协议,并解决因TCP传输问题而产生的粘包,半包现象;而且epoll空轮询会导致CPU占用率达到100%;但是Netty对传统的NIO进行了改进与增强,解决了很多传统NIO的问题,并对NIO中的很多功能进行了封装,同时也对很多的API进行了增强,使之更加的易用,比如将ThreadLocal增强为FastThreadLocal,将ByteBuffer增强为ByteBuf;
2. Netty入门案例
-
服务器端代码:
package com.zhyn.netty; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInitializer; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.string.StringDecoder; public class HelloServer { public static void main(String[] args) { // 1. 启动器,负责组装netty的组件,启动服务器 new ServerBootstrap() // 2. 添加组件,NioEventLoopGroup()包含了线程以及选择器 // 可以简单的理解为线程池 + 选择器 // 在客户端发起连接的时候服务器端处理accept事件 .group(new NioEventLoopGroup()) // 3. 指定ServerSocketChannel的实现方式 .channel(NioServerSocketChannel.class) // 4. boss负责处理连接,worker(child)负责处理读写,以下的方法决定了worker(child)要执行哪些具体的操作(handler) // 5. channel代表和客户端连接后进行读写的通道,ChannelInitializer负责添加别的handler,是一个初始化器,初始化器只执行一次 // 连接建立之后initChannel会被调用,为刚刚创建的SocketChannel添加更多的处理器 .childHandler(new ChannelInitializer<NioSocketChannel>() { @Override protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception { // 6. 添加具体的handler // handler可以理解为是一个个的工序,多道工序连在一起就是一个流水线pipeline() // pipeline()会负责将事件传播给每一个handler,handler会对自己感兴趣的事件进行处理 nioSocketChannel.pipeline().addLast(new StringDecoder()); // 将ByteBuf转换为字符串 nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() { // 自定义handler @Override // 处理读事件,msg其实就是上一步转化得到的字符串 // 将channel理解为通道,msg理解为流动的数据 public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { // 打印上一步转换好的字符串 System.out.println(msg); } }); } }) // 7. 绑定监听端口 .bind(8080); } }
-
客户端代码
package com.zhyn.netty; import io.netty.bootstrap.Bootstrap; import io.netty.channel.ChannelInitializer; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.string.StringEncoder; import java.net.InetSocketAddress; public class HelloClient { public static void main(String[] args) throws InterruptedException { // 1. 启动类 new Bootstrap() // 2. 添加 EventLoop .group(new NioEventLoopGroup()) // 3. 选择客户端SocketChannel的实现,NioSocketChannel表示客户端的SocketChannel是基于NIO实现的 .channel(NioSocketChannel.class) // 4. 添加处理器 .handler(new ChannelInitializer<NioSocketChannel>() { @Override // 在连接建立之后被调用 protected void initChannel(NioSocketChannel nioSocketChannel) { // 将字符串转化为ByteBuf,并发送给EventLoop nioSocketChannel.pipeline().addLast(new StringEncoder()); } }) // 5. 连接服务器 .connect(new InetSocketAddress(8080)) // 阻塞方法,直到连接建立之后才继续向下运行 .sync() // 代表的是连接对象,其实就是通道的抽象,可以用它进行数据的读写操作,并且netty对channel进行了封装 .channel() // 6. 向服务器发送数据,写入消息并清空缓冲区 .writeAndFlush("hello world"); } }
-
执行结果
-
执行流程分析
-
对代码中出现的各个组件的理解:
- channel可以理解为数据传输的通道
- msg可以理解为流动的数据,最开始输入的是ByteBuf,经过pipeline中的各个handler的加工处理会变为其它类型的对象,但是在最后输出的时候依然是ByteBuf;
- handler可以理解为数据处理的工序;工序有多道,和在一起就是一个pipeline(所以可以将pipeline理解为一个流水线,也就是各个工序的传输途径,在处理时pineline会依次调用自己内部的多个handler),pipeline会负责发布事件(读事件,读取完成事件等)并传播给每一个handler,handler会对自己感兴趣的事件进行处理(重写了相应的事件处理方法);handler分为Inbound入站和Outbound出站两类;
- eventLoop可以理解为处理数据的工人:eventLoop可以管理多个channel的IO操作,并且当某个channel被某个eventLoop负责之后,eventLoop就会与相应的channel进行绑定,以后该channel中的IO操作就由该eventLoop负责;
- eventLoop既可以处理IO操作,也可以进行任务处理,每一个eventLoop都有自己的任务队列,队列里面可以存放多个channel的待处理任务;eventLoop要处理的任务分为普通任务和定时任务;
- eventLoop按照pipeline中handler的顺序依次按照handler中的代码处理数据;我们可以为每一道工序(handler)指定不同的eventLoop,因为只有对IO操作进行处理时eventLoop才需要和channel进行绑定,对于其他的操作来说,如果我们想要在中间加入一些自己的业务处理,可以通过为每一道工序指定不同的eventLoop来实现;
3. 组件
-
EventLoop:事件循环对象EventLoop本质上是一个单线程执行器,同时维护了一个Selector,里面的run方法处理一个或多个Channel上源源不断的IO事件;
-
EventLoop的继承关系如下所示:
- 一条线路继承自JUC.ScheduledExecutorService,因此事件循环对象包含了线程池中的所有方法;
- 一条继承自Netty自己的OrderedEventExecutor;它提供了boolean inEventLoop(Thread thread)方法判断一个线程是不是属于此EventLoop,以及EventLoopGroup parent()方法来看看自己属于哪一个EventLoopGroup;
-
EventLoopGroup:事件循环组EventLoopGroup是一组EventLoop的集合,Channel通常会调用EventLoopGroup的register方法来绑定其中的一个EventLoop,后续这个Channel上的IO事件就都由此EventLoop来处理(保证了对IO事件进行处理时的线程安全);EventLoopGroup继承了Netty自己的EventExecutorGroup,实现了Iterable接口以提供遍历EventLoop的能力,另外还有next方法获取集合中的下一个EventLoop;
-
处理普通任务与定时任务
package com.zhyn.netty; import io.netty.channel.nio.NioEventLoopGroup; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.TimeUnit; @Slf4j public class TestEventLoop { public static void main(String[] args) { // 1. 创建一个事件循环组 // 该事件循环组可以同时处理普通的任务,定时任务,以及IO事件 // 不指定参数的话会默认会拥有 (2 * CPU) 核心数个事件循环并且保证至少有一个线程,哪怕我们传递的参数是0 // 我们可以直接指定参数或者是在netty的配置文件中指定参数 NioEventLoopGroup group = new NioEventLoopGroup(2); // 2. 获取下一个事件循环对象,这里有两个事件循环对象,第三次获取的时候将会得到第一个事件循环对象 // 简单的实现了负载均衡 System.out.println(group.next()); System.out.println(group.next()); System.out.println(group.next()); System.out.println(group.next()); // 3. 执行普通任务,该任务会被事件循环组中某一个事件循环对象执行 // 同时也实现了异步处理 // 如果当前的线程不想执行一个执行时间可能很长的任务,可以将该任务交给事件循环组中的一个线程 group.next().submit(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("OK"); }); log.debug("main"); // 4. 执行定时任务,定时任务可以实现连接的保活 group.next().scheduleAtFixedRate(() -> {log.debug("OS");}, 0, 1, TimeUnit.SECONDS); } }
-
处理普通任务执行结果
主线程将普通任务交给channel之后以异步的方式继续向下执行,然后由channel中的线程将执行结果返回给主线程; -
处理定时任务执行结果
主线程将定时任务交给channel之后以异步的方式继续向下执行,之后channel每间隔一定的时间(这里是1s)就会将执行的结果返回给主线程; -
关闭EventLoopGroup:以上的代码并没有对EventLoopGroup进行关闭,但是在需要的时候我们需要对EventLoopGroup进行关闭,Netty提供了一个shutdownGracefully()方法,该方法首先会切换EventLoopGroup到关闭状态从而拒绝新的任务加入,然后在任务队列中的任务都执行完毕之后就停止线程的运行,从而确保应用整体是在正常有序的状态下退出的;
-
处理IO任务
package com.zhyn.netty; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInitializer; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import java.nio.charset.StandardCharsets; public class MyServer { public static void main(String[] args) { new ServerBootstrap() .group(new NioEventLoopGroup(2)) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.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); } }
package com.zhyn.netty; import io.netty.bootstrap.Bootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelInitializer; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.string.StringEncoder; import java.io.IOException; import java.net.InetSocketAddress; public class MyClient { public static void main(String[] args) throws InterruptedException, IOException { Channel channel = new Bootstrap() .group(new NioEventLoopGroup()) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new StringEncoder()); } }) .connect(new InetSocketAddress(8080)) .sync() .channel(); System.out.println(channel); // 此处打断点调试,调用 channel.writeAndFlush(...); System.in.read(); } }
-
执行结果
-
从执行结果中可以看出,一个EventLoop可以负责多个Channel,而且一旦EventLoop与某一个Channel绑定之后,就一直负责处理该Channel中的事件;
-
我们可以将EventLoop的职责进行更加细致的划分,比如和使用NIO时一样划分为boss和worker,Bootstrap的group()可以传入两个EventLoopGroup参数,分别负责处理不同的事件,第一个只负责ServerSocketChannel上的accept事件,第二个只负责socketChannel上的读写事件;ServerSocketChannel只会和一个EventLoop进行绑定,因为ServerSocketChannel只有一个,第二个worker的线程可以根据情况自己设置
public class EventLoopServer { public static void main(String[] args) { new ServerBootstrap() .group(new NioEventLoopGroup(), new NioEventLoopGroup(2)) ... } }
-
使用非NIO实现的EventLoopGroup:当有一个任务需要较长的时间进行处理的时候,我们可以使用非NIO实现的EventLoopGroup,避免同一个NioEventLoop中的其它Channel在较长的时间之内都得不到处理;
-
经过完善之后的服务器和客户端的代码实现:
package com.zhyn.netty; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBuf; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import lombok.extern.slf4j.Slf4j; import java.nio.charset.Charset; @Slf4j public class EventLoopServer { public static void main(String[] args) { EventLoopGroup group = new DefaultEventLoopGroup(); new ServerBootstrap() // 可以将职责划分的更细致,比如和使用NIO时一样划分为boss和worker // 第一个只负责ServerSocketChannel上的accept事件,worker(第二个)只负责socketChannel上的读写事件 // ServerSocketChannel只会和一个EventLoop进行绑定,因为ServerSocketChannel只有一个 // 第二个worker的线程可以根据情况自己设置 // 如果某个handler执行时间过长,就不要使用worker的NIO线程,否则它就会影响NIO的读写操作 // 当然也可以再进行一次细分,创建一个独立的EventLoopGroup专门处理执行时间过长的handler .group(new NioEventLoopGroup(), new NioEventLoopGroup(2)) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<NioSocketChannel>(){ @Override protected void initChannel(NioSocketChannel ch) throws Exception { ch.pipeline().addLast("handler-1", new ChannelInboundHandlerAdapter(){ @Override // ByteBuf public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf) msg; log.debug(buf.toString(Charset.defaultCharset())); ctx.fireChannelRead(msg); // 将消息传递给下一个handler } // 以下代码使用的不是worker对应的线程,而是DefaultEventLoopGroup中的线程 }).addLast(group, "handler-2", new ChannelInboundHandlerAdapter(){ @Override // ByteBuf public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf) msg; log.debug(buf.toString(Charset.defaultCharset())); } }); } }) .bind(8080); } }
package com.zhyn.netty; import io.netty.bootstrap.Bootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelInitializer; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.string.StringEncoder; import lombok.extern.slf4j.Slf4j; import java.net.InetSocketAddress; /** * NIO的客户端都是单线程的,但是Netty的客户端都是多线程的 */ @Slf4j public class EventLoopClient { public static void main(String[] args) throws InterruptedException { // 1. 启动类 // 带有Future,Promise的类型都是和异步方法搭配使用的,用来正确处理结果 ChannelFuture channelFuture = new Bootstrap() // 2. 添加 EventLoop .group(new NioEventLoopGroup()) // 3. 选择客户端channel的实现 .channel(NioSocketChannel.class) // 4. 添加处理器 .handler(new ChannelInitializer<NioSocketChannel>() { @Override // 在连接建立之后被调用 protected void initChannel(NioSocketChannel nioSocketChannel) { // 将字符串转化为ByteBuf,并发送给EventLoop nioSocketChannel.pipeline().addLast(new StringEncoder()); } }) // 5. 连接服务器 // connect()方法是一个异步非阻塞方法,Netty中很多的方法都是异步非阻塞的,main发起了调用,真正执行connect操作的是nio线程 // 但是nio线程执行连接操作的事件是非常长的(通常大于其它的一些操作),我们假设是1s .connect(new InetSocketAddress(8080)); // sync()是阻塞方法,直到连接建立之后才继续向下运行 // sync()是必不可少的,不写的话会导致服务器端接收不到数据 // 使用sync()方法同步处理结果 // 将获取channel的线程(也就是发起sync()方法的线程)阻塞住,从而使执行connect的操作与获取channel的操作同步执行 // 中间建立连接的线程是nio线程,获取channel的线程是发起sync()方法的线程,也就是主线程 channelFuture.sync(); // 获取channel的操作会在连接建立之前立即非阻塞的向下运行,但是此时是无法得到已经建立好连接的channel的 Channel channel = channelFuture.channel(); // 在没有调用sync()方法的情况下,打印channel的话并不会得到连接到的端口的信息,因为根本就没有获取到经建立好连接的channel // 所以说我们必须加上sync()方法从而避免错误 log.debug("{}", channel); // 向服务器发送数据 channel.writeAndFlush(("Netty")); // 也可以使用addListener(回调对象)方法异步处理结果,此时等待连接结果的线程也不是主线程了 // 主线程只用通知其他线程将来这个需要的结果完成之后要进行什么样的操作,也就是给一个回调对象 // 在这里主线程会将回调对象传递给nio线程,nio线程在连接建立好之后会调用回调对象中的 operationComplete 方法 channelFuture.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { // 将来调用以下代码的不再是主线程,而是nio线程 Channel channel1 = future.channel(); log.debug("{}", channel1); channel1.writeAndFlush("Redis"); } }); } }
-
执行结果:从执行结果中可以看出客户端与服务器之间的事件被NioEventLoopGroup和DefaultEventLoopGroup分别处理;
-
不同的EventLoopGroup切换的实现原理:由上图可以看出,当handler中绑定的group不同的时候需要切换不同的group来执行任务,具体的原理的核心代码以及注释如下所示:
// 如果两个handler绑定的是同一个线程,那么就直接进行方法的调用; // 否则就把进行方法调用的代码封装为一个任务对象,由下一个handler的线程来调用 static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) { final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next); // EventLoop最终继承了Executor,Executor是一个框架,它实现的就是线程池的功能,可以理解为Executor就是一个线程池框架 // 所以我们可以认为next.executor()返回的是带有线程池功能的下一个handler的eventLoop EventExecutor executor = next.executor(); // 下一个handler的eventLoop(executor中的线程)和当前handler中的线程是否和是同一个线程 if (executor.inEventLoop()) { // 是的话就直接进行方法调用,在同一个线程之中触发下一个handler的read事件 next.invokeChannelRead(m); // 不是的话就将要执行的代码作为任务提交给下一个事件循环处理(换人) } else { // executor中存在对应于下一个handler的线程 executor.execute(new Runnable() { @Override public void run() { // 触发下一个handler的read事件不能够在当前handler的线程之中执行 next.invokeChannelRead(m); } }); } }
-
Channel:
-
Channel中常见的方法:
- close() 可以用来关闭Channel
- closeFuture() 用来处理 Channel 的关闭
- sync() 方法作用是同步等待 Channel 关闭
- addListener() 方法是异步等待 Channel 关闭
- pipeline() 方法用于添加处理器
- write() 方法将数据写入:因为缓冲机制,数据被写入到 Channel 中以后并不会被立即发送,只有当缓冲满了或者调用了flush()方法后,才会将数据通过 Channel 发送出去,writeAndFlush() 方法就是将数据写入并立即发送(刷出);
-
-
ChannelFuture以及对EventLoopClient的进一步说明:我们借助于以下的客户端代码对ChannelFuture进行说明;
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 和发送数据的线程都是主线程;下面还有一个addListener()方法,用于异步获取建立连接后的 Channel 和发送数据,使得执行这些操作的线程是 NIO 线程(去执行connect操作的线程);通过这种方法可以在NIO线程中获取 Channel 并发送数据,而不是在主线程中执行这些操作;
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()); } }) // 该方法为异步非阻塞方法,主线程调用后不会被阻塞,真正去执行连接操作的是NIO线程 // NIO线程就是NioEventLoop中的线程 .connect(new InetSocketAddress("localhost", 8080)); // 当connect方法执行完毕后,也就是连接真正建立后 // 会在NIO线程中调用operationComplete方法 channelFuture.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture channelFuture) throws Exception { Channel channel = channelFuture.channel(); channel.writeAndFlush("hello world"); } }); System.in.read(); } }
-
处理关闭操作:在程序执行完之后我们需要将相关的资源进行关闭,比如EventLoopGroup在使用完之后要进行关闭;
package com.zhyn.netty; import io.netty.bootstrap.Bootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelInitializer; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.string.StringEncoder; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; import lombok.extern.slf4j.Slf4j; import java.net.InetSocketAddress; import java.util.Scanner; /** * 由用户不断的在控制台输入信息,然后客户端将信息发送到服务器 * 输入q表示退出 */ @Slf4j public class CloseFutureClient { public static void main(String[] args) throws InterruptedException { NioEventLoopGroup group = new NioEventLoopGroup(); // 通过pipeline上面的多个handler把数据加工成我们需要的形式 ChannelFuture channelFuture = new Bootstrap() // 在客户端关闭之后,NioEventLoopGroup()中的线程需要全部关闭 .group(group) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<NioSocketChannel>() { @Override // 在连接建立后被调用 protected void initChannel(NioSocketChannel ch) throws Exception { // ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG)); ch.pipeline().addLast(new StringEncoder()); } }) .connect(new InetSocketAddress("localhost", 8080)); System.out.println(channelFuture.getClass()); Channel channel = channelFuture.sync().channel(); log.debug("{}", channel); new Thread(()->{ Scanner scanner = new Scanner(System.in); while (true) { String line = scanner.nextLine(); if ("q".equals(line)) { // close()方法也是一个异步操作 channel.close(); // log.debug("处理关闭之后的操作"); // 不能在这里善后 break; } channel.writeAndFlush(line); } }, "input").start(); // 获取ClosedFuture对象 (1)同步处理关闭 (2)异步处理关闭 ChannelFuture closeFuture = channel.closeFuture(); // 让主线程阻塞,客户端发送q之后才会继续运行 // (1) 以同步的方式处理关闭 // closeFuture.sync(); // log.debug("处理关闭之后的操作"); System.out.println(closeFuture.getClass()); closeFuture.addListener((ChannelFutureListener) future -> { log.debug("处理关闭之后的操作"); // 优雅的停止,先拒绝接受新的任务,把还没有运行完的步骤运行完,把没有发送完的数据发送完 // 之后才会停止正在运行的线程 group.shutdownGracefully(); }); } }
-
当我们要关闭channel时,可以调用channel.close()方法进行关闭。但是该方法也是一个异步方法。真正的关闭操作并不是在调用该方法的线程中执行的,而是在NIO线程中执行真正的关闭操作;如果我们想在channel真正关闭以后,执行一些额外的操作,可以选择以下两种方法来实现;
- 通过channel.closeFuture()方法获得对应的ChannelFuture对象,然后调用sync()方法阻塞执行操作的线程,等待channel真正关闭后,再执行其他操作;
- 调用closeFuture.addListener方法,添加close的后续操作;
-
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:
package com.zhyn.netty; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.*; @Slf4j public class TestJdkFuture { public static void main(String[] args) throws ExecutionException, InterruptedException { // 1. 获取线程池 final ExecutorService service = Executors.newFixedThreadPool(2); // 2. 提交任务 Future<Integer> future = service.submit(new Callable<Integer>() { @Override // 由执行任务的线程将执行的结果放置于Future,我们自己没有机会去干涉 public Integer call() throws Exception { log.debug("执行计算"); Thread.sleep(1000); return 99; } }); // 获得任务执行结果的线程和执行任务的线程不是同一个线程 // 两个线程通过Future进行通信 // 3. 主线程通过Future的阻塞方法get()来获取结果,也就是同步等待另外的线程执行完 log.debug("等待结果..."); log.debug("结果是 {}", future.get()); } }
-
执行结果
-
从执行结果中可以看出等待结果以及获取结果的线程是主线程,真正执行任务的线程是线程池中的线程;主线程只有在等待nio线程之行结束之后才能够继续向下执行;
-
Netty Future:
package com.zhyn.netty; import io.netty.channel.EventLoop; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; @Slf4j public class TestNettyFuture { public static void main(String[] args) throws ExecutionException, InterruptedException { // EventLoop不像线程池一样有多个线程,而是只有一个线程 // 如果想要多个线程的话可以穿件EventLoop组 final NioEventLoopGroup group = new NioEventLoopGroup(); final EventLoop eventLoop = group.next(); // 往eventLoop中提交任务时返回了Future对象,而不是我们自己创建的 // Future对象的创建权以及结果的控制权都不是我们能够控制的 // 如果我们想对其进行控制的话可以使用promise final Future<Integer> future = eventLoop.submit(new Callable<Integer>() { @Override public Integer call() throws Exception { log.debug("执行计算"); Thread.sleep(1000); return 99; } }); // 使用同步的方式获取结果 log.debug("等待结果"); // log.debug("结果是 {}", future.get()); // 使用异步的方式获取结果 future.addListener(new GenericFutureListener<Future<? super Integer>>() { @Override public void operationComplete(Future<? super Integer> future) throws Exception { // 回调方法都已经执行了,那么结果一定是已经存在的,使用getNow()即可 log.debug("接收结果 {}", future.getNow()); } }); } }
-
执行结果
-
从执行结果中可以看出执行计算和获取结果的线程都是nio线程,而不再是主线程;主线程也并没有因为nio线程的执行计算操作而同步等待;
-
Netty Promise:Promise相当于一个容器,可以用于存放各个线程中的结果,然后让其他线程去获取该结果;
package com.zhyn.netty; import io.netty.channel.EventLoop; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.util.concurrent.DefaultPromise; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.ExecutionException; @Slf4j public class TestNettyPromise { public static void main(String[] args) throws ExecutionException, InterruptedException { // 1. 准备EventLoop对象 final EventLoop eventLoop = new NioEventLoopGroup().next(); // 2. 可以主动创建promise,promise是一个结果容器 final DefaultPromise<Integer> promise = new DefaultPromise<>(eventLoop); new Thread(() -> { // 3. 任意一个线程执行计算,计算完毕后向promise填充结果 log.debug("开始计算..."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); promise.setFailure(e); } promise.setSuccess(99); }).start(); log.debug("等待结果..."); log.debug("结果是: {}", promise.get()); } }
-
执行结果
-
异步提升的是什么:为什么不在一个线程中去执行建立连接、去执行关闭 channel,那样不是也可以吗?非要用这么复杂的异步方式?为什么不一个线程发起建立连接,另一个线程去真正建立连接?我们不能说是因为 Netty 异步方式用了多线程、多线程就效率高。其实这些认识都比较片面,并不是因为线程多了才导致使用异步的方式在某一方面的表现有所提高;
-
其实我们看一个医院看病的例子就可以明白异步是怎么提升效率的:4 个医生给人看病,每个病人花费 20 分钟,而且医生看病的过程中是以病人为单位的,一个病人看完了,才能看下一个病人。假设病人源源不断地来,可以计算一下 4 个医生一天工作 8 小时,处理的病人总数是
4 * 8 * 3 = 96
个;
-
假设每个病人看病可以分为以下几个方面:
-
所以说我们可以异步的方式对以上的看病流程进行更改:只有一开始,医生 2、3、4 分别要等待 5、10、15 分钟才能执行工作,但只要后续病人源源不断地来,他们就能够满负荷工作,并且处理病人的能力提高到了
4 * 8 * 12
效率几乎是原来的四倍;
-
要注意的是单线程没法异步提高效率,必须配合多线程、多核 CPU 才能发挥异步的优势,而且异步并没有缩短响应时间以及单个任务的执行时间,反而有所增加,所以异步并没有提高单个任务的执行效率,但是异步提高的是单位时间内处理请求的数量,也就是提高了系统的吞吐量,同时合理进行任务拆分,也是利用异步的关键;
-
Handler与Pipeline:
package com.zhyn.netty; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import lombok.extern.slf4j.Slf4j; @Slf4j public class TestPipeline { public static void main(String[] args) { new ServerBootstrap() .group(new NioEventLoopGroup()) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<NioSocketChannel>() { @Override protected void initChannel(NioSocketChannel ch) throws Exception { // 1. 通过channel拿到pipeline // 通过pipeline上面的多个handler把数据加工成我们需要的形式 ChannelPipeline pipeline = ch.pipeline(); // 2. 添加处理器 head -> h1 -> h2 -> h3 -> tail,head和tail是Netty在建立流水线的时候自动添加的 // 处理器链底层是一个双向链表 // 处理器分为入站处理器和出站处理器,入站处理器关心的是读事件,出站处理器关心的是写事件 pipeline.addLast("h1", new ChannelInboundHandlerAdapter() { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { log.debug("1"); super.channelRead(ctx, msg); } }); pipeline.addLast("h2", new ChannelInboundHandlerAdapter() { @Override public void channelRead(ChannelHandlerContext ctx, Object name) throws Exception { log.debug("2"); // channelRead将数据传递给下个 handler,如果不调用的话调用链就会断开; // 或者调用ctx.fireChannelRead(student)将数据传递给下个 handler; super.channelRead(ctx, name); } }); pipeline.addLast("h3", new ChannelInboundHandlerAdapter() { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { log.debug("3"); // ctx的方法会从调用它的handler向前寻找出站处理器,并不是从尾巴开始向前寻找出站处理器 // ctx.writeAndFlush(ctx.alloc().buffer().writeBytes("server...".getBytes())); // ch的方法从尾巴开始向前寻找出站处理器 ch.writeAndFlush(ctx.alloc().buffer().writeBytes("server...".getBytes())); } }); // 当我们在channel中写入数据的时候出站处理器关心的写事件才会被触发 pipeline.addLast("h4", new ChannelOutboundHandlerAdapter(){ @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { log.debug("4"); super.write(ctx, msg, promise); } }); pipeline.addLast("h5", new ChannelOutboundHandlerAdapter(){ @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { log.debug("5"); super.write(ctx, msg, promise); } }); pipeline.addLast("h6", new ChannelOutboundHandlerAdapter(){ @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { log.debug("6"); super.write(ctx, msg, promise); } }); } }) .bind(8080); } }
-
执行结果
-
可以看到,ChannelInboundHandlerAdapter 是按照 addLast 的顺序执行的,而hannelOutboundHandlerAdapter 是按照 addLast 的逆序执行的;
-
通过channel.pipeline().addLast(name, handler)添加handler时,记得给handler取名字。这样可以调用pipeline的addAfter、addBefore等方法更灵活地向pipeline中添加handler;handler需要放入通道的pipeline中,这样我们才能根据handler的放入顺序来使用handler;pipeline是结构是一个带有head与tail指针的双向链表,其中的节点为handler;要通过ctx.fireChannelRead(msg)等方法,将当前handler的处理结果传递给下一个handler,不然的话任务就不能继续向下执行;当有入站(Inbound)操作时,会从head开始向后调用handler,直到handler不是处理Inbound的操作为止,当有出站(Outbound)操作时,会从tail开始向前调用handler,直到handler不是处理Outbound的操作为止;
-
当handler中调用socketChannel.writeAndFlush()方法进行写操作时,会触发Outbound操作,此时是从tail向前寻找OutboundHandler;
-
当handler中调用ctx.writeAndFlush()方法进行写操作时,会触发Outbound操作,此时是从当前handler向前寻找OutboundHandler;
-
EmbeddedChannel:EmbeddedChannel可以用于测试各个handler,通过其构造函数按顺序传入需要测试的handler,然后调用对应的Inbound和Outbound方法即可;
package com.zhyn.netty; import io.netty.buffer.ByteBufAllocator; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelOutboundHandlerAdapter; import io.netty.channel.ChannelPromise; import io.netty.channel.embedded.EmbeddedChannel; import lombok.extern.slf4j.Slf4j; @Slf4j public class TestEmbeddedChannel { public static void main(String[] args) { ChannelInboundHandlerAdapter h1 = new ChannelInboundHandlerAdapter() { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { log.debug("1"); super.channelRead(ctx, msg); } }; ChannelInboundHandlerAdapter h2 = new ChannelInboundHandlerAdapter() { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { log.debug("2"); super.channelRead(ctx, msg); } }; ChannelOutboundHandlerAdapter h3 = new ChannelOutboundHandlerAdapter() { @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { log.debug("3"); super.write(ctx, msg, promise); } }; ChannelOutboundHandlerAdapter h4 = new ChannelOutboundHandlerAdapter() { @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { log.debug("4"); super.write(ctx, msg, promise); } }; EmbeddedChannel channel = new EmbeddedChannel(h1, h2, h3, h4); // 模拟入站操作 channel.writeInbound(ByteBufAllocator.DEFAULT.buffer().writeBytes("hello".getBytes())); // 模拟出站操作 channel.writeOutbound(ByteBufAllocator.DEFAULT.buffer().writeBytes("netty".getBytes())); } }
-
ByteBuf:
package com.zhyn.netty; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import static io.netty.buffer.ByteBufUtil.appendPrettyHexDump; import static io.netty.util.internal.StringUtil.NEWLINE; public class TestByteBuf { public static void main(String[] args) { // ByteBuf默认是256个字节,而且可以自动扩展 // Netty默认使用直接内存为ByteBuf分配内存空间 final ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(16); System.out.println(buf.getClass()); log(buf); final StringBuilder builder = new StringBuilder(); for (int i = 0; i < 32; i++) { builder.append("a"); } buf.writeBytes(builder.toString().getBytes()); log(buf); } public 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是通过ByteBufAllocator选择allocator并调用对应的buffer()方法来创建的,默认使用直接内存为ByteBuf分配空间,且容量为256个字节,我们也可以指定初始容量的大小;当ByteBuf的容量无法容纳所有数据时,ByteBuf会进行扩容操作,扩容为下一个16的整数倍,当写入后的数据大小大于512字节时,扩容为下一个2^n;如果在handler中创建ByteBuf,建议使用ChannelHandlerContext ctx.alloc().buffer()来创建;
-
直接内存与堆内存:
通过该方法创建的ByteBuf,使用的是基于直接内存的ByteBufByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(16);
可以使用下面的代码来创建池化的基于堆的ByteBuf
ByteBuf buffer = ByteBufAllocator.DEFAULT.heapBuffer(16);
也可以使用下面的代码来创建池化的基于直接内存的ByteBuf
ByteBuf buffer = ByteBufAllocator.DEFAULT.directBuffer(16);
-
直接内存创建和销毁的代价昂贵,但读写性能高(少一次内存复制),适合配合池化功能一起用;直接内存对 GC 压力小,因为这部分内存不受 JVM 垃圾回收的管理,但也要注意及时主动释放;
package com.zhyn.netty; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; public class TestMemory { 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()); } }
-
执行结果
-
池化和非池化:池化的最大意义在于可以重用 ByteBuf,优点有:没有池化,则每次都得创建新的 ByteBuf 实例,这个操作对直接内存代价昂贵,就算是堆内存,也会增加 GC 压力;有了池化,则可以重用池中 ByteBuf 实例(对象已经被预先创建好了),并且采用了与 jemalloc 类似的内存分配算法提升分配效率;高并发时,池化功能更节约内存,减少内存溢出的可能,我们可以通过参数
-Dio.netty.allocator.type={unpooled|pooled}
来设置池化功能是否开启,Netty4.1 以后,非 Android 平台默认启用池化实现,Android 平台启用非池化实现,4.1 之前,池化功能还不成熟,默认是非池化实现; -
ByteBuf的组成:ByteBuf一共有四个部分组成,最开始的时候读写指针都在0位置;
- 在构造ByteBuf时,可传入两个参数,分别代表初始容量和最大容量,若未传入第二个参数(最大容量),最大容量默认为Integer.MAX_VALUE;
- 当ByteBuf容量无法容纳所有数据时,会进行扩容操作,若超出最大容量,会抛出java.lang.IndexOutOfBoundsException异常;
- 读写操作不同于ByteBuffer只用position进行控制,ByteBuf分别由读指针和写指针两个指针控制。进行读写操作时,无需进行模式的切换;读指针前的部分被称为废弃部分,是已经读过的内容;读指针与写指针之间的空间称为可读部分;写指针与当前容量之间的空间称为可写部分;
-
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) | 写入字符串 |
-
值得注意的是在这些方法中未指明返回值的,其返回值都是 ByteBuf,意味着可以链式调用;且在网络传输时默认习惯是使用 Big Endian;使用 writeInt(int value);
-
使用方法:
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); } }
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中的容量无法容纳写入的数据时,会进行扩容操作,从以上的代码以及执行结果来看,当我们添加数据7之后,ByteBuf进行了扩容操作,而且这次扩容操作并不是直接扩容为下一个16的整数倍,而是扩容到最大容量,之后容量还是不够的话就会抛出异常;
-
扩容规则
- 如何写入后数据大小未超过 512 字节,则选择下一个 16 的整数倍进行扩容,例如写入后大小为 12 字节,则扩容后 capacity 是 16 字节;如果写入后数据大小超过 512 字节,则扩容至下一个 2^n,例如写入后大小为 513 字节,则扩容后 capacity 是 2^10 = 1024个字节 (2^9=512 已经不够了);扩容不能超过maxCapacity,否则会抛出java.lang.IndexOutOfBoundsException异常;
Exception in thread "main" java.lang.IndexOutOfBoundsException: writerIndex(20) + minWritableBytes(8) exceeds maxCapacity(20): PooledUnsafeDirectByteBuf(ridx: 0, widx: 20, cap: 20/20) ...
- 如何写入后数据大小未超过 512 字节,则选择下一个 16 的整数倍进行扩容,例如写入后大小为 12 字节,则扩容后 capacity 是 16 字节;如果写入后数据大小超过 512 字节,则扩容至下一个 2^n,例如写入后大小为 513 字节,则扩容后 capacity 是 2^10 = 1024个字节 (2^9=512 已经不够了);扩容不能超过maxCapacity,否则会抛出java.lang.IndexOutOfBoundsException异常;
-
读取:读取主要是通过一系列read方法进行读取,读取时会根据读取数据的字节数移动读指针,如果需要重复读取,需要调用buffer.markReaderIndex()对读指针进行标记,并通过buffer.resetReaderIndex()将读指针恢复到mark标记的位置;
package com.zhyn.netty; import com.zhyn.nio.ByteBufferUtil; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.ByteBufUtil; import static io.netty.buffer.ByteBufUtil.appendPrettyHexDump; import static io.netty.util.internal.StringUtil.NEWLINE; public class ByteBufTest { public static void main(String[] args) { // 创建ByteBuf ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(16, 20); // 向buffer中写入数据 buffer.writeBytes(new byte[]{1, 2, 3, 4}); buffer.writeInt(5); // 读取4个字节 System.out.println(buffer.readByte()); System.out.println(buffer.readByte()); System.out.println(buffer.readByte()); System.out.println(buffer.readByte()); log(buffer); // 通过mark与reset实现重复读取 buffer.markReaderIndex(); System.out.println(buffer.readInt()); log(buffer); // 恢复到mark标记处 buffer.resetReaderIndex(); log(buffer); } public 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()); } }
1 2 3 4 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
-
起点,对于 NIO 实现来讲,在 io.netty.channel.nio.AbstractNioByteChannel.NioByteUnsafe.read 方法中首次创建 ByteBuf 并放入 pipeline;
-
入站 ByteBuf 处理原则
- 对原始 ByteBuf 不做处理,调用 ctx.fireChannelRead(msg) 向后传递,这时无须 release;
- 将原始 ByteBuf 转换为其它类型的 Java 对象,这时 ByteBuf 就没用了,必须 release;
- 如果不调用 ctx.fireChannelRead(msg) 向后传递,那么也必须 release;
- 注意各种异常,如果 ByteBuf 没有成功传递到下一个 ChannelHandler,必须 release;
- 假设消息一直向后传,那么 TailContext 会负责释放未处理消息(原始的 ByteBuf);
-
出站 ByteBuf 处理原则:出站消息最终都会转为 ByteBuf 输出,一直向前传,由 HeadContext flush 之后进行release操作;
-
异常处理原则:有时候不清楚 ByteBuf 被引用了多少次,但又必须彻底释放,可以循环调用 release 直到返回 true为止
while (!buffer.release()) {}
-
-
当ByteBuf被传到了pipeline的head与tail时,ByteBuf会被其中的方法彻底释放,但前提是ByteBuf被传递到了head与tail中;在中间就处理完的ByteBuf是不会被head和tail处理的;
-
TailConext中释放ByteBuf的源码:
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,是的话才需要释放;
public static boolean release(Object msg) { if (msg instanceof ReferenceCounted) { return ((ReferenceCounted) msg).release(); } return false; }
-
切片:ByteBuf的切片是零拷贝(减少数据复制)的体现之一,对原始 ByteBuf 进行切片成多个 ByteBuf,切片后的 ByteBuf 并没有发生内存复制,还是使用原始 ByteBuf 的内存,切片后的 ByteBuf 维护独立的 read,write 指针,得到分片后的buffer后,要调用其retain方法,使其内部的引用计数加一。避免原ByteBuf释放,导致切片buffer无法使用;由于原始的ByteBuf和切片slice的底层都是使用同一块内存的,所以说当其中的一方发生修改时,另一方也会受到影响,接下来对分片的各种情况以及使用进行说明:
-
对ByteBuf进行一些初始操作:
ByteBuf origin = ByteBufAllocator.DEFAULT.buffer(10); origin.writeBytes(new byte[]{1, 2, 3, 4}); origin.readByte(); System.out.println(ByteBufUtil.prettyHexDump(origin)); +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 02 03 04 |... | +--------+-------------------------------------------------+----------------+
这时调用 slice 进行切片,无参 slice 是从原始 ByteBuf 的 read index 到 write index 之间的内容进行切片,切片后的 max capacity 被固定为这个区间的大小,因此不能追加 write
ByteBuf slice = origin.slice(); System.out.println(ByteBufUtil.prettyHexDump(slice)); // slice.writeByte(5); 如果执行,会报 IndexOutOfBoundsException 异常 +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 02 03 04 |... | +--------+-------------------------------------------------+----------------+
如果原始 ByteBuf 再次读操作(又读了一个字节)
origin.readByte(); System.out.println(ByteBufUtil.prettyHexDump(origin)); +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 03 04 |.. | +--------+-------------------------------------------------+----------------+
这时的 slice 不受影响,因为它有独立的读写指针
System.out.println(ByteBufUtil.prettyHexDump(slice)); +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 02 03 04 |... | +--------+-------------------------------------------------+----------------+
如果 slice 的内容发生了更改
slice.setByte(2, 5); System.out.println(ByteBufUtil.prettyHexDump(slice)); +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 02 03 05 |... | +--------+-------------------------------------------------+----------------+
这时,原始 ByteBuf 也会受影响,因为底层都是同一块内存
System.out.println(ByteBufUtil.prettyHexDump(origin)); +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 03 05 |... | +--------+-------------------------------------------------+----------------+
-
duplicate:零拷贝的体现之一,就好比截取了原始 ByteBuf 所有内容,并且没有 max capacity 的限制,也是与原始 ByteBuf 使用同一块底层内存,只是读写指针是独立的;
-
copy:会将底层内存数据进行深拷贝,因此无论读写,都与原始 ByteBuf 无关;
-
CompositeByteBuf:零拷贝的体现之一,可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf,避免拷贝;有两个 ByteBuf 如下:
ByteBuf buf1 = ByteBufAllocator.DEFAULT.buffer(5); buf1.writeBytes(new byte[]{1, 2, 3, 4, 5}); ByteBuf buf2 = ByteBufAllocator.DEFAULT.buffer(5); buf2.writeBytes(new byte[]{6, 7, 8, 9, 10}); System.out.println(ByteBufUtil.prettyHexDump(buf1)); System.out.println(ByteBufUtil.prettyHexDump(buf2));
+-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 01 02 03 04 05 |..... | +--------+-------------------------------------------------+----------------+ +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 06 07 08 09 0a |..... | +--------+-------------------------------------------------+----------------+
我们可以使用以下的代码将buf1和buf2合并到一个新的 ByteBuf
ByteBuf buf3 = ByteBufAllocator.DEFAULT .buffer(buf1.readableBytes()+buf2.readableBytes()); buf3.writeBytes(buf1); buf3.writeBytes(buf2); System.out.println(ByteBufUtil.prettyHexDump(buf3));
+-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 01 02 03 04 05 06 07 08 09 0a |.......... | +--------+-------------------------------------------------+----------------+
但是这种方法不太好,因为进行了数据的内存复制操作,我们可以使用CompositeByteBuf对其进行优化;
CompositeByteBuf buf3 = ByteBufAllocator.DEFAULT.compositeBuffer(); // true 表示增加新的 ByteBuf 自动递增 write index, 否则 write index 会始终为 0 buf3.addComponents(true, buf1, buf2);
+-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 01 02 03 04 05 06 07 08 09 0a |.......... | +--------+-------------------------------------------------+----------------+
CompositeByteBuf 是一个组合的 ByteBuf,它内部维护了一个 Component 数组,每个 Component 管理一个 ByteBuf,记录了这个 ByteBuf 相对于整体偏移量等信息,代表着整体中某一段的数据。
- 优点是对外是一个虚拟视图,组合这些 ByteBuf 不会产生内存复制;
- 缺点是复杂了很多,多次操作会带来性能的损耗;
-
Unpooled:Unpooled 是一个工具类,类如其名,提供了非池化的 ByteBuf 创建、组合、复制等操作,这里仅介绍其跟零拷贝相关的 wrappedBuffer 方法,它可以用来包装 ByteBuf;
ByteBuf buf1 = ByteBufAllocator.DEFAULT.buffer(5); buf1.writeBytes(new byte[]{1, 2, 3, 4, 5}); ByteBuf buf2 = ByteBufAllocator.DEFAULT.buffer(5); buf2.writeBytes(new byte[]{6, 7, 8, 9, 10}); // 当包装 ByteBuf 个数超过一个时, 底层使用了 CompositeByteBuf ByteBuf buf3 = Unpooled.wrappedBuffer(buf1, buf2); System.out.println(ByteBufUtil.prettyHexDump(buf3));
+-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 01 02 03 04 05 06 07 08 09 0a |.......... | +--------+-------------------------------------------------+----------------+
该方法也可以用来包装普通字节数组,底层也不会有拷贝操作;
ByteBuf buf4 = Unpooled.wrappedBuffer(new byte[]{1, 2, 3}, new byte[]{4, 5, 6}); System.out.println(buf4.getClass()); System.out.println(ByteBufUtil.prettyHexDump(buf4));
+-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 01 02 03 04 05 06 |...... | +--------+-------------------------------------------------+----------------+
-
ByteBuf的优势
- 池化可以重用池中 ByteBuf 实例,更节约内存,减少内存溢出的可能;
- 读写指针分离,不需要像 ByteBuffer 一样切换读写模式;
- 可以自动扩容;
- 支持链式调用,使用更流畅;
- 很多地方体现零拷贝,例如 slice、duplicate、CompositeByteBuf;