Netty基本使用-Netty组件

Posted cfy137000

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Netty基本使用-Netty组件相关的知识,希望对你有一定的参考价值。

Netty基本使用(二)-Netty组件

1. EventLoop

事件循环对象

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

EventLoop的继承关系

  • 从根上看, 他继承了java的的ScheduledExecutorService.所以他拥有线程池中的所有方法
  • 另外Netty在线程池的基础上,又扩展了EventExecutor,提供了一些额外的方法:
    • boolean inEventLoop(Thread thread) 方法判断一个线程是否属于此 EventLoop
    • parent 方法来看看自己属于哪个 EventLoopGroup

事件循环组对象(EventLoopGroup)

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

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

一个简单的例子

// 内部创建了两个 EventLoop, 每个 EventLoop 维护一个线程
DefaultEventLoopGroup group = new DefaultEventLoopGroup(2);
System.out.println(group.next());
System.out.println(group.next());
System.out.println(group.next());

输出:

group的next方法,会一个一个的获取下一个EventLoop对象

shutdownGracefully

优雅关闭 shutdownGracefully 方法。该方法会首先切换 EventLoopGroup 到关闭状态从而拒绝新的任务的加入,然后在任务队列的任务都处理完成后,停止线程的运行。从而确保整体应用是在正常有序的状态下退出的

2. Channel

channel的主要作用

  • close() 可以用来关闭 channel
  • closeFuture() 用来处理 channel 的关闭
    • sync 方法作用是同步等待 channel 关闭
    • 而 addListener 方法是异步等待 channel 关闭
  • pipeline() 方法添加处理器
  • write() 方法将数据写入
  • writeAndFlush() 方法将数据写入并刷出

ChannelFuture

上一篇中的客户端代码:(Netty基本使用(一))

new Bootstrap()  
        .group(new NioEventLoopGroup())  
        .channel(NiosocketChannel.class)  
        .handler(new ChannelInitializer<>()   
            @Override  
            protected void initChannel(Channel ch)   
                ch.pipeline().addLast(new StringEncoder());  
              
        )  
        .connect("127.0.0.1", 8080)  
        .sync()  
        .channel()  
        .writeAndFlush("hello Chenfy!");

把它拆开来:

ChannelFuture channelFuture = new Bootstrap()  
        .group(new NioEventLoopGroup())  
        .channel(NioSocketChannel.class)  
        .handler(new ChannelInitializer<>()   
            @Override  
 protected void initChannel(Channel channel)   
                channel.pipeline().addLast(new StringEncoder());  
   
        )  
        .connect("127.0.0.1", 8080);  
channelFuture.sync()  
        .channel()  
        .writeAndFlush("hello Chenfy!");

实际上👆只有2行代码, 第一行代码获取了一个ChannelFuture对象,而第二行代码的作用就是利用channel()方法来获取Channel对象

注意 connect 方法是异步的,意味着不等连接建立,方法执行就返回了。因此 channelFuture 对象中不能【立刻】获得到正确的 Channel 对象,所以使用sync方法,同步等待连接的建立

除了用 sync 方法可以让异步操作同步以外,还可以使用回调的方式:

channelFuture.addListener((ChannelFutureListener) future ->   
    var channel = future.channel();  
    channel.writeAndFlush("hello Chenfy!");  
);

CloseFuture

通过调用closeFuture方法, 可以获取当channel关闭的时候,会通知的那个ChannelFuture对象

比如当我们获取到channel对象之后, 想要关闭这个channel:

//... 省略通过connect方法获取channel对象
new Thread(() ->   
    Scanner scanner = new Scanner(System.in);  
 while (true)   
        String line = scanner.nextLine();  
 if ("q".equals(line))   
            channel.close(); // close 异步操作  
 break;  
   
        channel.writeAndFlush(line);  
   
, "input").start();

👆创建了一个用来接收用户输入的线程, 当用户输入q时,关闭channel(对应上面的channel.close()方法)

注意: 不能在调用channel.close()之后, 立刻处理善后工作, 因为 netty的关闭方法是异步的,此时channel并没有真正关闭

我们如果想要在关闭之后, 执行某些代码,就需要使用ChannelFuture来进行监控:

同步方法:

ChannelFuture closeFuture = channel.closeFuture();  
closeFuture.sync();  
// 处理关闭之后的操作  
group.shutdownGracefully();

异步方法

ChannelFuture closeFuture = channel.closeFuture();  
closeFuture.addListener((ChannelFutureListener) future ->   
    // 处理关闭之后的操作  
 group.shutdownGracefully();  
);

3. Future & Promise

在异步处理时,经常用到这两个接口

首先要说明 netty 中的 Future 与 jdk 中的 Future 同名,但是是两个接口,netty 的 Future 继承自 jdk 的 Future,而 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--设置失败结果

4.Handler & Pipeline

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

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

打个比喻,每个 Channel 是一个产品的加工车间,Pipeline 是车间中的流水线,ChannelHandler 就是流水线上的各道工序,而后面要讲的 ByteBuf 是原材料,经过很多工序的加工:先经过一道道入站工序,再经过一道道出站工序最终变成产品

先搞清楚顺序,服务端

new ServerBootstrap()
    .group(new NioEventLoopGroup())
    .channel(NioServerSocketChannel.class)
    .childHandler(new ChannelInitializer<NioSocketChannel>() 
        protected void initChannel(NioSocketChannel ch) 
            ch.pipeline().addLast(new ChannelInboundHandlerAdapter()
                @Override
                public void channelRead(ChannelHandlerContext ctx, Object msg) 
                    System.out.println(1);
                    ctx.fireChannelRead(msg); // 1
                
            );
            ch.pipeline().addLast(new ChannelInboundHandlerAdapter()
                @Override
                public void channelRead(ChannelHandlerContext ctx, Object msg) 
                    System.out.println(2);
                    ctx.fireChannelRead(msg); // 2
                
            );
            ch.pipeline().addLast(new ChannelInboundHandlerAdapter()
                @Override
                public void channelRead(ChannelHandlerContext ctx, Object msg) 
                    System.out.println(3);
                    ctx.channel().write(msg); // 3
                
            );
            ch.pipeline().addLast(new ChannelOutboundHandlerAdapter()
                @Override
                public void write(ChannelHandlerContext ctx, Object msg, 
                                  ChannelPromise promise) 
                    System.out.println(4);
                    ctx.write(msg, promise); // 4
                
            );
            ch.pipeline().addLast(new ChannelOutboundHandlerAdapter()
                @Override
                public void write(ChannelHandlerContext ctx, Object msg, 
                                  ChannelPromise promise) 
                    System.out.println(5);
                    ctx.write(msg, promise); // 5
                
            );
            ch.pipeline().addLast(new ChannelOutboundHandlerAdapter()
                @Override
                public void write(ChannelHandlerContext ctx, Object msg, 
                                  ChannelPromise promise) 
                    System.out.println(6);
                    ctx.write(msg, promise); // 6
                
            );
        
    )
    .bind(8080);

客户端

new Bootstrap()
    .group(new NioEventLoopGroup())
    .channel(NioSocketChannel.class)
    .handler(new ChannelInitializer<Channel>() 
        @Override
        protected void initChannel(Channel ch) 
            ch.pipeline().addLast(new StringEncoder());
        
    )
    .connect("127.0.0.1", 8080)
    .addListener((ChannelFutureListener) future -> 
        future.channel().writeAndFlush("hello,world");
    );

服务端打印:

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

  • 入站处理器中,ctx.fireChannelRead(msg) 是 调用下一个入站处理器
    • 如果注释掉 1 处代码,则仅会打印 1
    • 如果注释掉 2 处代码,则仅会打印 1 2
  • 3 处的 ctx.channel().write(msg) 会 从尾部开始触发 后续出站处理器的执行
    • 如果注释掉 3 处代码,则仅会打印 1 2 3
  • 类似的,出站处理器中,ctx.write(msg, promise) 的调用也会 触发上一个出站处理器
    • 如果注释掉 6 处代码,则仅会打印 1 2 3 6
  • ctx.channel().write(msg) vs ctx.write(msg)
    • 都是触发出站处理器的执行
    • ctx.channel().write(msg) 从尾部开始查找出站处理器
    • ctx.write(msg) 是从当前节点找上一个出站处理器
    • 3 处的 ctx.channel().write(msg) 如果改为 ctx.write(msg) 仅会打印 1 2 3,因为节点3 之前没有其它出站处理器了
    • 6 处的 ctx.write(msg, promise) 如果改为 ctx.channel().write(msg) 会打印 1 2 3 6 6 6… 因为 ctx.channel().write() 是从尾部开始查找,结果又是节点6 自己

👆服务端 pipeline 触发的原始流程,图中数字代表了处理步骤的先后次序

5. ByteBuf

是对字节数组的封装

直接内存和堆内存

可以使用下面的代码来创建池化基于堆的 ByteBuf

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

也可以使用下面的代码来创建池化基于直接内存的 ByteBuf

ByteBuf buffer = ByteBufAllocator.DEFAULT.directBuffer(10);
  • 直接内存创建和销毁的代价昂贵,但读写性能高(少一次内存复制),适合配合池化功能一起用
  • 直接内存对 GC 压力小,因为这部分内存不受 JVM 垃圾回收的管理,但也要注意及时主动释放

池化和非池化

池化的最大意义在于可以重用 ByteBuf,优点有

  • 没有池化,则每次都得创建新的 ByteBuf 实例,这个操作对直接内存代价昂贵,就算是堆内存,也会增加 GC 压力
  • 有了池化,则可以重用池中 ByteBuf 实例,并且采用了与 jemalloc 类似的内存分配算法提升分配效率
  • 高并发时,池化功能更节约内存,减少内存溢出的可能

池化功能是否开启,可以通过下面的系统环境变量来设置

-Dio.netty.allocator.type=unpooled|pooled
  • 4.1 以后,非 android 平台默认启用池化实现,Android 平台启用非池化实现
  • 4.1 之前,池化功能还不成熟,默认是非池化实现

组成

ByteBuf 由四部分组成

最开始读写指针都在 0 位置

写入

方法列表,省略一些不重要的方法

方法签名含义备注
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

扩容

扩容规则是:

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

retain & release

由于 Netty 中有堆外内存的 ByteBuf 实现,堆外内存最好是手动来释放,而不是等 GC 垃圾回收。

  • UnpooledHeapByteBuf 使用的是 JVM 内存,只需等 GC 回收内存即可
  • UnpooledDirectByteBuf 使用的就是直接内存了,需要特殊的方法来回收内存
  • PooledByteBuf 和它的子类使用了池化机制,需要更复杂的规则来回收内存

回收内存的源码实现,请关注下面方法的不同实现

protected abstract void deallocate()

Netty 这里采用了引用计数法来控制回收内存,每个 ByteBuf 都实现了 ReferenceCounted 接口

  • 每个 ByteBuf 对象的初始计数为 1
  • 调用 release 方法计数减 1,如果计数为 0,ByteBuf 内存被回收
  • 调用 retain 方法计数加 1,表示调用者没用完之前,其它 handler 即使调用了 release 也不会造成回收
  • 当计数为 0 时,底层内存会被回收,这时即使 ByteBuf 对象还在,其各个方法均无法正常使用

谁来负责 release 呢?

不是我们想象的(一般情况下)

ByteBuf buf = ...
try 
    ...
 finally 
    buf.release();

请思考,因为 pipeline 的存在,一般需要将 ByteBuf 传递给下一个 ChannelHandler,如果在 finally 中 release 了,就失去了传递性(当然,如果在这个 ChannelHandler 内这个 ByteBuf 已完成了它的使命,那么便无须再传递)

基本规则是,谁是最后使用者,谁负责 release,详细分析如下

  • 起点,对于 NIO 实现来讲,在 io.netty.channel.nio.AbstractNioByteChannel.NioByteUnsafe#read 方法中首次创建 ByteBuf 放入 pipeline(line 163 pipeline.fireChannelRead(byteBuf))
  • 入站 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 释放未处理消息逻辑

// 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);
    

具体代码

// io.netty.util.ReferenceCountUtil#release(java.lang.Object)
public static boolean release(Object msg) 
    if (msg instanceof ReferenceCounted) 
        return ((ReferenceCounted) msg).release();
    
    return false;

以上就是Netty各个组件的介绍

以上是关于Netty基本使用-Netty组件的主要内容,如果未能解决你的问题,请参考以下文章

Netty基本使用-Netty组件

Netty基本使用-Netty组件

Netty搭建TCP服务实践

Netty源码面试解析实战(02)-基本组件

Netty框架之概述及基本组件介绍

超详细解释从Java NIO到Netty的每一步