目录
ChannelOption.SO_SNDBUF/ChannelOption.SO_RCVBUF
ServerBootstrap/BootStrap
BootStrap的意思是引导程序,BootStrap作为Netty框架的启动类和主入口类,主要作用是配置Netty框架中事件循环组、通道类型、处理器以及端口等设置。分为服务端ServerBootstrap和客户端BootStrap。
NioEventLoop
Netty 的 NioEventLoop
维护了一个线程和一个任务队列,支持异步任务的提交和执行。当 NioEventLoop
启动时,会调用其 run
方法,在其中执行 I/O 任务和非 I/O 任务:
I/O 任务:与 SelectionKey
相关的事件,这些事件代表了网络层面的操作,比如接受连接 (accept
)、建立连接 (connect
)、读取数据 (read
)、写入数据 (write
) 等。这些任务通常由 processSelectedKeys
方法触发,负责处理选择器中已准备就绪的键,并执行相应的操作。
非 I/O 任务:添加到任务队列 (taskQueue
) 中的任务。这些任务包括注册通道 (register0
)、绑定端口 (bind0
) 等操作。这些任务通常由 runAllTasks
方法触发,在事件循环中执行任务队列中的所有待处理任务。
NioEventLoopGroup
管理 eventLoop 的生命周期,可以理解为线程池,内部维护了一组线程,每个线程(NioEventLoop)负责处理多个 Channel 上的事件,而一个 Channel 只对应于一个线程。
ChannelFuture
Netty中的所有I/O操作都是异步进行的,因此需要一种机制来获取异步执行的结果。在Java中,java.util.concurrent
(JUC)包中的Future
提供了一种方法来跟踪异步操作的状态,并在操作完成时获取结果。然而,JDK提供的Future
使用起来相对复杂,为此Netty提供了自己的异步操作结果的实现——ChannelFuture
。
ChannelFuture
是Netty中用于异步操作的结果表示。它允许你在执行异步操作时了解操作的状态,并在操作完成时获得通知。每个Netty的I/O操作都会返回一个ChannelFuture
,你可以通过这个ChannelFuture
来检查操作是否成功、失败或是否完成。此外,ChannelFuture
提供了监听器机制,允许你在操作完成时执行特定的操作或逻辑。
Channel
Netty网络通信的组件,能够执行 I/O 操作,如读、写、连接和绑定。Channel为用户提供:
通道的当前状态(例如,它是否打开?是否已连接?);
通道的 配置参数 (例如接收缓冲区大小);
通道支持的 I/O 操作(例如读、写、连接和绑定),以及处理 ChannelPipeline 与通道关联的所有 I/O 事件和请求。
Channel的生命周期
channelRegistered: 当该连接分配到具体的worker线程后,该回调会被调用。
channelActive:channel的准备工作已经完成,所有的pipeline添加完成,并分配到具体的线程上,说明该channel准备就绪,可以使用。
channelRead:客户端向服务端发来数据,每次都会回调此方法,表示有数据可读;
channelReadComplete:服务端每次读完一次完整的数据之后,回调该方法,表示数据读取完毕;
channelInactive:当连接断开时,该回调会被调用,说明这时候底层的TCP连接已经被断开了。
channelUnRegistered: 对应channelRegistered,当连接关闭后,释放绑定的worker线程;
Channel重要方法
eventLoop: 返回分配给Channel 的EventLoop
pipeline: 返回Channel 的ChannelPipeline,也就是说每个Channel 都有自己的
ChannelPipeline。
isActive: 如果Channel 是活动的,则返回true。活动的意义可能依赖于底层的传输。例如,一个Socket 传输一旦连接到了远程节点便是活动的,而一个Datagram 传输一旦被打开便是活动的。
localAddress: 返回本地的SokcetAddress
remoteAddress: 返回远程的SocketAddress
write: 将数据写到远程节点,注意,这个写只是写往Netty内部的缓存,还没有真正写往socket。
flush: 将之前已写的数据冲刷到底层socket进行传输。
writeAndFlush: 等同于调用write()并接着调用flush()。
ChannelHandler
处理 I/O 事件或截获 I/O 操作,并将其转发到其 ChannelPipeline子类型。
ChannelHandler 本身没有提供很多方法,但你通常必须实现它的子类型之一:
ChannelInboundHandler 处理入站 I/O 事件
ChannelOutboundHandler 处理出站 I/O 操作。
或者,为方便起见,提供了以下适配器类:
ChannelInboundHandlerAdapter 处理入站 I/O 事件。
ChannelOutboundHandlerAdapter 处理出站 I/O 操作。
ChannelDuplexHandler 处理入站和出站。
ChannelHandler 的生命周期
在ChannelHandler被添加到ChannelPipeline 中或者被从ChannelPipeline 中移除时会调用下面这些方法。这些方法中的每一个都接受一个ChannelHandlerContext 参数
handlerAdded :当把ChannelHandler 添加到ChannelPipeline 中时被调用
handlerRemoved: 当从ChannelPipeline 中移除ChannelHandler 时被调用
exceptionCaught :当处理过程中在ChannelPipeline 中有错误产生时被调用
ChannelInboundHandler接口
下面是接口 ChannelInboundHandler 的生命周期方法。这些方法将会在数据被接收时或者与其对应的Channel 状态发生改变时被调用。这些方法和Channel 的生命周期密切相关。
channelRegistered:当Channel 已经注册到它的EventLoop 并且能够处理I/O 时被调用
channelUnregistered:当Channel 从它的EventLoop 注销并且无法处理任何I/O时被调用
channelActive:当Channel 处于活动状态时被调用;Channel 已经连接/绑定并且已经就绪
channelInactive: 当Channel 离开活动状态并且不再连接它的远程节点时被调用
channelReadComplete: 当Channel上的一个读操作完成时被调用
channelRead: 当从Channel 读取数据时被调用
ChannelWritabilityChanged: 当Channel 的可写状态发生改变时被调用。可以通过调用Channel 的isWritable()方法来检测Channel 的可写性。与可写性相关的阈值可以通过Channel.config().setWriteHighWaterMark()和Channel.config().setWriteLowWaterMark()方法来设置userEventTriggered 当ChannelnboundHandler.fireUserEventTriggered()方法被调用时被调用。
ChannelOutboundHandler 接口
出站操作和数据将由ChannelOutboundHandler 处理。它的方法将被Channel、
ChannelPipeline 以及ChannelHandlerContext 调用。
bind(ChannelHandlerContext,SocketAddress,ChannelPromise)当请求将Channel 绑定到本地地址时被调用
connect(ChannelHandlerContext,SocketAddress,SocketAddress,ChannelPromise)当请求将Channel 连接到远程节点时被调用
disconnect(ChannelHandlerContext,ChannelPromise)当请求将Channel 从远程节点断开时被调用
close(ChannelHandlerContext,ChannelPromise) 当请求关闭Channel 时被调用
deregister(ChannelHandlerContext,ChannelPromise)当请求将Channel 从它的EventLoop 注销时被调用
read(ChannelHandlerContext) 当请求从Channel 读取更多的数据时被调用
flush(ChannelHandlerContext) 当请求通过Channel 将入队数据冲刷到远程节点时被调用
write(ChannelHandlerContext,Object,ChannelPromise) 当请求通过Channel将数据写到远程节点时被调用
ChannelHandler的适配器
Netty 提供了ChannelHandler适配器类,以简化自定义 ChannelHandler
的编写。这些适配器提供了对应接口中所有方法的默认实现,从而降低了编写自定义处理器的复杂性。当你不需要处理所有事件时,这些适配器类尤为有用。
Netty 中有两个主要的适配器类:
ChannelInboundHandlerAdapter:用于处理入站事件。
ChannelOutboundHandlerAdapter:用于处理出站事件。
这两个适配器分别提供了 ChannelInboundHandler
和 ChannelOutboundHandler
接口的基本实现,允许你只覆盖自己关心的方法。因为它们继承自抽象类 ChannelHandlerAdapter
,所以自动获得了 ChannelHandler
超接口中的方法。
这些适配器类的主要作用是提供默认实现,从而使自定义 ChannelHandler
更加简单和方便。如果你只想处理特定的入站或出站事件,可以从相应的适配器类开始,然后根据需要覆盖相关方法。这样,你可以避免实现不必要的接口方法,从而集中精力在真正需要处理的事件上。
Handler的共享和并发安全性
ChannelHandlerAdapter 还提供了实用方法isSharable()。如果其对应的实现被标注为Sharable,那么这个方法将返回true,表示它可以被添加到多个ChannelPipeline。
每个socketChannel有自己的pipeline而且每个socketChannel又是和线程绑定的,所以这些Handler的实例之间完全独立的,只要Handler的实例之间不是共享了全局变量,Handler的实例是线程安全的。
但是如果业务需要我们在多个socketChannel之间共享一个Handler的实例呢?比如统计服务器接受到和发出的业务报文总数,我们就需要用一个Handler的实例来横跨所有的socketChannel来统计所有socketChannel业务报文数。为了实现这一点,我们可以实现Handler,并且在Handler上使用Netty的@Sharable注解,然后在安装Handler实例到pipeline时,共用一个即可。当然,因为Handler实例是共享的,所以在实现Handler的具体功能时,需要注意线程安全。
SimpleChannelInboundHandler
Netty在处理网络数据时,需要Buffer,在Read网络数据时由Netty创建Buffer,Write网络数据时Buffer往往是由业务方创建的。不管是读和写,Buffer用完后都必须进行释放,否则可能会造成内存泄露。在Write网络数据时,可以确保数据被写往网络,Netty会自动进行Buffer的释放,因为Netty会在pipeline中安装两个Handle:
Handler的顺序为:网络--> Head --> 自定义Handler --> Tail
可以看到Head同时会处理出站和入站,在Head中会负责将出站的Buffer释放。
但是如果Write网络数据时,我们有outBoundHandler处理(重写/拦截)了write()操作并丢弃数据,没有继续往下写,要由我们负责释放这个Buffer,就必须调用ReferenceCountUtil.release方法,否则就可能会造成内存泄露。
在Read网络数据时,如果我们可以确保每个InboundHandler都把数据往后传递了,也就是调用了相关的fireChannelRead方法,Netty也会帮我们释放,这个是由Tail负责的 ,同样如果我们有InboundHandler处理了数据,又不继续往后传递,又不调负责释放的ReferenceCountUtil.release方法,就可能会造成内存泄露。
但是由于消费入站数据是一项常规任务,所以Netty 提供了一个特殊的被称为
SimpleChannelInboundHandler 的ChannelInboundHandler 实现。这个实现会在数据被channelRead0()方法消费之后自动释放数据。
ChannelHandlerContext
保存 Channel 相关的所有上下文信息,同时关联一个 ChannelHandler 对象。
使ChannelHandler能够与其ChannelPipeline和其他处理程序进行交互。除其他事项外,处理程序可以通知ChannelPipeline中的下一个 ChannelHandler,以及动态修改它所属的 ChannelPipeline。
ChannelPipline
保存 ChannelHandler 的 双向链表,用于处理或拦截 Channel 的入站事件和出站操作。ChannelPipeline 实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及 Channel 中各个的 ChannelHandler 如何相互交互。
Netty 中每个 Channel 都有且仅有一个 ChannelPipeline 与之对应,它们的组成关系如下:
一个 Channel 包含了一个 ChannelPipeline,而 ChannelPipeline 中又维护了一个由 ChannelHandlerContext 组成的双向链表,并且每个 ChannelHandlerContext 中又关联着一个 ChannelHandler。
read事件(入站事件)和write事件(出站事件)在一个双向链表中,入站事件会从链表 head 往后传递到最后一个入站的 handler,出站事件会从链表 tail 往前传递到最前一个出站的 handler,两种类型的 handler 互不干扰。
ChannelInitializer
Netty 提供了一个特殊的ChannelInboundHandlerAdapter 子类:ChannelInitializer
ChannelInitializer
是一个用于将多个 ChannelHandler
添加到 ChannelPipeline
中的便捷方式。要使用它,只需将 ChannelInitializer
的实现提供给 Bootstrap
或 ServerBootstrap
的实例。当 Channel
注册到它的 EventLoop
后,Netty 会调用 initChannel()
方法。在此方法返回后,ChannelInitializer
将自动从 ChannelPipeline
中移除自身。这使得它成为用于配置 ChannelPipeline
的理想工具。
在实际应用中,如果有某个处理器(handler)只需要使用一次,也可以模仿 ChannelInitializer
的行为。处理完相关任务后,可以自动将自己从 ChannelPipeline
中移除。例如,某个授权处理器用于检查客户端的初次连接授权。通过验证后,就可以将该处理器移除。当客户端断开连接并重新连接时,由于这是一个新的连接,授权处理器会再次添加到 ChannelPipeline
,重新进行授权检查。
这种方式提供了一种灵活且有效的机制,确保 ChannelPipeline
中的处理器仅在必要时存在,减少不必要的资源占用和复杂性。
ChannelOption
ChannelOption.SO_BACKLOG
在 TCP/IP 协议中,服务器通过 listen
函数的 backlog
参数来控制连接请求的处理。由于服务器是顺序处理客户端的连接请求,因此同一时间只能处理一个客户端连接。当多个客户端同时请求连接时,服务器会将未能及时处理的连接请求放入等待队列中。
在操作系统中,通常有两个队列来管理这些连接请求:
ACCEPT 队列:用于保存已经完成 TCP 三次握手的连接请求。这些连接处于准备接受状态。
SYN 队列:用于保存尚未完成 TCP 三次握手的连接请求,等待客户端和服务器之间的连接建立。
backlog
参数指定 ACCEPT 队列的最大长度,即服务器可以同时排队等待处理的连接请求的数量。默认值是 128,意味着如果同时有超过 128 个连接请求排队等待处理,服务器可能会拒绝新的连接请求。这个参数的大小会影响服务器在高并发情况下的性能和连接处理能力。
ChannelOption.SO_REUSEADDR
对应于套接字选项中的SO_REUSEADDR,这个参数表示允许重复使用本地地址和端口。
ChannelOption.SO_KEEPALIVE
对应于 TCP 套接字选项中的 SO_KEEPALIVE
。该选项用于保持 TCP 连接的活跃状态,特别适用于可能长时间没有数据传输的连接。
当启用 SO_KEEPALIVE
时,如果在两小时内没有数据通信,TCP 会自动发送一个保持活动的探测数据包,以检测连接状态。这有助于确定连接是否仍然有效,并在连接断开或中断时及时通知应用程序。
通过设置 SO_KEEPALIVE
,可以确保长时间闲置的连接不会因为没有数据传输而意外中断。这对于需要保持长时间连接的应用程序非常有用,例如长时间的客户端与服务器连接,或者基于 WebSocket 的应用程序。
ChannelOption.SO_SNDBUF/ChannelOption.SO_RCVBUF
ChannelOption.SO_SNDBUF
和 ChannelOption.SO_RCVBUF
分别对应于套接字选项中的 SO_SNDBUF
和 SO_RCVBUF
。这两个参数用于设置 TCP 连接的发送缓冲区和接收缓冲区的大小。
发送缓冲区(SO_SNDBUF):用于暂存即将发送的数据,直到数据成功发送到网络中。如果缓冲区太小,可能会导致数据发送延迟,尤其是在高流量环境下。
接收缓冲区(SO_RCVBUF):用于存储从网络层接收到的数据,直到应用程序读取它们。如果缓冲区过小,可能会导致数据丢失或接收延迟,尤其是在数据传输量大的情况下。
通过调整这两个缓冲区的大小,可以优化网络传输的性能。较大的缓冲区可以提高吞吐量,减少数据丢失风险,但也可能增加内存占用。调整缓冲区大小时,需要根据应用程序的具体需求和网络环境进行权衡