type
status
date
urlname
summary
tags
category
icon
password
catalog
sort
前言
上一小节我们梳理了NioEventLoop,通过梳理我们知道,NioEventLoop本质上就是封装的执行线程,线程通过NioEventLoopGroup构造创建。NioEventLoop通过
select()
方法监听selectKey触发对应连接,读/写事件。并在每次的循环中执行一定的任务队列中的任务。而每次事件的触发都是基于Channel实现的。这里的Channel不是NIO中的Channel,而是我们Netty中封装了NIO Channel实现的Channel。这一小节我们将深入理解Channel的一些功能原理和具体的一些实现细节。介绍
Channel是Netty抽象出来的对网络I/O进行读写的相关接口,与NIO中的Channel接口类似。Channel的主要功能有网络I/O的读写、客户端发起连接、主动关闭连接、获取通信双方网络地址等。Channel接口下有一个重要的抽象类—AbstractChannel,一些公共的基础方法都在这个抽象类中实现,一些特定的功能可以通过各个不同的实现类去实现,最大限度的实现了接口的功能和接口的重用。
AbstractChannel融合了Netty的线程模型、事件驱动模型。但由于网络I/O模型及各种协议种类比较多,除了TCP协议,Netty还支持很多其他的连接协议,并且每种协议都有传统阻塞I/O和NIO(非阻塞IO)的版本区别。不同阻塞类型的连接有不同的Channel类型与之对应,因此AbstractChannel并没有直接与网络I/O相关的直接操作。每种阻塞与非阻塞Channel在AbstractChannel上都会继续抽象一层,如AbstractNioChannel,既是Netty重新封装的Epoll SockeChannel实现,也是其他非阻塞I/O Channel的抽象层。
AbstractChannel 源码分析
AbstractChannel 抽象类包括以下几个重要属性:
EventLoop
:每个Channel对应一条EventLoop线程。
DefaultChannelPipeline
:一个Handler的容器,也可以将其理解为一个Handler链。Handler主要处理数据的编/解码和业务逻辑。
Unsafe
:实现具体的连接与读/写数据,如网络的读/写、链路关闭、发起连接等。命名为Unsafe表示不对外提供使用,并非不安全。
下图是AbstractChannel的功能图,从图中可以看出,Unsafe属性的功能非常全面,并且AbstractChannel中有一个Unsafe抽象类—AbstractUnsafe。
AbstractUnSafe的大部分方法都采用了模版设计模式,具体细节由其子类完成。例如bind()方法:
AbstractNioChannel源码剖析
AbstractNioChannel也是一个抽象类,不过他在AbstractChannel的基础上有封装了一层属性和方法,AbstractChannel没有涉及NIO的任何属性和具体方法,包括AbstractUnsafe。AbstractNioChannel有以下三个重要属性:
SelectableChannel
是java.nio.SocketChannel和java.nio.ServerSocketChannel公共的抽象类,同时它也是Java NIO Channel的接口实现。
readInterestOp
用于区分当前Channel监听的事件类型。
selectionKey
它是将Channel注册到Selector上后的返回值。
从这些属性不难看出,在AbstractChannel中,已经将Netty的Channel和Java NIO的channel关联起来了。AbstractNioChannel的方法都很简洁,下面是一个很重要的方法
doRegister()
的源码剖析doRegister()方法在AbstractUnsafe的register0()方法中被调用。
在AbstractNioChannel中有一个非常重要的子类—AbstractNioUnsafe,是AbstractUnsafe类的Nio实现,主要实现了
connect()
、flush0()
等方法。它还实现了NioUnsafe接口,实现了其finishConnect()
、forceFlush()
、ch()
等方法,其中,forceFlush()
与flush0()
最终调用的NioSocketChannel的doWrite()
方法,来完成缓存数据写入Socket的工作。connect()
和 finishConnect()
这两个方法只有在Netty客户端中才会用到。下面是对这两个方法的解析:finishConnect()方法解读分析:
finishConnect() 只是在连接完成后调用,用于设置一些状态操作位置,并不是结束连接的字面意思。。不是关闭连接不是关闭连接的意思。
AbstractNioByteChannel源码剖析
AbstractNioChannel拥有NIO的Channel,具备NIO的注册、连接等功能。但IO的读写交给了其子类,Netty对IO的读写分配POJO对象与ByteBuf和FileRegion,因此在AbstractNioChannel的基础上继续抽象一层,分为AbstractNioMessageChannel与AbstractNioByteChannel。这一个部分我们详细深入AbstractNioByteChannel。它发送和读取的对象是ByteBuf与FileRegion类型。
首先flushTask为Task任务,主要负责刷新发送缓存链表中的数据,由于
write()
的数据没有直接写在Socket中,而是写在ChannelOutboundBuffer缓存中,所以当调用flush()
方法的时,会把数据写入Socket中并向网络中发送。因此当缓存中的数据未发送完成时,需要将任务添加到EventLoop线程中,等待EventLoop线程的再次发送。wirte 只是写入了缓存中,只有flush()才是写入了Socket发送。
doWrite()与doWriteInteral()方法在AbstractChannel的
flush0()
方法中被调用,主要功能是从ChannelOutboundBuffer缓存中获取待发送的数据,进行循环发送,发送的结果分为下面三种:- 发送成功,跳出循环直接返回。
- 由于TCP缓存区已满,成功发送的字节数为0,跳出循环,并将写操作OP_WRITE事件添加到选择Key兴趣事件集中。
- 默认当写了16此数据还未发送完成时,把选择Key的OP_WRITE事件从兴趣事件集合中移除,并添加一个flushTask任务,先去执行其他任务,当监测到此任务时再发送。
NioByteUnsafe的read()方法的实现思路大致分为以下3步:
- 获取Channel的配置对象、内存分配器
RecvByteBufAllocator.Handler
。
- 进入for循环。循环体本身的作用:使用内存分配器获取数据容器
ByteBuf
,调用doReadBytes()
方法将数据读取到容器中,如果本次循环没有读到数据或者数据链路已经关闭,则跳出循环。另外,当循环次数达到属性METADATA
的defaultMaxMessagePerRead
次数(默认为16次)时,也会跳出循环。由于TCP传输会产生粘包问题,因此每次读取都会触发channelRead
事件,进而调用业务逻辑处理Handler。
- 跳出循环后,表示本次读取已经完成。调用
allocHandler
的readComplete()
方法,并读取记录,用于下次分配合理内存。
AbstractNioMessageChannel 源码剖析
AbstractNioMessageChannel写入和读取的数据类型是Object,而不是字节流**。**在读数据时,AbstractNioMessageChannel数据不存在粘包问题,因此AbstractNioMessageChannnel在read()方法中先循环数据包,再触发channelRead事件。在写数据的时,AbstractNioMessageChannel数据逻辑简单。
它把缓存outboundBuffer中的数据包依此写入Channel中。如果Channel中写满了,或循环写、默认写的次数为子类Channel属性METADATA中的
defaultMaxMessagesPerRead
次数,则在Channel的SelectionKey上设置OP_WRITE事件,随后推出,其后OP_WRITE事件处理逻辑和Byte字节流写逻辑一样。read()和doWrite()方法代码解读如下:NioSocketChannel 源码剖析
前面分析Channel都是抽象类,NioSocketChannel是AbstractNioByteChannel的子类,也是
io.netty.channel.socket.SocketChannel
的实现类。Netty服务的每个连接都会生成一个NioSocketChannel对象。NioSocketChannel在AbstractNioByteChannel的基础上封装了NIO中的SocketChannel,实现了IO的读写连接操作,其核心功能如下。- SocketChannel在NioSocketChannel构造方法中由
SelectorProvider.provider().openSocketChannel()
创建,提供javaChannel()方法以获取SocketChannel。
- 实现
doReadByte()
方法,从SocketChannel中读取数据。
- 重写
doWrite()
方法、实现doWriteBytes()方法,将数据写入Socket中。
- 实现
doConnect()
方法和客户端连接。
下图为NioSocketChannel 的核心功能脑图,注明了这些功能会在哪些地方会被调用,从图中可以看出,大部分方法都是被其辅助对象Unsafe调用。
I/O的读写的核心代码解读如下:
NioServerSocketChannel 源码剖析
NioServerSocektChannel是AbstractNioMessageChannel的子类,由于NioServerSocektChannel由服务端使用,并且只负责监听Socket的接入,不关心IO读写,所以与NioServerChannel相比要简单很多,它封装了NIO中的ServerSocketChannel,并通过newSocket()方法打开ServerSocketChannel。它的多路复用器注册与NioSocketChannel的多路复用注册一样,由父类AbstractNioChannel实现。
下面重点关注它是如何监听新加入的连接的(需要由doReadMessages()方法来完成)。具体代码解析如下:
鸟瞰Channel
我们梳理部分Channel的一些重要的实现类和一些重要的方法,我们很容易发现,Channel的主要方法都是围绕网络IO的读写,客户端发起连接、关闭连接等功能。AbstractChannel通过模版方法实现做一些基础的实现,子类通过继承的方式完成具体的实现,提高代码的复用率。
这几个实现类的特点:
- AbstractChannel :基础父类,没有直接参与网络直接有关操作,但是提供了部分模版方法。并且提供了Unsafe内部类。
- AbstractNioChannel:每种IO类型都会在AbstractChannel上在进行一层封装,针对每种IO类型的特点进行封装,例如AbstractNioChannel中提供了三个重要属性:
SelectableChannel
NIO真正用到的Channel、readInterestOp
监听感兴趣的事件,selectionKey
注册到Selector后获取的Key。
- AbstractNioByteChannel:它是AbstractNioChannel的子类,但是并不是最终的实现类。它具备NIO注册、连接等功能。IO读写会交给子类来处理,在NioByteChannel中发送和读写的对象类型是ByteBuf和FileRegion。
- NioSocketChannel:AbstractNioByteChannel的子类,也是我们最常使用到的Channel实现类。Netty服务的每次连接都会生成一个NioSocketChannel对象。并在AbstractNioByteChannel的基础上实现了IO的读写连接功能。
- AbstractNioMessageChannel:同样也是AbstractNioChannel的子类,和AbstractNioByteChannel类似,但是它读写的对象是Object对象。因此它不会存在粘包的问题。
- NioServerSocketChannel:AbstractNioMessageChannel的子类,是一个仅仅只处理新连接的Channel,在
doReadMessages
的实现类中,也是关于新连接的处理。创建连接、读写方法都会进行异常处理。
其中有一个特殊的点抽象类中具体的连接与读写都是使用Unsafe实现的。同样的子类中也会对Unsafe接口继承或是实现。Unsafe表示不对外提供功能,并非不安全。
总结
这一小节我们简单梳理了Netty的Channel,我们不难发现,Channel完成了对NIO的Channel的实现与拓展。我们从AbstractChannel开始进行梳理,AbstractChannel 实现了Channel接口,提供了一些类的基础模版实现和一些Channel中的基础字段,例如标识Channel唯一的ChannelId、实现具体操作连接,数据读写的Unsafe、Channel容器Pipeline等。随后向下梳理了AbstractNioChannel的源码分析,AbstractNioChannel也是一个抽象类。这个抽象类是NioChannel的抽象类,这个类继承拓展了AbstractChannel。这个类正对NioChannel进行了拓展和补充。随后我们依据读取数据类型的不同,分析梳理了AbstractNioByteChannel 和AbstractNioMessageChannel。前者使用byteBuf读写的是二进制数据,因此存在粘包问题,后者读写Object数据,不存在粘包问题。这一层我们分析了他们读写方法,他们的读写方法在大体结构上是相似的,但是在读取具体数据的实现细节上又有不同。在往下一层就是我们最常使用到的NioSocketChannel 和NioServerSocketChannel,其中NioSocketChannel中对读写连接操作都有具体的实现,NioServerSocketChannel中则是抛出UnsupportedOperationException(); NioServerSocketChannel则会用来监听Socket接入的Accept连接操作。 因此我们在日常Netty开发中,会使用NioServerSocketChannel通常配置处理连接的bossGroup使用,而NioSocketChannel则会在配置处理读写的workerGroup时使用。
- 作者:Honesty
- 链接:https://blog.hehouhui.cn/archives/1050c7d0-9e17-808e-a5a9-f7bfdbe441e5
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
相关文章