2. Channel
io.netty.channel.Channel
,实现 AttributeMap、ChannelOutboundInvoker、Comparable 接口,Netty Channel 接口
而 Netty 的 Channel 则提供的一系列的 API ,它大大降低了直接与 Socket 进行操作的复杂性。而相对于原生 NIO 的 Channel,Netty 的 Channel 具有如下优势
- 在 Channel 接口层,采用 Facade 模式进行统一封装,将网络 I/O 操作、网络 I/O 相关联的其他操作封装起来,统一对外提供。
- Channel 接口的定义尽量大而全,为 SocketChannel 和 ServerSocketChannel 提供统一的视图,由不同子类实现不同的功能,公共功能在抽象父类中实现,最大程度地实现功能和接口的重用。
- 具体实现采用聚合而非包含的方式,将相关的功能类聚合在 Channel 中,由 Channel 统一负责和调度,功能实现更加灵活。
- 自身基本信息有
#id()
、#parent()
、#config()
、#localAddress()
、#remoteAddress()
方法。 - 每个 Channel 都有的核心组件有
#eventLoop()
、#unsafe()
、#pipeline()
、#alloc()
方法。
我们看到除了 #read()
和 #flush()
方法,其它方法的返回值的类型都是 ChannelFuture ,这表明这些操作是异步 IO 的过程
3. Unsafe
Unsafe 接口,定义在在 io.netty.channel.Channel
内部,和 Channel 的操作紧密结合,下文我们将看到。
Unsafe 直译中文为“不安全”,就是告诉我们,无需且不必要在我们使用 Netty 的代码中,不能直接调用 Unsafe 相关的方法。Netty 注释说明如下:
对于 Channel 和 Unsafe 来说,类名中包含 Byte 是属于客户端的,Message 是属于服务端的。
ChanelId 的默认实现类为 io.netty.channel.DefaultChannelId
,我们主要看看它是如何生成 Channel 的两种编号的
5. ChannelConfig
io.netty.channel.ChannelConfig
,Channel 配置接口
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Channel(二)之 accept
- 服务端 NioServerSocketChannel 的 boss EventLoop 线程轮询是否有新的客户端连接接入。
- 当轮询到有新的连接接入,封装连入的客户端的 SocketChannel 为 Netty NioSocketChannel 对象。
- 选择一个服务端 NioServerSocketChannel 的 worker EventLoop ,将客户端的 NioSocketChannel 注册到其上。并且,注册客户端的 NioSocketChannel 的读事件,开始轮询该客户端是否有数据写入。
2. NioMessageUnsafe#read
因此,对于 NioServerSocketChannel 来说,每次只接受一个新的客户端连接。当然,因为服务端 NioServerSocketChannel 对 Selectionkey.OP_ACCEPT
事件感兴趣,所以后续的新的客户端连接还是会被接受的。
读取过程中发生异常,记录该异常到 exception
中,同时结束循环
调用 ChannelPipeline#fireChannelRead(Object msg)
方法,触发 Channel read 事件到 pipeline 中。
调用 ChannelPipeline#fireChannelReadComplete()
方法,触发 Channel readComplete 事件到 pipeline 中
AbstractNioMessageChannel#doReadMessages
doReadMessages(List<Object> buf)
抽象方法,读取客户端的连接到方法参数 buf
中
4. ServerBootstrapAcceptor
ServerBootstrapAcceptor ,继承 ChannelInboundHandlerAdapter 类,服务器接收器( acceptor ),负责将接受的客户端的 NioSocketChannel 注册到 EventLoop 中。
4.2 channelRead
#channelRead(ChannelHandlerContext ctx, Object msg)
方法,将接受的客户端的 NioSocketChannel 注册到 work EventLoop 中。代码如下:
在注册完成之后,该 worker EventLoop 就会开始轮询该客户端是否有数据写入
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Channel(三)之 read
NioSocketChannel 读取( read )对端的数据的过程,简单来说:
- NioSocketChannel 所在的 EventLoop 线程轮询是否有新的数据写入。
- 当轮询到有新的数据写入,NioSocketChannel 读取数据,并提交到 pipeline 中进行处理。
- 当
(readyOps & SelectionKey.OP_READ) != 0
时,这就是 NioSocketChannel 所在的 EventLoop 的线程轮询到有新的数据写入。 - 然后,调用
NioByteUnsafe#read()
方法,读取新的写入数据。
调用 AbstractNioByteChannel#doReadBytes(ByteBuf buf)
方法,读取数据
一般情况下,我们会在自己的 Netty 应用程序中,自定义 ChannelHandler 处理读取到的数据。 当然,此时读取的数据,大多数情况下是需要在解码( Decode )
如果没有自定义 ChannelHandler 进行处理,最终会被 pipeline 中的尾节点 TailContext 所处理。
调用 ChannelPipeline#fireChannelReadComplete()
方法,触发 Channel readComplete 事件到 pipeline
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Channel(四)之 write
而 Netty Channel 竟然有三种方法,我们来一个个看看:
- write 方法:将数据写到内存队列中。
- 也就是说,此时数据并没有写入到对端。
- flush 方法:刷新内存队列,将其中的数据写入到对端。
- 也就是说,此时数据才真正写到对端。
- writeAndFlush 方法:write + flush 的组合,将数据写到内存队列后,立即刷新内存队列,又将其中的数据写入到对端。
- 也就是说,此时数据已经写到对端。
因为 Netty Channel 的 #write(Object msg, ...)
和 #writeAndFlush(Object msg, ...)
方法,是异步写入的过程,需要通过监听返回的 ChannelFuture 来确实是真正写入
有一点一定非常肯定要注意,#write(Object msg, ...)
方法返回的 Promise 对象,只有在数据真正被 #flush()
方法调用执行完成后,才会被回调通知
2. AbstractChannel
会调用对应的 ChannelPipeline#write(Object msg, ...)
方法,将 write 事件在 pipeline 上传播
最终会传播 write 事件到 head
节点,将数据写入到内存队列中,是一个Outbound出站事件
在方法内部,会调用 TailContext#write(Object msg, ...)
方法,将 write 事件在 pipeline 中,从尾节点向头节点传播
-
缺少的
promise
方法参数,通过调用#newPromise()
方法,进行创建 Promise 对象
返回 promise
对象。一般情况下,出现这种情况是 promise
已经被取消,所以不再有必要写入数据。或者说,写入数据的操作被取消
调用 #write(Object msg, boolean flush, ChannelPromise promise)
方法,写入消息( 数据 )到内存队列
方法参数 flush
为 true
时,该方法执行的是 write + flush 的组合操作,即将数据写到内存队列后,立即刷新内存队列,又将其中的数据写入到对端
随着 write 或 writeAndFlush 事件不断的向下一个节点传播,最终会到达 HeadContext 节点
AbstractUnsafe#write(Object msg, ChannelPromise promise)
方法,将数据写到内存队列中
为什么会实现 SingleThreadEventLoop.NonWakeupRunnable 接口呢?write 操作,仅仅将数据写到内存队列中,无需唤醒 EventLoop ,从而提升性能
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Channel(五)之 flush 操作
Netty Channel 的 #flush()
方法,刷新内存队列,将其中的数据写入到对端
我们会发现,#flush()
方法和 #write(Object msg, ...)
正常情况下,经历的流程是差不多的,例如在 pipeline 中对事件的传播,从 tail
节点传播到 head
节点,最终交由 Unsafe 处理,而差异点就是 Unsafe 的处理方式不同
- write 方法:将数据写到内存队列中。
- flush 方法:刷新内存队列,将其中的数据写入到对端
调用 AbstractChannel#doWrite(outboundBuffer)
方法,执行真正的写入到对端
调用 ChannelConfig#getWriteSpinCount()
方法,获得自旋写入次数 N 。在【第 6 至 76 行】的代码,我们可以看到,不断自旋写入 N 次,直到完成写入结束
调用 NioSocketChannelConfig#getMaxBytesPerGatheringWrite()
方法,获得每次写入的最大字节数
写入字节小于等于 0 ,说明 NIO Channel 不可写,所以注册 SelectionKey.OP_WRITE
,等待 NIO Channel 可写,并返回以结束循环
执行 NIO write 调用,写入多个 ByteBuffer 对象到对端
8.1.1 newInstance
#newInstance(Object msg, int size, long total, ChannelPromise promise)
静态方法,创建 Entry 对象
8.1.4 cancel
#cancel()
方法,标记 Entry 对象,取消写入到对端。在 ChannelOutboundBuffer 里,Entry 数组是通过链式的方式进行组织,而当某个 Entry 对象( 节点 )如果需要取消写入到对端,是通过设置 canceled = true
来标记删除
8.3 addMessage
#addMessage(Object msg, int size, ChannelPromise promise)
方法,写入消息( 数据 )到内存队列。注意,promise
只有在真正完成写入到对端操作,才会进行通知
8.6 nioBuffers
#nioBuffers(int maxCount, long maxBytes)
方法,获得当前要写入到对端的 NIO ByteBuffer 数组,并且获得的数组大小不得超过 maxCount
,字节数不得超过 maxBytes
。我们知道,在写入数据到 ChannelOutboundBuffer 时,一般使用的是 Netty ByteBuf 对象,但是写到 NIO SocketChannel 时,则必须使用 NIO ByteBuffer 对象,因此才有了这个方法。考虑到性能,这个方法里会使用到“缓存”
如果超过 NIO ByteBuffer 数组的大小,调用 #expandNioBufferArray(ByteBuffer[] array, int neededSpace, int size)
方法,进行扩容
8.7.1 progress
#progress(long amount)
方法,处理当前消息的 Entry 的写入进度,主要是通知 Promise 消息写入的进度
8.7.4 clearNioBuffers
#clearNioBuffers()
方法,清除 NIO ByteBuff 数组的缓存
9. NioEventLoop
在上文 「7. NioSocketChannel」 中,在写入到 Channel 到对端,若 TCP 数据发送缓冲区已满,这将导致 Channel 不写可,此时会注册对该 Channel 的 SelectionKey.OP_WRITE
事件感兴趣。从而实现,再在 Channel 可写后,进行强制 flush
通过 Selector 轮询到 Channel 的 OP_WRITE
就绪时,调用 AbstractNioUnsafe#forceFlush()
方法,强制 flush
10. ChannelOutboundBuffer 写入控制
当我们不断调用 #addMessage(Object msg, int size, ChannelPromise promise)
方法,添加消息到 ChannelOutboundBuffer 内存队列中,如果不及时 flush 写到对端( 例如程序一直未调用 Channel#flush()
方法,或者对端接收数据比较慢导致 Channel 不可写 ),可能会导致 OOM 内存溢出
10.1.1 bytesBeforeUnwritable
#bytesBeforeUnwritable()
方法,获得距离不可写还有多少字节数
基于高水位阀值来判断。
totalPendingSize
小于低水位阀值时,调用 #setWritable(boolean invokeLater)
方法,设置为可写
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Channel(六)之 writeAndFlush 操作
write + flush 的组合,将数据写到内存队列后,立即刷新内存队列,又将其中的数据写入到对端。
在方法内部,会调用 TailContext#writeAndFlush(Object msg, ...)
方法,将 write 和 flush 两个事件在 pipeline 中,从尾节点向头节点传播
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Channel(七)之 close 操作
- 客户端 NioSocketChannel
- 客户端关闭 NioSocketChannel ,断开和服务端的连接。
- 服务端关闭 NioSocketChannel ,断开和客户端的连接。
- 服务端 NioServerSocketChannel
- 服务端关闭 NioServerSocketChannel ,取消端口绑定,关闭服务。
上面的关闭,可能是客户端/服务端主动关闭,也可能是异常关闭
在方法内部,会调用对应的 ChannelPipeline#close()
方法,将 close 事件在 pipeline 上传播。而 close 事件属于 Outbound 事件,所以会从 tail
节点开始,最终传播到 head
节点,使用 Unsafe 进行关闭
调用 #doClose0(promise)
方法,执行真正的关闭
正在 flush 中,在 EventLoop 中的线程中,调用 #fireChannelInactiveAndDeregister(boolean wasActive)
方法,执行取消注册,并触发 Channel Inactive 事件到 pipeline 中
真正关闭 Channel ,需要阻塞直到延迟时间到或发送缓冲区中的数据发送完毕。如果不取消该 Channel 的 SelectionKey.OP_READ
事件的感兴趣,就会不断触发读事件,导致 CPU 空轮询。为什么呢?在 Channel 关闭时,会自动触发 SelectionKey.OP_READ
事件。而且,会不断不断不断的触发,如果不进行取消 SelectionKey.OP_READ
事件的感兴趣
2.4 AbstractUnsafe#doClose0
AbstractUnsafe#doClose0(ChannelPromise promise)
方法,执行真正的关闭
Channel Inactive 事件属于 Inbound 事件,所以会从 head
节点开始,最终传播到 tail
节点
Channel Unregistered 事件属于 Inbound 事件,所以会从 head
节点开始,最终传播到 tail
节点
5. 服务端处理客户端主动关闭连接
在客户端主动关闭时,服务端会收到一个 SelectionKey.OP_READ
事件的就绪,在调用客户端对应在服务端的 SocketChannel 的 #read()
方法会返回 -1 ,从而实现在服务端关闭客户端的逻辑
++++++++++++++++++++++++++++++++++++++++++++++++++
Channel(八)之 disconnect
Java 原生 NIO SocketChannel 不存在,当调用 Netty NioSocketChannel#disconnect(ChannelPromise promise)
时,会自动转换成 close 操作