Netty in Action ——— Netty的组件和设计

本文是Netty文集中“Netty in action”系列的文章。主要是对Norman Maurer and Marvin Allen Wolfthal 的 《Netty in action》一书简要翻译,同时对重要点加上一些自己补充和扩展。

本章涵盖

  • Netty的技术和结构方面
  • Channel、EventLoop和ChannelFuture
  • ChannelHandler和ChannelPipeline
  • 引导

Channel,EventLoop,and ChannelFuture

下面我们将增加对Channel、EventLoop和ChannelFuture类的讨论,这些类一起代表了Netty网络的抽象

  • Channel —— Sockets
  • EventLoop —— 控制流,多线程和并发
  • ChannelFuture —— 异步的通知
Channel接口

基本的I/O操作( bind(),connect(),read(),and write() )依赖于原生底层网络传输的支持。在基于Java的网络中,基本的结构是Socket类。Netty的Channel接口提供了一个API,这更好的减少了直接使用Sockets工作的复杂性。此外,Channel是扩展类系统中的根,拥有许多的预定义实现,比如:

  • EmbeddedChannel
  • LocalServerChannel
  • NioDatagramChannel
  • NioSctpChannel
  • NioSocketChannel
Interface EventLoop

EventLoop定义了“Netty处理一个连接的生命周期中遇到的所有事件”的核心抽象概念。 Channels,EventLoops,Threads 和 EventLoopGroups 的关系:

  • 一个EventLoopGroup包含一个或多个EventLoop
  • 一个EventLoop被绑定到一个单线程上,在这个EventLoop的整个生命周期。
  • 所有的I/O事件处理通过一个EventLoop在一个专门的线程上被处理。
  • 一个Channel的整个生命周期里只会被注册到一个EventLoop
  • 一个EventLoop可能被分配给一个或多个Channel

注意这种设计,给定Channel的I/O会在同一个Thread上执行,实质上这消除了同步的必要。

ChannelFuture接口

正如我们所解释的,Netty中的所以I/O操作都是异步的。因为操作可能立即返回,之后我们需要一个方式去检测这个操作的返回。为了这个目的,Netty提供了ChannelFuture,ChannelFuture的addListener()方法可以注册一个ChannelFutureListener,该ChannelFutureListener在操作完成( 无论操作成功与否 )时将被通知。

更多关于ChannelFuture 想象ChannelFuture作为一个占位符用于一个操作的结果,它将在未来某个时刻被执行。什么时候会被执行可能依赖几个因素,这可能无法精确的预测,但是能保证的是它在未来某个时刻一定会被执行。此外,所有属于同一个Channel的操作将保证按照调用的顺序被执行。

ChannelHandler 和 ChannelPipeline

ChannelHandler接口

从应用开发者的观点来看,Netty最主要的组件就是ChannelHandler,ChannelHandler作为所有应用逻辑的容器,用于处理出站和入站的数据。这可能是因为ChannelHandler的方法会被网络事件触发。事实上,一个ChannelHandler能致力于几乎所有的动作类型,比如将数据格式从一种转换到另一种或者处理执行过程中抛出的异常。 举个例子,ChannelInboundHandler是一个子接口,你将频繁实现这个接口。这种类型接受入站事件和数据,你的应用逻辑会对其进行处理。你还能在一个ChannelInboundHandler里刷新数据(flush data)当你要发送一个响应到一个连接的客户端时。我们的应用逻辑将经常属于一个或多个ChannelInboundHandlers。

ChannelPipeline接口

一个ChannelPipeline提供了一个ChannelHandlers 链的容器,并且定义了API用于在ChannelHandlers链中传播入站流和出站事件。 当一个Channel被创建的时候,它将自动分配它自己的ChannelPipeline。 ChannelHandlers被装进ChannelPipeline遵循如下步骤:

  • 一个ChannelInitializer实现被注册到一个ServerBootstrap
  • 当ChannelInitializer.initChannel()被调用,ChannelInitializer将一个自定义的ChannelHandlers集合安装至管道中。
  • 将ChannelInitializer自己从ChannelPipeline中移除。

让我们更深入ChannelPipeline和ChannelHandler的生态关系以观察当你发送或接受数据时都发生了什么事。

ChannelHandlers接收事件,执行已经实现的处理逻辑,并传递处理后的数据到链中的下一个处理器(ChannelHandler)。ChannelHandler执行的顺序取决于它们被加入到链中的顺序。

入站和出站处理器能被放入到同一个管道中。如果一个消息或者任何其他的进站事件被读取,它将从管道的头开始传递给第一个ChannelInboundHandler。这个处理器可能会也可能不会真实的修改数据,这依赖于特定的功能,接下来数据会被传递到链中的下一个ChannelInboundHandler,最后数据将到达pipeline的尾部,到此为止入站数据的所有处理结束。 数据出站和入站是类似的,出站数据从ChannelPIpeline的尾部的第一个ChannelOutboundHandler开始,直到数据到达pipeline头。越过这个点,出站数据将到达网络传输,这里显示为Socket。最经典的,socket将触发一个写操作。

更多关于入站和出站处理器 通过ChannelHandlerContext能将一个event传递到chain中的下一个handler,该ChannelHandlerContext在作为一个参数支持于每个方法中( 即,每个方法都有ChannelHandlerContext这个参数 )。 因为在某些时候你想忽略你所不感兴趣的事件,所以Netty提供了抽象基类ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter。这两个抽象基类简单实现了所以的方法:通过调用ChannelHandlerContext对应的方法将事件传递给下一个handler。你能继承这类并重写你所感兴趣的方法。

考虑出站和入站操作的不同,你可能会担心当两个类型的处理器混合在一个ChannelPipeline中会发生什么。虽然,入站和出站处理器都继承了ChannelHandler,但Netty区分了ChannelInboundHandler和ChannelOutboundHandler的实现并确保数据只会在两个相同方向类型的处理器间传递。

当一个ChannelHandler被加到一个ChannelPipeline中,它分配了一个ChannelHandlerContext,该ChannelHandlerContext代表了一个ChannelHandler和ChannelPipeline的绑定。虽然ChannelHandlerContext对象能被用于获取底层的Channel,但大多时候可以直接利用ChannelHandlerContext去写一个出站数据。

Netty两种发送消息的方法: ①通过Channel来发送,如:ChannelHandlerContext.channel.writer(obj) 通过Channel发送的数据会从ChannelPipeline尾部开始传递到ChannelPipeline的头部,接着进行数据的网络传输 ②通过在与之关联的ChannelHandler中的ChannelHandlerContext写数据,如:ChannelHandlerContext.writer(obj) 而通过某个ChannelHandler关联的ChannelHandlerContext进行数据发送时,数据将从当前ChannelHandler对应的下一个ChannelHandler开始执行,即写入的数据会直接传递给链中的下一个ChannelOutboundHandler。

进一步看ChannelHandlers

正如我们前面所说的,ChannelHandlers有许多不同类型的,并且它们的功能很大程度上取决于它们的父类。Netty提供了许多默认处理器的实现以适配器类的形式,这么做的目的在于简化应用程序的开发。 你已经看到pipeline中的每一个ChannelHandler负责传递事件到链中的下一个handler(这个传递工作实际上是由ChannelHandlerContext完成的)。这些适配器(或其子类)将自动帮我们完成。

为什么使用适配器 这些适配器最大程度上的帮助我们减小了自定义ChannelHandler的工作量,因为他们提供了对应接口所有方法的默认实现。 下面这些适配器在你创建自定义的处理器时会经常使用到:

  • ChannelHandlerAdapter
  • ChannelInboundHandlerAdapter
  • ChannelOutboundHandlerAdapter
  • ChannelDuplexHandlerAdapter
编码器和解码器

编码和解码:当你在Netty发送或者接收一个消息时,一个数据进行转换的地方。 一个入站消息将被解码,这是将字节转换为另一个数据格式,典型的例子是转换为一个java对象。如果是出站消息,这将是相反的:当前数据格式将编码成字节。这两个转换的原因是因为:网络数据总是一系列字节。 Netty提供了多种类型的编码和解码抽象类,对应于具体的需求。还提供了将消息转换成另一种中间格式,而不立即转换成字节,这样的编码器需要不同的父类来派生。 基本的编码器、解码器,如:MessageToByteEncoder、ByteToMessageDecoder 专业的类型的编码器、解码器,如:ProtobufEncoder、ProtobufDecoder 所有Netty提供的encoder/decoder 适配器类要么实现了ChannelInboundHandler,要么实现了ChannelOutboundHandler 解码器重写了channelRead方法,channelRead方法中调用了解码器提供的decode方法,并将解码后的数据传递给了pipline中的下一个ChannelInboundHandler。编码器则与之相反。

SimpleChannelInboundHandler抽象类

在大多时候你的应用将引用一个handler用于接收解码后的数据,并对该数据进行商业逻辑处理。想创建这样的一个ChannelHandler,你只需要继承基类SimpleChannelInboundHandler<T>, T 是你想要处理的消息的java类型。在这个处理器中,你将重写一个或多个基类中的方法并获取一个ChannelHandlerContext引用,该ChannelHandlerContext引用会作为一个参数在所有的处理器方法中。 channelRead0(ChannelHandlerContext, T)是一个非常重要的方法在SimpleChannelInboundHandler中。除了要求当前I/O线程不被堵塞外,这个方法实现完全取决于你。

Bootstrapping

Netty的启动引导类提供了用于应用网络层配置的容器,包括绑定程序到一个给定端口或一个程序通过指定的host、port连接到另一个程序。 一般而言,我们将前一种情况称为引导一个服务端,后一种情况为引导一个客户端。这个术语简单又方便,但是轻微模糊了“服务端”和“客户端”表示不同网络行为的重要事实。也就是,‘监听进来的连接’与‘和一个或多个进程建立连接’。

此外,bootstrap有两种类型:一个用于客户端(称为简单Bootstrap),另一个用于服务端(ServerBootstrap)。无论你的应用使用哪种协议或数据类型,唯一决定使用哪种引导类的是它的功能,是将作为一个客户端还是服务端。

Bootstrap 和 ServerBootstrap 的区别 ①一个ServerBootstrap绑定一个端口,因为服务端必须监听连接。而Bootstrap用于想要连接远端的客户端应用。 ②引导一个客户端和需要一个EventLoopGroup,而服务端(ServerBootstrap)需要两个EventLoopGroup( 这两个可以是同一个实例 )。 一个服务端需要两个不同的Channel集合。第一个集合包含了ServerChannel,该ServerChannel代表服务自己所监听的绑定本地端口的socket。第二个集合将包含所有已经创建了的Channel,这些Channel ( 该Channel由ServerChannel创建 )用于处理客户端连接,服务端收到的每一个客户端的连接都将创建一个Channel。

ServerChannel所关联的EventLoopGroup会分配一个EventLoop用于负责在收到连接请求时创建Channel。一旦接收一个连接,第二个EventLoopGroup就会分配一个EventLoop给创建好的Channel。 注意,这里Channel的创建是由ServerChannel所在的EventLoop( 实际上是EventLoop所在的线程上 )完成的。而且一个ServerChannel只会注册到一个EventLoop上。

后记

本文主要对Netty主要的组件进行了介绍,同时介绍了Netty框架的一些设计思想。是一篇很浅的概述介绍文章,其中涉及的组件都会在其他章节进行详细展开以及深入的学习。 若文章有任何错误,望大家不吝指教:)

参考

《Netty in action》

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏用户2442861的专栏

浅谈UML的概念和模型之UML九种图

http://blog.csdn.net/jiuqiyuliang/article/details/8552956

571
来自专栏owent

Lua 挺好用的样子

其实对于理解Javascipt的人来说,Lua也很容易理解,因为他们太多的地方相像了。

1023
来自专栏技术专栏

慕课网Flask构建可扩展的RESTful API-3. 自定义异常对象

因为注册的形式就非常多,所以我们不可能用万能的方式来解决。如果我们不能很好的处理多种多样的形式,我们的代码就会非常的杂乱

752
来自专栏守候书阁

重构 - 设计API的扩展机制

上篇文章,主要介绍了重构的一些概念和一些简单的实例。这一次,详细的说下项目中的一个重构场景--给API设计扩展机制。目的就是为了方便以后能灵活应对需求的改变。当...

932
来自专栏Java与Android技术栈

从API到DSL —— 使用 Kotlin 特性为爬虫框架进一步封装

NetDiscovery 是一款基于 Vert.x、RxJava 2 等框架实现的爬虫框架。

904
来自专栏守候书阁

重构 - 设计API的扩展机制

上篇文章,主要介绍了重构的一些概念和一些简单的实例。这一次,详细的说下项目中的一个重构场景--给API设计扩展机制。目的就是为了方便以后能灵活应对需求的改变。当...

44217
来自专栏Hongten

JSP 三讲

481
来自专栏用户2442861的专栏

Base64编码原理与应用

2015年,我们在青云平台上实现了“百度云观测”应用。青云应用本质上是一个iframe,在向iframe服务方发送的请求中会携带一些数据,青云平台会使用Bas...

452
来自专栏葡萄城控件技术团队

Mobile First! Wijmo 5 之 架构

本文就开发者关心的话题之一架构,展开叙述。 ? Wijmo 5是一组JavaScript控件,但是不要与Widgets混淆。在此前开发Wijmo的时候,我们能够...

20010
来自专栏FreeBuf

Retargetable Decompiler 免费的在线反编译引擎

写在前面 其实谁开发了一个反编译引擎跟我们并没有什么关系,但它开放了在线的免费服务就不一样了。 正文 Retargetable Decompiler的主要目的...

30710

扫码关注云+社区