Netty网络框架学习笔记-4(Netty核心知识_2022-02-21)
Netty 核心知识点
Channel
Channel是 Java NIO 的一个基本构造。可以看作是传入或传出数据的载体。因此,它可以被打开或关闭,连接或者断开连接。
config()
方法是获取通道相关配置参数。获取channel的状态
boolean isOpen(); //如果通道打开,则返回true
boolean isRegistered(); //如果通道注册到EventLoop,则返回true
boolean isActive(); //如果通道处于活动状态并且已连接,则返回true
boolean isWritable(); //当且仅当I/O线程将立即执行请求的写入操作时,返回true。不同协议、不同的阻塞类型的连接都有不同的 Channel 类型与之对应,常用的 Channel 类型:
NioSocketChannel
,异步的客户端 TCP Socket 连接。
NioServerSocketChannel
,异步的服务器端 TCP Socket 连接。
NioDatagramChannel
,异步的 UDP 连接。
NioSctpChannel
,异步的客户端 Sctp 连接。
NioSctpServerChannel
,异步的 Sctp 服务器端连接,这些通道涵盖了 UDP 和 TCP 网络 IO 以及文件 IO。
EventLoop 与 EventLoopGroup
Netty 抽象出两组线程池,BossGroup 专门负责接收客户端连接,WorkerGroup 专门负责网络读写操作。
EventLoop 定义了Netty的核心抽象,用来处理连接的生命周期中所发生的事件,在内部,将会为每个Channel分配一个EventLoop。
EventLoopGroup 是一个 EventLoop 池,包含很多的 EventLoop (默认是CPU核心数 * 2)。
Netty 为每个 Channel 分配了一个 EventLoop,用于处理用户连接请求、对用户请求的处理等所有事件。EventLoop 本身只是一个线程驱动,在其生命周期内只会绑定一个线程,让该线程处理一个 Channel 的所有 IO 事件, 从消息的读取->解码->处理->编码->发送。
每个 NioEventLoop 中包含有一个 Selector,一个 taskQueue
每个 NioEventLoop 的 Selector 上可以注册监听多个 NioChannel
每个 NioChannel 只会绑定在唯一的 NioEventLoop 上
每个 NioChannel 都绑定有一个自己的 ChannelPipeline
一个 Channel 一旦与一个 EventLoop 相绑定,那么在 Channel 的整个生命周期内是不能改变的。一个 EventLoop 可以与多个 Channel 绑定。即 Channel 与 EventLoop 的关系是 n:1,而 EventLoop 与线程的关系是 1:1。
多个Channel 情况下 EventLoopGroup 是通过next()方法 轮询 NioEventLoop 进行处理
ServerBootstrap 与 Bootstrap
Bootstarp 和 ServerBootstrap 被称为引导类,指对应用程序进行配置,并使他运行起来的过程。Netty处理引导的方式是使你的应用程序和网络层相隔离。
Bootstrap 是客户端的引导类,Bootstrap 在调用 bind()(连接UDP)和 connect()(连接TCP)方法时,会新创建一个 Channel,仅创建一个单独的、没有父 Channel 的 Channel 来实现所有的网络交换。
ServerBootstrap 是服务端的引导类,ServerBootstarp 在调用 bind() 方法时会创建一个 ServerChannel 来接受来自客户端的连接,并且该 ServerChannel 管理了多个子 Channel 用于同客户端之间的通信。
配置属性:
服务端需要设置两个线程组、 客户端只需要设置一个线程组
option()
设置的是服务端用于接收进来的连接,也就是boosGroup线程。
childOption()
是提供给父管道接收到的连接,也就是workerGroup线程。
channel()
是用于设置服务器使用什么通道。
childHandler()
是用于设置配置初始化线程组处理器, 同时存在childHandler()
与handler()
方法时候, childHandler是设置workerGroup线程组处理器, handler是设置bossGroup线程组的处理器
ChannelOption 参数说明 (简书、 )
一般使用到就是
ChannelOption.SO_BACKLOG(对应TCP/ip协议的初始化连接队列大小)
、ChannelOption.SO_KEEPALIVE(保持连接活动状态)
ChannelHandler
ChannelHandler 是一个接口, 是对 Channel 中数据的处理器, 处理 I/O 事件或拦截 I/O 操作,并将其转发到其 ChannelPipeline(业务处理链) 中的下一个处理程序。
这些处理器可以是系统本身定义好的编解码器,也可以是用户自定义的。这些处理器会被统一添加到一个 ChannelPipeline 的对象中,然后按照添加的顺序对 Channel 中的数据进行依次处理。
- ChannelInboundHandlerAdapter(入站处理器)、
- ChannelOutboundHandler(出站处理器)
- ChannelDuplexHandler(出入站组合处理器)
入站指的是数据从底层java NIO Channel到Netty的Channel。 出站指的是通过Netty的Channel来操作底层的java NIO Channel。
- ChannelInboundHandlerAdapter处理器常用的事件有:
- 注册事件 fireChannelRegistered。
- 连接建立事件 fireChannelActive。
- 读事件和读完成事件 fireChannelRead、fireChannelReadComplete。
- 异常通知事件 fireExceptionCaught。
- 用户自定义事件 fireUserEventTriggered。
- Channel 可写状态变化事件 fireChannelWritabilityChanged。
- 连接关闭事件 fireChannelInactive。
- ChannelOutboundHandler处理器常用的事件有:
- 端口绑定 bind。
- 连接服务端 connect。
- 写事件 write。
- 刷新时间 flush。
- 读事件 read。
- 主动断开连接 disconnect。
- 关闭 channel 事件 close
Pipeline (管道)
ChannelPipeline 是一个 Handler 的集合,它负责处理和拦截 inbound 或者 outbound 的事件和操作,相当于一个贯穿 Netty 的链。(也可以这样理解:ChannelPipeline 是 保存 ChannelHandler 的 List,用于处理或拦截 Channel 的入站事件和出站操作)
常用方法 :
ChannelPipeline addFirst(ChannelHandler... handlers),把一个业务处理类(handler)添加到链中的第一个位置 ChannelPipeline addLast(ChannelHandler... handlers),把一个业务处理类(handler)添加到链中的最后一个位置
ChannelPipeline 实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及 Channel 中各个的 ChannelHandler 如何相互交互
在 Netty 中每个 Channel 都有且仅有一个 ChannelPipeline 与之对应,它们的组成关系如下
ChannelHandlerContext
保存 Channel 相关的所有上下文信息,同时关联一个 ChannelHandler 对象, 对象中包含一个具 体 的 事 件 处 理 器 ChannelHandler , 同 时 ChannelHandlerContext 中也绑定了对应的 pipeline 和 Channel 的信息,方便对 ChannelHandler 进行调用.
常用方法如下:
close(); // 关闭通道 flush(); // 刷新 writeAndFlush(); // 将数据写入到通道中当前的下一个处理器开始处理(出站)
💡 handler 执行中如何换人?
关键代码
io.netty.channel.AbstractChannelHandlerContext#invokeChannelRead()
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 的线程来调用
Unpooled 类
Netty 提供一个专门用来操作缓冲区, 通过分配新空间或包装或复制现有字节数组、字节缓冲区和字符串来创建新字节缓冲区, (即 Netty 的数据容器)的工具类
可以分配直接内存与堆内存, 常用方法如下
- copiedBuffer(CharSequence string, Charset charset) 分配一个
DEFAULT_MAX_CAPACITY = Integer.MAX_VALUE
最大容量大小, 自动分配大小字节数组大小的堆内字节缓冲区 - readableBytes() 返回可读字节大小
- buffer(10) 创建一个默认最大容量, 指定字节数组大小的
ByteBuf
- capacity() 返回分配的字节数组大小
- wrappedBuffer(byte[] array) 分配一个数组大小堆内存的缓冲区(PS: 这个方法最大容量也是字节数组的大小, 新增会报错), wrappedBuffer(ByteBuffer buffer) 分配一个数组大小直接内存的缓冲区
- **isReadable() ** 用于判断 ByteBuf 是否可读,如果 writerIndex 大于 readerIndex,那么 ByteBuf 是可读的,否则是不可读状态。
- readerIndex() 返回该缓冲区从那里开始读的索引, 有参数是设置从那里开始读的索引,
- writerIndex() 返回该缓冲区从那里开始写的索引, 有参数是设置从那里开始写的索引,
writeByte()
写完后索引会自增 - readBytes(byte[] dst) & writeBytes(byte[] src)
readBytes() 是将 ByteBuf 的数据读取相应的字节到字节数组 dst 中,readBytes() 经常结合 readableBytes() 一起使用,dst 字节数组的大小通常等于 readableBytes() 的大小,
readByte()
读取完毕后索引会自增,getByte()
读完不会自增
- getCharSequence(2,3,Charset.defaultCharset()) 范围读方法, 从那里开始读, 读多少个, 以什么字符读取
内存管理 API
- release() & retain() 每调用一次 release() 引用计数减 1,每调用一次 retain() 引用计数加 1。
- slice() & duplicate()
slice() 等同于 slice(buffer.readerIndex(), buffer.readableBytes()),默认截取 readerIndex 到 writerIndex 之间的数据,最大容量 maxCapacity 为原始 ByteBuf 的可读取字节数,底层分配的内存、引用计数都与原始的 ByteBuf 共享。
duplicate() 与 slice() 不同的是,duplicate()截取的是整个原始 ByteBuf 信息,底层分配的内存、引用计数也是共享的。如果向 duplicate() 分配出来的 ByteBuf 写入数据,那么都会影响到原始的 ByteBuf 底层数据。
- copy() copy() 会从原始的 ByteBuf 中拷贝所有信息,所有数据都是独立的,向 copy() 分配的 ByteBuf 中写数据不会影响原始的 ByteBuf。
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 设计缓存池化有非常大的帮助,当引用计数为 0,该 ByteBuf 可以被放入到对象池中,避免每次使用 ByteBuf 都重复创建,对于实现高性能的内存管理有着很大的意义。
此外 Netty 可以利用引用计数的特点实现内存泄漏检测工具。JVM 并不知道 Netty 的引用计数是如何实现的,当 ByteBuf 对象不可达时,一样会被 GC 回收掉,但是如果此时 ByteBuf 的引用计数不为 0,那么该对象就不会释放或者被放入对象池,从而发生了内存泄漏。Netty 会对分配的 ByteBuf 进行抽样分析,检测 ByteBuf 是否已经不可达且引用计数大于 0,判定内存泄漏的位置并输出到日志中,你需要关注日志中 LEAK 关键字。
ChannelFuture
Netty 中所有的 I/O 操作都是异步的,即操作不会立即得到返回结果,所以 Netty 中定义了一个 ChannelFuture 对象作为这个异步操作的“代言人”,表示异步操作本身。如果想获取到该异步操作的返回值,可以通过该异步操作对象的addListener() 方法为该异步操作添加监 NIO 网络编程框架 Netty 听器,为其注册回调:当结果出来后马上调用执行。
Netty 的异步编程模型都是建立在 Future 与回调概念之上的。
异步模型
1.0 异步概念
异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的组件在完成后,通过状态、通知和回调来通知调用者。
Netty 中的 I/O 操作是异步的,包括 Bind、Write、Connect 等操作会简单的返回一个 ChannelFuture, 调用者并不能立刻获得结果,而是通过 Future-Listener 机制,用户可以方便的主动获取或者通过通知机制获得 IO 操作结果
Netty 的异步模型是建立在 future 和 callback 的之上的。callback 就是回调。重点说 Future,它的核心思想是:假设一个方法 fun,计算过程可能非常耗时,等待 fun 返回显然不合适。那么可以在调用 fun 的时候,立马返回一个 Future,后续可以通过 Future 去监控方法 fun 的处理过程(即 : Future-Listener 机制)
2.0 Future 说明
可以同步等待任务结束得到结果,也可以异步方式得到结果,但都是要等任务结束, Future表示异步的执行结果, 可以通过它提供的方法来检测执行是否完成,比如检索计算等等.
ChannelFuture 是一个接口 : public interface ChannelFuture extends Future 我们可以添加监听器,当监听的事件发生时,就会通知到监听器.
工作原理示意图
在使用 Netty 进行编程时,拦截操作和转换出入站数据只需要您提供 callback 或利用 future 即可。这使得链式操作简单、高效, 并有利于编写可重用的、通用的代码。
2.1 Future-Listener 机制
当 Future 对象刚刚创建时,处于非完成状态,调用者可以通过返回的 ChannelFuture 来获取操作执行的状态,注册监听函数来执行完成后的操作。
常见有如下操作
- 通过 isDone 方法来判断当前操作是否完成;
- 通过 isSuccess 方法来判断已完成的当前操作是否成功;
- 通过 getCause 方法来获取已完成的当前操作失败的原因;
- 通过 isCancelled 方法来判断已完成的当前操作是否被取消;
- 通过 addListener 方法来注册监听器,当操作已完成(isDone 方法返回完成),将会通知指定的监听器;如果Future 对象已完成,则通知指定的监听器
例子: 监控客户端连接结果
// 启动客户端
ChannelFuture future = bootstrap.connect("192.0.0.1", 8886); /*.sync() 改为异步*/
future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isSuccess()) {
log.info("客户端启动,成功连接到服务端, 地址:{}", future.channel().remoteAddress());
} else {
log.info("客户端启动,连接到服务端失败, 原因:{}", future.cause());
}
}
});
// ----------------------------------------------------
17:06:01.719 [main] DEBUG io.netty.buffer.ByteBufUtil - -Dio.netty.maxThreadLocalCharBufferSize: 16384
17:06:30.159 [nioEventLoopGroup-2-1] INFO com.zhihao.netty.rudiments.NettyClient - 客户端启动,连接到服务端失败, 原因:{}
io.netty.channel.AbstractChannel$AnnotatedConnectException: Connection timed out: no further information: /192.0.0.1:8886
Caused by: java.net.ConnectException: Connection timed out: no further information
ChannelFuture future = bootstrap.connect("127.0.0.1", 8888); /*.sync() 改为异步*/
future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isSuccess()) {
log.info("客户端启动,成功连接到服务端, 地址:{}",future.channel().remoteAddress());
}else {
log.info("客户端启动,连接到服务端失败, 原因:{}",future.cause());
}
}
});
// ---------------------------------------------------------------------------------------
17:08:40.098 [nioEventLoopGroup-2-1] INFO com.zhihao.netty.rudiments.NettyClient - 客户端启动,成功连接到服务端, 地址:/127.0.0.1:8888
17:08:41.579 [nioEventLoopGroup-2-1] INFO com.zhihao.netty.rudiments.NettyClientHandler - 客户端通道准备完成, 线程: nioEventLoopGroup-2-1
17:08:46.610 [nioEventLoopGroup-2-1] INFO com.zhihao.netty.rudiments.NettyClientHandler - 客户端读取线程: nioEventLoopGroup-2-1
17:08:46.618 [nioEventLoopGroup-2-1] INFO com.zhihao.netty.rudiments.NettyClientHandler - 服务端发送消息是:延迟5秒后发送帅哥客户端你好
17:08:46.618 [nioEventLoopGroup-2-1] INFO com.zhihao.netty.rudiments.NettyClientHandler - 服务端地址:/127.0.0.1:8888