一、Channel、EventLoop 和 ChannelFuture
上一篇博文我们在构建服务端和客户端中出现了一些新的类,可能有些同学还有些不了解它们的具体功能。没关系,接下来我们对于 Channel、EventLoop 和 ChannelFuture 类进行的讨论增添更多的细节,这些类合在一起,可以被认为是 Netty 网络抽象的代表:
- Channel : Socket;
- EventLoop : 控制流、多线程处理、并发;
- ChannelFuture : 异步通知。
1.1 Channel 接口
基本的 I/O 操作(bind()、connect()、read()和 write())依赖于底层网络传输所提供的原语。在基于 Java 的网络编程中,其基本的构造是 class Socket。Netty 的 Channel 接口所提供的 API,大大地降低了直接使用 Socket 类的复杂性。此外,Channel 也是拥有许多预定义的、专门化实现的广泛类层次结构的根,下面是一个简短的部分清单:
- EmbeddedChannel;
- LocalServerChannel;
- NioDatagramChannel;
- NioSctpChannel;
- NioSocketChannel。
1.2 EventLoop 接口
EventLoop 定义了 Netty 的核心抽象,用于处理连接的生命周期中所发生的事件。如图在高层次上说明了 Channel、EventLoop、Thread 以及 EventLoopGroup 之间的关系。
(资料图片仅供参考)
这些关系可以表述为:
- 一个 EventLoopGroup 包含一个或者多个 EventLoop;
- 一个 EventLoop 在它的生命周期内只和一个 Thread 绑定;
- 所有由 EventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理;
- 一个 Channel 在它的生命周期内只注册于一个 EventLoop;
- 一个 EventLoop 可能会被分配给一个或多个 Channel。
注意,在这种设计中,一个给定 Channel 的 I/O 操作都是由相同的 Thread 执行的,实际上消除了对于同步的需要。
1.3 ChannelFuture 接口
Netty 中所有的 I/O 操作都是异步的。因为一个操作可能不会立即返回,所以我们需要一种用于在之后的某个时间点确定其结果的方法。为此,Netty 提供了ChannelFuture 接口,其 addListener()方法注册了一个ChannelFutureListener,以便在某个操作完成时(无论是否成功)得到通知。
可以将 ChannelFuture 看作是将来要执行的操作的结果的占位符。它究竟什么时候被执行则可能取决于若干的因素,因此不可能准确地预测,但是可以肯定的是它将会被执行。此外,所有属于同一个 Channel 的操作都被保证其将以它们被调用的顺序被执行。
二、ChannelHandler 和 ChannelPipeline
2.1 ChannelHandler 接口
从应用程序开发人员的角度来看,Netty 的主要组件是 ChannelHandler,它充当了所有处理入站和出站数据的应用程序逻辑的容器。这是可行的,因为 ChannelHandler 的方法是由网络事件(其中术语“事件”的使用非常广泛)触发的。事实上,ChannelHandler 可专门用于几乎任何类型的动作,例如将数据从一种格式转换为另外一种格式,或者处理转换过程中所抛出的异常。
举例来说,ChannelInboundHandler 是一个我们会经常实现的子接口。这种类型的ChannelHandler 接收入站事件和数据,这些数据随后将会被我们的应用程序的业务逻辑所处理。当我们要给连接的客户端发送响应时,也可以从 ChannelInboundHandler 冲刷数据。我们的应用程序的业务逻辑通常驻留在一个或者多个 ChannelInboundHandler 中。
Netty 以适配器类的形式提供了大量默认的 ChannelHandler 实现,其旨在简化应用程序处理逻辑的开发过程。如ChannelPipeline中的每个ChannelHandler将负责把事件转发到链中的下一个 ChannelHandler。这些适配器类(及它们的子类)将自动执行这个操作,所以我们只重写那些你想要特殊处理的方法和事件。
那么为什么要用适配器的形式提供这些?
那是因为有一些适配器类可以将编写自定义的 ChannelHandler 所需要的努力降到最低限度,因为它们提供了定义在对应接口中的所有方法的默认实现。下面这些是编写自定义 ChannelHandler 时经常会用到的适配器类:
- ChannelHandlerAdapter
- ChannelInboundHandlerAdapter
- ChannelOutboundHandlerAdapter
- ChannelDuplexHandler
2.2 ChannelPipeline 接口
ChannelPipeline 提供了 ChannelHandler 链的容器,并定义了用于在该链上传播入站和出站事件流的 API。当 Channel 被创建时,它会被自动地分配到它专属的 ChannelPipeline。ChannelHandler 安装到 ChannelPipeline 中的过程如下所示:
- 一个ChannelInitializer的实现被注册到了ServerBootstrap中或用于客户端的Bootstrap
- 当 ChannelInitializer.initChannel()方法被调用时,ChannelInitializer将在 ChannelPipeline 中安装一组自定义的 ChannelHandler;
- ChannelInitializer 将它自己从 ChannelPipeline 中移除。
为了审查发送或者接收数据时将会发生什么,让我们来更加深入地研究 ChannelPipeline和 ChannelHandler 之间的共生关系吧。
ChannelHandler 是专为支持广泛的用途而设计的,可以将它看作是处理往来 ChannelPipeline 事件(包括数据)的任何代码的通用容器。如图,其展示了从 ChannelHandler 派生的 ChannelInboundHandler 和ChannelOutboundHandler 接口。使得事件流经 ChannelPipeline 是 ChannelHandler 的工作,它们是在应用程序的初始化或者引导阶段被安装的。这些对象接收事件、执行它们所实现的处理逻辑,并将数据传递给链中的下一个 ChannelHandler(有点类似责任链模式)。它们的执行顺序是由它们被添加的顺序所决定的。实际上,被我们称为 ChannelPipeline 的是这些 ChannelHandler 的编排顺序。
如图,说明了一个 Netty 应用程序中入站和出站数据流之间的区别。从一个客户端应用程序的角度来看,如果事件的运动方向是从客户端到服务器端,那么我们称这些事件为出站的,反之则称为入站的。从上图看入站和出站 ChannelHandler 可以被安装到同一个 ChannelPipeline中。如果一个消息或者任何其他的入站事件被读取,那么它会从 ChannelPipeline 的头部开始流动,并被传递给第一个 ChannelInboundHandler。这个 ChannelHandler 不一定会实际地修改数据,具体取决于它的具体功能,在这之后,数据将会被传递给链中的下一个ChannelInboundHandler。最终,数据将会到达 ChannelPipeline 的尾端,届时,所有处理就都结束了。
数据的出站运动(即正在被写的数据)在概念上也是一样的。在这种情况下,数据将从ChannelOutboundHandler 链的尾端开始流动,直到它到达链的头部为止。在这之后,出站数据将会到达网络传输层,这里显示为 Socket。通常情况下,这将触发一个写操作。
ps:通过使用作为参数传递到每个方法的 ChannelHandlerContext事件可以被传递给当前ChannelHandler 链中的下一个ChannelHandler。因为你有时会忽略那些不感兴趣的事件,所以 Netty提供了抽象基类 ChannelInboundHandlerAdapter 和 ChannelOutboundHandlerAdapter。 ChannelHandlerContext 上的对应方法,每个都提供了简单地将事件传递给下一ChannelHandler的方法的实现。随后,你可以通过重写你所感兴趣的那些方法来扩展这些类。
上图中出站和入站的ChannelHandler都在同一个ChannelPipeline中,那么ChannelPipeline是如何区分和处理这两种不同的类别的呢?
虽然 ChannelInboundHandle 和ChannelOutboundHandle 都扩展自 ChannelHandler,但是 Netty 能区分 ChannelInboundHandler 实现和 ChannelOutboundHandler 实现,并确保数据只会在具有相同定向类型的两个 ChannelHandler 之间传递。
当ChannelHandler 被添加到ChannelPipeline 时,它将会被分配一个ChannelHandlerContext,其代表了 ChannelHandler 和 ChannelPipeline 之间的绑定。虽然这个对象可以被用于获取底层的 Channel,但是它主要还是被用于写出站数据。
在 Netty 中,有两种发送消息的方式。我们可以直接写到 Channel 中,也可以 写到和 ChannelHandler相关联的ChannelHandlerContext对象中。前一种方式将会导致消息从ChannelPipeline 的尾端开始流动,而后者将导致消息从 ChannelPipeline 中的下一个 ChannelHandler 开始流动。
总结一下:
- 将消息写入Channel 它将从尾端开始流动。
- 将消息写入ChannelHandler中,它将会从下一个ChannelHandler开始流动。
2.3 编码器和解码器
当我们通过 Netty 发送或者接收一个消息的时候,就将会发生一次数据转换。入站消息会被解码;也就是说,从字节转换为另一种格式,通常是一个 Java 对象。如果是出站消息,则会发生相反方向的转换:它将从它的当前格式被编码为字节。这两种方向的转换的原因很简单:网络数据总是一系列的字节。(编解码)
对应于特定的需要,Netty 为编码器和解码器提供了不同类型的抽象类。例如,我们的应用程序可能使用了一种中间格式,而不需要立即将消息转换成字节。我们将仍然需要一个编码器,但是它将派生自一个不同的超类。为了确定合适的编码器类型,我们可以应用一个简单的命名约定。通常来说,这些基类的名称将类似于 ByteToMessageDecoder 或 MessageToByteEncoder。对于特殊的类型,我们会发现类似于 ProtobufEncoder 和 ProtobufDecoder这样的名称——预置的用来支持 Google 的 Protocol Buffers。
严格地说,其他的处理器也可以完成编码器和解码器的功能。但是,正如有用来简化ChannelHandler 的创建的适配器类一样,所有由 Netty 提供的编码器/解码器适配器类都实现了 ChannelOutboundHandler 或者 ChannelInboundHandler 接口。
我们会发现对于入站数据来说,channelRead 方法/事件已经被重写了。对于每个从入站Channel 读取的消息,这个方法都将会被调用。随后,它将调用由预置解码器所提供的 decode()方法,并将已解码的字节转发给 ChannelPipeline 中的下一个 ChannelInboundHandler。出站消息的模式是相反方向的:编码器将消息转换为字节,并将它们转发给下一个ChannelOutboundHandler。
2.4 抽象类 SimpleChannelInboundHandler
最常见的情况是,你的应用程序会利用一个 ChannelHandler 来接收解码消息,并对该数据应用业务逻辑。要创建一个这样的 ChannelHandler,我们只需要扩展基类 SimpleChannelInboundHandler
在这种类型的 ChannelHandler 中,最重要的方法是 channelRead0(ChannelHandlerContext,T)。除了要求不要阻塞当前的 I/O 线程之外,其具体实现完全取决于我们。
三、引导
Netty 的引导类为应用程序的网络层配置提供了容器,这涉及将一个进程绑定到某个指定的端口(服务端),或者将一个进程连接到另一个运行在某个指定主机的指定端口上的进程(客户端)。
严格来说,“连接”这个术语仅适用于面向连接的协议,如 TCP,其保证了两个连接端点之间消息的有序传递。
因此,有两种类型的引导:一种用于客户端(简单地称为 Bootstrap),而另一种(ServerBootstrap)用于服务器。无论我们的应用程序使用哪种协议或者处理哪种类型的数据,唯一决定它使用哪种引导类的是它是作为一个客户端还是作为一个服务器(后面我们会单独开出一篇来写引导,它很重要)。
类别 | Bootstrap | ServerBootstrap |
---|---|---|
网络编程中的作用 | 连接到远程主机和端口 | 绑定到一个本地端口 |
EventLoopGroup 的数目 | 1 | 2 |
ps:实际上,ServerBootstrap 类也可以只使用一个 EventLoopGroup,此时其将在两个场景下共用同一个 EventLoopGroup。
细心的同学应该发现了,ServerBootstrap使用了2个EventLoopGroup,这是因为服务器需要两组不同的 Channel。
- 第一组将只包含一个 ServerChannel,代表服务器自身的已绑定到某个本地端口的正在监听的套接字。(专门用来创建Channel )
- 而第二组将包含所有已创建的用来处理传入客户端连接(对于每个服务器已经接受的连接都有一个)的 Channel。(专门为Channel分配EventLoop)
它们的关系如图:ServerChannel 相关联的 EventLoopGroup 将分配一个负责为传入连接请求创建Channel 的 EventLoop。一旦连接被接受,第二个 EventLoopGroup 就会给它的 Channel分配一个 EventLoop。
关键词:
-
Netty实战(三)
一、Channel、EventLoop和ChannelFuture上一篇博文我们在构建服务端和客户端中出现了一些新的类,可能有些
-
每日看点!意大利新发一起非洲猪瘟疫情
意大利新发一起非洲猪瘟疫情
-
453.44亩!金山这个“城中村”改造项目通过认定!|全球观点
5月25日下午,上海市房管局组织市发展改革、财政、规划资源、农业农村、绿化市容等部门进行联合会审,对亭
-
肯尼亚PVOC认证流程|世界快播
肯尼亚PVOC认证最新流程:根据肯尼亚政府法令,进口商取得肯尼亚PVOC认证方可向肯尼亚海关办理清关手续。检
-
中科信息5月26日快速上涨 全球实时
以下是中科信息在北京时间5月26日09:58分盘口异动快照:5月26日,中科信息盘中快速上涨,5分钟内涨幅超过2%
-
对于标志性成分、卫生学、稳定性试验,需要重做或补做试验项目时,所用受试样品是否必须与原受试样品同批号?
保健食品——共性问题解答:对于标志性成分、卫生学、稳定性试验,需要重做或补做试验项目时,所用受试样品
-
南漳属于哪个省市_南漳属于哪个市|每日动态
1、南漳不属于哪个市,属于湖北省。2、80年代行政机构改革,把南漳拖给襄樊市来托管。3、他们属于托管关系
-
中央气象台:全国大部地区大气扩散条件较好 焦点短讯
西南区域未来一周,区域大部大气扩散和湿清除条件较好,无明显霾天气
-
5月25日基金净值:信澳匠心臻选两年持有期混合最新净值1.0825,涨0.76%
5月25日,信澳匠心臻选两年持有期混合最新单位净值为1 0825元,累计净值为1 0825元,较前一交易日上涨0 76%
-
动态:啤酒有小麦麦芽和大麦麦芽的区别
关于啤酒有小麦麦芽和大麦麦芽的区别的内容,包含大麦芽啤酒和小麦芽啤酒的区别?啤酒中同时使用小麦麦芽和
-
每日短讯:色眼是什么样的眼睛(色眼)
来为大家解答以上的问题。色眼是什么样的眼睛,色眼这个很多人还不知道,现在让我们一起来看看吧!1、要说你
-
美轮美奂是正确的吗(美仑美奂是什么意思)
导读1、原成语应为美轮美奂,“轮”的意思是高大;2、美轮美奂解释:轮:高大;奂:众多。2、原本多形容建
-
观焦点:最初起售价2.98万元,现在指导价7.99万元,新款长安奔奔E-Star上市
日前,长安汽车旗下2023款奔奔E-Star共推出1款多彩款磷酸铁锂版车型,官方指导价位7 99万元,新车上市后,其余
-
世界实时:讲不出再见粤语谐音唱法(讲不出再见粤语谐音)
1、似对似错哦雅吼,八必梭留。2、似运似奈以雅吼,八涩gie-Hill。3、何细更钟优,贝两桑地色优,由层蛮衣怎么
-
猪肺的做法大全爆炒(猪肺的做法大全)_天天视讯
相信目前很多小伙伴对于猪肺的做法大全都比较感兴趣,那么小搜今天在网上也是收集了一些与猪肺的做法大全相
-
山东菏泽医专贴吧_菏泽医专贴吧|天天精选
1、当然山东医专好了录取分数能比菏泽高50分但也不不能说菏泽医专多么差主要是地理问题。本文到此分享完毕
-
世界今日报丨精彩回顾第4弹 | 在均衡派与价值派的巅峰对话里 挖到了这些投资亮点……
投资如美食,没有最好的,只有最合适的。基金经理投资风格,就好像是不同的地方菜系,既有比较清淡的江浙菜
-
播报:凭证装订穿线方法视频(凭证装订穿线方法)
1、要打三个孔。2、一般是把曲别针拉直,然后在一头打个小圈,把钱穿里面,两头要留足够长。3、第一步,把
-
焦点日报:齐鲁政情|济南首发民营经济发展环境评价,高新区何以拔头筹?
近日,济南市召开2023年全市民营经济工作会议,全省首发民营经济发展环境评价。根据评价结果,济南高新区综
-
当前要闻:广博股份:公司将参与宁波跨境电商博览会,通过现场展会情况,洞察行业未来发展趋势,加快品牌出海步伐
广博股份(002103)05月25日在投资者关系平台上答复了投资者关心的问题。