Netty 入门 — 要想掌握 Netty,你必须知道它的这些核心组件

在上篇文章(Netty 入门 — 亘古不变的Hello World)中,我们简单认识了开发一个 Netty 服务端和客户端代码的主要步骤了,在这几大步骤中我们基本上可以看出 Netty 的几个核心组件。

在真正进入 Netty 的学习之前,我们非常有必要先对这些组件进行一个整体的认识,对于 Netty 入门阶段的讲解,大明哥采用整体 —> 分解 —> 总结的模式来阐述。对于一头牛,我们需要先知道这是一头牛,了解这头牛有哪些组织,然后再把这些组织一个一个地拆开来认识,清楚里面每一个组织的功能,最后再将这些组织组合成一头牛,是不是就会清晰很多。

Bootstrap:引导器

Bootstrap 的意思是引导,一个 Netty 应用通常是从 Bootstrap 开始的,所以在 hello world 的示例中,我第一步就是 new 一个 Bootstrap 对象。

Bootstrap 的作用就是配置整个 Netty 的组件,把这些组件构建成一个可运行的整体。

在 Netty 中,Bootstrap 有两个:

  1. Bootstrap:用于引导客户端
  2. ServerBootstrap:用于引导服务端

这里我们组装了第一个组件:

ByteBuf:数据传输的载体

几乎可以说所有的网络通信底层都是基于字节流来传输的,然而在实际应用中我们不可能直接使用底层的 byte 来进行数据交互,因为实在是太不方便了。基于实际应用,我们要求数据传输所使用的数据接口除了效率高外还需要使用方便。

所有的网络通信数据都有一个载体,Netty 的数据传输载体则是 ByteBuf,它提供了一个底层 Byte 的抽象视图

有小伙伴就说 Netty 是基于 NIO,在 JDK 原生的 NIO 中就有一个 ByteBuffer,使用起来也挺方便的,为什么不用它呢?因为原生的 ByteBuffer 有几个缺点:

  1. ByteBuffer 长度是固定的,无法动态扩展和收缩。
  2. ByteBuffer 只有一个标识位置的索引,读写的时候我们需要主动调用 flip() 进行模式切换,使用起来不方便又容易出错。
  3. ByteBuffer API 不够丰富,无法满足我们日常的开发需求。

而 Netty 提供的 ByteBuf 则很好地解决了这些问题,比原生的 ByteBuffer 更加灵活、高效。

ByteBuf 有几个非常重要的属性:

  • capacity:容量
  • readerIndex:读索引位置
  • writeIndex:写索引位置

由于有了两个索引,一个用于读,一个用于写,所以在使用 ByteBuf 的时候就不需要进行读写模式的切换了。readerIndex 和 writeIndex 的初始值都是 0,如下:

+-------------------------------+
|       writable bytes          |
+-------------------------------+
|                               |
0=readerIndex=writerIndex       capacity

当我们往 ByteBuf 中写入 N 个字节后:

+------------------+-------------------+
|  readable bytes  |    writable bytes |
+------------------+-------------------+
|                  |                   |
0=readerIndex      N=writerIndex       capacity

然后在读取 M 个字节,注意,这里 M 是不可能大于 N 的:

+-------------------+------------------+------------------+
| discardable bytes |  readable bytes  |  writable bytes  |
+-------------------+------------------+------------------+
|                   |                  |                  |
0               M=readerIndex    N=writerIndex       capacity

整体来说 ByteBuf 的工作机制还是非常好理解的:readerIndex 和 writeIndex 的初始值为 0 ,当我们写入 N 个字节时,writeIndex + N,当从 ByteBuf 读取 M 个字节时,readerIndex + M。

至此,我们的版图再增加一个 :ByteBuf。

Channel:数据传输的通道

有了 ByteBuf 后,我们就有了数据的源头,但是没有通道传输啊,所以 Netty 有提供了另外一个组件:Channel。

Channel 是 Netty 的核心概念之一,是 Netty 网络 IO 操作的抽象,即 Netty 网络通信的主体,由它来负责对端进行网络通信、注册、数据操作等一切 IO 相关的操作

其主要功能包括:

  1. 网络 IO 的读写
  2. 客户端发起连接
  3. 关闭连接
  4. 网络连接的相关参数
  5. 绑定端口
  6. Netty 框架相关操作,如获取 Channel 相关联的 EventLoop、pipeline 等。

Channel 提供的 API 非常丰富,主要包括下面几类:

  • 类 getter API:主要用于获取 Channel 相关的属性,如绑定地址,相关配置等等。
  • Future 相关 API:Channel 所有的操作都是异步的,使用 Channel 的时候并不能立刻知道操作的结果,所以Netty 在完成 IO 调用后会返回一个 Future 对象,该对象就是 Channel 异步 IO 的结果。
  • 判断状态 API:Channel 有四种状态,分别是 openregisteractiveclose,Channel 提供了相对应的方法来判断当前 Channel 处于哪种状态,毕竟一些操作是需要在特定的状态下才能进行的。
  • 事件触发类方法:触发 IO 事件的方法。

到这里我们版图再次增加 Netty 的通信通道:Channel。

ChannelHandler:数据加工厂

有了 Channel 后,数据就能够流通了,那么下一步我们就需要对数据进行加工了,这个对数据加工的就是 ChannelHandler。ChannelHandler 是我们在 Netty 开发过程中打交道最多的组件,我们的所有业务逻辑几乎都写在 ChannelHandler 里面。

ChannelHandler 充当了所有处理入站和出站数据的应用程序逻辑的容器,其家族如下:

从 ChannelHandler 的类图可以看出,它有两类子接口:

  • ChannelInboundHandler:处理入站事件和数据
  • ChannelOutboundHandler:处理出站事件和数据

在实际开发过程中,我们一般都不直接使用这两个子接口,而是使用他们的适配器:

  • ChannelInboundHandlerAdapter:处理入站 I/O 事件
  • ChannelOutboundHandlerAdapter:处理出站 I/O 事件

我们只需要继承这两个对应的适配器,重写自己感兴趣的方法就可以了,如下:

public class ChannelHandlerTest_1 extends ChannelInboundHandlerAdapter {@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {// do something}
}

方法有很多,在后面的文章中有详细介绍,这里就不展开阐述了。

至此,Netty 的版图多了一个 Netty 数据加工者:ChannelHandler

ChannelPipeline:服务编排器

ChannelHandler 是我们开发业务逻辑的主战场,但是我们的业务逻辑并不是很简单的,几乎都需要有多个 ChannelHandler 来相互配合完成一个业务逻辑,比如我们用户注册,就可以分为如下几个步骤:数据校验 —> 保存用户 —> 通知用户。难道这三个步骤我们都写在一个 ChannelHandler 里面吗?虽然也可以,但是整个逻辑都冗余在一起了,后期怎么维护,所以我们需要将这三个 ChannelHandler 拆分为 3 个,他们三个相互配合完成用户注册。那怎么配合呢?对不起,ChannelHandler 并不知道,他们只知道干活。这个时候就需要引入另外一个组件了:ChannelPipeline,handler 编排者。它来负责编排多个 ChannelHandler ,让他们相互配合完成一个完整的业务。例如注册,它会将 数据校验 —> 保存用户 —> 通知用户 三个 ChannelHandler 按照顺序编排在一起,一起来完成注册这个业务逻辑。

ChannelPipeline 我们可以看做是 ChannelHandler 的载体,它是由一组ChannelHandler 组成的,ChannelPipeline 采用责任链的方式将这些 ChannelHandler 组合成一个双向链表。当有 I/O 读写事件触发时,ChannelPipeline 会依次调用 ChannelHandler 来进行处理,事件是从头到尾处理的。

但是实际上 ChannelPipeline 并不是直接维护 ChannelHandler 的关系的,而是通过 ChannelHandlerContext 来维护的,为什么会多加这一层呢?因为在事件传播的过程中我们需要利用 ChannelHandlerContext 来保存上下文,如果没有这层的话,ChannelHandler 除了要处理自身的业务逻辑,还要兼顾维护前后置的关系,以及上下文的处理,这样就会显得整个 ChannelHandler 不够纯粹,不是很优雅。

加入服务编排器 ChannelPipeline 后,Netty 的版图如下:

EventLoop:I/O 事件核心处理引擎

有了数据,有了数据传输的通道,也有数据加工器和编排器,一个简单的网络 I/O 框架基本上就完成了,客户端向服务端发送 I/O 请求,经过 Channel 传输,经由 ChannelPipeline 编排的 ChannelHandler 进行业务逻辑处理,一条完美的流程就出来了。但是真的只有如此吗?我们认真考虑几个问题:

  1. 一个 I/O 请求就开一条线程处理吗?使用线程池能解决问题吗?
  2. Channel 与线程的关系如何?
  3. 没有空闲 CPU了,I/O 请求就傻傻地在那等着吗?
  4. 这条简单的数据加工线如何应对高并发?
  5. 有多线程就有线程安全问题,如何来保证线程安全?
  6. 等等

我想这条简单数据加工线并不能很好地解决上面的问题,就单单一个线程安全我估计就得处理疯吧。所以如果 Netty 的线程模型仅仅只是如何,那 Netty 也称不上一个优秀的网络 I/O 框架。而恰恰 Netty最精华的地方就在这里:基于 Reactor 模型的线程模型

Netty 的线程模型是基于 Reactor 线程模型,即 I/O 多路复用, 而 EventLoop 是 Netty Reactor 线程模型的核心处理引擎,**是 Netty 中最最核心的组件,也是 Netty 最精华的部分,它负责 Netty 中 I/O 事件的分发,也就是说,这个事件谁来做,它说了算。**Netty 为什么能处理成千上万客户端的连接,奥秘就在于这个地方。

一般来说我们都不是直接使用 EventLoop,而是通过 EventLoopGroup 提供的 API 来获取一个 EventLoop,EventLoopGroup 它是一组 EventLoop,它主要是来维护和管理 EventLoop。

Netty 推荐采用主从多线程模型,其中 BossEventLoopGroup 负责 ServerSocketChannel 的 Accept 事件,WorkerEventLoopGroup 负责 I/O 的读写事件。

加入 EventLoop 和 EventLoopGroup 后,Netty 版图如下:

总结

服务端组件执行流程

这里对 Netty 服务端整个流程做一个说明,阐述 Netty 服务端的工作流程,这样我们就清楚知道各个组件在 Netty 中扮演的是什么角色。

  1. 服务端在启动时,绑定本地端口,会初始化两个 EventLoopGroup,一个 BossEventLoopGroup 和 WorkEventLoopGroup ,其中 BossEventLoopGroup 专门负责接受客户端的连接(Accept 事件),WorkEventLoopGroup 专门负责网络读写。
  2. 当客户端连接服务端时,BossEventLoopGroup 响应请求,它会该客户端创建一个 Channel,该 Channel 会调用 EventLoopGroup 的 register() 方法,BossEventLoopGroup 会将该 Channel 与 WorkEventLoopGroup 中的某个 EventLoop 进行绑定,这种绑定关系是永久的,即在该 Channel 整个生命周期内的所有 I/O 读写事件都由该 EventLoop 来处理。
  3. 在创建 Channel 的时候,会创建一个 ChannelPipeline ,并将 Channel 与该 ChannelPipeline 绑定起来,ChannelPipeline 里面会构建一条完整的 ChannelHandler 处理链。
  4. 当有 I/O 读写事件发生时,则由 EventLoop 绑定的线程来执行,当然执行的主体还是 ChannelHandler。

服务端各组件的关系

大明哥这里再来说说各个组件之间的关系。

  1. 一个客户端对应一个 Channel。
  2. 一个 Channel 与一个 EventLoop 绑定,且这种绑定关系是永久的,在 Channel 的整个生命周期内的所有 I/O 读写事件都由该 EventLoop 处理。
  3. 一个 EventLoop 只与一个 Thread 线程进行绑定,该 EventLoop 的所有事件都由该 Thread 处理,所以 EventLoop 是一个单线程执行器,也就不会存在线程安全问题了。
  4. 一个 EventLoop 可以绑定多个 Channel,由于 EventLoop 的单线程执行器,所以如果单个 EventLoop 处理的 Channel I/O 读写事件较多则需要进行资源竞争了。
  5. 一个 Channel 与一个 ChannelPipeline 绑定,ChannelPipeline 里面包含了多个 ChannelHandler。

关系图如下:

到这里,整个 Netty 的核心组件就已经介绍完毕了,大明哥相信你们已经对整个 Netty 框架全貌有了一个初步的了解,下一步我们就来把这个 6 个组件拆分开来详细介绍。

注:Netty 最重要的就是这 6 个核心组件,大明哥将会花 30+ 篇文章来彻底讲清楚这 6 个核心组件的方方面面,分为入门篇和进阶篇。
入门篇:讲解基本概念及核心 API 的使用方法
进阶篇:深入分析核心概念,高阶功能以及用法

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/139347.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

docker-compose安装

如果提示docker-compose未找到命令,需要安装一下 curl -L https://github.com/docker/compose/releases/download/1.23.0-rc2/run.sh > /usr/local/bin/docker-compose 修改权限 chmod x /usr/local/bin/docker-compose 建立软连接 ln -s /usr/local/bin/do…

如何利用BIGEMAP软件查看历史影像

工具 Bigemap gis office地图软件 BIGEMAP GIS Office-全能版 Bigemap APP_卫星地图APP_高清卫星地图APP 很多人都在寻找历史影像图,这块的需求是非常大,历史影像一般可以用于历史地貌的变迁分析,还原以前的生态场景,对范围面积…

微信小程序获取手机号和openid

小程序通过wx.login组件会返回一个code,这个code用来获得用户的openid。 小程序写法为: wx.login({success (res) {if (res.code) {//发起网络请求wx.request({url: https://example.com/onLogin,// 后台给的请求地址data: {code: res.code}})} else {…

2023年中国熔盐储能装机量、新增装机量及行业投资规模分析[图]

熔盐储能是一种可以传递能量、长时间(6-8h)、大容量储能的技术路径,作为传热介质可以实现太阳能到热能的转换,作为储能介质可以实现将热能和电能的双向转换,可以很好的适应和解决以上两大矛盾。因此,熔盐储…

Linux上Docker的安装以及作为非运维人员应当掌握哪些Docker命令

目录 前言 1、安装步骤 2、理解镜像和容器究竟是什么意思 2.1、为什么我们要知道什么是镜像,什么是容器? 2.2、什么是镜像? 2.3、什么是容器? 2.4、Docker在做什么? 2.5、什么是镜像仓库? 2、Dock…

Mobpush智能化精准推送,助力求职者快人一步

近日,“BOSS”直聘崩了的消息又又又上了热搜,2023年9月15日上午,BOSS直聘在线统计超过4700万人。由此可见,随着金九银十招聘旺季的到来,求职软件成为人们的青睐。但是对于大多数使用招聘软件的用户而言,往往…

2023_Spark_实验十六:编写LoggerLevel方法及getLocalSparkSession方法

一、搭建Spark项目结构 在SparkProject模块的pom.xml文件中增加一下依赖&#xff0c;并等待依赖包下载完毕&#xff0c;如上图。 ​<!-- Spark及Scala的版本号 --><properties><scala.version>2.11</scala.version><spark.version>2.1.1</sp…

GPT绘制流程图咒语

【咒语】下面是我的一篇论文选取部分&#xff0c;为了让读者更好理解&#xff0c;我准备画一张图&#xff0c;请你阅读后为我设计一下这个图应该怎么画&#xff0c;更有说服力&#xff0c;更容易理解 论文片段&#xff1a; 多模态数据融合研究的基础在于有效的数据采集。首先&a…

进阶JAVA篇-如何理解作为参数使用的匿名内部类与 Arrays 类的常用API(九)

目录 目录 API 1.0 Arrays 类的说明 1.1 Arrays 类中的 toString() 静态方法 1.2 Arrays 类中的 copyOfRange(int[] original, int from, int to) 静态方法 1.3 Arrays 类中的 copyOf(int[] original, int newLength) 静态方法 1.4 Arrays 类中的 setAll(do…

旧手机热点机改造成服务器方案

如果你也跟我一样有这种想法, 那真的太酷了!!! ok,前提是得有root,不然体验大打折扣 目录 目录 1.做一个能爬墙能走百度直连的热点机(做热点机用) 2.做emby视频服务器 3.做文件服务, 存取文件 4.装青龙面板,跑一些定时任务 5.做远程摄像头监控 6.做web服务器 7.内网穿…

【Linux】文件IO基础知识——下篇

目录 一&#xff0c;stderr 2. errno全局变量 二&#xff0c;文件系统 1. 软链接 2. 硬链接 三&#xff0c;静态库 1. 制作静态库 2. 自动化生成静态库 & 自动发布库与头文件 3. 如何使用第三方库 法&#xff08;一&#xff09;&#xff1a;修改系统文件库 …

【milkv】更新rndis驱动

问题 由于windows升级到了11&#xff0c;导致rndis驱动无法识别到。 解决 打开设备管理器&#xff0c;查看网络适配器&#xff0c;没有更新会显示黄色的图标。 右击选择更新驱动