秃头系列-Netty-面试篇

260 阅读10分钟

「这是我参与2022首次更文挑战的第12天,活动详情查看:2022首次更文挑战

前言

  • 关于作者:励志不秃头的一个CURD的Java农民工,想挑战看看自己能完成多少天的更文挑战
  • 关于文章:以下内容单纯为作者了解的,如有不对,欢迎各路大神指导,下面简单聊聊Netty面试常用的知识点,文章最后附上Netty整个流程图!!!

Netty

Netty的核心组件

  • Channel
    • Channel 接口是Netty 对网络操作抽象类,它除了包括基本的I/O操作
    • 比较常用的Channel接口实现类是NioServerSocketChannel(服务端)和NioSocketChannel(客户端),这两个 Channel 可以和 BIO 编程模型中的ServerSocket以及Socket两个概念对应上。Netty 的 Channel 接口所提供的 API,大大地降低了直接使用 Socket 类的复杂性。
  • EventLoop
    • EventLoop 定义了 Netty 的核心抽象,用于处理连接的生命周期中所发生的事件。
    • EventLoop 的主要作用实际就是负责监听网络事件并调用事件处理器进行相关 I/O 操作的处理。
    • Channel 为 Netty 网络操作(读写等操作)抽象类,EventLoop 负责处理注册到其上的Channel 处理 I/O 操作,两者配合参与 I/O 操作。
  • ChannelFuture
    • Netty 是异步非阻塞的,所有的 I/O 操作都为异步的。
    • 可以通过 ChannelFuture 接口的 addListener() 方法注册一个 ChannelFutureListener,当操作执行成功或者失败时,监听就会自动触发返回结果。
    • 可以通过 ChannelFuture 接口的 sync()方法让异步的操作变成同步的。
  • ChannelHandler 和 ChannelPipline
    • ChannelHandler 是消息的具体处理器。他负责处理读写操作、客户端连接等事情。
    • ChannelPipeline 为 ChannelHandler 的链,提供了一个容器并定义了用于沿着链传播入站和出站事件流的 API 。当 Channel 被创建时,它会被自动地分配到它专属的 ChannelPipeline。
    • 我们可以在 ChannelPipeline 上通过 addLast() 方法添加一个或者多个ChannelHandler ,因为一个数据或者事件可能会被多个 Handler 处理。当一个 ChannelHandler 处理完之后就将数据交给下一个 ChannelHandler 。

EventloopGroup 了解么?和 EventLoop 啥关系?

  • EventLoopGroup 包含多个 EventLoop(每一个 EventLoop 通常内部包含一个线程),上面我们已经说了 EventLoop 的主要作用实际就是负责监听网络事件并调用事件处理器进行相关 I/O 操作的处理。
  • 并且 EventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理,即 Thread 和 EventLoop 属于 1 : 1 的关系,从而保证线程安全。

上图是一个服务端对 EventLoopGroup 使用的大致模块图,其中 Boss EventloopGroup 用于接收连接,Worker EventloopGroup 用于具体的处理(消息的读写以及其他逻辑处理)。

从上图可以看出:当客户端通过 connect 方法连接服务端时,bossGroup 处理客户端连接请求。当客户端处理完成后,会将这个连接提交给 workerGroup 来处理,然后 workerGroup 负责处理其 IO 相关操作。

Bootstrap 和 ServerBootstrap 了解么?

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

public class NettyClient {

  public static void main(String[] args) {
        EventLoopGroup group = new NioEventLoopGroup();
        try{
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new NettyClientHandler());
                        }
                    });
            System.out.printf("netty clint start");
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 9000).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally{
            group.shutdownGracefully();
        }
  }
}

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

public class NettyServer {

  public static void main(String[] args) {
      EventLoopGroup bossGroup = new NioEventLoopGroup();
      EventLoopGroup workGroup = new NioEventLoopGroup();

      try{
          ServerBootstrap bootstrap = new ServerBootstrap();
          bootstrap.group(bossGroup, workGroup)
                  .channel(NioServerSocketChannel.class)
                  .option(ChannelOption.SO_BACKLOG, 1024)
                  .childHandler(new ChannelInitializer<SocketChannel>() {
                      @Override
                      protected void initChannel(SocketChannel socketChannel) throws Exception {
                          socketChannel.pipeline().addLast(new NettyServerHandlerV1());
                      }
                  });
          System.out.printf("netty server start");
          ChannelFuture cf = bootstrap.bind(9000).sync();
          cf.channel().closeFuture().sync();
      }catch (InterruptedException e) {
          e.printStackTrace();
      }finally{
          bossGroup.shutdownGracefully();
          workGroup.shutdownGracefully();
      }
  }
}

从上面可以看出:

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

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

默认启动的构造函数会起 CPU核心数*2

NioEventLoopGroup 默认的构造函数 -> MultithreadEventLoopGroup ->

    // 从1,系统属性,CPU核心数*2 这三个值中取出一个最大的
    //可以得出 DEFAULT_EVENT_LOOP_THREADS 的值为CPU核心数*2
    private static final int DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));

    // 被调用的父类构造函数,NioEventLoopGroup 默认的构造函数会起多少线程的秘密所在
    // 当指定的线程数nThreads为0时,使用默认的线程数DEFAULT_EVENT_LOOP_THREADS
    protected MultithreadEventLoopGroup(int nThreads, ThreadFactory threadFactory, Object... args) {
        super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, threadFactory, args);
    }

Netty 线程模型

基于 Reactor 模式设计开发的。

  • Reactor 模式基于事件驱动,采用多路复用将事件分发给相应的 Handler 处理,非常适合处理海量 IO 的场景。
    Netty 主要靠 NioEventLoopGroup 线程池来实现具体的线程模型的 。
    实现服务端的时候,一般会初始化两个线程组:
  • bossGroup :用于处理客户端的 TCP 连接请求。
  • workerGroup :负责具体的处理,交由对应的 Handler 处理。

Netty 的零拷贝

零拷贝:指计算机执行操作时,CPU 不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省 CPU 周期和内存带宽。

Netty的零拷贝体现在以下方面:

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

Netty 服务端和客户端的启动过程了解么?

服务端

  1. 创建了两个 NioEventLoopGroup 对象实例:bossGroup 和 workerGroup。
  2. 创建了一个服务端启动引导/辅助类:ServerBootstrap,这个类将引导我们进行服务端的启动工作。
  3. 通过 .group() 方法给引导类 ServerBootstrap 配置两大线程组,确定了线程模型。
  4. 通过channel()方法给引导类 ServerBootstrap指定了 IO 模型为NIO
    • NioServerSocketChannel :指定服务端的 IO 模型为 NIO,与 BIO 编程模型中的ServerSocket对应
    • NioSocketChannel : 指定客户端的 IO 模型为 NIO, 与 BIO 编程模型中的Socket对应
  5. 通过 .childHandler()给引导类创建一个ChannelInitializer ,然后指定了服务端消息的业务处理逻辑 HelloServerHandler 对象
  6. 调用 ServerBootstrap 类的 bind()方法绑定端口

客户端

  1. 创建一个 NioEventLoopGroup 对象实例
  2. 创建客户端启动的引导类是 Bootstrap
  3. 通过 .group() 方法给引导类 Bootstrap 配置一个线程组
  4. 通过channel()方法给引导类 Bootstrap指定了 IO 模型为NIO
  5. 通过 .childHandler()给引导类创建一个ChannelInitializer ,然后指定了客户端消息的业务处理逻辑 HelloClientHandler 对象
  6. 调用 Bootstrap 类的 connect()方法进行连接,这个方法需要指定两个参数:
    • inetHost : ip 地址
    • netPort : 端口号

Netty 长连接、心跳机制了解么?

TCP长连接和短连接

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

心跳机制

在 TCP 保持长连接的过程中,可能会出现断网等网络异常出现,异常发生的时候, client 与 server 之间如果没有交互的话,它们是无法发现对方已经掉线的。为了解决这个问题, 我们就需要引入 心跳机制

心跳机制的工作原理是: 在 client 与 server 之间在一定时间内没有数据交互时, 即处于 idle 状态时, 客户端或服务器就会发送一个特殊的数据包给对方, 当接收方收到这个数据报文后, 也立即发送一个特殊的数据报文, 回应发送方, 此即一个 PING-PONG 交互。所以, 当某一端收到心跳消息后, 就知道了对方仍然在线, 这就确保 TCP 连接的有效性.

TCP 实际上自带的就有长连接选项,本身是也有心跳包机制,也就是 TCP 的选项:SO_KEEPALIVE。但是,TCP 协议层面的长连接灵活性不够。所以,一般情况下我们都是在应用层协议上实现自定义心跳机制的,也就是在 Netty 层面通过编码实现。通过 Netty 实现心跳机制的话,核心类是 IdleStateHandler 。

面经:select / poll / epoll的区别?epoll的数据结构

select,poll,epoll都是IO多路复用机制,都是同步I/O

  • select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。
  • select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。

Netty整体流程图

Netty.png

整整画了快一天了,希望对大家有一定的帮助,当然点赞评论就更好了,这也是Netty目前最后一篇文章了,未来有机会可能有继续输出有关Netty的文章;我是新生代农民工L_Denny,我们下篇文章聊聊单例模式,下篇文章见。