Netty

学习本章需要先知道IO多路复用,不清楚的请移步:IO多路复用

网络通信中,阻塞IO两大阻塞的地方:socket链接阻塞,等待读取文件阻塞。 本地文件io就只有一个等待文件阻塞

一.Reactor模型(Netty线程模型)

说Netty之前先说一下高性能网络模式Reactor。由于NIO是面向过程编写,效率太低。大佬们基于面向对象的思想,对 I/O 多路复用作了一层封装,让使用者不用考虑底层网络 API 的细节,只需要关注应用代码的编写。大佬们还为这种模式取了个让人第一时间难以理解的名字:Reactor 模式。意思对事件的反应,来了一个事件(连接请求,读写请求),Reactor 就有相对应的反应/响应。

Reactor有3种经典方案:


 单Reactor单线程:

Netty

  1. Reactor 对象通过 select (IO 多路复用接口) 监听事件,收到事件后通过 dispatch 进行分发,具体分发给 Acceptor 对象还是 Handler 对象,还要看收到的事件类型;
  2. 如果是连接建立的事件,则交由 Acceptor 对象进行处理,Acceptor 对象会通过 accept 方法 获取连接,并创建一个 Handler 对象来处理后续的响应事件;
  3. 如果不是连接建立事件, 则交由当前连接对应的 Handler 对象来进行响应;
  4. Handler 对象通过 read -> 业务处理 -> send 的流程来完成完整的业务流程。

 缺点:

  • 因为只有一个进程,无法充分利用 多核 CPU 的性能;
  • Handler 对象在业务处理时,整个进程是无法处理其他连接的事件的,如果业务处理耗时比较长,那么就造成响应的延迟;

适用场景:单 Reactor 单进程的方案不适用计算机密集型的场景,只适用于业务处理非常快速的场景。


 单Reactor多线程:

Netty

前面的三个步骤和单 Reactor 单线程方案是一样的,我们只看不一样的部分:

  1. Handler 对象不再负责业务处理,只负责数据的接收和发送,Handler 对象通过 read 读取到数据后,会将数据发给线程池中的线程进行业务处理;
  2. 处理完后,将结果发给主线程中的 Handler 对象,接着由 Handler 通过 send 方法将响应结果发送给 client;

 缺点:

  • 虽然充分了利用了多核CPU,但是会涉及共享资源的竞争。
  • 因为一个 Reactor 对象承担所有事件的监听和响应,而且只在主线程中运行,在面对瞬间高并发的场景时,容易成为性能的瓶颈的地方。

多Reactor多线程(主从Reactor多线程):

Netty

和单Reactor多线程唯一不同的是,将连接请求和读写请求分别发给不同的Reactor进行处理。该模式应用于Netty,Memcache等项目中


 那么我们看看Netty到底是什么样的模型:

Netty

  • Netty将主Reactor和从Reactor抽象成俩个组:Boss Group专门负责连接请求,Worker Group专门负责读写请求。
  • 每一个组包含多个NioEventLoopGroup,这是一个事件循环组,这个组内有多个事件循环(NioEventLoop),每一个NioEventLoop都有一个selector,用于监听网络的连接,读写请求。
  • 每一个连接请求被Boss Group的selector监测到,都会交给Worker Group中的一个事件循环组,由Worker Group的selector监测是否有读写操作。
  • 最后交给Pipeline的handler进行业务处理

具体细节下面详细说明!

二.Netty概述

 Netty 是什么?

  • Netty 是一个 基于 NIO 的 client-server(客户端服务器)框架,使用它可以快速简单地开发网络应用程序。
  • 它极大地简化并优化了 TCP 和 UDP 套接字服务器等网络编程,并且性能以及安全性等很多方面甚至都要更好。
  • 支持多种协议 如 FTP,SMTP,HTTP 以及各种二进制和基于文本的传统协议。

为什么要用 Netty?因为 Netty 具有下面这些优点,并且相比于直接使用 JDK 自带的 NIO 相关的 API 来说更加易用。

  • 统一的 API,支持多种传输类型,阻塞和非阻塞的。
  • 简单而强大的线程模型。
  • 自带编解码器解决 TCP 粘包/拆包问题。
  • 自带各种协议栈。
  • 真正的无连接数据包套接字支持。
  • 比直接使用 Java 核心 API 有更高的吞吐量、更低的延迟、更低的资源消耗和更少的内存复制。
  • 安全性不错,有完整的 SSL/TLS 以及 StartTLS 支持。
  • 社区活跃
  • 成熟稳定,经历了大型项目的使用和考验,而且很多开源项目都使用到了 Netty, 比如我们经常接触的 Dubbo、RocketMQ 等等。
  • ......

Netty 应用场景了解么?理论上来说,NIO 可以做的事情 ,使用 Netty 都可以做并且更好。Netty 主要用来做网络通信 :

  • 作为 RPC 框架的网络通信工具 
  • 实现一个自己的 HTTP 服务器:说到 HTTP 服务器的话,作为 Java 后端开发,我们一般使用 Tomcat 比较多。一个最基本的 HTTP 服务器可要以处理常见的 HTTP Method 的请求,比如 POST 请求、GET 请求等等。
  • 实现一个即时通讯系统 :使用 Netty 我们可以实现一个可以聊天类似微信的即时通讯系统,这方面的开源项目还蛮多的,可以自行去 Github 找一找。
  • 实现消息推送系统 :市面上有很多消息推送系统都是基于 Netty 来做的。
  • ...
三.Netty 核心组件有哪些?分别有什么作用?

Bootstrap,ServerBootstrap

引导,一个Netty通常又一个Bootstrap开始,主要作用是配置整个Netty程序,串联各个组件。

Bootstrap 是客户端的启动引导类/辅助类,具体使用方法如下:

 EventLoopGroup group = new NioEventLoopGroup();
        try {
            //创建客户端启动引导/辅助类:Bootstrap
            Bootstrap b = new Bootstrap();
            //指定线程模型
            b.group(group).
                    ......
            // 尝试建立连接
            ChannelFuture f = b.connect(host, port).sync();
            f.channel().closeFuture().sync();
        } finally {
            // 优雅关闭相关线程组资源
            group.shutdownGracefully();

ServerBootstrap 客户端的启动引导类/辅助类,具体使用方法如下:

 // 1.bossGroup 用于接收连接,workerGroup 用于具体的处理
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            //2.创建服务端启动引导/辅助类:ServerBootstrap
            ServerBootstrap b = new ServerBootstrap();
            //3.给引导类配置两大线程组,确定了线程模型
            b.group(bossGroup, workerGroup).
                   ......
            // 6.绑定端口
            ChannelFuture f = b.bind(port).sync();
            // 等待连接关闭
            f.channel().closeFuture().sync();
        } finally {
            //7.优雅关闭相关线程组资源
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

说明:

  • Bootstrap 通常使用 connet() 方法连接到远程的主机和端口,作为一个 Netty TCP 协议通信中的客户端。另外,Bootstrap 也可以通过 bind() 方法绑定本地的一个端口,作为 UDP 协议通信中的一端。
  • ServerBootstrap通常使用 bind() 方法绑定本地的端口上,然后等待客户端的连接。
  • Bootstrap 只需要配置一个线程组— EventLoopGroup ,而 ServerBootstrap需要配置两个线程组— EventLoopGroup ,一个用于接收连接,一个用于具体的处理。

Channel:(在代码层面,读写,连接操作看似交给BootStrap,其实是交给Channel)

Channel 接口是 Netty 对网络操作抽象类,它除了包括基本的 I/O 操作,如 bind()connect()read()write() 等。

比较常用的Channel接口实现类是NioServerSocketChannel(服务端)和NioSocketChannel(客户端),这两个 Channel 可以和 BIO 编程模型中的ServerSocket以及Socket两个概念对应上。


ChannelFuture

Netty 是异步非阻塞的,所有的 I/O 操作都为异步的。因此,我们不能立刻得到操作是否执行成功,但是,可通过下面2种方式知道:

  • 可以通过 ChannelFuture 接口的 addListener() 方法注册一个 ChannelFutureListener,当操作执行成功或者失败时,监听就会自动触发返回结果。
  • 可以通过ChannelFuture 接口的 sync()方法让异步的操作变成同步的。

EventLoop 与 EventLoopGroup:

  • EventLoopGroup 是一个 EventLoop 池,包含很多的 EventLoop。
  • EventLoop的主要作用实际就是负责监听网络事件并调用事件处理器进行相关 I/O 操作的处理。因为每一个EventLoop有一个自己的selector。
  • 每一个EventLoop都对应一个Thread。
  • 每一个Channel都需要注册到EventLoop上进行IO操作。
  • 对应到的实现类就是NioEventLoop和NioEventLoopGroup。

Selector和TaskQueue

  • 都属于EventLoop 内的组件。
  • Selector就是NIO 的selector,这里不多介绍,详细请看NIO多路复用
  • 关于TaskQueue:当handler里面有长时间的任务时候,可以把这个任务放到TaskQueue中,先响应客户端,再执行长任务,,相当于异步执行,但是其实还是这一个线程在执行。

 ChannelPipeline和ChannelHandler :

  • ChannelPipeline是一个双向链表,里面就是一些ChannelHandler,用来处理对应的Channel的业务。
  • ChannelPipeline有Channel的信息,Channel中也有ChannelPipeline的信息。
  • 我们可以在 ChannelPipeline上通过 addLast() 方法添加一个或者多个ChannelHandler,因为一个数据或者事件可能会被多个 Handler 处理。当一个ChannelHandler 处理完之后就将数据交给下一个ChannelHandler 。

Netty

四.Netty服务端和客户端的启动过程了解么?
    // 1.bossGroup 用于接收连接,workerGroup 用于具体的处理
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            //2.创建服务端启动引导/辅助类:ServerBootstrap
            ServerBootstrap b = new ServerBootstrap();
            //3.给引导类配置两大线程组,确定了线程模型
            b.group(bossGroup, workerGroup)
                    // (非必备)打印日志
                    .handler(new LoggingHandler(LogLevel.INFO))
                    // 4.指定 IO 模型
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) {
                            ChannelPipeline p = ch.pipeline();
                            //5.可以自定义客户端消息的业务处理逻辑
                            p.addLast(new HelloServerHandler());
                        }
                    });
            // 6.绑定端口,调用 sync 方法阻塞直到绑定完成
            ChannelFuture f = b.bind(port).sync();
            // 7.阻塞等待直到服务器Channel关闭(closeFuture()方法获取Channel 的CloseFuture对象,然后调用sync()方法)
            f.channel().closeFuture().sync();
        } finally {
            //8.优雅关闭相关线程组资源
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
 五.其他小问题

 Netty 长连接?

  • 我们知道 TCP 在进行读写之前,server 与 client 之间必须提前建立一个连接。建立连接的过程,需要我们常说的三次握手,释放/关闭连接的话需要四次挥手。这个过程是比较消耗网络资源并且有时间延迟的。
  • 所谓,短连接说的就是 server 端 与 client 端建立连接之后,读写完成之后就关闭掉连接,如果下一次再要互相发送消息,就要重新连接。短连接的有点很明显,就是管理和实现都比较简单,缺点也很明显,每一次的读写都要建立连接必然会带来大量网络资源的消耗,并且连接的建立也需要耗费时间。
  • 长连接说的就是 client 向 server 双方建立连接之后,即使 client 与 server 完成一次读写,它们之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。长连接的可以省去较多的 TCP 建立和关闭的操作,降低对网络资源的依赖,节约时间。对于频繁请求资源的客户来说,非常适用长连接。

Netty心跳机制了解么?

  • 在 TCP 保持长连接的过程中,可能会出现断网等网络异常出现,异常发生的时候, client 与 server 之间如果没有交互的话,它们是无法发现对方已经掉线的。为了解决这个问题, 我们就需要引入 心跳机制 。
  • 心跳机制的工作原理是: 在 client 与 server 之间在一定时间内没有数据交互时, 即处于 idle 状态时, 客户端或服务器就会发送一个特殊的数据包给对方, 当接收方收到这个数据报文后, 也立即发送一个特殊的数据报文, 回应发送方, 此即一个 PING-PONG 交互。所以, 当某一端收到心跳消息后, 就知道了对方仍然在线, 这就确保 TCP 连接的有效性.
  • TCP 实际上自带的就有长连接选项,本身是也有心跳包机制,也就是 TCP 的选项:SO_KEEPALIVE。但是,TCP 协议层面的长连接灵活性不够。所以,一般情况下我们都是在应用层协议上实现自定义心跳机制的,也就是在 Netty 层面通过编码实现。通过 Netty 实现心跳机制的话,核心类是 IdleStateHandler 。

Netty 的零拷贝了解么?

在 OS 层面上的 Zero-copy 通常指避免在 用户态(User-space) 与 内核态(Kernel-space) 之间来回拷贝数据。而在 Netty 层面 ,零拷贝主要体现在对于数据操作的优化。

  • 使用 Netty 提供的 CompositeByteBuf 类, 可以将多个ByteBuf 合并为一个逻辑上的 ByteBuf, 避免了各个 ByteBuf 之间的拷贝。
  • ByteBuf 支持 slice 操作, 因此可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf, 避免了内存的拷贝。
  • 通过 FileRegion 包装的FileChannel.tranferTo 实现文件传输, 可以直接将文件缓冲区的数据发送到目标 Channel, 避免了传统通过循环 write 方式导致的内存拷贝问题。

NioEventLoopGroup 默认的构造函数会起多少线程?

  • 默认是cpu核数*2个子线程(也就是nioEventLoop)

 

寄语:当努力到一定程度,幸运自会与你不期而遇

上一篇:RK3399平台开发系列讲解(进程调度篇)14.9、进程数据结构详解(一)


下一篇:RK3399平台开发系列讲解(高速设备驱动篇)6.51、PCI总线信号定义