1. Netty 介绍

Netty 是一个异步的、基于事件驱动的网络应用框架,用于快速开发可维护、高性能的网络服务器和客户端。Netty 在 Java 网络应用框架中的地位就好比:Spring 框架在 JavaEE 开发中的地位。

image.png

RocketMQ、ES、Dubbo、ZK

  1. Kafka 自己实现了网络模型做 RPC。底层基于 Java NIO,采用和 Netty 一样的 Reactor 线程模型。
  2. Redis 也是自己的 IO 多路复用和事件分发器,原理类似
MQ-Kafka

1.1. IO

[[IO多路复用]]

1.2. 原生 NIO 存在的问题

  1. NIO 的类库和 API 繁杂,使用麻烦:需要熟练掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等。
  2. 需要具备其他的额外技能:要熟悉 Java 多线程编程,因为 NIO 编程涉及到 Reactor 模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的 NIO 程序。
  3. 开发工作量和难度都非常大:例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理等等。
  4. JDK NIO 的 Bug:例如臭名昭著的 Epoll Bug,它会导致 Selector 空轮询,最终导致 CPU100%。直到 JDK1.7 版本该问题仍旧存在,没有被根本解决。

1.3. Netty 的优势

  • Netty vs NIO,工作量大,bug 多
    • 需要自己构建协议
    • 解决 TCP 传输问题,如粘包、半包
    • epoll 空轮询导致 CPU 100%
    • 对 API 进行增强,使之更易用,如 FastThreadLocal => ThreadLocal,ByteBuf => ByteBuffer
  • Netty vs 其它网络应用框架
    • Mina 由 apache 维护,将来 3.x 版本可能会有较大重构,破坏 API 向下兼容性,Netty 的开发迭代更迅速,API 更简洁、文档更优秀
    • 久经考验,16 年,Netty 版本

2. Epoll

[[框架源码专题-Redis-4、原理进阶-数据类型-单线程多路复用#^tmrkhl]]

3. 三种 Reactor 网络模型⭐️🔴

https://juejin.cn/post/6892687008552976398#heading-20
https://www.cnblogs.com/alimayun/p/12509771.html

3.1. 传统阻塞 I/O 服务模型->每客户端 1 线程

3.1.1. 工作原理图

黄色的框表示对象,蓝色的框表示线程
白色的框表示方法(API)

3.1.2. 模型特点

采用阻塞 IO 模式获取输入的数据
每个连接都需要独立的线程完成数据的输入,业务处理,数据返回

3.1.3. 问题分析

  1. 当并发数很大,就会创建大量的线程,占用很大系统资源
  2. 连接创建后,如果当前线程暂时没有数据可读,该线程会阻塞在 Handler 对象中的 read 操作,导致上面的处理线程资源浪费

image.png

3.2. 单 Reactor 单线程->连接应答和业务处理使用同一个线程

image.png

3.2.1. 方案说明

  1. Select 是前面 I/O 复用模型介绍的标准网络编程 API,可以实现应用程序通过一个阻塞对象监听多路连接请求
  2. Reactor 对象通过 Select 监控客户端请求事件,收到事件后通过 Dispatch 进行分发
  3. 如果是建立连接请求事件,则由 Acceptor 通过 Accept 处理连接请求,然后创建一个 Handler 对象处理连接完成后的后续业务处理
  4. 如果不是建立连接事件,则 Reactor 会分发调用连接对应的 Handler 来响应
  5. Handler 会完成 Read → 业务处理 → Send 的完整业务流程

结合实例:服务器端用一个线程通过多路复用搞定所有的 IO 操作(包括连接,读、写等),编码简单,清晰明了,但是如果客户端连接数量较多,将无法支撑,前面的 NIO 案例就属于这种模型。

3.2.2. 优缺点分析

  1. 优点:模型简单,没有多线程、进程通信、竞争的问题,全部都在一个线程中完成
  2. 缺点:性能问题,只有一个线程,无法完全发挥多核 CPU 的性能。Handler 在处理某个连接上的业务时,整个进程无法处理其他连接事件,很容易导致性能瓶颈
  3. 缺点:可靠性问题,线程意外终止,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障
  4. 使用场景:客户端的数量有限,业务处理非常快速,比如 Redis 6 之前,所有任务都是单线程完成,在业务处理的时间复杂度 O(1) 的情况。但 Redis 中具体的实现逻辑不一样,Redis 分为 3 个处理器:连接应答处理器、命令回复处理器、命令请求处理器。

image.png

3.3. 单 Reactor 多线程

image.png

3.3.1. 方案说明

  1. Reactor 对象通过 Select 监控客户端请求事件,收到事件后,通过 Dispatch 进行分发
  2. 如果是建立连接请求,则由 Acceptor 通过 accept 处理连接请求,然后创建一个 Handler 对象处理完成连接后的各种事件
  3. 如果不是连接请求,则由 Reactor 分发调用连接对应的 handler 来处理(也就是说连接已经建立,后续客户端再来请求,那基本就是数据请求了,直接调用之前为这个连接创建好的 handler 来处理)
  4. handler 只负责响应事件,不做具体的业务处理(这样不会使 handler 阻塞太久),通过 read 读取数据后,会分发给后面的 worker 线程池的某个线程处理业务。【业务处理是最费时的,所以将业务处理交给线程池去执行】
  5. worker 线程池会分配独立线程完成真正的业务,并将结果返回给 handler
  6. handler 收到响应后,通过 send 将结果返回给 client

3.3.2. 优缺点分析

优点:可以充分利用多核 CPU 的处理能力。
缺点:多线程数据共享和访问比较复杂;Reactor 承担所有事件的监听和响应,在单线程中运行,高并发场景下容易成为性能瓶颈。

3.4. 主从 Reactor 多线程⭐️🔴

image.png

针对单 Reactor 多线程模型中,Reactor 在单线程中运行,高并发场景下容易成为性能瓶颈,可以让 Reactor 在多线程中运行。

3.4.1. 方案说明

SubReactor 是可以有多个的,如果只有一个 SubReactor 的话那和单 Reactor 多线程就没什么区别了。

  1. Reactor 主线程 MainReactor 对象通过 select 监听连接事件,收到事件后,通过 Acceptor 处理连接事件
  2. 当 Acceptor 处理连接事件后,MainReactor 将连接分配给 SubReactor
  3. subreactor 将连接加入到连接队列进行监听,并创建 handler 进行各种事件处理
  4. 当有新事件发生时,subreactor 就会调用对应的 handler 处理
  5. handler 通过 read 读取数据,分发给后面的 worker 线程处理
  6. worker 线程池分配独立的 worker 线程进行业务处理,并返回结果
  7. handler 收到响应的结果后,再通过 send 将结果返回给 client
  8. Reactor 主线程可以对应多个 Reactor 子线程,即 MainRecator 可以关联多个 SubReactor

3.4.2. 优缺点分析

优点:父线程与子线程的数据交互简单职责明确,父线程只需要接收新连接,子线程完成后续的业务处理。

父线程与子线程的数据交互简单,Reactor 主线程只需要把新连接传给子线程,子线程无需返回数据。

这种模型在许多项目中广泛使用,包括 Nginx 主从 Reactor 多进程模型,Memcached 主从多线程,Netty 主从多线程模型的支持。

3.5. Reactor 模式小结

3.5.1. 3 种模式用生活案例来理解

单 Reactor 单线程,前台接待员和服务员是同一个人,全程为顾客服务
单 Reactor 多线程,1 个前台接待员,多个服务员,接待员只负责接待
主从 Reactor 多线程,多个前台接待员,多个服务生

3.5.2. Reactor 模式具有如下的优点

响应快,不必为单个同步时间所阻塞,虽然 Reactor 本身依然是同步的(比如你第一个 SubReactor 阻塞了,我可以调下一个 SubReactor 为客户端服务)
可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销
扩展性好,可以方便的通过增加 Reactor 实例个数来充分利用 CPU 资源
复用性好,Reactor 模型本身与具体事件处理逻辑无关,具有很高的复用性

3.5.3. 注意点

注意:前面介绍的四种 reactor 模式在具体实现时遵循的原则是:每个文件描述符只由一个线程操作。这样可以轻轻松松解决消息收发的顺序性问题,也避免了关闭文件描述符的各种 race condition。一个线程可以操作多个文件描述符,但是一个线程不能操作别的线程拥有的文件描述符。这一点不难做到。epoll 也遵循了相同的原则。Linux 文档中并没有说明,当一个线程证阻塞在 epoll_wait 时,另一个线程往 epoll fd 添加一个新的监控 fd 会发生什么。新 fd 上的事件会不会在此次 epoll_wait 调用中返回?为了稳妥起见,我们应该吧对同一个 epoll fd 的操作 (添加、删除、修改等等) 都放到同一个线程中执行。

3.5.4. 与 Epoll 关系

reactor 就是对 epoll 进行封装,进行网络 IO 与业务的解耦,将 epoll 管理 IO 变成管理事件,整个程序由事件进行驱动执行。就像下图一样,有就绪事件返回,reactor:由事件驱动执行对应的回调函数;epoll:需要自己判断。

4. Netty 的网络模型⭐️🔴

%%
▶3.🏡⭐️◼️【🌈费曼无敌🌈⭐️第一步⭐️】◼️⭐️-point-20230402-1010%%
❕ ^9ze94j

image.png

  1. Netty 抽象出两组线程池,BossGroup 专门负责接收客户端的连接,WorkerGroup 专门负责网络的读写
  2. BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup
  3. NioEventLoopGroup 相当于一个事件循环组,这个组中含有多个事件循环,每一个事件循环是 NioEventLoop
  4. NioEventLoop 表示一个不断循环的执行处理任务的线程,每个 NioEventLoop 都有一个 Selector,用于监听绑定在它上面的 socket 的网络通讯
  5. NioEventLoopGroup 可以有多个线程,即可以含有多个 NioEventLoop
  6. 每个 BossGroup 下面的 NioEventLoop 循环执行的步骤有 3 步
    1. 轮询 accept 事件
    2. 处理 accept 事件,与 client 建立连接,生成 NioScocketChannel,并将其注册到 workerGroup 的某个 NioEventLoop 上的 Selector 上面
    3. 继续处理任务队列的任务,即 runAllTasks
  7. 每个 WorkerGroup NioEventLoop 循环执行的步骤
    1. 轮询 read,write 事件
    2. 处理 I/O 事件,即 read,write 事件,在对应 NioScocketChannel 处理
    3. 处理任务队列的任务,即 runAllTasks
  8. 每个 Worker NioEventLoop 处理业务时,会使用 pipeline(管道),pipeline 中包含了 channel(通道),即通过 pipeline 可以获取到对应通道,管道中维护了很多的处理器。

https://blog.csdn.net/Youth_lql/article/details/115734142

5. Netty 的逻辑架构⭐️🔴

Netty 的逻辑处理架构是典型的网络分层架构设计的,分别为网络通信层、事件调度层和服务编排层。每一层各司其职,如下图所示:

image.png

5.1. 网络通信层

网络通信层的职责就是执行网络 I/O 的操作。它支持多种网络协议和 I/O 模型的连接操作。当网络数据读取到内核缓冲区后,会触发各种网络事件,这些网络事件会分发给事件调度层来处理。网络通信层有三个核心组件:Bootstrap、ServerBootstrap 和 Channel

image.png

5.1.1. Bootstrap

意思是引导,一个 Netty 应用通常由一个 Bootstrap 开始,主要作用是配置整个 Netty 程序,串联各个组件,Netty 中 Bootstrap 类是客户端程序的启动引导类,用于连接服务端的,一般用于 Client 的开发上

5.1.2. ServerBootstrap

是服务端启动引导类,用于服务端启动绑定本地端口,一般绑定两个: EventLoopGroup(主从多线程 Reactor),一个称为 boss,另外一个成为 worker。

Bootstrap/ServerBootstrap 组件更加方便我们配置和启动 Netty 程序,它是整个 Netty 程序的入口,串联了 Netty 所有核心组件的初始化工作。

5.1.3. channel

Netty 网络通信的组件,能够用于执行网络 I/O 操作。它是 Netty 网络通信的载体。Channel 提供了基本的 API 用于网络 I/O 操作,例如 register 注册事件、bind 绑定事件、connect 连接事件、read 读事件、write 写事件、flush 数据冲刷等。

Netty 中的 Channel 是基于 JDK NIO 的 Channel 上做了更高层次的抽象,同时屏蔽了底层 Socket 的复杂性,增强了 Channel 的功能,在使用 Netty 时不需要再直接跟 JDK Socket 打交道。

image.png

5.2. 事件调度层

事件调度层通过 Reactor 线程模型对各类事件进行聚合处理,通过 Selector 主循环线程集成多种事件 (I/O 事件、信号事件、定时事件等),实际的业务处理逻辑交给服务编排层的 ChannelHandler 来处理。

image.png

事件调度层最核心的组件就是 EventLoop 和 EventLoopGroup:EventLoopGroup 实质上是 Netty 基于 JDK 线程池的抽象,本质就是线程池,主要负责接收 I/O 请求,并分配线程执行处理。而 EventLoop 可以理解成一个线程,EventLoop 创建出来之后会跟一个线程绑定并且处理 Channel 中的事件。如下图所示:

image.png

5.3. 服务编排层

服务编排的职责是组装各类服务,它是 Netty 的核心处理链,用以实现网络事件的动态编排和有序传播。

服务编排的核心组件有:ChannelPipeline、ChannelHandler、ChannelHandlerContext。

5.3.1. ChannelPipeline

ChannelPipeline 是 Netty 的核心编排组件,负责组装各类的 ChannelHandler
ChannelPipeline 本质是一个双向链表,通过责任链模式将不同的 ChannelHandler 链接在一起。
每个 Channel 有且仅有一个 ChannelPipeline 与之对应。
当 I/O 读写事件触发时,ChannelPipeline 会一次调用 ChannelHandler 列表对 Channel 的数据进行拦截和处理。

image.png

image.png

5.3.2. ChannelHandler

实际数据的编解码以及加工处理的操作是由 ChannelHandler 来完成的

5.3.3. ChannelHandlerContext

每创建一个 Channel 都会绑定一个新的 ChannelPipeline,ChannelPipeline 中每加入一个 ChannelHandler 都会绑定一个 ChannelHandlerContext。由此可见,ChannelPipeline、ChannelHandlerContext、ChannelHandler 三个组件的关系是密切相关的。

ChannelHandlerContext 用于保存 ChannelHandler 上下文,通过 ChannelHandlerContext 我们可以知道 ChannelPipeline 和 ChannelHandler 的关联关系。

6. Netty 特性

6.1. 零拷贝

并发进阶-1、零拷贝

6.2. bytebuf

6.2.1. ByteBuf 优势

  • 池化 - 可以重用池中 ByteBuf 实例,更节约内存,减少内存溢出的可能
  • 读写指针分离,不需要像 ByteBuffer 一样切换读写模式
  • 可以自动扩容
  • 支持链式调用,使用更流畅
  • 很多地方体现零拷贝,例如 slice、duplicate、CompositeByteBuf

6.3. 高性能无锁队列 (Mpsc)

22 技巧篇:高性能无锁队列 Mpsc Queue

队列相关:并发编程专题-基础-19、Queue

Mpsc 的全称是 Multi Producer Single Consumer,多生产者单消费者。Mpsc Queue 可以保证多个生产者同时访问队列是线程安全的,而且同一时刻只允许一个消费者从队列中读取数据。Netty Reactor 线程中任务队列 taskQueue 必须满足多个生产者可以同时提交任务,所以 JCTools 提供的 Mpsc Queue 非常适合 Netty Reactor 线程模型。
MpscArrayQueue 还只是 Jctools 中的冰山一角,其中蕴藏着丰富的技术细节,我们对 MpscArrayQueue 的知识点做一个简单的总结。

  • 通过大量填充 long 类型变量解决伪共享问题。
  • 环形数组的容量设置为 2 的次幂,可以通过位运算快速定位到数组对应下标。
  • 入队 offer() 操作中 producerLimit 的巧妙设计,大幅度减少了主动获取消费者索引 consumerIndex 的次数,性能提升显著。
  • 入队和出队操作中都大量使用了 UNSAFE 系列方法,针对生产者和消费者的场景不同,使用的 UNSAFE 方法也是不一样的。Jctools 在底层操作的运用上也是有的放矢,把性能发挥到极致。

6.4. 内存池

13 举一反三:Netty 高性能内存管理设计(上)
https://miaowenting.site/2020/02/09/Netty%E5%86%85%E5%AD%98%E6%B1%A0%E5%8C%96%E7%AE%A1%E7%90%86/

6.5. 解决黏包 (粘包) 半包

6.5.1. 黏包 (粘包)

现象,发送 abc def,接收 abcdef
原因
1. 应用层:接收方 ByteBuf 设置太大(Netty 默认 1024)
2. 滑动窗口:假设发送方 256 bytes 表示一个完整报文,但由于接收方处理不及时且窗口大小足够大,这 256 bytes 字节就会缓冲在接收方的滑动窗口中,当滑动窗口中缓冲了多个报文就会粘包
3. Nagle 算法:会造成粘包

6.5.2. 半包

  • 现象,发送 abcdef,接收 abc def
  • 原因
    • 应用层:接收方 ByteBuf 小于实际发送数据量
    • 滑动窗口:假设接收方的窗口只剩了 128 bytes,发送方的报文大小是 256 bytes,这时放不下了,只能先发送前 128 bytes,等待 ack 后才能发送剩余部分,这就造成了半包
    • MSS 限制:当发送的数据超过 MSS 限制后,会将数据切分发送,就会造成半包

本质是因为 TCP 是流式协议,消息无边界

7. 面试题

请说一下Netty中Reactor模式的理解

Xnip2023-06-13_14-15-17.png

[[BIO,NIO,AIO,Netty面试题 35道.pdf]]

[[3.358道.pdf]]

https://www.bilibili.com/video/BV1hG411G7MN/?spm_id_from=..search-card.all.click&vd_source=c5b2d0d7bc377c0c35dbc251d95cf204

https://www.bilibili.com/video/BV1sN411F7C8/?spm_id_from=..search-card.all.click&vd_source=c5b2d0d7bc377c0c35dbc251d95cf204

8. 实战经验

9. 参考与感谢

%%
▶2.🏡⭐️◼️【🌈费曼无敌🌈⭐️第一步⭐️】◼️⭐️-point-20230528-0902%%
❕ ^hefe8a

9.1. 黑马

9.1.1. 视频

https://www.bilibili.com/video/BV1py4y1E7oA/?spm_id_from=333.337.search-card.all.click&vd_source=c5b2d0d7bc377c0c35dbc251d95cf204

9.1.2. 资料

1
/Users/taylor/Nutstore Files/Obsidian_data/pages/002-schdule/001-Arch/001-Subject/003-并发编程专题/000-Netty/黑马-Netty教程源码资料

9.2. 尚硅谷 Netty 视频教程(B 站超火,好评如潮)

https://www.bilibili.com/video/BV1DJ411m7NR?p=4&vd_source=c5b2d0d7bc377c0c35dbc251d95cf204

9.3. 网络笔记

Netty 架构: https://www.maishuren.top/archives/netty-zheng-ti-jia-gou-jie-xi

尚硅谷学习笔记: https://blog.csdn.net/Youth_lql/article/details/115734142