Netty In Action中文版 - 第三章:Netty核心概念

时间:2022-07-04 20:27:08

        在这一章我们将讨论Netty的10个核心类。清楚了解他们的结构对使用Netty非常实用。可能有一些不会再工作中用到。可是也有一些非经常常使用也非常核心,你会遇到。

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

       本节的目的就是介绍以上这些概念。帮助你了解它们的使用方法。

3.1 Netty Crash Course

        在我们開始之前,假设你了解Netty程序的一般结构和大致使用方法(client和server都有一个类似的结构)会更好。
        一个Netty程序開始于Bootstrap类,Bootstrap类是Netty提供的一个能够通过简单配置来设置或"引导"程序的一个非常重要的类。Netty中设计了Handlers来处理特定的"event"和设置Netty中的事件,从而来处理多个协议和数据。

事件能够描写叙述成一个非常通用的方法,由于你能够自己定义一个handler,用来将Object转成byte[]或将byte[]转成Object;也能够定义个handler处理抛出的异常。

        你会经常编写一个实现ChannelInboundHandler的类,ChannelInboundHandler是用来接收消息。当有消息过来时。你能够决定怎样处理。当程序须要返回消息时能够在ChannelInboundHandler里write/flush数据。

能够觉得应用程序的业务逻辑都是在ChannelInboundHandler中来处理的。业务罗的生命周期在ChannelInboundHandler中。

        Netty连接client端或绑定server须要知道怎样发送或接收消息,这是通过不同类型的handlers来做的,多个Handlers是怎么配置的?Netty提供了ChannelInitializer类用来配置Handlers。ChannelInitializer是通过ChannelPipeline来加入ChannelHandler的。如发送和接收消息,这些Handlers将确定发的是什么消息。ChannelInitializer自身也是一个ChannelHandler,在加入完其它的handlers之后会自己主动从ChannelPipeline中删除自己。
        全部的Netty程序都是基于ChannelPipeline。

ChannelPipeline和EventLoop和EventLoopGroup密切相关,由于它们三个都和事件处理相关。所以这就是为什么它们处理IO的工作由EventLoop管理的原因。

        Netty中全部的IO操作都是异步运行的,比如你连接一个主机默认是异步完毕的。写入/发送消息也是同样是异步。也就是说操作不会直接运行。而是会等一会运行,由于你不知道返回的操作结果是成功还是失败,可是须要有检查是否成功的方法或者是注冊监听来通知;Netty使用Futures和ChannelFutures来达到这样的目的。Future注冊一个监听,当操作成功或失败时会通知。ChannelFuture封装的是一个操作的相关信息,操作被运行时会立马返回ChannelFuture。

3.2 Channels,Events and Input/Output(IO)

        Netty是一个非堵塞、事件驱动的网络框架。Netty实际上是使用多线程处理IO事件,对于熟悉多线程编程的读者可能会须要同步代码。

这样的方式不好,由于同步会影响程序的性能,Netty的设计保证程序处理事件不会有同步。

        下图显示一个EventLoopGroup和一个Channel关联一个单一的EventLoop。Netty中的EventLoopGroup包括一个或多个EventLoop。而EventLoop就是一个Channel运行实际工作的线程。EventLoop总是绑定一个单一的线程,在其生命周期内不会改变。
Netty In Action中文版 - 第三章:Netty核心概念
当注冊一个Channel后。Netty将这个Channel绑定到一个EventLoop。在Channel的生命周期内总是被绑定到一个EventLoop。在Netty IO操作中。你的程序不须要同步,由于一个指定通道的全部IO始终由同一个线程来运行。
        为了帮助理解,下图显示了EventLoop和EventLoopGroup的关系:
Netty In Action中文版 - 第三章:Netty核心概念
EventLoop和EventLoopGroup的关联不是直观的。由于我们说过EventLoopGroup包括一个或多个EventLoop。可是上面的图显示EventLoop是一个EventLoopGroup。这意味着你能够仅仅使用一个特定的EventLoop。

3.3 什么是Bootstrap?为什么使用它?

        “引导”是Netty中配置程序的过程,当你须要连接client或server绑定指定port时须要使用bootstrap。如前面所述。“引导”有两种类型,一种是用于client的Bootstrap(也适用于DatagramChannel),一种是用于服务端的ServerBootstrap。不管程序使用哪种协议。不管是创建一个client还是server都须要使用“引导”。
        两种bootsstraps之间有一些类似之处,事实上他们有非常多类似之处,也有一些不同。Bootstrap和ServerBootstrap之间的差异:
  • Bootstrap用来连接远程主机,有1个EventLoopGroup
  • ServerBootstrap用来绑定本地port,有2个EventLoopGroup

        事件组(Groups)。传输(transports)和处理程序(handlers)分别在本章后面讲述,我们在这里仅仅讨论两种"引导"的差异(Bootstrap和ServerBootstrap)。第一个差异非常明显,“ServerBootstrap”监听在server监听一个port轮询client的“Bootstrap”或DatagramChannel是否连接server。通常须要调用“Bootstrap”类的connect()方法。可是也能够先调用bind()再调用connect()进行连接,之后使用的Channel包括在bind()返回的ChannelFuture中。

        第二个差别或许是最重要的。clientbootstraps/applications使用一个单例EventLoopGroup,而ServerBootstrap使用2个EventLoopGroup(实际上使用的是同样的实例),它可能不是显而易见的。可是它是个好的方案。

一个ServerBootstrap能够觉得有2个channels组。第一组包括一个单例ServerChannel,代表持有一个绑定了本地port的socket。第二组包括全部的Channel。代表server已接受了的连接。

下图形象的描写叙述了这样的情况:

Netty In Action中文版 - 第三章:Netty核心概念
上图中,EventLoopGroup A唯一的目的就是接受连接然后交给EventLoopGroup B。Netty能够使用两个不同的Group。由于server程序须要接受非常多client连接的情况下,一个EventLoopGroup将是程序性能的瓶颈,由于事件循环忙于处理连接请求,没有多余的资源和空暇来处理业务逻辑,最后的结果会是非常多连接请求超时。若有两EventLoops, 即使在高负载下。全部的连接也都会被接受,由于EventLoops接受连接不会和哪些已经连接了的处理共享资源。
         EventLoopGroup和EventLoop是什么关系?EventLoopGroup能够包括非常多个EventLoop,每一个Channel绑定一个EventLoop不会被改变,由于EventLoopGroup包括少量的EventLoop的Channels,非常多Channel会共享同一个EventLoop。这意味着在一个Channel保持EventLoop繁忙会禁止其它Channel绑定到同样的EventLoop。我们能够理解为EventLoop是一个事件循环线程,而EventLoopGroup是一个事件循环集合。
        假设你决定两次使用同样的EventLoopGroup实例配置Nettyserver。下图显示了它是怎样改变的:
Netty In Action中文版 - 第三章:Netty核心概念
Netty同意处理IO和接受连接使用同一个EventLoopGroup,这在实际中适用于多种应用。上图显示了一个EventLoopGroup处理连接请求和IO操作。
        下一节我们将介绍Netty是怎样运行IO操作以及在什么时候运行。

3.4 Channel Handlers and Data Flow(通道处理和数据流)

        本节我们一起来看看当你发送或接收数据时发生了什么?回忆本章開始提到的handler概念。要明确Netty程序wirte或read时发生了什么,首先要对Handler是什么有一定的了解。

Handlers自身依赖于ChannelPipeline来决定它们运行的顺序,因此不可能通过ChannelPipeline定义处理程序的某些方面,反过来不可能定义也不可能通过ChannelHandler定义ChannelPipeline的某些方面。

不是必需说我们必须定义一个自己和其它的规定。

本节将介绍ChannelHandler和ChannelPipeline在某种程度上细微的依赖。

        在非常多地方,Netty的ChannelHandler是你的应用程序中处理最多的。

即使你没有意思到这一点,若果你使用Netty应用将至少有一个ChannelHandler參与。换句话说,ChannelHandler对非常多事情是关键的。那么ChannelHandler到底是什么?给ChannelHandler一个定义不easy,我们能够理解为ChannelHandler是一段运行业务逻辑处理数据的代码,它们来来往往的通过ChannelPipeline。

实际上。ChannelHandler是定义一个handler的父接口,ChannelInboundHandler和ChannelOutboundHandler都实现ChannelHandler接口,例如以下图:

Netty In Action中文版 - 第三章:Netty核心概念
上图显示的比較easy,更重要的是ChannelHandler在数据流方面的应用,在这里讨论的样例仅仅是一个简单的样例。ChannelHandler被应用在很多方面,在本书中会慢慢学习。
        Netty中有两个方向的数据流,上图显示的入站(ChannelInboundHandler)和出站(ChannelOutboundHandler)之间有一个明显的差别:若数据是从用户应用程序到远程主机则是“出站(outbound)”。相反若数据时从远程主机到用户应用程序则是“入站(inbound)”。
        为了使数据从一端到达还有一端,一个或多个ChannelHandler将以某种方式操作数据。这些ChannelHandler会在程序的“引导”阶段被加入ChannelPipeline中,而且被加入的顺序将决定处理数据的顺序。

ChannelPipeline的作用我们能够理解为用来管理ChannelHandler的一个容器。每一个ChannelHandler处理各自的数据(比如入站数据仅仅能由ChannelInboundHandler处理),处理完毕后将转换的数据放到ChannelPipeline中交给下一个ChannelHandler继续处理,直到最后一个ChannelHandler处理完毕。

        下图显示了ChannelPipeline的处理过程:
Netty In Action中文版 - 第三章:Netty核心概念

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvYWJjX2tleQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="" />

上图显示ChannelInboundHandler和ChannelOutboundHandler都要经过同样的ChannelPipeline。
        在ChannelPipeline中,假设消息被读取或有不论什么其它的入站事件。消息将从ChannelPipeline的头部開始传递给第一个ChannelInboundHandler。这个ChannelInboundHandler能够处理该消息或将消息传递到下一个ChannelInboundHandler中,一旦在ChannelPipeline中没有剩余的ChannelInboundHandler后。ChannelPipeline就知道消息已被全部的饿Handler处理完毕了。
        反过来也是如此。不论什么出站事件或写入将从ChannelPipeline的尾部開始,并传递到最后一个ChannelOutboundHandler。

ChannelOutboundHandler的作用和ChannelInboundHandler同样,它能够传递事件消息到下一个Handler或者自己处理消息。

不同的是ChannelOutboundHandler是从ChannelPipeline的尾部開始。而ChannelInboundHandler是从ChannelPipeline的头部開始,当处理完第一个ChannelOutboundHandler处理完毕后会出发一些操作,比方一个写操作。

        一个事件能传递到下一个ChannelInboundHandler或上一个ChannelOutboundHandler,在ChannelPipeline中通过使用ChannelHandlerContext调用每一个方法。Netty提供了抽象的事件基类称为ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter。

每一个都提供了在ChannelPipeline中通过调用对应的方法将事件传递给下一个Handler的方法的实现。

我们能覆盖的方法就是我们须要做的处理。

        可能有读者会奇怪,出站和入站的操作不同。能放在同一个ChannelPipeline工作?Netty的设计是非常巧妙的。入站和出站Handler有不同的实现,Netty能跳过一个不能处理的操作,所以在出站事件的情况下,ChannelInboundHandler将被跳过,Netty知道每一个handler都必须实现ChannelInboundHandler或ChannelOutboundHandler。
        当一个ChannelHandler加入到ChannelPipeline中时获得一个ChannelHandlerContext。一般是安全的获得这个对象的引用,可是当一个数据报协议如UDP时这是不对的,这个对象能够在之后用来获取底层通道,由于要用它来read/write消息。因此通道会保留。

也就是说Netty中发送消息有两种方法:直接写入通道或写入ChannelHandlerContext对象。

这两种方法的主要差别例如以下:

  • 直接写入通道导致处理消息从ChannelPipeline的尾部開始
  • 写入ChannelHandlerContext对象导致处理消息从ChannelPipeline的下一个handler開始

3.5 编码器、解码器和业务逻辑:细看Handlers

        如前面所说。有非常多不同类型的handlers,每一个handler的依赖于它们的基类。Netty提供了一系列的“Adapter”类。这让事情变的非常easy。每一个handler负责转发时间到ChannelPipeline的下一个handler。在*Adapter类(和子类)中是自己主动完毕的。因此我们仅仅须要在感兴趣的*Adapter中重写方法。这些功能能够帮助我们非常easy的编码/解码消息。

有几个适配器(adapter)同意自己定义ChannelHandler,一般自己定义ChannelHandler须要继承编码/解码适配器类中的一个。Netty有一下适配器:

  • ChannelHandlerAdapter
  • ChannelInboundHandlerAdapter
  • ChannelOutboundHandlerAdapter

三个ChannelHandler涨,我们重点看看ecoders,decoders和SimpleChannelInboundHandler<I>。SimpleChannelInboundHandler<I>继承ChannelInboundHandlerAdapter。

3.5.1 Encoders(编码器), decoders(解码器)

        发送或接收消息后,Netty必须将消息数据从一种形式转化为还有一种。接收消息后。须要将消息从字节码转成Java对象(由某种解码器解码);发送消息前,须要将Java对象转成字节(由某些类型的编码器进行编码)。

这样的转换一般发生在网络程序中,由于网络上仅仅能传输字节数据。

        有多种基础类型的编码器和解码器,要使用哪种取决于想实现的功能。要弄清楚某种类型的编解码器,从类名就能够看出,如“ByteToMessageDecoder”、“MessageToByteEncoder”,还有Google的协议“ProtobufEncoder”和“ProtobufDecoder”。
        严格的说其它handlers能够做编码器和适配器,使用不同的Adapter classes取决你想要做什么。假设是解码器则有一个ChannelInboundHandlerAdapter或ChannelInboundHandler。全部的解码器都继承或实现它们。“channelRead”方法/事件被覆盖,这种方法从入站(inbound)通道读取每一个消息。

重写的channelRead方法将调用每一个解码器的“decode”方法并通过ChannelHandlerContext.fireChannelRead(Object
msg)传递给ChannelPipeline中的下一个ChannelInboundHandler。

        类似入站消息,当你发送一个消息出去(出站)时。除编码器将消息转成字节码外还会转发到下一个ChannelOutboundHandler。

3.5.2 业务逻辑(Domain logic)

        或许最常见的是应用程序处理接收到消息后进行解码,然后供相关业务逻辑模块使用。

所以应用程序仅仅须要扩展SimpleChannelInboundHandler<I>,也就是我们自己定义一个继承SimpleChannelInboundHandler<I>的handler类,当中<I>是handler能够处理的消息类型。

通过重写父类的方法能够获得一个ChannelHandlerContext的引用,它们接受一个ChannelHandlerContext的參数,你能够在class中当一个属性存储。

        处理程序关注的主要方法是“channelRead0(ChannelHandlerContext ctx, I msg)”,每当Netty调用这种方法,对象“I”是消息。这里使用了Java的泛型设计。程序就能处理I。怎样处理消息全然取决于程序的须要。在处理消息时有一点须要注意的,在Netty中事件处理IO一般有非常多线程。程序中尽量不要堵塞IO线程,由于堵塞会减少程序的性能。
        必须不堵塞IO线程意味着在ChannelHandler中使用堵塞操作会有问题。

幸运的是Netty提供了解决方式,我们能够在加入ChannelHandler到ChannelPipeline中时指定一个EventExecutorGroup,EventExecutorGroup会获得一个EventExecutor。EventExecutor将运行ChannelHandler的全部方法。EventExecutor将使用不同的线程来运行和释放EventLoop。