Netty 源码分析 07 Channel

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

  1. 服务端 NioServerSocketChannel 的 boss EventLoop 线程轮询是否有新的客户端连接接入
  2. 当轮询到有新的连接接入,封装连入的客户端的 SocketChannel 为 Netty NioSocketChannel 对象。
  3. 选择一个服务端 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 )对端的数据的过程,简单来说:

  1. NioSocketChannel 所在的 EventLoop 线程轮询是否有新的数据写入。
  2. 当轮询到有新的数据写入,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 操作

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值