2019独角兽企业重金招聘Python工程师标准>>>
前言
很久没有在开源中国上写博客了,今天回到这里继续写。今年打算研究几个开源项目的源码。今天从netty4.1.8的源码开始研究。本文是netty源码研究系列的开篇。
各种互联网、移动互联网应用的流行,从去年开始直播又变的特别热门,催生了当前云技术,微服务技术和物联网等技术的广泛应用,而这背后有一个最基础的能力就是网络通讯,netty作为当前最流行的一个java 网络nio的开发框架,是值得我们掌握的一项基本技能。
我之前简单使用过netty开发过一些东西,给我的直观感觉是事件驱动编程模型的与传统的链式调用编程模型有比较大的差异,从设计程序,理解程序到调试程序都有较大的差异。
本文开篇的一系列文章打算将netty4.1.8.Final的核心源码深入剖析一遍。目的是让自己深入掌握netty技术的使用,并且能够掌握它背后实现的原理及实现。
架构图
任何成熟一些的开源项目代码量都较大,我们该如何下手阅读理解这个项目呢?
先从宏观入手,了解该项目的架构及项目结构。再从中找到关键处入手。上图是netty官方公布的软件架构图我们先分析一下它的结构。
- Core(核心部分)。核心部分是底层的网络通讯的一些通用抽象。这部分内容是关键。
- Zero-Copy-Capable Rich Byte Buffer. 支持零拷贝能力的字节缓存。 这是netty字节流缓存的关键,支持“零拷贝”技术,以提升性能。
- Universal Communication API.通用的通讯API层,netty定义了一套抽象的通用通讯层api。
- Extensible Event Model。可扩展的事件模型。netty是一个典型的事件驱动模型场景。
- Transport Services(传输服务)。具体的网络传输能力的定义以及一些实现。
- Socket和数据报。
- HTTP。http 通道。
- In-VM Piple。虚拟机内部的管道流传输。
- Protocol Support(协议支持)。netty对于一些通用协议的编码解码实现。
- 支持常见的多种协议。例如http,redis,dns等协议。
包结构图
common:该项目是一个通用的工具类项目,几乎被所有的其它项目依赖使用,它提供了一些数据类型处理工具类,并发编程以及多线程的扩展,计数器等等通用的工具类。
buffer:该项目下是netty自行实现的一个ByteBuf字节缓冲区。该包的实现相对于jdk自带的ByteBuffer有很多优点,无论是API的功能,使用体验,性能都要更有,这些实现满足不同需求的实现类。它提供了一系列的抽象定义以及实现。该项目实现了netty架构图中的Zerocopy ByteBuf;
transport:该项目是网络传输通道的抽象和实现,定义通讯的统一通讯API,它统一了jdk的oio,nio和aio等多种编程接口,它提供了多种实现,包括native-epoll,sctp,udt和sctp等子项目实现不同的传输实现类型。该项是核心项目,实现了netty架构图中Transport Services、Universal Communication API和Extensible Event Model等多部分内容。
codec:该项目是协议编解码的抽象与实现。通过不同的子项目来实现http/http2/dns/redis/protocolbuffer等不同协议的支持。它与其它协议的客户端包和组件的区别是非常灵活,可以开发一些特定的场景的客户端或者服务端,该项目实现了netty架构图中的Protocal Support。
高层类图
上面的类图是忽略了接口或者所在的包,并且省略了很多属性和方法细节,只是为了体现netty的类组成结构以及类与类之间的关系,不是为了详细展现每个细节。下面我们对于这些接口或者类的职责和关系做一些分析。
Bootstrape&ServerBootstrape
这2个类都继承了AbstractBootstrap,因此它们有很多相同的方法和职责。它们都是启动器,能够帮助netty使用者更加方便地组装和配置netty,也可以更方便地启动netty应用程序,比使用者自己从头去将netty的各部分组装起来要方便地多,降低了使用者的学习和使用成本,它是我们使用netty的入口和最重要的API,可以通过它来连接到一个主机和端口上,也可以通过它来绑定到一个本地的端口上,它们两者之间相同之处要大于不同。
它们和其它类之间的关系是它将netty的其它各类进行组装和配置,它会组合和直接或间接依赖其它的类。
Boostrape往往是用于启动一个netty客户端或者UDP的一端。它通常使用connet()方法连接到远程的主机和端口,当然也可以通过bind()绑定本地的一个端口,它仅仅需要使用一个EventLoopGroup。
ServerBoostrape往往是用于启动一个netty服务端。它通常使用bind()方法绑定本地的端口上,然后等待客户端的连接,它使用两个EventLoopGroup对象(当然这个对象可以引用同一个对象),第一个用于处理它本地socket连接的io事件处理,而第二个责负责处理远程客户端的io事件处理。
该部分的详细源码分析见博客——https://my.oschina.net/ywbrj042/blog/868798
EventLoopGroup&EventLoop
Netty是一个事件驱动型框架,处理网络io事件是重要的组成部分,而EventLoopGroup和EventLoop这个接口是承担这一职责的重要角色。
EventLoop负责处理注册到其上的Channel的所有I/O事件循环处理,同一个实例可以处理一个或者多个Channel的I/O事件,这取决于它的具体实现细节。EventLoop又是一个EventLoopGroup子接口,可以理解为一种特殊的EventLoopGroup,这个设计有点儿特殊,暂时还不能理解作者的精髓。(亦或者本身这就是作者的一个败笔也未可知)
EventLoopGroup是一个EventLoop的组,它可以获取到一个或者多个EventLoop对象,因此它提供了迭代出EventLoop对象的方法。
Channel
Channel是netty中定义的一个网络连接通道。它是连接到网络套接字或者组件的输入或输出操作,例如读字节流,写字节流,连接到远程服务器和绑定本机的端口等网络操作。
Channel提供了用户这些操作接口:
- 查看当前连接通道的状态。例如是否打开,是否已连接等。
- 连接通道的配置ChannelConfig对象。例如连接的接收缓冲区大小。
- 连接通道支持的I/O操作。例如读,写,连接,绑定等。
- 获得关联的处理器链ChannelPipeline对象。负责处理所有的I/O事件和与该通道关联的请求。
Channel的所有I/O操作都是异步的,不是直接返回一个结果,而是返回一个ChannelFuture对象,用户可以在该对象上绑定监听器,监听事件完成或者错误的处理。
Channel是分层次的,通过parent()方法可以获得它的父通道。这取决于它的具体实现逻辑。例如SocketChannel是由ServerSocketChannel接收并且创建的,则它的parent方法则可以获取到一个ServerSocketChannel对象。
Channel是一个通用且抽象的连接通道,它不会保留那些具体transport技术特有的I/O操作方法,因此如果要调用具体的操作,则需要强制类型转换为它的具体子接口或者实现类,例如DatagramChannel类型的子接口是数据报(datagram transport)传输通道的扩展接口,将对象转为改类型的引用就可以调用它的join,leave和block等特有方法。
ChannelPipeline
ChannelPipeline是一个负责处理或拦截输入事件和输出操作的处理器链表。它让用户可以完全控制事件的处理方式及流程,是一个典型的责任链模式的实现。
每个Channel都有它关联的ChannelPipeline对象,是在连接通道Channel被创建的时候会自动创建一个处理器链ChannelPipeline对象,
* I/O Request * via {@link Channel} or * {@link ChannelHandlerContext} * | * +---------------------------------------------------+---------------+ * | ChannelPipeline | | * | \|/ | * | +---------------------+ +-----------+----------+ | * | | Inbound Handler N | | Outbound Handler 1 | | * | +----------+----------+ +-----------+----------+ | * | /|\ | | * | | \|/ | * | +----------+----------+ +-----------+----------+ | * | | Inbound Handler N-1 | | Outbound Handler 2 | | * | +----------+----------+ +-----------+----------+ | * | /|\ . | * | . . | * | ChannelHandlerContext.fireIN_EVT() ChannelHandlerContext.OUT_EVT()| * | [ method call] [method call] | * | . . | * | . \|/ | * | +----------+----------+ +-----------+----------+ | * | | Inbound Handler 2 | | Outbound Handler M-1 | | * | +----------+----------+ +-----------+----------+ | * | /|\ | | * | | \|/ | * | +----------+----------+ +-----------+----------+ | * | | Inbound Handler 1 | | Outbound Handler M | | * | +----------+----------+ +-----------+----------+ | * | /|\ | | * +---------------+-----------------------------------+---------------+ * | \|/ * +---------------+-----------------------------------+---------------+ * | | | | * | [ Socket.read() ] [ Socket.write() ] | * | | * | Netty Internal I/O Threads (Transport Implementation) | * +-------------------------------------------------------------------+
上图展示了一个I/O请求是如何被处理器链执行的完整过程。
提供了往里面加入ChannelHandler对象的操作,还有迭代每个ChannelHandler对象的操作。
ChannelHandler
连接通道处理器类。它处理I/O事件或者拦截I/O操作,并且转发到处理器链中的下一个处理器。
改接口自身提供的操作并不多,但是从它扩展了一系列的子接口和各种实现类。
ChannelInboundHandler是专门负责处理输入I/O事件的扩展接口。它的实现类是还包括一系列的Decoder类,对输入字节流进行解码。
ChannelOutboundHandler是专门负责处理输出I/O事件的扩展接口。它的实现类是还包括一系列的Encoder类,对输入字节流进行编码。
ChannelDuplexHandler可以同时处理输入和输出事件。
还有其它的一系列的抽象实现Adapter,还有一些具体的实现,用于实现具体的协议。
ChannelInitializer
ChannelInitializer是一个特殊的ChannelInboundHandlerAdapter抽象类,通过该初始化器可以将用户自定义的ChannelHandler对象加入到处理器链中,而它字节也是一个特殊的ChannelInboundHandler实现类,在初始化结束之后,它会被自己从处理器链中删除,它是一个没有实际处理事件意义的处理器类,是专门负责初始化的。这种设计非常奇怪。
参考资料
1.《netty in action》。
2.netty-4.1.8.Final版本源码。