Netty In Action(三)

引言

    本章我们将着重介绍以下几个概念,不同的是我们不会分开介绍而是通过讲解他们是如何协同工作的来帮助我们在脑海中构造一张Netty工作机制的蓝图。

  • Bootstrap or ServerBootstrap
  • EventLoop
  • EventLoopGroup
  • ChannelPipeline
  • Channel
  • Future or ChannelFuture
  • ChannelInitializer
  • ChannelHandler

Netty速成

    Netty应用是通过Bootstrap进行启动的,Bootstrap使配置Netty属性变得更加简单方便。为了能够支持多种协议以及处理数据,Netty使用了Handler机制,Handler主要用于处理一个或者一组特定的事件,这些事件包括数据编码或处理数据过程中的异常捕获等。

    我们最常使用的一种Handler是直接实现ChannelInboundHandler接口,通过该Handler我们可以接受数据并对其进行处理。当我们的应用还需要响应客户端并提供一个response时,我们还需要从ChannelInboundHandler中将数据写回客户端。换句话说,我们应用的业务逻辑通常都是在ChannelInboundHandler中的。

    当使用Netty实现一个客户端或者服务器应用时,我们需要知道如何来处理接收到或者需要发送的数据。这时就需要不同的Handler来处理数据。Netty通过ChannelInitializer来配置Handler。ChannelInitializer的作用是将ChannelHandler添加到被称作ChannelPipeline的对象中。当我们发送或者接受数据时,这些handler就会对这些数据进行处理。其实ChannelInitializer本身也是一个ChannelHandler,只不过它在将其他handler加到ChannelPipeline之后就会从其上边将自己移除。

    所有的Netty应用都是基于ChannelPipeline,并且ChannelPipeline和EventLoop及EventLoopGroup密切相关

    EventLoop的目的是为Channel处理IO事件,一个EventLoop可以为多个Channel处理事件,而一个EventLoopGroup可以包含多个EventLoop。

    Channel代表一个网络连接或者一些能够执行IO操作的组件,因此Channel是被EventLoop管理的,后者是专门用来处理IO事件的。

    在Netty应用中的所有IO操作都是异步执行的,例如当我们连接服务器或者收发数据时都是异步的。由于我们没法立即获取执行的数据或者不知道执行的操作是否完成,所以我们需要延迟检测操作是否执行成功或者注册一个listener来监听操作执行的结果。Netty使用Future或者ChannelFutures机制来注册listener用来监听操作是否执行成功。

Channels, Events and Input/Output (IO)

    Netty是一个非阻塞的,基于事件驱动的网络框架,也就是说Netty需要通过线程来处理IO事件。一提到多线程,我们首先想到的就是要保证我们写的代码线程安全,但实际上我们没有必要这么做,下图说明了Netty处理事件的机制。

img1.png

    由上图可知Netty中包含多个EventLoopGroup,而每个EventLoopGroup又包含多个EventLoop。实际上我们可以将EventLoop当做处理具体网络事件的线程。

EventLoop与Thread的关系

EventLoop在整个生命周期中只能绑定到一个线程(a single Thread)上

    当我们注册一个Channel时,Netty将该Channel绑定到一个EventLoop上,而EventLoop又被绑定到一个线程中,所以我们在执行IO操作时没有必要使用同步机制因为所有发生在该Channel上的IO操作都被同一个线程执行。

    为了能够更进一步解释上图关系,我们了解一下EventLoop与EventLoopGroup的类继承关系。

EventLoop与EventLoopGroup类关系图

    我们知道EventLoopGroup可以包含多个EventLoop,但实际上EventLoop与EventLoopGroup满足“is-a”关系,也就是说EventLoop本身也是一个EventLoopGroup,所以任何可以使用EventLoopGroup的地方都可以使用一个特定的EventLoop。

Bootstrapping: What and Why

    在Netty中Bootstrapping是用来配置Netty应用的。由上一章可知,Netty中包含两种类型的BootStrap,一种专门供客户端或者DatagramChannel使用(Bootsrap),另一种专门供服务端使用(ServerBootstrap)。无论我们的应用使用何种协议,最终决定我们使用哪种Bootstrap的是我们是要创建一个客户端(Client)应用还是服务端(Server)应用。

如下表为BootStrap与ServerBootstrap对比

Bootstrap ServerBootstrap
作用 连接远程服务器 绑定并监听本地端口
EventLoopGroup个数 1 2

    我们先看一下Bootstrap与ServerBootstrap的不同之处,第一个不同之处是ServerBootstrap需要绑定到一个端口并且监听客户端连接而Bootstrap主要用于客户端连接服务器。Bootstrap最常用的方法是connect(),但我们也可以调用bind()来绑定端口并用其返回的ChannelFuture中的Channel的connect()方法完成连接。

    第二个不同之处是比较重要的,Bootstrap只包含一个EventLoopGroup而ServerBootstrap则包含两个EventLoopGroup(两个EventLoopGroup可以使用同一个实例)。实际上ServerBootstrap中包含两种类型的channel。第一种只包含一个ServerChannel,这个ServerChannel是绑定到本地监听端口的一个socket。第二中包含了所有客户端请求的连接。如下图所示:

img3.png

    如图所示,EventLoopGroupA的唯一作用就是接受新的连接并将其注册到EventLoopGroupB上。Netty使用这种机制的原因是:当一个应用的请求非常大时,如果只有一个EventLoopGroup,那么当这个EventLoopGroup的EventLoop忙于处理已经建立连接IO事件时,新到来的连接就有可能会超时。而使用两个EventLoopGroup即使在qps较高的情况下也会接受所有的连接。因为接受新连接的EventLoop与处理已经建立连接的IO事件的EventLoop用的不是同一个线程。

EventLoopGroup和EventLoop

    EventLoopGroup可以包含多个EventLoop,根据这种关系,每一个Channel一旦被建立都会绑定到一个EventLoop上并且不会再改变。因为EventLoopGroup不会包含太多个EventLoop,所以会有很多个Channel共享一个EventLoop。也就是说如果EventLoop中的一个Channel性能比较低的话会影响所有绑定到这个EventLoop中的其他Channel。这就是Netty为什么要求所有的操作都是非阻塞的原因。

    Netty允许使用同一个EventLoopGroup同时处理IO事件并接受新的连接。如下图所示为ServerBootstrap中两个EventLoopGroup使用同一个实例的情况。

img4.png

Channel Handlers and Data Flow

将ChannelPipeline和handler组合在一起

    ChannelHandler是Netty应用程序中最主要的部分,任何一个Netty应用至少包含一个ChannelHandler。ChannelHandler主要是用来处理ChannelPipeline中穿梭的数据流的。在Netty中,ChannelHandler作为一个父接口,ChannelInboundHandler及ChannelOutboundHandler继承自ChannelHandler,如下图所示:

ChanelHandler关系图

    在Netty中,数据流有两个方向,正如上图所示的流入(ChannelInboundHandler)和流出(ChannelOutboundHandler)。当数据从应用程序流向远端时我们称之为流出(outbound),反之数据从远端流入我们的应用程序称之为流入(inbound)。

    为了使数据从一端流向另一端,一个或多个Handler将会在同一个方向上处理这些数据。这些Handler会在应用启动时添加,并且他们添加的顺序决定了何时处理数据。

    这些拥有特定顺序的ChannelHandler组成了所谓的ChannelPipeline。换句话说,ChannelPipeline是一系列ChannelHandler的组合。每一个ChannelHandler处理完数据后将其传送给下一个ChannelHandler直到最后一个。

ChannelPipeline示例

    如上图所示,ChannelInboundHandler和ChannelOutboundHandler可以在ChannelPipeline中混排。在上图ChannelPipeline中,如果读取一条数据或者一个流入事件,将会在ChannelPipeline的头部将该条信息传输给第一个ChannelInboundHandler。这个ChannelInboundHandler会处理该事件并将其传输给CHannelPipeline中的下一个ChannelInboundHandler。当传输到最后一个ChannelInboundHandler时也就到达了ChannelPipeline的尾部,也就意味着所有的操作都完成了。

    反过来,所有输出事件都是从ChannelPipeline的尾部开始的,经过ChannelOutboundHandler传输直到ChannelPipeline头部。

    既然在ChannelPipeline中ChannelInboundHandler和ChannelOutboundHandler是混合在一起的,那他们是如何区分的呢。其实根据上面的ChannelHandler关系图可知,输入流(inbound)Handler与输出流(outbound)Handler实现了不同的接口。这也就意味着Netty可以跳过不是同一个类型的Handler只将数据传送给需要的Handler。

    ChannelHandler被添加到ChannelPipeline中后,就会持有一个被称作ChannelHandlerContext的对象。一般来说我们只需要获取该对象的引用即可,但是在UDP等面向非连接的数据报协议中这样做是不安全的。这个对象稍后可以用来获取Channel对象但一般来说我们只是用它来读写数据。这意味着在Netty中有两种方式传输数据:直接向Channel中写数据或者直接写到ChannelHandlerContext对象中。不同的是前一种会导致数据从ChannelPipeline的尾部开始处理而后一种会导致数据从下一个Handler处理。

近距离观察Handler(Encoders,Decoders和Domain Logic)

    我们前面介绍过,Netty中有各种各样的Handler,不同的Handler功能取决于他们继承自哪一个接口(ChannelInboundHandler或ChannelOutboundHandler)。其实Netty提供了一系列的Adapter类来使事情变得简单,因为在pipline中,每一个Handler都要将事件推送给下一个Handler。而Adapter类则自动完成了这种操作,这样我们就可以只关心我们的业务逻辑。除了Adapter类,Netty还提供了编码(encode)/解码(decode)信息的功能。

适配类(Adapter classes)

Adapter类可以使我们更容易的编写代码。当我们需要些自己的ChannelHandler时建议继承Adapter类或者ecoder/decode类(其实也继承了Adapter类)。Netty提供了下列Adapter类

  • ChannelHandlerAdapter
  • ChannelInboundHandlerAdapter
  • ChannelOutboundHandlerAdapter
  • ChannelDuplexHandlerAdapter

    我们着重观察一下encoder/decode以及SimpleChannelInboundHandler< T >(ChannelInboundHandlerAdapter类)

Encoders,decoders

    当我们运用Netty接受或者传输数据时,我们需要将数据从一种类型转换为另一种类型。当我们接受一条消息时,我们需要将其从字节类型(bytes)转换为一个Java对象(decode),当我们发送一条消息时我们需要将其从Java对象转换为字节类型(encode)。这种转换在网络传输中非常常见(bute-message,message-byte),因为在网络中只能传输字节类型。

    在Netty中中多种encode/decode的基类,我们可以根据实际应用场景来决定使用哪个基类。一般来说,encode/decode的基类都有相似的名字,例如”MessageToByteEncoder”或者”ByteToMessageDecoder”。

    严格来说,其他的Handler也可以实现encoder和decoder的功能,但我们之前说过我们根据需要来选择不同的适配器类。实际上所有的decoder都实现或者继承了ChannelInboundHandlerAdapter或者ChannelInboundHandler。复写了“channelRead”方法,这个方法会读取流入(inbound)Channel的数据然后执行解码(decode)操作将解码过的信息传递给ChannelPipeline中的下一个ChannelInboundHandler。

    类似于流入信息,流出信息处理过程相似,encoder会将消息对象转化为字节流并传递给下一个ChannelOutboundHandler。

业务逻辑

    一般来说,我们最常用的handler主要用来处理解码(decode)过的信息,并加入我们的业务逻辑。创建这样的Handler,我们只需要简单地继承SimpleChannelInboundHandler< T >,其中T表示我们的Handler可以处理的类型。在这个Handler中,我们可以根据自己需求复写不同的方法,所有方法参数都包含ChannelHandlerContext参数。

    在所有的基类方法中,最需要注意的是”channelRead0(ChannelHandlerContext,T)”方法,这个方法无论在哪里调用,T都是传递的信息,我们的应用可以对其随意处理。但需要注意的是即使我们用多线程来处理IO事件,我们也千万不能阻塞IO线程,因为这样有可能会影响程序性能。

阻塞操作

    之前我们说过在Netty中千万不要阻塞IO,这也就意味着在我们的ChannelHandler中不能进行进行阻塞操作。幸运的是Netty提供了一个解决方案。在我们向ChannelPipeline中增加ChannelHandler时我们可以指定一个EventExecutorGroup,这个EventExecutorGroup会获得一个EventExecutor来执行该ChannelHandler的所有方法。不同的是EventExecutor会用一个不同的线程执行ChannelHandler的方法并在执行完毕后释放其绑定的EventLoop。