从头学习Netty

来源:互联网 发布:我的世界优化fpsmod 编辑:程序博客网 时间:2024/06/05 03:07

从头学习Netty

  在本章,我们会从10千英尺的视角来审视Netty。这样可以帮助你Netty的组件是如何组装到一起,为什么这些对你很有用。
有些东西如果缺少应用就无法运行(在Netty中,它们是我们遇到的最重要、最普遍的组件)。

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

  这些是本章的目的,引进所有的这些概念为本书的其它部分做好准备。我们将描述这些组件是如何协作,而不是独立的介绍它们,这样在你的脑海中会构建这些组件的组装图。

Netty快速教程

  在我们开始之前,对于Netty的结构有大体的了解是很有用的(客户端和服务器端结构类似)。
  Netty应用以Bootstrap类开始,Bootstrap是个构造器,使得Netty很容易的对应用的构建和引导进行配置。
  为了允许多协议和多种方式的数据处理,Netty中有处理器。处理器,如同它们的名字所表示的那样,被设计用来处理Netty中特定的事件或者事件组。一个事件是描述它的普遍方式,例如你有一个处理器,负责将对象转为字节,或者相反;有如,你有一个处理器,在处理过程中出现异常,会被通知和处理。
  实现ChannelInboundHandler是你最有可能写的一种方式。ChannelInboundHandler接收的数据你可以处理,并且由你决定如何处理。当你的应用需要响应的时候,你可以在ChannelInboundHandler内写入、冲刷数据。换句话说,你的业务逻辑通常在ChannelInboundHandler中。



业务逻辑


业务逻辑代表你的程序的一部分,这部分程序处理从网络端接收并且转换成一定格式的数据。这部分代码可能是存储到数据库,或者其它的一些事情。究竟做些什么完全依赖于你的应用,但通常是你的应用的核心。


  当Netty连接客户端或者绑定服务器的时候,它需要知道如何处理发送和接收到的数据。这是通过不同种类的处理器来完成的,但是在Netty中是使用ChannelInitializer来配置这些处理器。ChannelInitializer的任务是向ChannelPipeline中添加实现的ChannelHandler。当发送和接收消息的时候,这些处理器决定如何处理消息。ChannelInitializer本身就是ChannelHandler,它将其它处理器添加完之后,会将本身从ChannelPipeline中移除。

  所有的Netty应用都基于ChannelPipeline。ChannelPipeline和EventLoop、EventLoopGroup紧密相关,三者都和事件、事件处理相关。
emsp; 应用中的EventLoops的作用是为通道处理IO操作。一个EventLoop通常处理多个通道的事件。EventLoopGroup本身包含多个EventLoop,可以被用来获取一个EventLoop。
  一个通道代表一个套接字连接或者其它一些可以执行IO操作的组件,因此他被EventLoop管理着处理IO任务。

  Netty中的所有IO操作都是异步执行。例如,当你连接到主机的时候,默认情况下异步执行。写和发送数据也是这样。这意味着操作有可能不直接执行,而是在后面的某个时候捡起来执行。正因为这样,当一个操作返回之后,你并不知道这个操作成功与否,但是需要在后面检核是否成功,或者需要一种方式注册通知。为了修正这一问题,Netty使用Future和ChannelFuture。这个Future可以被用来注册监听器,操作成功或者失败的时候会被调用。



ChannelFuture是什么?


就像前面强调的那样,Netty中的所有的IO操作都是异步的,当你执行的时候不是直接完成,而是在后面的某个时间点完成。这时候ChannelFuture派上用场。ChannelFuture是一个特殊的java.util.concurrent.Future,它允许你在ChannelFuture上注册ChannnelFutureListeners。这些ChannnelFutureListeners会在操作完成的时候被触发。
所以从本质上讲ChannelFuture是一个未来的操作结果。因为它的执行依赖于许多实际情况,所以很难说。但是有一点可以肯定,它会被执行;所有的操作返回属于同一个通道的ChannelFuture会按照正确的顺序执行,和这些操作的执行顺序相同。(此处不明白作者想表达什么,没有看明白,请参照原文
)


  在你看其它章节的时候,会看到这些概念被反复提及。描述有可能比此处复杂,到时你可以跳回此处重新阅读这一简化版本。

  在本章的其它部分,我们会逐一扩展刚刚讲到的东西。

通道、事件和IO

Netty是一个非阻塞、事件驱动的网络框架。在现实中,这意味着Netty使用线程来处理IO事件。如果你对多线程编程熟悉的话,你会倾向于同步你的代码块。完全没有必要,图3.1展示了为什Netty的设计使得在处理Netty事件的时候不需要同步代码块。
EventLoopGroup和永久绑定到EventLoop的通道
图3.1 EventLoopGroup和永久绑定到EventLoop的通道

图3.1展示了Netty拥有的EventLoopGroup,这些EventLoopGroup拥有一个或者多个EventLoop。可以把EventLoop看作为通道执行真正工作的线程。



EventLoop和Thread之间关系


EventLoop总是绑定到一个线程上,这个线程在EventLoop的生命周期内不会改变。


当一个通道注册的时候,Netty将通道绑定到一个EventLoop(也是一个线程),这回存在于通道的整个生命周期。这就是你的应用不需要同步Netty IO操作的原因,因为一个通道的的所有IO操作都由同一个线程处理。
为了说明这些,图3.2给出EventLoop和EventLoopGroup之间的关系。
EventLoop和EventLoopGroup的继承关系
图3.2 EventLoop和EventLoopGroup的继承关系
EventLoop和EventLoopGroup之间的关系关系并不是那么直观,因为我们说过EventLoopGroup包含一个或者多个EventLoop,但是从图中可以看出,EventLoop实际上是EventLoopGroup。这意味着当需要传入EventLoopGroup的时候你可以传入EventLoop。

引导什么?为什需要引导?

  在Netty中引导是配置你的Netty应用的过程。当你需要连接客户端到一些主机和端口的时候,或者讲服务器绑定到端口上你可以使用Bootstrap。如前面生命中所隐含的那样,存在两种类型的Bootstrap一种在客户端使用,也可是用于DatagramChannel(简单Bootstrap)另一种服务器端使用(坚持ServerBootstrap)。不管你的应用使用什么协议,决定你使用的引导的类型是你创建的是客户端还是服务器端。
  这两种类型的Bootstrap有许多共同点,实际上共同点要比区别多。表格3.1中展示了两者之间的关键相同点和不同点。
表3.1 两种类型的相同点和不同点

相同点 Bootstrap ServerBootstrap 职责 连接到远程主机和端口 绑定本地端口 EventLoopGroup数目 1 2

组、传输和处理器将在本章的后面分别介绍,这里我们只看一看两种Bootstrap的关键不同点。第一个不同点很明显,ServerBoostrap绑定到端口因为服务器需要监听连接,而Bootstrap在客户端应用或者DatagramChannel中使用。在Bootstrap中通常使用connect(),当然你可以调用bind(),然后使用bind()返回的ChannelFuture中的通道连接。
第二个不同也许最重要。客户端Bootstrap程序使用1个EventLoopGroup,而服务器端使用2个(实际上两个可以一样)。也许一段时间不是很明白要这么做,但是这是很合理的。ServerBootstrap可以被认为有两组通道。第一组包含了一个ServerChannel代表服务器自身的套接字,绑定到本地端口。第二组包含了服务器接受的连接的所有通道。图3.3可视化这两者。
服务器和客户端组的区别
图3.3 服务器和客户端组的区别
在图3.3中EventLoopGroup A的唯一目的是接收连接,并且将它转给EventLoopGroup B。Netty使用两个不同的组集合的原因是在有些场合,应用接收极大数量的连接,一个EventLoopGroup会存在瓶颈,因为EventLoop有可能忙于处理已接收的连接,有可能不能及时接收新的连接。导致的最终结果是一些连接超时。通过使用两个EventLoopGroup,所有的连接都会被接受,即使在很高的负载下,因为接收连接的EventLoop和处理已接收连接的EventLoop不是共享的。



EventLoopGroup 和EventLoop


EventLoopGroup 可以包含多于1个EventLoop,这取决于配置。每一个通道在创建的时候会被绑定1个EventLoop,这个不会改变。因为EventLoopGroup 中包含的EventLoop数目可能小于通道的数目,所以许多通道共享1个EventLoop。这意味着一个EventLoop中的一个通道过于忙碌会阻止其中的其它通道处理。这就是为什么你不能阻塞EventLoop的原因。


  Netty允许使用同一个EventLoopGroup处理IO和接收连接。在生产中许多应用都运行很好。图3.4对这一过程进行了说明。
图3.4展示了在Netty服务器上使用同一个EventLoopGroup实例两次。
Netty服务器配置1个EventLoopGroup
图3.4 Netty服务器配置1个EventLoopGroup

在下一部分我们将讨论Netty如何、合适执行IO操作。

通道处理器和数据流

我们要看一看当你要发送和接收数据的时候发生了什么。回忆本章开始我们提及的Netty的处理器概念。为了了解当写或者读的时候数据究竟发生了什么,我们首先需要了解处理器究竟是什么。处理器本身依赖于前面提及的ChannelPipeline来描述它们的执行顺序。因此ChannelPipeline和处理器密不可分,不可能只有处理器没有ChannelPipeline,也不能只有ChannelPipeline而没有任何处理器。不用说,我们从一个处理器开始,参考自身,进入另一个,诸如此类。在下一子部分我们将介绍ChannelHandler和ChannelPipeline使得这一周期性的依赖微不足道。

ChannelPipeline和Handler

在大多数情况下,Netty中的ChannelHandler是你的应用中最多的。即使你没有意识到这一点,若果你使用Netty应用,至少有一个ChannelHandler涉及其中。换句话说,它们是关键性的东西。那它究竟是什么?给出ChannelHandler一个定义并不容易,应为它是如此普通,它可以被认为是处理进出ChannelPipeline的任何代码块。实际上有一个处理器的抽象接口ChannelHandler。ChannelInboundHandler和ChannelOutboundHandler都继承于它,如图3.5所示。
ChannelHandler和其子类
图3.5 ChannelHandler和其子类
如果从例子的数据流的角度来阐述ChannelHandler很简单。ChannelHandler可以采取许多方式应用,在本书的其它部分你会看到。
在Netty中数据流向两个方向,在图3.5中展示了流入处理器(ChannelInboundHandler)和流出处理器(ChannelOutboundHandler)。数据从用户应用流向远方叫做流出。相反数据从远方流入应用叫做流入。
通常,数据从一个节点流向于另外一个节点,一个或者多个ChannelHandler会以某种方式操作数据。这些处理器会在应用的引导阶段加入。它们添加的顺序决定它们操作数据的顺序。
ChannelHandler的指定顺序的高效安排是由ChannelPipeline来完成的。换句话说,ChannelPipeline是一组ChannelHandler的排序组合。每一个ChannelHandler都会对数据进行处理(如果它可以处理,例如流入数据只有ChannelInboundHandler可以处理),紧接着将转换的数据传递给ChannelPipeline中的下一个ChannelHandler,直到没有ChannelHandler为止。



ChannelHandler和Servlet的相似性


实际上Netty中的设计和Servlet中的类似。ChannelHandler处理数据,将数据传递给ChannelPipeline中的下一个ChannelHandler。另外一个经常的行为是不进行任何处理,将指定的事件传递给ChannelPipeline中的下一个ChannelHandler。这一个ChannelHandler会处理它并且或者再次将它转给下一个ChannelHandler。


图3.6展示了ChannelPipeline组合

ChannelPipeline组合的例子
图3.6 ChannelPipeline组合的例子
如图3.6所显示的那样,在同一个ChannelPipeline中可以同时混入ChannelInboundHandler和ChannelOutboundHandler。
在这个ChannelPipeline中,如果一个消息被读取或者任何其它的流入事件,它将从ChannelPipeline的头部开始,传递给第一个ChannelInboundHandler。这个ChannelInboundHandler会处理这个事件,或者不处理,然后传递给ChannelPipeline中的下一个ChannelInboundHandler。一旦ChannelPipeline中没有ChannelInboundHandler,说明它到达ChannelPipeline的尾部,这意味着不会再有处理。
反过来也是正确的,任何一个流出事件,会从ChannelPipeline的尾部开始,会传递给ChannelPipeline中的最后一个ChannelOutboundHandler。和流入处理类似,它有可能处理,也有可能不处理,然后传递给ChannelPipeline中的下一个ChannelOutboundHandler。不同点是这里的下一个ChannelOutboundHandler实际上是“前一个”,因为流出事件是从ChannelPipeline的尾部流向头部。一旦没有ChannelOutboundHandler可以传递,它会到达传输层,并且触发一些操作。例如,写操作。



ChannelInboundHandler和ChannelOutboundHandler的基类


一个事件可以通过传递给方法的ChanneHandlerContext使得前往下一个ChannelInboundHandler或者前一个ChannelOutboundHandler。因为这些都是你通常想要操作的,我们并不感兴趣,所以Netty提供了基类ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter。这些基类提供了使用在方法中使用ChanneHandlerContext将事件传递给下一个或者上一个的实现。你可以覆写方法来做你想做的事情。


你也许怀疑,流出和流入操作不一样,当处理器混在在ChannelPipeline中,它是如何工作的。参考图3.5,流入和流出处理器有不同的接口,它们都是扩展ChannelHandler接口。这意味着,Netty可以跳过不是特定类型的处理器,因为这些处理器不能处理给定的操作。例如是一个流出事件,ChannelInboundHandler会被跳过,因为Netty知道每一个处理器是ChannelInboundHandler还是ChannelOutboundHandler的实现。
一旦一个ChannelHandler被添加到ChannelPipeline上,可以获得ChannelHandlerContext。通常获得这个引用并且保存它是安全的。当使用报文协议例如UDP的时候就不一定正确了。这一对象可以用来获取底层的通道,通常情况下你会将它保存,因为你使用它来写和发送消息。这意味着在Netty中存在两种发送消息的方式,你可以直接写入通道或者写到ChannelHandlerContext对象中。它们之间的不同是:直接写入通道会使得消息从ChannelPipeline的尾部开始,写入上下文对象使得消息从ChannelPipeline中的下一个处理器开始。

编码、解码和领域逻辑:进一步了解处理器

像我们先前说的那样,存在各种各样的处理器。每一个做什么依赖于它们继承的基类。Netty提供了一系列适配器类,使得事情更为简单。因为在管线中,每一个处理器负责指定Netty事件在ChannelPipeline中的下一个处理器。有了适配器类,这些都自动完成,你只需要覆写你感兴趣的方法。除了这些适配器类,还有一些类继承自适配器类提供额外的功能,例如使得编码、解码更容易。



适配器


有一些适配器类使得你写通道处理器更容易。当你想写一个通道处理器的时候,建议你继承的一个适配器类或者编码、解码类(实质上是从适配器类继承的)。Netty中有一些适配器类:

  • ChannelHandlerAdapter
  • ChannelInboundHandler
  • ChannelOutboundHandler
  • ChannelDuplexHandlerAdapter


这里我们看三个特殊的处理器编码、解码和SimpleChannelInboundHandler(它是ChannelInboundHandlerAdapter的子类)

编码器、解码器

  当你在Netty中发送或者接收消息的时候,消息必须从一种形式转换成另外一种形式。如果接收消息,那么它必须从字节数组转换为Java对象(由某种类型的解码器解码)。如果发送消息,那么必须将Java实体转换为字节数组(由某种类型的编码器编码)。当通过网络发送数据的时候这一转换经常发生,因为网络上只能发送字节数组。
存在不同类型的编码和解码基础类,依赖于你的需求。例如,你的应用不需要立即将消息转换为字节数组,而是从转换为另外一种消息。同样需要一个编码器,但是使用不同的基类。为了知道哪一个基类可用,基类的名字之间存在一点约定。一般情况下基类的名字类似ByteToMessageDecoder或者MessageToByteEncoder。一些特殊的类型你会发现ProtobufEncoder和ProtobufDecoder,它们被用来支持Google支持的协议缓存。
  严格的讲,其它处理器可以做编码器和解码器做的事情,但是回忆一下,依赖于你想做的事情存在不同的适配器类。对于解码器ChannelInboundHandlerAdapter或者ChannelInboundHandler是所有的解码器都要继承或者实现的。“channelRead”方法被覆写了,这一方法在从流入通道读入数据的时候会被调用。覆写的channelRead方法会调用每一个解码器的“decode”方法,并且通过调用ChannelHandlerContext.fireChannelRead(decodedMessage) 将解码的消息传递给ChannelPipeline中的下一个ChannelInboundHandler。

  在你发送消息的时候,类似的事情会发生,除了编码器将消息转换为字节数组,并且传递给下一个ChannelOutboundHandler。

领域逻辑

也许你的应用中最普遍的一个处理器是接收解码后的消息,对这个消息做领域逻辑处理。为了创建一个这样的处理器,你的应用只需要继承SimpleChannelInboundHandler这个基类,其中T是处理器能够处理的类型。在这个处理器中,通过覆写基类的一个方法,你的应用可以获取一个ChannelHandlerContext的引用,所有的方法把ChannelHandlerContext作为参数传入,你可以将它作为类的一个字段保存。
  这个处理器涉及的主要方法是“channelRead0(ChannelHandlerContext,T)”。当Netty触发这一方法时,T对象是消息,你的应用可以处理它。如何处理这个消息完全依赖于你还有应用的需求。当处理消息的时候有一个事情要注意,尽管在Netty中存在多线程来处理IO,你的应用应该尽量不要阻塞IO线程,因为这样在高并发的情况下存在性能问题。



阻塞操作


就像前面说的那样,你一定不要阻塞IO线程。这意味着在通道处理器中进行阻塞操作是有问题的。幸运的是存在一个解决方案。Netty允许你在添加通道处理器的时候指定EventExecutorGroup。这个EventExecutorGroup会被用来获取一个EventExecutor,这个EventExecutor会执行这个事件处理器的所有方法。这个EventExecutor使用一个完全不同的线程,从而缓解了EventLoop的压力。


总结

本章用一种方式呈现了Netty的许多概念,目的是是你了解这些东西如何组装到一起。意味以后的章节会分别在每一章详尽的讨论它们,在那里讨论的主体与其它组件的关系并不是总清晰。在本章中,你浏览了以下主题

  • Bootstrap和ServerBootstrap
  • EventLoop
  • EventLoopGroup
  • ChannelPipeline
  • Channel
  • Future和ChannelFuture
  • ChannelInitializer
  • ChannelHandler和子类型

    在后面的章节,你会再次遇到这些主题,它们会被更详尽的描述。

0 0
原创粉丝点击