这个章节包括:
1)Netty的架构设计和技术点
2)Channel,EventLoop和ChannelFuture
3)ChannelHandler 和 ChannelPipeline
4)Bootstrap
在第一章节中,我们讲述了java在高性能的网络编程的发展历史和对网络方面的技术基础的积累,这给对Netty的核心组件和构建模块分析提供了一个很好的氛围
在第二章节中,我们扩大了我们的讨论范围,我们构建了我们第一个基于Netty的应用,通过构建简单的服务器端和客户端让我们了解了如何启动Netty,也让我们有了亲身的体验用于业务逻辑处理的最最重要的ChannelHandler的API是如何使用的了,同时,你也验证了Netty的开发环境是否搭建成功了
在这本书的接下来的内容中,我们是通过两种视角去学习探索Netty的,一个是从java的的class文件的角度去学习,一个是将Netty当做一个框架去学习的,这两个角度是不同的,但是却是很接近的,对于Netty来说,对于写出高效,可重复利用且易维护的代码,这两个角度度都是极其重要的
从一个更高的角度去审视,Netty解决了两个不同领域的问题,这两个领域我们一般称之为技术领域和架构领域,首先在技术层面来说,Netty是构建于Java的NIO基于异步事件驱动的框架,能够在高负载的情况保证应用的性能和可扩展性,其实在架构层面,Netty包含了很多的设计模式来使得应用程序的逻辑处理与网络层的处理解耦,在简化代码的同时,保证了代码的最大的可测试性,模块化性,和最大的可重复利用性
因为Netty的种种技术优势和卓越的设计,我们将更加深层次的去探究Netty的一个个独特奇妙的组件,并且去研究这些组件是如何一起协作工作来构建这么好的用户体验的框架架构的,通过了解这些设计和技术初衷,我们将从Netty的使用中获取莫大的好处,牢记这个目的,我们将深层次的去介绍我们之前已经粗浅讲解过的Netty的组件
3.1 Channel, EventLoop, and ChannelFuture
在接下来的几个小节中,我们将为你讲解Channel,EventLoop,ChannelFuture这几个组件的细节的讨论,这几个组件可以视为Netty网络模块抽象的几个典范
1)Channel-------关键字:Sockets
2)EventLoop-------关键字:控制流,多线程,并发
3)ChannelFuture-------关键字:异步通知
3.1.1 Interface Channel
基本的I/O操作(bind(),connect(),read()和write())这些都依赖于网络传输的最原始的支持,对于基于java构建的网络模型,最基础的构造应该是Socket,Netty的Channel接口提供的API可以大大减少直接使用Socket的复杂性,但是Channel只是一个原始接口,有很多具体的实现类实现了Channel中之前定义好的方法,下面给出一些具体类的清单:
3.1.2 Interface EventLoop
EventLoop定义了在整个连接的生命周期里当有事件发生的时候处理的核心抽象,我们将在第七章Netty的多线程模型中详细介绍EventLoop,现在我们只需要了解3.1图中向我们展示的Channel,EventLoop,Thread和EventLoopGroup之间的关系就可以了
这些关系包括:
1)一个EventLoopGroup包含多个EventLoop
2)一个EventLoop在一个生命周期中只绑定一个线程
3)一个channel上的所有I/O操作将被EventLoop绑定的指定线程处理
4)一个channel在其生命周期里只注册到一个单独的EventLoop
5)一个EventLoop给可以被分配多个channel
注意到这种设计,在一个channel中的I/O操作时被同一个线程处理执行的,无形中消除了同步的需要
3.1.3 Interface ChannelFuture
与我们之前说明的一样,所有的I/O操作在Netty中操作是异步的,因为操作结果并不会立即返回,我们需要通过一个方法在稍后的时间里确定结果,出于这个目的,Netty提供了ChannelFuture,它的addListener()方法注册一个channelFutureListener来通知操作是否被完成了(不管成功与否)
MORE ON CHANNELFUTURE我们可以将ChannelFuture当成一个操作在未来返回的结果的一个占位符,虽然在执行的时候,无法精确的预测它的结果,因为它返回的结果取决于一些未知的因素,但是有一点可以肯定,结果肯定会被执行到,不管是对是错,还有一点可以保证,所有属于同一个Channel的操作可以保证按照它调用的顺序被执行
我们将在第七章深入讨论EventLoop和EventLoopGroup
3.2 ChannelHandler and ChannelPipeline
现在我们将详细的探讨一下管理数据流和执行应用程序业务逻辑的组件
3.2.1 Interface ChannelHandler
从一个应用程序开发者的角度上看,最最重要的Netty组件就是ChannelHandler了,因为它作为一个服务容器用来处理关于输入输出数据的业务逻辑,这是因为ChannelHandler中的方法将被网络的一些事件所触发,事实上,ChannelHandler致力于所有事件动作的处理,例如将数据从一端转化格式传输到另一端或者处理异常操作
举例来说,ChannelInboundHandler是ChannelHandler的一个子类,我们经常去实现这个接口,这个接口可以去接收输入的数据然后按照你应用的业务逻辑去处理数据,你也可以从一个ChannelInboundHandler中刷新数据当你发送一个响应反馈信息到一个连接着的客户端,应用程序的业务部分一般存在一个或者多个ChannelInboundHandler
3.2.2 Interface ChannelPipeline
一个ChannelPipeline提供了ChannelHandler链的容器,定义了在这个链中的数据的输入输出的传播方法的API,当一个channel创建的时候,它会被自动安排到属于它的ChannelPipeline中去,ChannelHandler被装载到ChannelPipeline中遵循以下几个原则:
1)一个ChannelInitializer被注册到ServerBootStraping上
2)当ChannelInitializer的initChannel方法被调用的时候,ChannelInitializer会装载自定义的一些ChannelHandler到管道上
3)ChannelInitializer将它自己从ChannelPipeline上移除
让我们更加深层次的去探究一下ChannelPipeline和ChannelHandler的共生关系,去检测一下当数据输入输出的时候发生了事件流程
ChannelHandler被设计用来具体的去支持一些用户的使用,你可以将其想象成一些正常代码的容器用于处理在ChannelPipeline中流入流出的数据,在下图3.2展示了2个ChannelHandler的衍生类
在管道中要做的行为动作已经在ChannelHandler初始化的时候定义好了,然后装载到ChannelPipeline中,在应用的启动运行阶段,这些对象接收事件,执行他们已经实现的业务逻辑处理,处理结束之后将数据传送到链中的下一个handler,顺序与它们被添加到链中的顺序一致,为了这些实用的目的,将ChannelHandler安排的井井有条,我们就定义了ChannelPipeline
图3.3说明了在Netty应用中输入输出数据不同的区别,从客户端的角度来说,如果移动的方向是从客户端到服务器端事件就被定义成outbound,相反则被定义成inbound
图3.3也向我们展示了输入输出的处理可以被装载在同一个管道中,如果任何信息或者输入流被读取,它的过程是这样的,从管道的头部开始然后通过第一个ChannelInboundHandler,这个处理可能会改变里面的信息或者数据,这完全取决你具体的操作,之后数据会陆续通过链中其他的handler,最后数据会达到数据的尾端,截止到现在,数据才被处理完毕
输出数据的行为特征在理论上也是与输入一样的,输出数据流从尾部进入经过链式的ChannelOutboundHandler的处理达到头部,此时,输出数据到达了网络传输端,在这个图中特指的就是socket,典型的这种输出数据流,就是触发写的时候会发生
TIPS:关于inbound和outbound处理的更多细节
每个事件可以在当前链式处理器中指定它的下一个处理器通过ChannelHandlerContext,这个可以看做一个参数传递给每个方法,因为你有时候想忽略那些你不敢兴趣的处理,Netty提供了一个抽象类ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter,这两个类通过调用ChannelHandlerContext中相应的方法来很方便的过滤到一些不感兴趣的事件处理,你可以自己重写扩展这些方法来让你的数据流只经过你敢兴趣的处理器处理
鉴于输入输出操作时不一样的,那么你肯定会有疑问如果将这两种类型的处理放置在同一个ChannelPipeline时会不会发生些什么呢,尽管输入和输出的Handler都是继承自ChannelHandler,但是Netty还是区分了他们的实现,一个叫ChannelInboundHandler,另一个叫ChannelOutboundHandler,这样做的好处就是保证数据在传输处理的过程中被同向的处理器处理
当一个ChannelHandler被加入到ChannelPipeline中的时候,它将被分配一个对应的ChannelHandlerContext,这个代表着ChannelHandler和ChannelPipeline之间的绑定关系,尽管ChannelHandlerContext这个对象可以直接获取到底层的Channel但是大多数情况下它被用为写入输出数据
在Netty中,有两种方式去发送信息,第一种你可以直接将数据写入Channel,第二种你将输入写入与channelHandler关联的ChannelHandlerContext中,第一种方法会使消息从ChannelPipeline的尾部开始,后一个方法会使信息从ChannelPipeline的下一个处理器开始处理
3.2.3 A closer look at ChannelHandlers
我们之前说过,ChannelHandler有很多不同的类型,每一个具体的ChannelHandler的功能都被它的父类或者超类定义好了,Netty也以适配类的形式提供了大量的默认的处理实现,这样做的意向是简化应用程序的处理逻辑的开发,你已经看过了,每一个在管道中的ChannelHandler为传入下一个处理器的事件做好准备,这种准备一般由适配类或者他们的子类去自动完成,而你只需要重写你需要特殊处理的方法
TIPS:适配器?
一些适配的类是用来减少我们自己编写一些比较苍白的自定义的实现类,因为这些适配的类为所有的方法在对应的接口中提供了一些默认的实现
当创建你自己的自定义的处理器的时候,一下的一些适配类是你经常调用的
1)ChannelHandlerAdapter
2)ChannelInboundHandlerAdapter
3)ChannelOutboundHandlerAdapter
4)ChannelDuplexHandlerAdapter
接下来,我们将会调查一下ChannelHandler的子对象:encoders,decoders和ChannelInboundHandlerAdapter的子类SimpleChannelInboundHandler<T>
3.2.4 Encoders and decoders
当你使用Netty来接收或者发送信息的时候,数据的转化是必然发生的,输入获取的数据需要解码,解码的意思就是字节转化成另一个格式,一般是java对象,如果是数据输出,那么过程就是相反的,数据需要进行编码,编码就是将对象变成字节,转化的原因也是很简单的,因为网络传输的数据一致是字节
各种各样的抽象类提供了解码和编码的功能,根据具体的情况而言,使用不同的解码编码,你的应用也许有时候需要只用中间状态的格式,而不是直接立刻将你的信息转化成字节,此时你依旧需要编码,这可以从父类衍生出来,为了确定一个最为合适的,你可以自定义一个简单的命名规范
一般而言,基本的类将有类似MessageToByteEncoder和MessageToByteEncoder这样的编码和解码的工具类,对于一些具体的对象类,你可以发现类似ProtobufEncoder和ProtobufDecoder编码解码类,用来支持谷歌的protocol buffers
严格来说,其他的处理器也可以进行编码和解码的处理,但是因为有一些适配处理类来简化创建channel处理器的原因,所有的由netty提供的解码编码的适配类要么继承ChannelInboundHandler要么继承ChannelOutboundHandler
你会发现对于数据的输入,我们一般重写channelRead的方法,这个方法在数据从channel中读入的时候调用,此时它会调用解码类提供的decode方法,解码后给管道中的下一个handler,对于数据输出,过程则是相反的
3.2.5 Abstract class SimpleChannelInboundHandler
大多数情况下,你需要一个处理器去接收一个需要解码信息,然后将你的业务逻辑处理作用于这些数据之上,为了创建这个一个功能的channelHandler,你只需要继承SimpleChannelInboundHandler<T>就可以了,这里的T值得是你想要将信息转化成的java对象类型,在这个处理中你需要重写一个或者多个方法并且获取一个ChannelHandlerContext的引用,这个应用是当做每个方法的参数被开发者获取使用的
这个处理器最最重要的方法是channelRead0这个方法,这个方法在当前的I/O线程不被阻塞的情况下完全取决于你的实现,详细的信息我们下次讨论
3.3 Bootstrapping
Netty的bootstrap类提供了应用程序网络层的配置的功能,一般包括两块功能,第一是绑定一个进程到给定的端口,第二是连接另一个在一个具体端口正在运行的进程
一般而言,我们将第一种方式视为启动服务端,第二种方式视为启动客户端,这种定义是简单且方便的,但是这样有点掩盖了“server”和“client”这两个重要的因素,这两个术语代表了不同的网络行为,一个是监听连接一个是建立一个或者多个进程的连接
CONNECTION-ORIENTED PROTOCOLS 请你牢记于心,并且当你要说“connection”这个术语的时候请谨慎,因为connection只指的是基于连接的协议,例如TCP协议,这个保证了两个终端的信息传输顺序
因此,有两种类型的启动类,一个是用于client端的,一般被叫做Bootstrap,另一个是用于server端的,被叫做serverBootstrap,无论你应用程序使用的协议是什么类型和也无论数据所表现的形式是什么,唯一事情bootstrap要做的事情就是确定此时它运行的是客户端还是服务器端,表3.1比较了这两种启动类的不同
两种bootstrap第一种不同是:ServerBootstrap绑定端口,因为server端必须监听连接,而Bootstrap被用在client端的应用上来连接一个远程主机。第二个不同的是意义重大的,客户端的Bootstraping只需要一个EcentLoopGroup,二ServerBootstrap却需要两个,即便这两个是同样的实例,那么为什么呢?因为server端需要两个区分开的Channels,第一个包含一个serverChannel用来代码server端的自己的监听socket,绑定一个本地的端口,第二个用来所有来自client端连接的且server端接收的所有创建好的channel的处理,图3.4说明了这个模型,这就是为什么需要2个EventLoopGroup了
与ServerChannel相关的EventLoopGroup会分配一个EventLoop,这个EventLoop负责创建channel来处理新连接的连接请求,一旦连接建立成功,第二个EventLoopGroup会为每一个新创建的channel分配一个对象的EventLoop
3.4 Summary
这个章节,我们从技术和架构两个角度讨论了一下Netty,我们回顾且深入讨论了我们之前章节介绍的一些概念和Netty的组件,特别是ChannelHandler,ChannelPipeline,Bootstraping
我们也介绍了ChannelHandler的一些衍生类,介绍了解码编码,描述了他们在网络字节传输是对数据进行转化的具体实现
接下来的很多章节将会更加深入的介绍这些组件,目前的介绍的概述将有利于你心中对netty架构蓝图的形成
接下来的一个章节,我们将展示Netty提供的一些网络传输服务,也会告诉你根据你的应用场景选择正确的传输服务,做到物尽其用