【博学谷学习记录】超强总结,用心分享|狂野架构师Netty(二)

Netty 核心组件剖析

Bootstrap

作用和类型

Bootstrap是引导的意思,它的作用是配置整个Netty程序,将各个组件都串起来,最后绑定端口、启动Netty服务

Netty中提供了2种类型的引导类,一种用于客户端(Bootstrap),而另一种(ServerBootstrap)用于服务器,区别在于:

1、ServerBootstrap 将绑定到一个端口,因为服务器必须要监听连接,而 Bootstrap 则是由想要连接到远程节点的客户端应用程序所使用的

2、引导一个客户端只需要一个EventLoopGroup,但是一个ServerBootstrap则需要两个

Channel

概念和作用

1、 Netty中的Channel是与网络套接字相关的,可以理解为是socket连接,在客户端与服务端连接的时候就会

建立一个Channel,它负责基本的IO操作,比如:bind()、connect(),read(),write() 等

2、 主要作用:

  1. 通过Channel可获得当前网络连接的通道状态。
  2. 通过Channel可获得网络连接的配置参数(缓冲区大小等)。
  3. Channel提供异步的网络I/O操作,比如连接的建立、数据的读写、端口的绑定等。

3、 不同协议、不同的I/O类型的连接都有不同的 Channel 类型与之对应

EventLoopGroup 和 EventLoop

作用和类型

Netty是基于事件驱动的,比如:连接注册,连接激活;数据读取;异常事件等等,有了事件,就需要一个组件去监控事件的产生和事件的协调处理,这个组件就是EventLoop(事件循环/EventExecutor),在Netty 中每个Channel 都会被分配到一个 EventLoop。一个 EventLoop 可以服务于多个 Channel。每个EventLoop 会占用一个 Thread,同时这个 Thread 会处理 EventLoop 上面发生的所有 IO 操作和事件。

EventLoopGroup 是用来生成 EventLoop 的,包含了一组EventLoop(可以初步理解成Netty线程池)

EventLoopGroup核心线程数:

核心线程数默认:cpu核数*2

核心线程数在创建时可通过构造函数指定

EventLoopGroup boss = new NioEventLoopGroup();
EventLoopGroup worker = new NioEventLoopGroup();

对于boss group,我们其实也只用到了其中的一个线程,因为服务端一般只会绑定一个端口启动

ByteBuf

概念和作用

1.Java NIO 提供了ByteBuffer 作为它的字节容器,但是这个类使用起来过于复杂,而且也有些繁琐。Netty使用ByteBuf来替代ByteBuffer,它是一个强大的实现,既解决了JDK API 的局限性, 又为网络应用程序的开发者提供了更好的API

2.从结构上来说,ByteBuf 由一串字节数组构成。数组中每个字节用来存放信息,ByteBuf提供了两个索引,一个用于读取数据(readerIndex ),一个用于写入数据(writerIndex)。这两个索引通过在字节数组中移动,来定位需要读或者写信息的位置。而JDK的ByteBuffer只有一个索引,因此需要使用flip方法进行读写切换

ByteBuf的三个指针

读、写、最大容量

1.readerIndex:指示读取的起始位置, 每读取一个字节, readerIndex自增累加1。 如果readerIndex 与 writerIndex 相等,ByteBuf 不可读。

2.writerIndex:指示写入的起始位置, 每写入一个字节, writeIndex自增累加1。如果增加到 writerIndex 与capacity() 容量相等,表示 ByteBuf 已经不可写,但是这个时候,并不代表不能往 ByteBuf 中写数据了, 如果 发现往ByteBuf 中写数据写不进去的话,Netty 会自动扩容 ByteBuf,直到扩容到底层的内存大小为 maxCapacity

3.maxCapacity:指示ByteBuf 可以扩容的最大容量, 如果向ByteBuf写入数据时, 容量不足, 可以进行扩容的最大容量

常用API

容量API

1 capacity():表示 ByteBuf 底层占用了多少字节的内存(包括丢弃的字节、可读字节、可写字节),不同的底层实

现机制有不同的计算方式。

2、maxCapacity(): ByteBuf 底层最大能够占用多少字节的内存,当向 ByteBuf 中写数据的时候,如果发现容量不足,则进行扩容,直到扩容到 maxCapacity,超过这个数,就抛异常。

3.readableBytes() 与 isReadable():readableBytes() 表示 ByteBuf 当前可读的字节数,它的值等于

writerIndex-readerIndex,如果两者相等,则不可读,isReadable() 方法返回 false

4、writableBytes()、 isWritable() 、maxWritableBytes():writableBytes() 表示 ByteBuf 当前可写的字节数,它的值等于 capacity()-writerIndex,如果两者相等,则表示不可写,isWritable() 返回 false,但是这个时候,并不代表不能往 ByteBuf 中写数据了, 如果发现往ByteBuf 中写数据写不进去的话,Netty 会自动扩容 ByteBuf,直到扩容到底层的内存大小为 maxCapacity,而 maxWritableBytes() 就表示可写的最大字节数,它的值等于maxCapacity-writerIndex。

读写指针相关的API

1、readerIndex() 与 readerIndex(int readerIndex):前者表示返回当前的读指针 readerIndex, 后者表示设置读指针

2、 writeIndex() 与 writeIndex(int writerIndex):前者表示返回当前的写指针 writerIndex, 后者表示设置写指针

3、 markReaderIndex() 与markWriterIndex():表示把当前的读指针/写指针保存起来,操作形式为:

markedReaderIndex = readerIndex / markedWriterIndex = writerIndex

读写操作API

1、writeBytes(byte[] src): 表示把字节数组 src 里面的数据全部写到 ByteBuf,src字节数组大小的长度通常小于等于 writableBytes()

2、 readBytes(byte[] dst):把 ByteBuf 里面的数据全部读取到 dst,dst 字节数组的大小通常等readableBytes()

3、 writeByte(int value)、readByte():writeByte() 表示往 ByteBuf 中写一个字节,而 readByte() 表示从 ByteBuf 中读取一个字节,类似的 API 还有 writeBoolean()、writeChar()、writeShort()、writeInt()、writeLong()、writeFloat()、writeDouble() 与 readBoolean()、readChar()、readShort()、readInt()、readLong()、readFloat()、readDouble() 等等

丢弃、清理,释放

1、 discardReadBytes(): 丢弃已读取的字节空间,可写空间变多

2、 clear():重置readerIndex 、 writerIndex 为0,需要注意的是,重置并没有删除真正的内容

3、 release():真正去释放bytebuf中的数据,

4、 ReferenceCountUtil.release(buf):工具方法,内部还是调用release()

wrap

通过Wrap操作可以快速转换或得到一个ByteBuf对象,Unpooled 工具类中提供了很多重载的wrappedBuffer方法

三类ByteBuf

Ø 堆缓冲区(HeapByteBuf):内存分配在jvm堆,分配和回收速度比较快,可以被JVM自动回收,缺点是,如果进行

socket的IO读写,需要额外做一次内存复制,将堆内存对应的缓冲区复制到内核Channel中,性能会有一定程度的下

降。由于在堆上被 JVM 管理,在不被使用时可以快速释放。可以通过 ByteBuf.array() 来获取 byte[] 数据。

Ø 直接缓冲区(DirectByteBuf):内存分配的是堆外内存(系统内存),相比堆内存,它的分配和回收速度会慢一些,

但是将它写入或从Socket Channel中读取时,由于减少了一次内存拷贝,速度比堆内存块。

Ø 复合缓冲区(CompositeByteBuf):顾名思义就是将两个不同的缓冲区从逻辑上合并,让使用更加方便。

Netty默认使用的是DirectByteBuf,如果需要使用HeapByteBuf模式,则需要进行系统参数的设置

//设置HeapByteBuf模式,但ByteBuf 的分配器ByteBufAllocator要设置为非池化,否则不能 切换到堆缓冲器模式 System.setProperty("io.netty.noUnsafe", "true")

关于堆外内存的理解

JVM内部

堆(heap)+ 非堆(non heap)

JVM外部

堆外(off heap)

优点:

1、减轻gc压力

2、避免复制

不足:

1、创建速度稍慢

2、受操作系统管理

ByteBuf 的分配器

BufAllocator

Netty 提供了两种 ByteBufAllocator 的实现,分别是:

Ø PooledByteBufAllocator:实现了 ByteBuf 的对象的池化,提高性能减少并最大限度地减少内存碎片,池化思想

通过预先申请一块专用内存地址作为内存池进行管理,从而不需要每次都进行分配和释放

Ø UnpooledByteBufAllocator:没有实现对象的池化,每次会生成新的对象实例

Netty默认使用了PooledByteBufAllocator,但可以通过引导类设置非池化模式

源码:DefaultChannelConfig种的allocator属性

//引导类中设置非池化模式 bootstrap.childOption(ChannelOption.ALLOCATOR, UnpooledByteBufAllocator.DEFAULT) //或者通过系统参数设置 System.setProperty("io.netty.allocator.type", "pooled"); System.setProperty("io.netty.allocator.type", "unpooled");

ByteBuf-pooled & unpooled

对于Pooled类型的ByteBuf,不管是PooledDirectByteBuf还是PooledHeapByteBuf都只能由Netty内部自己使用(构

造是私有和受保护的),开发者可以使用Unpooled类型的ByteBuf。

Ø Netty提供Unpooled工具类创建的ByteBuf都是unpooled类型,默认采用的Allocator是direct类型;当然用户可以自

己选择创建UnpooledDirectByteBuf和UnpooledHeapByteBuf

ByteBuf 的释放

引用计数

ByteBuf如果采用的是堆缓冲区模式的话,可以由GC回收,但是如果采用的是直接缓冲区,就不受GC的管理,就得手

动释放,否则会发生内存泄露,Netty自身引入了引用计数,提供了ReferenceCounted接口,当对象的引用计数>0

时要保证对象不被释放,当为0时需要被释放

关于ByteBuf的释放,分为手动释放与自动释放:

Ø 手动释放,就是在使用完成后,调用ReferenceCountUtil.release(byteBuf); 进行释放,这种方式的弊端就是一旦忘

记释放就可能会造成内存泄露

Ø 自动释放有三种方式,分别是:入站的TailHandler(

TailContext)、继承SimpleChannelInboundHandler、

HeadHandler(HeadContext)的出站释放

Ø TailContext:Inbound流水线的末端,如果前面的handler都把消息向后传递最终由TailContext释放该消息,需

要注意的是,如果没有进行向下传递,是不会进行释放操作的

Ø SimpleChannelInboundHandler:自定义的InboundHandler继承自SimpleChannelInboundHandler,在

SimpleChannelInboundHandler中自动释放

Ø HeadContext:outbound流水线的末端,出站消息一般是由应用所申请,到达最后一站时,经过一轮复杂的调

用,在flush完成后终将被release掉

ByteBuf总结

对于入站消息:

Ø 对原消息不做处理,依次调用 ctx.fireChannelRead(msg)把原消息往下传,如果能到TailContext,那不用做什

么释放,它会自动释放

Ø 将原消息转化为新的消息并调用 ctx.fireChannelRead(newMsg)往下传,那需要将原消息release掉

Ø 如果已经不再调用ctx.fireChannelRead(msg)传递任何消息,需要把原消息release掉。

对于出站消息:则无需用户关心,消息最终都会走到HeadContext,flush之后会自动释放

Future/Promise异步模型

1、 future和promise,目的是将值(future)与其计算方式(promise)分离,从而允许更灵活地进行计算,特别是通过并行化。Future 表示目标计算的返回值,Promise 表示计算的方式,这个模型将返回结果和计算逻辑分离,目的是为了让计算逻辑不影响返回结果,从而抽象出一套异步编程模型。而计算逻辑与结果关联的纽带就是 callback。

2、 Netty中有非常多的异步调用,譬如:client/server的启动,连接,数据的读写等操作都是支持异步的。 Promise机制Netty的Future,只是增加了监听器。整个异步的状态,是不能进行设置和修改的,于是Netty的 Promise接口扩展了

ChannelFuture

跟Channel的操作有关,Netty中的Handler处理都是异步IO,通过ChannelFuture添加事件监听,可获取Channel异步IO操作的结果;当然也可等待获取,但最好不要在handler中通过future的sync或await来获取异步操作的结果。

Netty的Future接口,可以设置异步执行的结果。在IO操作过程,如果顺利完成、或者发生异常,都可以设置Promise的结果,并且通知Promise的Listener们。

在 Java 的 Future 中,业务逻辑为一个Callable 或 Runnable 实现类,该类的 call()或 run()执行完毕意味着业务逻辑的完结,在Promise 机制中,可以在业务逻辑中人工设置业务逻辑的成功与失败,这样更加方便的监控自己的业务逻辑。

ChannelPromise

ChannelPromise接口,则继承扩展了Promise和ChannelFuture。所以,ChannelPromise既绑定了Channel,又具备了设置监听回调的功能,还可以设置IO操作的结果,是Netty实际编程使用的最多的接口。

ChannelPipeline & ChannelHandler

处理器ChannelHandler

ChannelPipeline 提供了 ChannelHandler 链的容器。以服务端程序为例,客户端发送过来的数据要接收,读取

处理,我们称数据是入站的,需要经过一系列Handler处理后;如果服务器想向客户端写回数据,也需要经过一

系列Handler处理,我们称数据是出站的。

ChannelHandler 分类

inbound/outbound

对于数据的出站和入站,有着不同的ChannelHandler类型与之对应:

  1. ChannelInboundHandler 入站事件处理器
  2. ChannelOutBoundHandler 出站事件处理器
  3. ChannelHandlerAdapter提供了一些方法的默认实现,可减少用户对于ChannelHandler的编写
  4. ChannelDuplexHandler:混合型,既能处理入站事件又能处理出站事件。

ChannelHandler 体系结构

inbound/outbound

1.inbound入站事件处理顺序(方向)是由链表的头到链表尾,outbound事件的处理顺序是由链表尾到链表头。

2.inbound入站事件由netty内部触发,最终由netty外部的代码消费。

3.outbound事件由netty外部的代码触发,最终由netty内部消费。

执行顺序

InboundHandler是按照Pipleline的加载顺序(addLast),顺序执行

OutboundHandler是按照Pipeline的加载顺序(addLast),逆序执行

如何让outboundHandler 一定能执行到?

如果想让所有的OutboundHandler都能被执行到,可以选择把OutboundHandler放在最后一个有效的InboundHandler之前

有一种做法是通过addFirst加载所有OutboundHandler,再通过addLast加载所有InboundHandler;另外也推荐:使用

addLast先加载所有OutboundHandler,然后加载所有InboundHandler(注意考虑加载顺序和执行顺序)

posted @ 2022-08-28 21:46  谢十二♡Lgy  阅读(141)  评论(0编辑  收藏  举报