Netty入门学习总结

157 阅读24分钟

本文内容来自B站黑马课程及相关书籍学习总结

1 Netty概述

Netty 是一个异步的、基于事件驱动的网络应用框架,用于快速开发可维护、高性能的网络服务器和客户端。

2 入门案例:Hello World

添加Netty依赖

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.76.Final</version>
</dependency>

2.1 服务端

public class HelloServer {
    public static void main(String[] args) {
        //1.创建服务端引导类
        ServerBootstrap b = new ServerBootstrap();
        //2.创建反应器轮询组
        NioEventLoopGroup bossLoopGroup = new NioEventLoopGroup(1);
        NioEventLoopGroup workerLoopGroup = new NioEventLoopGroup();
        //3.为引导类设置反应器轮询组
        b.group(bossLoopGroup, workerLoopGroup);
        //4.设置通道类型
        b.channel(NioServerSocketChannel.class);
        //5.装配子通道流水线
        b.childHandler(new ChannelInitializer<NioSocketChannel>() {
            @Override
            protected void initChannel(NioSocketChannel ch) throws Exception {
                //添加处理器
                ch.pipeline().addLast(new StringDecoder());
                ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                    @Override
                    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                        System.out.println(msg);
                    }
                });
            }
        });
        //6.绑定监听端口
        b.bind(8080);
    }
}

2.2 客户端

public class HelloClient {
    public static void main(String[] args) throws InterruptedException {
        //1.创建引导类
        Bootstrap b = new Bootstrap();
        //2.创建反应器轮询组
        NioEventLoopGroup workLoopGroup = new NioEventLoopGroup();
        //3.为引导类设置反应器轮询组
        b.group(workLoopGroup);
        //4.设置通道类型
        b.channel(NioSocketChannel.class);
        //5.转配通道流水线
        b.handler(new ChannelInitializer<NioSocketChannel>() {
            @Override
            protected void initChannel(NioSocketChannel ch) throws Exception {
                ch.pipeline().addLast(new StringEncoder());
            }
        });
        //6.连接服务端,并发送数据
        b.connect(new InetSocketAddress("localhost", 8080))
                .sync()
                .channel()
                .writeAndFlush("hello, world");
​
    }
}

3 Netty组件

3.1 Bootstrap

Bootstrap类是Netty提供的一个便利的工厂类,可以通过它来完成Netty的客户端或服务端的Netty组件的组装,以及Netty程序的初始化和启动执行。

Netty有两个引导类,分别用于服务器和客户端,即ServerBootStrap和BootStrap。

3.1.1 父子通道

在Netty中,将有接收关系的监听通道和传输通道叫作父子通道。其中,负责服务器连接监听和接收的监听通道(如NioServerSocketChannel)也叫父通道(Parent Channel),对应于每一个接收到的传输类通道(如NioSocketChannel)也叫子通道(Child Channel)。

3.1.2 EventLoopGroup

EventLoop事件循环对象

EventLoop 本质是一个单线程执行器(同时维护了一个 Selector),里面有 run 方法处理 Channel 上源源不断的 io 事件。

它的继承关系比较复杂

  • 一条线是继承自 j.u.c.ScheduledExecutorService,因此包含了线程池中所有的方法

  • 另一条线是继承自 netty 自己的 OrderedEventExecutor,

    • 提供了 boolean inEventLoop(Thread thread) 方法判断一个线程是否属于此 EventLoop
    • 提供了 parent 方法来看看自己属于哪个 EventLoopGroup

image-20230226225906266.png

EventLoopGroup事件循环组

EventLoopGroup 是一组 EventLoop,Channel 一般会调用 EventLoopGroup 的 register 方法来绑定其中一个 EventLoop,后续这个 Channel 上的 io 事件都由此 EventLoop 来处理(保证了 io 事件处理时的线程安全)

  • 继承自 netty 自己的 EventExecutorGroup

    • 实现了 Iterable 接口提供遍历 EventLoop 的能力
    • 另有 next 方法获取集合中下一个 EventLoop

演示NioEventLoop处理普通任务和定时任务

NioEventLoop除了可以处理io事件,还可以处理普通任务和定时任务,而DefaultEventLoop只能处理普通任务和定时任务。

public static void main(String[] args) {
  //1. 创建事件轮询组
  NioEventLoopGroup group = new NioEventLoopGroup(2);//io事件 普通任务 定时任务
  //        DefaultEventLoop group = new DefaultEventLoop();//普通任务 定时任务
  //2. 获取下一个事件循环对象
  System.out.println(group.next());
  System.out.println(group.next());
  System.out.println(group.next());
  System.out.println(group.next());
  //3. 执行普通任务
  group.execute(() -> {
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }
    log.debug("hello!");
  });
  //4. 执行定时任务
  group.scheduleAtFixedRate(() -> {
    log.debug("ok");
  }, 0, 1, TimeUnit.SECONDS);
  log.debug("main");
}

演示NioEventLoop处理IO事件

创建两个NioEventLoopGroup,一个bossLoopGroup用于处理Accept事件,另一个workerLoopGroup用于处理其他IO读写事件。

public static void main(String[] args) {
    //1.创建服务端引导类
    ServerBootstrap b = new ServerBootstrap();
    //2.创建事件循环组
    NioEventLoopGroup bossLoopGroup = new NioEventLoopGroup(1);
    NioEventLoopGroup workerLoopGroup = new NioEventLoopGroup(2);
    //3.为引导类设置事件循环组
    b.group(bossLoopGroup, workerLoopGroup);
    //4.设置通道类型
    b.channel(NioServerSocketChannel.class);
    //5.装配子通道流水线
    b.childHandler(new ChannelInitializer<NioSocketChannel>() {
        @Override
        protected void initChannel(NioSocketChannel ch) throws Exception {
            //添加处理器
            ch.pipeline().addLast("handle1", new ChannelInboundHandlerAdapter() {
                @Override
                public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                    ByteBuf buf = (ByteBuf) msg;
                    log.debug(buf.toString(StandardCharsets.UTF_8));
                }
            });
        }
    });
    //6.绑定监听端口
    b.bind(8080);
}

通道处理器默认使用workerLoopGroup的线程执行任务,如果当前连接数较多,且handler处理耗时较长,那么处理效率不高。此时可以创建一个DefaultEventLoopGroup,用于处理handler任务。

//创建独立的非nio事件循环组处理普通任务
DefaultEventLoopGroup group = new DefaultEventLoopGroup();
//...
b.childHandler(new ChannelInitializer<NioSocketChannel>() {
  @Override
  protected void initChannel(NioSocketChannel ch) throws Exception {
    //添加处理器
    ch.pipeline().addLast("handle1", new ChannelInboundHandlerAdapter() {
      @Override
      public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf) msg;
        log.debug(buf.toString(StandardCharsets.UTF_8));
        super.channelRead(ctx, msg);
      }
    });
    ch.pipeline().addLast(group, "handle2", new ChannelInboundHandlerAdapter() {
      @Override
      public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf) msg;
        log.debug(buf.toString(StandardCharsets.UTF_8));
      }
    });
  }
});

Handler如何切换EventLoop执行

static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
    final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
    // 下一个 handler 的事件循环是否与当前的事件循环是同一个线程
    EventExecutor executor = next.executor();
    
    // 是,直接调用
    if (executor.inEventLoop()) {
        next.invokeChannelRead(m);
    } 
    // 不是,将要执行的代码作为任务提交给下一个事件循环处理(换人)
    else {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                next.invokeChannelRead(m);
            }
        });
    }
}
  • 如果两个 handler 绑定的是同一个线程,那么就直接调用
  • 否则,把要调用的代码封装为一个任务对象,由下一个 handler 的线程来调用

3.1.3 BootStrap启动流程

以服务端为例,大致步骤如下:

第1步:创建事件循环组,并设置到ServerBootstrap引导类实例;

第2步:设置通道的IO类型;

第3步:设置传输通道的配置选项;

//step4:设置通道的参数
b.option(ChannelOption.SO_KEEPALIVE, true);
b.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);

第4步:装配子通道的Pipeline;

第5步:开始绑定服务器新连接的监听端口

第7步:自我阻塞,直到监听通道关闭

第8步:关闭EventLoopGroup

3.1.4 ChannelOption

无论是对于NioServerSocketChannel父通道类型还是对于NioSocketChannel子通道类型,都可以设置一系列的ChannelOption(通道选项):

  1. SO_RCVBUF和SO_SNDBUF

    这两个为TCP传输选项,每个TCP socket(套接字)在内核中都有一个发送缓冲区和一个接收缓冲区,这两个选项就是用来设置TCP连接的两个缓冲区大小的。

  2. TCP_NODELAY

    此为TCP传输选项,如果设置为true就表示立即发送数据。TCP_NODELAY用于开启或关闭Nagle算法。

  3. SO_KEEPALIVE

    此为TCP传输选项,表示是否开启TCP的心跳机制。true为连接保持心跳,默认值为false。

  4. SO_BACKLOG

    此为TCP传输选项,表示服务端接收连接的队列长度,如果队列已满,客户端连接将被拒绝。多个客户端到来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,队列的大小通过SO_BACKLOG指定。

3.2 Channel

channel 的主要作用

  • close() 可以用来关闭 channel

  • closeFuture() 用来处理 channel 的关闭

    • sync 方法作用是同步等待 channel 关闭
    • 而 addListener 方法是异步等待 channel 关闭
  • pipeline() 方法添加处理器

  • write() 方法将数据写入

  • writeAndFlush() 方法将数据写入并刷出

3.2.1 同步和异步回调执行发送

同步方式:

//连接服务端
ChannelFuture channelFuture = b.connect(new InetSocketAddress("localhost", 8080));
//使用sync方法同步等待连接完成
channelFuture.sync();
Channel channel = channelFuture.channel();
log.debug("{}",channel);
channel.writeAndFlush("hello,world!");//客户端发送数据

注意: connect 方法是异步的,意味着不等连接建立,方法执行就返回了。如果此时直接从channelFuture获取channel,那么channel对象可能不是正确的。

异步回调方式:

//使用addListener(回调对象)方法异步等待连接建立后执行发送
channelFuture.addListener(new ChannelFutureListener() {
    @Override
    public void operationComplete(ChannelFuture future) throws Exception {
        Channel channel = future.channel();
        log.debug("{}",channel);
        channel.writeAndFlush("hello,world!");//客户端发送数据
    }
});

3.2.2 关闭问题

使用channel.closeFuture()方法,该方法返回一个ChannelFuture对象,当channel执行关闭时,会通知该future,用于执行一些通道关闭后的操作,如释放底层资源。

同步方式执行通道关闭后的操作:

//获取closeFuture对象,当通道关闭时会通知该future
ChannelFuture closeFuture = channel.closeFuture();
log.debug("waiting close...");
closeFuture.sync();//阻塞等待通道关闭
log.debug("执行关闭之后的操作");
workLoopGroup.shutdownGracefully();//优雅关闭事件循环组

异步方式执行通道关闭后的操作:

closeFuture.addListener(new ChannelFutureListener(){
    @Override
    public void operationComplete(ChannelFuture future) throws Exception {
        log.debug("执行关闭之后的操作");
        workLoopGroup.shutdownGracefully();//优雅关闭事件循环组
    }
});

💡 异步提升的是什么

  • 单线程没法异步提高效率,必须配合多线程、多核 cpu 才能发挥异步的优势
  • 异步并没有缩短响应时间,反而有所增加
  • 合理进行任务拆分,也是利用异步的关键

3.3 Future & Promise

Netty的Future接口继承自java.util.concurrent.Future接口,并进行扩展;而Netty的Promise接口则继承了Netty的Future接口并进行了扩展。

  • jdk Future 只能同步等待任务结束(或成功、或失败)才能得到结果
  • netty Future 可以同步等待任务结束得到结果,也可以异步方式得到结果,但都是要等任务结束
  • netty Promise 不仅有 netty Future 的功能,而且脱离了任务独立存在,只作为两个线程间传递结果的容器
功能/名称jdk Futurenetty FuturePromise
cancel取消任务--
isCanceled任务是否取消--
isDone任务是否完成,不能区分成功失败--
get获取任务结果,阻塞等待--
getNow-获取任务结果,非阻塞,还未产生结果时返回 null-
await-等待任务结束,如果任务失败,不会抛异常,而是通过 isSuccess 判断-
sync-等待任务结束,如果任务失败,抛出异常-
isSuccess-判断任务是否成功-
cause-获取失败信息,非阻塞,如果没有失败,返回null-
addLinstener-添加回调,异步接收结果-
setSuccess--设置成功结果
setFailure--设置失败结果

3.3.1 Jdk Future示例

@Slf4j
public class TestJdkFuture {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //1. 创建线程池
        ExecutorService service = Executors.newFixedThreadPool(2);
        //2. 提交任务
        Future<Integer> future = service.submit(() -> {
            log.debug("执行任务");
            Thread.sleep(1000);
            return 50;
        });
        log.debug("等待结果...");
        log.debug("结果是:{}", future.get());
    }
}

3.3.2 Netty Future示例

@Slf4j
public class TestNettyFuture {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //1. 创建时间循环组
        DefaultEventLoopGroup executors = new DefaultEventLoopGroup();
        //2. 提交任务
        Future<Integer> future = executors.submit(() -> {
            log.debug("执行任务");
            Thread.sleep(1000);
            return 50;
        });
        log.debug("等待结果...");
        //异步回调接收结果
        future.addListener(new GenericFutureListener<Future<? super Integer>>() {
            @Override
            public void operationComplete(Future<? super Integer> future) throws Exception {
                log.debug("结果是:{}", future.getNow());
            }
        });
    }
}

3.3.3 Netty Promise示例

@Slf4j
public class TestNettyPromise {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //1. 创建EventLoopGroup
        DefaultEventLoop executors = new DefaultEventLoop();
        //2. 创建Promise
        DefaultPromise<Integer> promise = new DefaultPromise<>(executors);
        //3. 提交任务,计算完成后填充结果
        executors.execute(() -> {
            log.debug("开始计算...");
            try {
                Thread.sleep(1000);
                promise.setSuccess(80);
            } catch (Exception e) {
                promise.setFailure(e);
            }
        });
        //4. 接收结果
        log.debug("等待结果...");
        log.debug("结果是:{}",promise.get());
    }
}

3.4 Handler & Pipeline

ChannelHandler 用来处理 Channel 上的各种事件,分为入站、出站两种。所有 ChannelHandler 被连成一串,就是 Pipeline 流水线

  • 入站处理器通常是 ChannelInboundHandlerAdapter 的子类,主要用来读取客户端数据,写回结果

  • 出站处理器通常是 ChannelOutboundHandlerAdapter 的子类,主要对写回结果进行加工

3.4.1 ChannelInboundHandler入站处理器

ChannelInboundHandlerAdapter核心方法:

  1. channelRegistered()

    当通道注册完成后,触发通道注册事件,在通道流水线注册过的入站处理器的channelRegistered()回调方法会被调用。

  2. channelActive()

    当通道激活完成后,触发通道激活事件,在通道流水线注册过的入站处理器的channelActive()回调方法会被调用。

  3. channelRead()

    当通道缓冲区可读时,触发通道可读事件,在通道流水线注册过的入站处理器的channelRead()回调方法会被调用,以便完成入站数据的读取和处理。

  4. channelReadComplete()

    当通道缓冲区读完时,触发通道缓冲区读完事件,在通道流水线注册过的入站处理器的channelReadComplete()回调方法会被调用。

  5. channelInactive()

    当连接被断开或者不可用时,触发连接不可用事件,在通道流水线注册过的入站处理器的channelInactive()回调方法会被调用。

  6. exceptionCaught()

    当通道处理过程发生异常时,触发异常捕获事件,在通道流水线注册过的入站处理器的exceptionCaught()方法会被调用。

3.4.2 ChannelOutboundHandler出站处理器

当业务处理完成后,需要操作Java NIO底层通道时,通过一系列的ChannelOutboundHandler出站处理器完成Netty通道到底层通道的操作,比如建立底层连接、断开底层连接、写入底层Java NIO通道等。

ChannelOutboundHandler核心方法:

  1. bind()

    监听地址(IP+端口)绑定:完成底层Java IO通道的IP地址绑定,如果使用TCP传输协议,这个方法用于服务端。

  2. connect()

    连接服务端:完成底层Java IO通道的服务端的连接操作。如果使用TCP传输协议,那么这个方法将用于客户端。

  3. write()

    写数据到底层:完成Netty通道向底层Java IO通道的数据写入操作。

  4. flush()

    将底层缓存区的数据腾空,立即写出到对端。

  5. read()

    从底层读数据:完成Netty通道从Java IO通道的数据读取。

  6. disConnect()

    断开服务器连接:断开底层Java IO通道的socket连接。如果使用TCP传输协议,此方法主要用于客户端。

  7. close()

    主动关闭通道:关闭底层的通道,例如服务端的新连接监听通道。

3.4.3 执行顺序

public static void main(String[] args) throws InterruptedException {
    ServerBootstrap b = new ServerBootstrap();
    NioEventLoopGroup bossLoopGroup = new NioEventLoopGroup(1);
    NioEventLoopGroup workerLoopGroup = new NioEventLoopGroup();
    b.group(bossLoopGroup,workerLoopGroup);
    b.channel(NioServerSocketChannel.class);
    b.childHandler(new ChannelInitializer<NioSocketChannel>() {
        @Override
        protected void initChannel(NioSocketChannel ch) throws Exception {
            ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                @Override
                public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                    log.debug("1");
                    super.channelRead(ctx, msg);
                }
            });
            ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                @Override
                public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                    log.debug("2");
                    super.channelRead(ctx, msg);
                }
            });
            ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                @Override
                public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                    log.debug("3");
                    ch.write("收到!");
                }
            });
            ch.pipeline().addLast(new ChannelOutboundHandlerAdapter(){
                @Override
                public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                    log.debug("4");
                    super.write(ctx, msg, promise);
                }
            });
            ch.pipeline().addLast(new ChannelOutboundHandlerAdapter(){
                @Override
                public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                    log.debug("5");
                    super.write(ctx, msg, promise);
                }
            });
            ch.pipeline().addLast(new ChannelOutboundHandlerAdapter(){
                @Override
                public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                    log.debug("6");
                    super.write(ctx, msg, promise);
                }
            });
        }
    });
    b.bind(8080).sync();
}

从日志打印结果可以看出,ChannelInboundHandlerAdapter 是按照 addLast 的顺序执行的,而 ChannelOutboundHandlerAdapter 是按照 addLast 的逆序执行的。ChannelPipeline 的实现是一个 ChannelHandlerContext(包装了 ChannelHandler) 组成的双向链表。

0008.png

  • super.channelRead(ctx, msg)方法内部调用的是ctx.fireChannelRead(msg) ,即调用下一个入站处理器

  • ctx.channel().write(msg) 会 从尾部开始触发 后续出站处理器的执行

  • 出站处理器中,ctx.write(msg, promise) 的调用会 触发上一个出站处理器

  • ctx.channel().write(msg) vs ctx.write(msg)

    • 都是触发出站处理器的执行
    • ctx.channel().write(msg) 从尾部开始查找出站处理器
    • ctx.write(msg) 是从当前节点找上一个出站处理器

3.4.4 EmbeddedChannel

EmbeddedChannel仅仅是模拟入站与出站的操作,底层不进行实际传输,不需要启动Netty服务器和客户端。

使用EmbeddedChannel,开发人员可以在单元测试用例中方便、快速地进行ChannelHandler业务处理器的单元测试,避免每开发一个业务处理器都进行服务器和客户端的重复启动。

主要方法:

  • writeInbound()

    调用writeInbound()方法,向EmbeddedChannel写入一个入站数据(如二进制ByteBuf数据包),模拟底层的入站包,从而被入站处理器处理到

  • writeInbound()

    调用writeOutbound()方法,向模拟通道写入一个出站数据(如二进制ByteBuf数据包),该包将进入处理器流水线,被待测试的出站处理器所处理

案例:

//创建处理器
ChannelInitializer<EmbeddedChannel> initializer = new ChannelInitializer<EmbeddedChannel>() {
​
    @Override
    protected void initChannel(EmbeddedChannel ch) throws Exception {
        ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                ByteBuf buf = (ByteBuf) msg;
                log.debug(buf.toString(StandardCharsets.UTF_8));
            }
        });
        ch.pipeline().addLast(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(initializer);
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
//模拟入站操作
channel.writeInbound(buffer.writeBytes("hello".getBytes()));
//模拟出站操作
channel.writeOutbound(buffer.writeBytes("hello".getBytes()));

3.5 ButeBuf

Netty提供了ByteBuf缓冲区组件来替代Java NIO的ByteBuffer缓冲区组件,以便更加快捷和高效地操纵内存缓冲区。

3.5.1 创建ByteBuf

ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(10);

3.5.2 ByteBuf的分配器

Netty通过ByteBufAllocator分配器来创建缓冲区和分配内存空间。Netty提供了两种分配器实现:PoolByteBufAllocator(池化分配器)和UnpooledByteBufAllocator(非池化分配器)。

池化和非池化对比:

  • 使用非池化,每次调用时创建一个新的ByteBuf实例;使用完之后,通过Java的垃圾回收机制回收或者直接释放(对于直接内存而言)。
  • 有了池化,则可以重用池中 ByteBuf 实例,并且采用了与 jemalloc 类似的内存分配算法提升分配效率
  • 高并发时,池化功能更节约内存,减少内存溢出的可能

Netty默认的分配器为ByteBufAllocator.DEFAULT。该默认分配器可以通过系统参数(System Property)选项io.netty.allocator.type进行配置来选择开启池化功能:

-Dio.netty.allocator.type={unpooled|pooled}

Netty4.1版本默认的分配器为PooledByteBufAllocator(池化内存分配器)。

3.5.3 ByteBuf缓冲区类型

根据内存的管理方不同,缓冲区分为堆缓冲区(Heap ByteBuf)直接缓冲区(Direct ByteBuf)

创建池化基于堆的 ByteBuf:

ByteBuf buffer = ByteBufAllocator.DEFAULT.heapBuffer(10);

创建池化基于直接内存的 ByteBuf:

ByteBuf buffer = ByteBufAllocator.DEFAULT.directBuffer(10);

直接内存:

  • 直接内存不属于Java堆内存,所分配的内存其实是调用操作系统malloc()函数来获得的,由Netty的本地Native堆进行管理
  • 直接内存创建和销毁的代价昂贵,但读写性能高(少一次内存复制),适合配合池化功能一起用
  • 直接内存对 GC 压力小,因为这部分内存不受 JVM 垃圾回收的管理,但也要注意及时主动释放

3.5.4 ByteBuf组成

ByteBuf是一个字节容器,内部是一个字节数组。从逻辑上来分,字节容器内部可以分为四个部分

0010.png

第一部分是已用字节,表示已经使用完的废弃的无效字节;

第二部分是可读字节,这部分数据是ByteBuf保存的有效数据,从ByteBuf中读取的数据都来自这一部分;

第三部分是可写字节,写入ByteBuf的数据都会写到这一部分中;

第四部分是可扩容字节,表示的是该ByteBuf最多还能扩容的大小。

3.5.5 ByteBuf的方法

写入方法:

方法签名含义备注
isWritable()ByteBuf是否可写
writeTYPE(TYPE value)写入基本数据类型
writeBytes(ByteBuf src)写入 netty 的 ByteBuf
writeBytes(byte[] src)写入 byte[]
writeBytes(ByteBuffer src)写入 nio 的 ByteBuffer
int writeCharSequence(CharSequence sequence, Charset charset)写入字符串

还有一类方法是 set 开头的一系列方法,也可以写入数据,但不会改变写指针位置

读取方法:

读取和上述写入的方法一一对应;

如果需要重复读取废弃内容,则可以使用如下方法:

markReaderIndex()与resetReaderIndex():前一种方法表示把当前的读指针readerIndex保存在markedReaderIndex属性中;后一种方法表示把保存在markedReaderIndex属性的值恢复到读指针readerIndex中。

还有种办法是采用 get 开头的一系列方法,这些方法不会改变 read index。

3.5.6 ByteBuf扩容

扩容规则是

  • 如何写入后数据大小未超过 512,则选择下一个 16 的整数倍,例如写入后大小为 12 ,则扩容后 capacity 是 16
  • 如果写入后数据大小超过 512,则选择下一个 2^n,例如写入后大小为 513,则扩容后 capacity 是 2^10=1024(2^9=512 已经不够了)
  • 扩容不能超过 max capacity 会报错

3.5.7 ByteBuf的引用计数

Netty的ByteBuf的内存回收工作是通过引用计数方式管理的。

ByteBuf引用计数的大致规则如下:

  • 在默认情况下,当创建完一个ByteBuf时,引用计数为1;

  • 每次调用retain()方法,引用计数加1;

  • 每次调用release()方法,引用计数减1;

  • 当引用为0,再次访问这个ByteBuf对象,将会抛出异常;此时这个ByteBuf没有被引用,它占用的内存会回收。

当ByteBuf的引用计数已经为0时,Netty会进行ByteBuf的回收,分为以下两种场景:

  • 如果属于池化的ByteBuf内存,回收方法是:放入可以重新分配的ByteBuf池,等待下一次分配。

  • 如果属于未池化的ByteBuf缓冲区,需要细分为两种情况:如果是堆(Heap)结构缓冲,会被JVM的垃圾回收机制回收;如果是直接(Direct)内存类型,则会调用本地方法释放外部内存(unsafe.freeMemory)

除了通过ByteBuf成员方法retain()和release()管理引用计数之外,Netty还提供了一组用于增加和减少引用计数的通用静态方法:

  • ReferenceCountUtil.retain(Object):增加一次缓冲区引用计数的静态方法,从而防止该缓冲区被释放。
  • ReferenceCountUtil.release(Object):减少一次缓冲区引用计数的静态方法,如果引用计数为0,缓冲区将被释放。

3.5.8 ByteBuf的自动创建与自动释放

  • 起点,对于 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

TailContext自动释放

Netty默认会在ChannelPipline的最后添加一个TailContext(尾部上下文,也是一个入站处理器)。它实现了默认的入站处理方法,在这些方法中会帮助完成ByteBuf内存释放的工作。

沿着channelRead()方法向下,最后通过ReferenceCountUtil.release()释放了缓冲区。

// io.netty.channel.DefaultChannelPipeline#onUnhandledInboundMessage(java.lang.Object)
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缓存区向后传递,则需要手动进行释放。

SimpleChannelInboundHandler自动释放

继承SimpleChannelInboundHandler基类实现处理器,此时必须将业务处理代码移动到重写的channelRead0(ctx, msg)方法中,SimpleChannelInboundHandler的channelRead方法在执行完channelRead0()方法后,会释放缓冲器:

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    boolean release = true;
    try {
        if (acceptInboundMessage(msg)) {
            @SuppressWarnings("unchecked")
            I imsg = (I) msg;
            channelRead0(ctx, imsg);
        } else {
            release = false;
            ctx.fireChannelRead(msg);
        }
    } finally {
        if (autoRelease && release) {
            //释放缓冲区
            ReferenceCountUtil.release(msg);
        }
    }
}

出站处理时的自动释放

出站缓冲区的自动释放方式是HeadContext自动释放。通过write()方法写入流水线时,调用ctx.writeAndFlush(ByteBuf msg),就会让ByteBuf缓冲区进入流水线的出站处理流程。在每一个出站Handler业务处理器中的处理完成后,数据包(或消息)会来到出站处理的最后一棒HeadContext,在完成数据输出到通道之后,ByteBuf会被释放一次,如果计数器为零,就将被彻底释放掉。

3.5.9 ByteBuf复制和零拷贝

大部分场景下,在Netty接收和发送ByteBuffer的过程中会使用直接内存进行Socket通道读写,使用JVM的堆内存进行业务处理,会涉及直接内存、堆内存之间的数据复制。内存的数据复制效率非常低,Netty提供了多种方法,以帮助应用程序减少内存的复制。

1. Slice切片浅层复制

ByteBuf的slice()方法可以获取到一个ByteBuf的切片。一个ByteBuf可以进行多次切片浅层复制;多次切片后的ByteBuf对象可以共享一个存储区域(没有发生内存复制,还是使用原ByteBuf的内存),切片后的 ByteBuf 维护独立的 read,write 指针。

0011.png

Slice()方法有两个重载版本:

  • public ByteBuf slice()

  • public ByteBuf slice(int index, int length)

第一个无参数slice()方法的返回值是ByteBuf实例中可读部分的切片;带参数的slice(int index, int length)方法可以通过灵活地设置不同起始位置和长度来获取到ByteBuf不同区域的切片。

代码示例:

public static void main(String[] args) {
    ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(10);
    buf.writeBytes(new byte[]{'a','b','c','d','e','f','g','h','i','j'});
    log(buf);
​
    //在切片过程中,没有发生数据复制
    ByteBuf slice1 = buf.slice(0, 5);
    ByteBuf slice2 = buf.slice(0, 5);
    log(slice1);
    log(slice2);
​
    slice1.setByte(0,'b');//修改切片ByteBuf,会同步修改原ByteBuf
    log(slice1);
    log(buf);
}

总结:

  • 切片不会复制源ByteBuf的底层数据,底层数组和源ByteBuf的底层数组是同一个。
  • 切片不会改变源ByteBuf的引用计数。
2. duplicate整体浅层复制

duplicate()和slice()方法都是浅层复制。不同的是,slice()方法是切取一段的浅层复制,而duplicate()是整体的浅层复制

0012.png

3. 浅层复制的问题

浅层复制方法不会实际去复制数据,也不会改变ByteBuf的引用计数,会导致一个问题:在源ByteBuf调用release()方法之后,一旦引用计数为零,就变得不能访问了;

因此,在调用浅层复制实例时,可以通过调用一次retain()方法来增加引用,表示它们对应的底层内存多了一次引用。在浅层复制实例用完后,需要调用release()方法,将引用计数减1,这样就不会影响源ByteBuf的内存释放了。

4. copy

ByteBuf的copy方法会将底层内存数据进行深拷贝,因此无论读写,都与原始 ByteBuf 无关

5. CompositeByteBuf实现零拷贝

CompositeByteBuf可以把需要合并的多个ByteBuf组合起来,对外提供统一的readIndex和writerIndex。

CompositeByteBuf只是在逻辑上是一个整体,在CompositeByteBuf内部,合并的多个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));
CompositeByteBuf buf3 = ByteBufAllocator.DEFAULT.compositeBuffer();
buf3.addComponents(true, buf1, buf2);
System.out.println(ByteBufUtil.prettyHexDump(buf3));
  • 优点,对外是一个虚拟视图,组合这些 ByteBuf 不会产生内存复制
  • 缺点,复杂了很多,多次操作会带来性能的损耗
6. wrap操作实现零拷贝

Unpooled提供了一系列的wrap包装方法,可以快速地包装出CompositeByteBuf实例或者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 buf4 = Unpooled.wrappedBuffer(buf1, buf2);

除了通过Unpooled包装CompositeByteBuf之外,还可以将byte数组包装成ByteBuf,所得到的ByteBuf对象和bytes数组共用同一个存储空间。

💡 ByteBuf 优势

  • 池化 - 可以重用池中 ByteBuf 实例,更节约内存,减少内存溢出的可能
  • 读写指针分离,不需要像 ByteBuffer 一样切换读写模式
  • 可以自动扩容
  • 支持链式调用,使用更流畅
  • 很多地方体现零拷贝,例如 slice、duplicate、CompositeByteBuf

4 回显服务器案例

服务端代码:

@Slf4j
public class NettyEchoServer {
    public static void main(String[] args) throws InterruptedException {
        //创建服务端引导类
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        //创建事件循环组
        NioEventLoopGroup bossLoopGroup = new NioEventLoopGroup(1);
        NioEventLoopGroup workerLoopGroup = new NioEventLoopGroup();
        try {
            //3.为引导类设置事件循环组
            serverBootstrap.group(bossLoopGroup, workerLoopGroup);
            //4.设置通道类型
            serverBootstrap.channel(NioServerSocketChannel.class);
            //5.添加处理器
            serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
                @Override
                protected void initChannel(NioSocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        @Override
                        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                            ByteBuf buf = (ByteBuf) msg;
                            //服务端打印数据
                            log.debug("服务端接受数据:{}", buf.toString(StandardCharsets.UTF_8));
                            //向客户端写回数据
                            ByteBuf response = ctx.alloc().buffer();
                            response.writeBytes(buf);
                            ctx.writeAndFlush(response);
                            super.channelRead(ctx, buf);
                        }
                    });
                }
            });
            //6.绑定监听端口号
            ChannelFuture channelFuture = serverBootstrap.bind(8081);
​
            channelFuture.addListener((future) -> {
                if (future.isSuccess()) {
                    log.info(" ========》反应器线程 回调 服务器启动成功,监听端口: " +
                            channelFuture.channel().localAddress());
​
                }
            });
​
            log.info(" 调用线程执行的,服务器启动成功,监听端口: " +
                    channelFuture.channel().localAddress());
​
            // 7. 等待通道关闭的异步任务结束
            // 服务监听通道会一直等待通道关闭的异步任务结束
            ChannelFuture closeFuture = channelFuture.channel().closeFuture();
            closeFuture.sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 8 优雅关闭EventLoopGroup,
            // 释放掉所有资源包括创建的线程
            workerLoopGroup.shutdownGracefully();
            bossLoopGroup.shutdownGracefully();
        }
    }
}

客户端代码:

@Slf4j
public class NettyEchoClient {
    public static void main(String[] args) throws InterruptedException {
        //1.创建客户端引导类
        Bootstrap bootstrap = new Bootstrap();
        //2.创建事件轮询组
        NioEventLoopGroup workerEventGroup = new NioEventLoopGroup();
        //3.引导类设置=事件轮询组
        bootstrap.group(workerEventGroup);
        //4.设置通道类型
        bootstrap.channel(NioSocketChannel.class);
        //5.添加处理器
        bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
            @Override
            protected void initChannel(NioSocketChannel ch) throws Exception {
                ch.pipeline().addLast(new StringEncoder());//出站编码器
                ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                    @Override
                    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                        ByteBuf buf = (ByteBuf) msg;
                        log.debug("打印服务端返回:{}", buf.toString(StandardCharsets.UTF_8));
                        super.channelRead(ctx, msg);
                    }
                });
            }
        });
        //6.连接服务端
        ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress("localhost", 8081));
        channelFuture.sync();
​
        //7.向服务端发送数据
        Channel channel = channelFuture.channel();
        new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            while(true){
                String line = scanner.nextLine();
                if (line.equals("q")){
                    channel.close();
                    break;
                }
                channel.writeAndFlush(line);
            }
        }).start();
​
        //8. 关闭释放资源
        ChannelFuture closeFuture = channel.closeFuture();
        closeFuture.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                workerEventGroup.shutdownGracefully();
            }
        });
    }
}