写在之前

Netty有很多不错的考察点,今天就来总结一下常见的 Netty 面试题,面试题主要来自于牛客网网友分享的面经,答案为自己参考《Netty实战》及众多资料,避免闭门造车。

由于 Netty 知识可考察的点比较多,本文主要针对于 Netty 基础提出15连问,基本上都是面试常考题目。

准备发车

1. Netty 是什么

Netty 是一款异步的事件驱动的网络应用程序框架,支持快速地开发可维护的高性能的面向协议的服务器 和客户端。

2. 为什么要使用Netty?

这个问题也有其他的问法,比如原生NIO有什么问题呢

  1. NIO 的类库和API繁杂,使用麻烦:需要熟练掌握 SelectorServerSocketChannelSocketChannelByteBuffer 等。

  2. 需要具备其他的额外技能:要熟悉 Java 多线程编程,因为 NIO 编程涉及到 Reactor 模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的 NIO 程序。

  3. 开发工作量和难度都非常大:例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理等等。

  4. JDK NIOBug:例如臭名昭著的 Epoll Bug,它会导致 Selector空轮询,最终导致 CPU 100%。直到 JDK 1.7 版本该问题仍旧存在,没有被根本解决。

3. Netty有什么优点

主要从以下几个方面展开回答。

设计:

  1. 统一的 API,支持多种传输类型,阻塞的和非阻塞的
  2. 简单而强大的线程模型
  3. 真正的无连接数据报套接字支持
  4. 链接逻辑组件以支持复用

易用性:

  1. 详实的 Javadoc 和大量的示例集

性能:

  1. 拥有比 Java 的核心 API 更高的吞吐量以及更低的延迟
  2. 得益于池化和复用,拥有更低的资源消耗
  3. 最少的内存复制

健壮性:

  1. 不会因为慢速、快速或者超载的连接而导致 OutOfMemoryError
  2. 消除在高速网络中 NIO 应用程序常见的不公平读/写比率

安全性:

  1. 完整的 SSL/TLS 以及 StartTLS 支持
  2. 可用于受限环境下,如 AppletOSGI

4. netty高性能主要依赖了哪些特性

  1. IO 线程模型:同步非阻塞,用最少的资源做更多的事。
  2. 内存零拷贝:尽量减少不必要的内存拷贝,实现了更高效率的传输。
  3. 内存池设计:申请的内存可以重用,主要指直接内存。内部实现是用一颗二叉查找树管理内存分配情况。
  4. 串形化处理读写:避免使用锁带来的性能开销。
  5. 高性能序列化协议:支持 protobuf 等高性能序列化协议。

5. 为什么BIO比NIO性能差?简单讲讲区别

BIO:服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。

传统IO的缺点:
第一,在任何 时候都可能有大量的线程处于休眠状态,只是等待输 入或者输出数据就绪,这可能算是一种资源浪费
第 二,需要为每个线程的调用栈都分配内存,其默认值 大小区间为 64 KB 到 1 MB,具体取决于操作系统
第 三,即使 Java 虚拟机(JVM)在物理上可以支持非常大数量的线程,但是远在到达该极限之前,上下文切换所带来的开销就会带来麻烦

NIO:一个请求一个线程,但客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理,相对于BIO来说比较灵活。

6. 简单说下 BIO、NIO 和 AIO区别

概念本质不同

BIO:一个连接一个线程,客户端有连接请求时服务器端就需要启动一个线程进行处理。线程开销大。

NIO:一个请求一个线程,但客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。

AIO:一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。

底层实现区别

  1. BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多

  2. BIO 是阻塞的,NIO 则是非阻塞的

  3. BIO基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道

7. 说说NIO的主要组成

1、Buffer

一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。与 Channel 进行交互,数据是从Channel 读入缓冲区,从缓冲区写入 Channel 中的。

2、Channel

NIO的通道类似于流,但有些区别

  1. 通道可以同时进行读写,而流只能读或者只能写
  2. 通道可以实现异步读写数据
  3. 通道可以从缓存读数据,也可以写数据到缓存

【博客大赛】Netty面试灵魂15问_Java channel与Buffer关系

3、Selector

能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。

8. 说说对于Netty的零拷贝理解

什么是零拷贝?

从操作系统的角度来看,文件的传输不存在CPU的拷贝,只存在DMA拷贝(直接内存拷贝,不使用CPU完成)。零拷贝是网络编程的关键,很多性能优化都离不开它。

Netty对于零拷贝方式

  1. Netty 的接收和发送 ByteBuffer 采用 DIRECT BUFFERS,使用堆外直接内存进行 Socket 读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HEAP BUFFERS)进行 Socket 读写,JVM 会将堆内存 Buffer 拷贝一份到直接内存中,然后才写入Socket中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
  2. Netty 提供了组合 Buffer 对象,可以聚合多个 ByteBuffer 对象,用户可以像操作一个 Buffer 那样方便的对组合 Buffer 进行操作,避免了传统通过内存拷贝的方式将几个小 Buffer 合并成一个大的 Buffer
  3. Netty 的文件传输采用了 transferTo 方法,它可以直接将文件缓冲区的数据发送到目标 Channel,避免了传统通过循环 write 方式导致的内存拷贝问题。

9. 说说Netty线程模型

Netty线程模型主要基于主从 Reactor 多线程模型做了一定的改进,其中主从Reactor多线程模型有多个 Reactor

内部实现了两个线程池,boss 线程池和 work 线程池,其中 boss 线程池的线程负责处理请求的连接事件,当接收到连接事件的请求时,把对应的socket封装到一个NioSocketChannel 中,并交给 work 线程池,其中 work 线程池负责请求的 readwrite 事件,由对应的 Handler 处理。

其本质将线程连接和具体的业务处理区分开来。

10. Netty中有哪些重要组件

1、Bootstrap、ServerBootstrap:一个 Netty 应用通常由一个 Bootstrap 开始,主要作用是配置整个 Netty 程序,串联各个组件,NettyBootstrap 类是客户端程序的启动引导类,ServerBootstrap 是服务端启动引导类。

2、Future、ChannelFuture:Netty 中所有的 IO 操作都是异步的,不能立刻得知消息是否被正确处理。但是可以过一会等它执行完成或者直接注册一个监听,具体的实现就是通过 FutureChannelFutures,他们可以注册一个监听,当操作执行成功或失败时监听会自动触发注册的监听事件。

3、Channel:Netty 网络操作抽象类,它除了包括基本的 I/O 操作,如 bind、connect、read、write 等

4、Selector:基于 Selector 对象实现I/O多路复用,通过 Selector 一个线程可以监听多个连接的 Channel 事件,Selector 内部的机制就可以自动不断地查询(Select) 这些注册的 Channel是否有已就绪的I/O 事件(例如可读,可写,网络连接完成等)

5、ChannelHandler:充当了所有处理入站和出站数据的逻辑容器。ChannelHandler 主要用来处理各种事件,这里的事件很广泛,比如可以是连接、数据接收、异常、数据转换等。

6、EventLoop:主要是配合 Channel 处理 I/O 操作,用来处理连接的生命周期中所发生的事情

7、ChannelPipeline:为 ChannelHandler 链提供了容器,当 channel 创建时,就会被自动分配到它专属的 ChannelPipeline,这个关联是永久性的。

8、ChannelHandlerContext:包 含 一 个 具 体 的 事 件 处 理 器 ChannelHandler , 同 时ChannelHandlerContext 中也绑定了对应的 pipeline 和 Channel 的信息,方便对 ChannelHandler进行调用。

11. 说说什么是拆包和粘包

TCP 是面向连接的,面向流的,提供高可靠性服务。

收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发给接收端的包,更有效的发给对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样做虽然提高了效率,但是接收端就难于分辨出完整的数据包了,由于TCP无消息保护边界,需要在接收端处理消息边界问题。这就是拆包和粘包问题。

比如:

【博客大赛】Netty面试灵魂15问_Java _02拆包和粘包图解

假设客户端同时发送了两个数据包D1和D2给服务端,由于服务端一次读取到字节数是不确定的,固可能存在以下四种情况:

  1. 服务端分两次读取到了两个独立的数据包,分别是 D1 和 D2 ,没有粘包和拆包
  2. 服务端一次接受到了两个数据包,D1 和 D2 粘合在一起,称之为 TCP 粘包
  3. 服务端分两次读取到了数据包,第一次读取到了完整的 D1 包和 D2 包的部分内容,第二次读取到了 D2 包的剩余内容,这称之为 TCP 拆包
  4. 服务端分两次读取到了数据包,第一次读取到了 D1 包的部分内容 D1_1 ,第二次读取到了D1包的剩余部分内容D1_2和完整的D2包。

12. Netty如何解决拆包和粘包问题

主要思路:在数据包的前面加上一个固定字节数的数据长度,如加上一个 int(固定四个字节)类型的数据内容长度。

就算客户端同时发送两个数据包到服务端,当服务端接受时,也可以先读取四个字节的长度,然后根据长度获取消息的内容,这样就不会出现多读取或者少读取的情况了。

【博客大赛】Netty面试灵魂15问_Java _03Netty解决拆包和粘包问题示意图

13. Netty主要采用了哪种设计模式

Netty中利用到了众多的设计模式,有很多常见的设计模式,比如观察者模式、策略模式(在初始化 EventLoopGroup 时选择何种 DefaultEventExecutorChooserFactor-newChooser 时使用了),但是使用的最多的还是属于责任链模式,pipeline 就像一个责任链,ChannelHandler 就是其中处理逻辑的节点,通过自定义 Handler 来决定每个业务的执行逻辑。

14. 说说netty中的责任链设计模式

nettypipeline 设计,就采用了责任链设计模式,底层采用双向链表的数据结构,将链上的各个处理器(Handler)串联起来。

客户端每一个请求的到来,netty 认为,pipeline 中的所有的处理器都有机会处理它,因此,对于入栈的请求,全部从头节点开始往后传播,一直传播到尾节点。

开发者可以自主的删除或者添加责任链中的某个节点。

15. Netty 是如何保持长连接的

什么是长连接?

客户端和服务器之间定期发送的一种特殊的数据包,通知对方自己还在线, 以确保 TCP 连接的有效性。但是由于网络不稳定性,有可能在 TCP 保持长连接的过程中,由于某些突发情况, 例如网线被拔出, 突然掉电等。 会造成服务器和客户端的连接中断。在这些突发情况下, 如果恰好服务器和客户端之间没有交互的话,那么它们是不能在短时间内发现对方已经掉线的。

如何保持长连接?

利用心跳维护长连接信息。

在服务器和客户端之间一定时间内没有数据交互时,即处于 idle 状态时,客户端或服务器会发送一个特殊的数据包给对方,当接收方收到这个数据报文后, 也立即发送一个特殊的数据报文, 回应发送方, 此即一个 PING-PONG 交互。

当某一端收到心跳消息后, 就知道了对方仍然在线, 这就确保 TCP 连接的有效性。

Netty有三种类型保持心跳类型

  • readerIdleTime:为读超时时间(即测试端一定时间内未接受到被测试端消息)。
  • writerIdleTime:为写超时时间(即测试端一定时间内向被测试端发送消息)。
  • allIdleTime:所有类型的超时时间。

总结

针对于 Netty ,本身利用比较广泛,比如国内流行的 RPC 框架 Dubbo ,由于开发者本身无须深入了解其原理就可以很好的进行业务开发,因此许多人对于Netty了解甚少,但是想要了解一些进阶的 Java 编程,Netty 是一个不错的学习框架,本篇文章结合面试题开发,整体串起 Netty 的核心知识。