Netty4实战第三章:Netty基础

  这一章,我们来学习一下Netty的基础知识。学习完基础知识我们就会知道Netty的组件是如何搭配在一起的和各自的作用。

  下面列一下Netty核心组件,有的组件如果没有应用就根本不会工作,其他的在你实际使用过程中也是很常见或者很重要的。

  • Bootstrap或ServerBootstrap
  • EventLoop
  • EventLoopGroup
  • ChannelPipeline
  • Channel
  • Future或ChannelFuture
  • ChannelInitializer
  • ChannelHandler
  这一章的目的,就是学习这些核心组件,为后面的学习打下基础。在详细介绍这些组件之前,这里会首先描述一下它们如何在一起协同工作的,让你有一个整体的概念。

一、Netty速成

  开始学习之前,如果你对Netty应用的结构有一个整体认识是有很多好处,服务端和客户端的结构基本类似。

  一个Netty应用首先从Bootstrap类开始,Bootstrap可以帮助你轻松构造Netty应用,使用它来配置你的应用或者启动你的应用。

  为了让更多协议和其他各种方式处理数据,Netty有了Handler组件。人如其名,Handler就是为了处理Netty里面的指定事件或一组事件。事件就是你可以用Handler去处理的东西,比如将对象转字节数组,或者反过来,或者出现异常去捕获处理。

  实现ChannelInboundHandler接口是非常通用的形式。你实现的ChannelInboundHandler可以接收数据并决定怎么处理这些数据。你也可能在ChannelInboundHandler的实现方法里写数据给对方。换句话说,你的主要业务逻辑都是在你实现的ChannelInboundHandler类中。

 业务逻辑

  简单理解,业务逻辑就是你收到数据要怎么处理或者要发送什么样的数据给对方。类似你从数据库读的数据要怎么处理和要向数据库写什么样的数据。业务逻辑需要你自己决定要怎么写,这部分代码算是你的应用的核心。

  无论是Netty客户端还是服务端,都需要知道接收或发送的数据要怎么处理。这些可以通过配置多个不同的Handler来处理,配置Handler就需要用到ChannelInitializer。ChannelInitializer的作用就是将Handler添加到ChannelPipeline中。当你发送或收到消息的时候,这些Handler就决定怎么处理你的数据。看ChannelInitializer源码可以发现起始它也是一个Handler,当添加了其他Handler之后它就从ChannelPipeline里移除了。

  ChannelPipeline是Netty应用的核心基础。ChannelPipeline和EventLoop以及EventLoopGroup都是密切相关的因为他们都与事件和事件处理有关。

  EventLoops的作用是为Channel处理IO操作。一个EventLoop通常会为多个Channel处理事件。EventLoopGroup通常包含多个EventLoop,可以用来获取EventLoop。

  Channel可以理解为代表Socket连接或者其他可能处理IO操作的组件,EventLoop是用来处理IO的,所以EventLoop管理Channel。

  Netty中所有的IO操作的表现形式都是异步的。比如当你去连接一个地址的时候,默认是异步完成的。收发消息同样是异步的。

这就意味着执行这些操作不会立即返回结果,过一会可能才会有结果。所以当方法执行完后你并不知道执行结果成功还是失败了,不过可以通过注册鉴定器之类的方式等待结果通知给你。为了纠正这一点,Netty使用了Futures和ChannelFutures。这个Future可以用来注册监听器,当操作失败或者成功你就会得到通知。

什么是ChannelFuture

  前面说过,Netty的IO操作都是异步,因此执行结果不会立即返回。ChannelFuture就会在这里发挥作用。ChannelFuture实现了java.util.concurrent.Future,可以用来注册ChannnelFutureListeners。当那些异步操作完成后ChannnelFutureListeners会收到通知。总体来说,ChannelFuture就是异步执行结果的占位符,具体什么时候执行依赖很多因素而且也说不清楚。唯一能确定的事情就是它肯定会被执行,而且所有的异步操作都会返回ChannelFuture,属于同一个Channel的ChannelFuture会按照方法执行的顺序去顺序执行。

  本书后面的内容会经常提到上面这些概念。但是他们其实并不太复杂,如果你在后面的章节中发现看起来很复杂的这些概念的详细解释,可以回过头来看看这一节简单版本的解释。

  下面的章节,我们继续介绍上面提到的每个概念。

二、Channels,Events和Input/Output(IO)

  Netty是一个非阻塞,事件驱动的网络框架。对你来说Netty就是用线程处理IO事件。这听起来很像多线程应用,你可能还在想是否需要同步代码块处理并发问题。完全没必要,下图将要处理Netty事件的时候为什么不需要同步代码块。


  上图展示了Netty应用有EventLoopGroups,里面有一个或多个EventLoops,可以把EventLoops想像成Channel实际工作的线程。

EventLoop和线程的关系

  在EventLoop的生命周期中,它只存在于一个线程中。

  每当注册了一个Channel,在Channel的生命周期中Netty就会把这个Channel绑定到唯一一个EventLoop上(也就是一个线程中)。在这个Channel上的所有IO操作只会在一个线程中,所以你的Netty应用进行IO操作的时候不需要同步代码块。

  为了帮助大家理解,下图展示了EventLoops和EventLoopGroups的关系。


  EventLoop和EventLoopGroup的关系可能不是太直观,因为我们前面说过一个EventLoopGroup包含一个或多个EventLoop,但是上图显示的确实EventLoopGroup继承了EventLoop。也就是说当你的参数是EventLoopGroup时你也可以传EventLoop。

三、Bootstrapping

  启动器主要是用来配置你的Netty应用。客户端和服务端都需要这玩意。通过上一章的内容,我们有使用到两个启动器,一个(Bootstrap)是给客户端用的,DatagramChannel也是用这个,另一个(ServerBootstrap)是给服务端用的。不管你的应用使用什么协议,用什么启动器取决于你开发客户端还是开发服务端。

  这两个启动器有很多相似之处,实际上,它们之间的相同点远多于不同点。下表列出它们关键的不同点。

 作用EventLoopGroups数量
Bootstrap
连接远程地址和端口1
ServerBootstrap
绑定本地端口2

     

  

  Groups,transports和handlers后面的章节会详细介绍,所以我们现在只研究下两个启动器关键的不同点。第一点ServerBoostrap要作为服务端监听本地端口等待新连接进来,而Bootstrap用于客户端去连接服务端或者用于DatagramChannel。用Bootstrap一般调用connect()方法不过你也可以调用bind()方法,连接成功时候通过ChannelFuture注册Channel。

  第二点不同是比较重要的。客户端Bootstrap使用1个EventLoopGroup而服务端ServerBootstrap使用2个EventLoopGroup(也可以配置成1个)。一个ServerBootstrap可以拥有两组Channel。第一组只包含一个ServerChannel处理新进来的连接事件,第二组的所有的Channel来处理已经接受的连接的事件。下图尽量让大家这种设计结构。


  上图中,EventLoopGroup A只负责处理新连接然后把他们交给EventLoopGroup B。之所以使用两组EventLoopGroup来处理是因为网络应用在高并发量的情况下,只有一个EventLoopGroup如果处理很多已经接收的连接事件,就没有多余能力再去接收新的连接。这样会造成一部分客户端连接超时。如果有两组EventGroup,即使很高的并发量,所有的新连接也都会被接收,因为接收新连接和处理已经接收的连接是不同的EventGroup。

EventLoopGroup和EventLoop

  EventLoopGroup可能会包含多个EventLoop,不过这是可配置的。每个Channel一旦创建就会绑定一个EventLoop,而且终生不会改变。因为EventLoop的数量肯定比Channel的数量少,所以很多Channel会共享一个EventLoop。也就是说如果你的应用里的EventLoop工作量比较大,那绑定在这个EventLoop上的其他Channel可能不会得到响应。所以在你的应用中千万不能阻塞EventLoop。

  下图展示了如果你的服务端配置只使用一个EventLoopGroup的执行流程。


  Netty允许只使用一个EventLoopGroup处理IO和接收新连接。对于并发量不大的应用,这样使用一般也不会出现什么问题。上图就基本展示了应用中只使用一个EventLoopGroup同时接收新连接和处理IO的情况

  下一小节,我们将要讨论Netty执行IO操作的相关知识。

四、Channel Handlers和数据流转

  现在我们来看看当Netty收到和发送数据的时候发生了什么。本章开头我们就说过Netty的核心组件之一就是Handler。要想理解读写数据的时候发生了什么,就先得理解什么是Handler。一个应用一般有一个或多个Handler,它们的执行顺序由ChannelPipeline决定。他们之间是相互依赖的,因此它们都是Netty中重要的部分。

4.1、ChannelPipeline和handlers

  一般来说,开发Netty应用大部分逻辑都是在写在ChannelHandler中。可能你还没意识到,你的Netty应用至少有一个ChannelHandler。可见这玩意确实很重要。但到底什么是ChannelHandler,简单来说,ChannelPipeline是一个管道,Handler就是里面一层一层要对数据进行处理的东东。Netty中的ChannelHandler有一个最上层的父接口,名字就叫ChannelHandler。下面有两种类型的子接口ChannelInboundHandler和ChannelOutboundHandler,请看下图。


  下面再来介绍Handler在处理数据时干了什么,这部分知识可能看起来很简单,但一定要牢记,因为它非常重要。后面的章节,会介绍一些ChannelHandlers用在其他方面的知识。

  Netty中的数据流转有2个方向,数据进的时候与ChannelInboundHandler有关,数据出的时候与ChannelOutboundHandler有关。数据流进的意思是你的应用收到对端发来的数据,数据流出的意思是你的应用发送数据给对端。

  为了将数据从一端送到另一端,一般都会有一个或多个ChannelHandler用各种方式对数据进行操作。而这些ChannelHandlers在Netty应用启动配置过程中就已经添加进去了。

  决定这些Handler以一种特定的顺序处理数据的是ChannelPipeline。也就是说ChannelPipeline其实就是一串ChannelHandler。每一个ChannelHandler都会执行它的数据操作(进的时候执行进的Handler,也就是ChannelInboundHandler),然后把数据传给下一个ChannelHandler,只到ChannelPipeline中没有其他的ChannelHandler。这很类似Java EE的过滤器或者Spring MVC的拦截器。

  下图展示了数据在ChannelPipeline的流转过程。


  ChannelInboundHandler和ChannelOutboundHandler可以混在同一个ChannelPipeline里。

  在上图中,当应用收到数据时,首先从ChannelPipeline的头部进入到一个ChannelInboundHandler。第一个ChannelInboundHandler处理后传给下一个ChannelInboundHandler。然后ChannelPipeline中没有其他的ChannelInboundHandler了数据就会达到ChannelPipeline的尾部,也就是应用对收到数据的处理已经完成了。

  数据流出的是反过来的,首先从ChannelPipeline的尾部开始进入到最后一个ChannelOutboundHandler,最后一个ChannelOutboundHandler处理后传给它前面一个ChannelOutboundHandler。和进是不同的,进是从前到后,而出的时候是从后到前。没有多余的ChannelOutboundHandler的时候,数据进入实际网络中传输,触发一些IO操作。

  可能你会有疑问,数据在进出的时候操作是不同的,那么它们为什么能混合在同一个ChannelPipeline中。很简单,因为进出的Handler实现的接口是不同的,上面提高过,一个是ChannelInboundHandler,出的时候是ChannelOutboundHandler。进出的时候会跳过不符合当前操作的Handler。例如,进的时候会跳过出的Handler,出的时候会跳过进的Handler。只需要简单判断现在的Handler实现的哪个接口,就能知道该不该跳过这个Handler了。

  一旦一个ChannelHandler被添加到ChannelPipeline中它就会获得一个ChannelHandlerContext。一般情况下获得这个对象并持有它是安全的。不过在数据包协议的时候不一样安全例如UDP协议。这个对象可以用来获取底层Channel,不过一般都是持有它然后用它发送数据。也就是说在Netty中你有两种发送数据的方式。你可以直接写到Channel中或者使用ChannelHandlerContext对象。它们的主要区别是,直接写到Channel,则数据会从ChannelPipeline头部传到尾部,每一个ChannelHandler都会处理数据;而使用ChannelHandlerContext则是将数据传送到下一个ChannelHandler。

五、编码、解码和领域逻辑:进一步了解Handlers

  我们之前提过,有很多不同类型的Handler。它们的继承的基类是什么。Netty提供了一组适配器类来解决这个问题。这样做的原因是Handler的工作大致都是处理数据然后传给下一个Handler。使用适配器类这些相同的操我都已经帮你完成了,你需要重写你感兴趣的方法即可。除了这些适配器类还有一些继承适配器类并提供了其他功能的类,例如方便编码/解码消息的功能。
  接下来我们仔细研究三种ChannelHandler包括编码器,解码器和SimpleChannelInboundHandler<T>,SimpleChannelInboundHandler是ChannelInboundHandlerAdapter的子类。

5.1、编码器、解码器

  当你用Netty接收或发送消息,必须将其从一种格式转成另一种格式。比如收消息,你得把消息从字节转为Java对象。发消息就是将Java对象转成字节发出去。当数据通过网络时这种转换操作是必须存在的,因为网络只能传输字节。
  Netty中有各种各样的编码器和解码器基类,用什么取决于你需要做什么。例如你的应用的消息不需要转字节而去转成另外一种类型如JSON。一个编码器仍在使用但是存在不同的基类。为了找出哪个基类是你需要用的其实可以通过类名来查看。一般基类都有非常相似的名称如ByteToMessageDecoderMessageToByteEncoder。或者特殊类型如ProtobufEncoder和ProtobufDecoder就是用来支持Google的protocol buffers。
  严格的说,所有的Handler都可以是编码器和解码器,但是前面说过,使用什么基类取决与你想做什么。例如解码器,都是继承ChannelInboundHandlerAdapter或者实现了ChannelInboundHandler。当读到数据时会调用channelRead方法,所以这个方法被重写了。然后就会调用decode方法解码消息,并且会执行ChannelHandlerContext.fireChannelRead(decodedMessage)方法将解码后的消息传给下一个ChannelInboundHandler
  当发送消息的时候,也执行了类似的过程。编码器会将消息转为字节,然后传给下一个ChannelOutboundHandler。

5.2、领域逻辑

  也许你的应用最常用的Handler就是接收解码后的消息然后对消息执行你的领域逻辑。这种Handler一般只需要继承SimpleChannelInboundHandler<T>,T代表你的消息类型,很常见的泛型用法。重写方法你会得到一个ChannelHandlerContext对象的引用,所有的方法都会将ChannelHandlerContext当参数传过来,你可以用类属性将它存起来。

  继承SimpleChannelInboundHandler<T>的类的主要方法是channelRead0(ChannelHandlerContext,T)。无论Netty什么时候执行这个方法,T就是你的消息然后你可以对消息进行处理,想怎么处理就怎么处理。不过有一点,虽然Netty使用多线程处理IO,但是尽量不要去阻塞IO线程,否则在高并发的场景会有性能问题。

阻塞操作

  之前提到过,一定不能去阻塞IO线程。也就是说在ChannelHandler中有阻塞操作是有问题的。幸福的是,这里有一个解决方案。当将ChannelHandlers添加到ChannelPipeline中时可以指定一个EventExecutorGroup。EventExecutorGroup用来获取一个EventExecutor并且EventExecutor执行ChannelHandler所有的方法。EventExecutor使用不同的线程处理IO从而释放EventLoop。

六、总结

  这一章主要是学习了Netty的核心组件,并简单介绍了一下它们是如何协同工作的。目的是后面的章节会详细讨论每一个组件,但是可能不会顾及到这些组件的关系。学习完本章,你应该基本了解下面几个关键组件了。

  • Bootstrap和ServerBootstrap
  • EventLoop
  • EventLoopGroup
  • ChannelPipeline
  • Channel
  • Future和ChannelFuture
  • ChannelInitializer
  • ChannelHandler和它的子类
  后面的章节,你会再次遇到他们,也会学到更多关于它们的详细知识。


  

  

  



  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值