Netty网络框架学习笔记-4(Netty核心知识_2022-02-28)
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
1
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)