连续15个关键Netty面试问题深度解析
写在之前
Netty
有很多不错的考察点,今天就来总结一下常见的 Netty
面试题,面试题主要来自于牛客网网友分享的面经,答案为自己参考《Netty实战》及众多资料,避免闭门造车。
由于 Netty
知识可考察的点比较多,本文主要针对于 Netty
基础提出15连问,基本上都是面试常考题目。
准备发车
1. Netty 是什么
Netty
是一款异步的事件驱动的网络应用程序框架,支持快速地开发可维护的高性能的面向协议的服务器 和客户端。
2. 为什么要使用Netty?
这个问题也有其他的问法,比如原生NIO有什么问题呢。
-
NIO
的类库和API
繁杂,使用麻烦:需要熟练掌握Selector
、ServerSocketChannel
、SocketChannel
、ByteBuffer
等。 -
需要具备其他的额外技能:要熟悉 Java 多线程编程,因为 NIO 编程涉及到
Reactor
模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的NIO
程序。 -
开发工作量和难度都非常大:例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理等等。
-
JDK NIO
的Bug
:例如臭名昭著的Epoll Bug
,它会导致Selector
空轮询,最终导致CPU 100%
。直到JDK 1.7
版本该问题仍旧存在,没有被根本解决。
3. Netty有什么优点
主要从以下几个方面展开回答。
设计:
- 统一的 API,支持多种传输类型,阻塞的和非阻塞的
- 简单而强大的线程模型
- 真正的无连接数据报套接字支持
- 链接逻辑组件以支持复用
易用性:
- 详实的
Javadoc
和大量的示例集
性能:
- 拥有比
Java
的核心API
更高的吞吐量以及更低的延迟 - 得益于池化和复用,拥有更低的资源消耗
- 最少的内存复制
健壮性:
- 不会因为慢速、快速或者超载的连接而导致
OutOfMemoryError
- 消除在高速网络中
NIO
应用程序常见的不公平读/写比率
安全性:
- 完整的
SSL/TLS
以及StartTLS
支持 - 可用于受限环境下,如
Applet
和OSGI
4. netty高性能主要依赖了哪些特性
- IO 线程模型:同步非阻塞,用最少的资源做更多的事。
- 内存零拷贝:尽量减少不必要的内存拷贝,实现了更高效率的传输。
- 内存池设计:申请的内存可以重用,主要指直接内存。内部实现是用一颗二叉查找树管理内存分配情况。
- 串形化处理读写:避免使用锁带来的性能开销。
- 高性能序列化协议:支持 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先完成了再通知服务器应用去启动线程进行处理。
底层实现区别
-
BIO
以流的方式处理数据,而NIO
以块的方式处理数据,块I/O
的效率比流I/O
高很多 -
BIO
是阻塞的,NIO
则是非阻塞的 -
BIO
基于字节流和字符流进行操作,而NIO
基于Channel
(通道)和Buffer
(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector
(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道
7. 说说NIO的主要组成
1、Buffer
一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。与 Channel
进行交互,数据是从Channel
读入缓冲区,从缓冲区写入 Channel
中的。
2、Channel
NIO的通道类似于流,但有些区别
- 通道可以同时进行读写,而流只能读或者只能写
- 通道可以实现异步读写数据
- 通道可以从缓存读数据,也可以写数据到缓存
3、Selector
能够检测多个注册的通道上是否有事件发生(注意:多个Channel
以事件的方式可以注册到同一个Selector
),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
8. 说说对于Netty的零拷贝理解
什么是零拷贝?
从操作系统的角度来看,文件的传输不存在CPU的拷贝,只存在DMA拷贝(直接内存拷贝,不使用CPU完成)。零拷贝是网络编程的关键,很多性能优化都离不开它。
Netty对于零拷贝方式
-
Netty
的接收和发送ByteBuffer
采用DIRECT BUFFERS
,使用堆外直接内存进行 Socket 读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HEAP BUFFERS)进行 Socket 读写,JVM
会将堆内存Buffer
拷贝一份到直接内存中,然后才写入Socket
中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。 -
Netty
提供了组合Buffer
对象,可以聚合多个ByteBuffer
对象,用户可以像操作一个Buffer
那样方便的对组合Buffer
进行操作,避免了传统通过内存拷贝的方式将几个小Buffer
合并成一个大的Buffer
。 -
Netty
的文件传输采用了transferTo
方法,它可以直接将文件缓冲区的数据发送到目标Channel
,避免了传统通过循环write
方式导致的内存拷贝问题。
9. 说说Netty线程模型
Netty
线程模型主要基于主从 Reactor
多线程模型做了一定的改进,其中主从Reactor
多线程模型有多个 Reactor
。
内部实现了两个线程池,boss
线程池和 work
线程池,其中 boss
线程池的线程负责处理请求的连接事件,当接收到连接事件的请求时,把对应的socket封装到一个NioSocketChannel
中,并交给 work
线程池,其中 work
线程池负责请求的 read
和 write
事件,由对应的 Handler
处理。
其本质将线程连接和具体的业务处理区分开来。
10. Netty中有哪些重要组件
1、Bootstrap、ServerBootstrap:一个 Netty
应用通常由一个 Bootstrap
开始,主要作用是配置整个 Netty
程序,串联各个组件,Netty
中 Bootstrap
类是客户端程序的启动引导类,ServerBootstrap
是服务端启动引导类。
2、Future、ChannelFuture:Netty
中所有的 IO
操作都是异步的,不能立刻得知消息是否被正确处理。但是可以过一会等它执行完成或者直接注册一个监听,具体的实现就是通过 Future
和 ChannelFutures
,他们可以注册一个监听,当操作执行成功或失败时监听会自动触发注册的监听事件。
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无消息保护边界,需要在接收端处理消息边界问题。这就是拆包和粘包问题。
比如:
假设客户端同时发送了两个数据包D1和D2给服务端,由于服务端一次读取到字节数是不确定的,固可能存在以下四种情况:
- 服务端分两次读取到了两个独立的数据包,分别是 D1 和 D2 ,没有粘包和拆包
- 服务端一次接受到了两个数据包,D1 和 D2 粘合在一起,称之为 TCP 粘包
- 服务端分两次读取到了数据包,第一次读取到了完整的 D1 包和 D2 包的部分内容,第二次读取到了 D2 包的剩余内容,这称之为 TCP 拆包
- 服务端分两次读取到了数据包,第一次读取到了 D1 包的部分内容 D1_1 ,第二次读取到了D1包的剩余部分内容D1_2和完整的D2包。
12. Netty如何解决拆包和粘包问题
主要思路:在数据包的前面加上一个固定字节数的数据长度,如加上一个 int
(固定四个字节)类型的数据内容长度。
就算客户端同时发送两个数据包到服务端,当服务端接受时,也可以先读取四个字节的长度,然后根据长度获取消息的内容,这样就不会出现多读取或者少读取的情况了。
13. Netty主要采用了哪种设计模式
Netty中利用到了众多的设计模式,有很多常见的设计模式,比如观察者模式、策略模式(在初始化 EventLoopGroup
时选择何种 DefaultEventExecutorChooserFactor-newChooser
时使用了),但是使用的最多的还是属于责任链模式,pipeline
就像一个责任链,ChannelHandler
就是其中处理逻辑的节点,通过自定义 Handler
来决定每个业务的执行逻辑。
14. 说说netty中的责任链设计模式
netty
的 pipeline
设计,就采用了责任链设计模式,底层采用双向链表的数据结构,将链上的各个处理器(Handler
)串联起来。
客户端每一个请求的到来,netty
认为,pipeline
中的所有的处理器都有机会处理它,因此,对于入栈的请求,全部从头节点开始往后传播,一直传播到尾节点。
开发者可以自主的删除或者添加责任链中的某个节点。
15. Netty 是如何保持长连接的
什么是长连接?
客户端和服务器之间定期发送的一种特殊的数据包,通知对方自己还在线, 以确保 TCP
连接的有效性。但是由于网络不稳定性,有可能在 TCP
保持长连接的过程中,由于某些突发情况, 例如网线被拔出, 突然掉电等。 会造成服务器和客户端的连接中断。在这些突发情况下, 如果恰好服务器和客户端之间没有交互的话,那么它们是不能在短时间内发现对方已经掉线的。
如何保持长连接?
利用心跳维护长连接信息。
在服务器和客户端之间一定时间内没有数据交互时,即处于 idle
状态时,客户端或服务器会发送一个特殊的数据包给对方,当接收方收到这个数据报文后, 也立即发送一个特殊的数据报文, 回应发送方, 此即一个 PING-PONG
交互。
当某一端收到心跳消息后, 就知道了对方仍然在线, 这就确保 TCP 连接的有效性。
Netty有三种类型保持心跳类型
-
readerIdleTime
:为读超时时间(即测试端一定时间内未接受到被测试端消息)。 -
writerIdleTime
:为写超时时间(即测试端一定时间内向被测试端发送消息)。 -
allIdleTime
:所有类型的超时时间。
总结
针对于 Netty
,本身利用比较广泛,比如国内流行的 RPC
框架 Dubbo
,由于开发者本身无须深入了解其原理就可以很好的进行业务开发,因此许多人对于Netty
了解甚少,但是想要了解一些进阶的 Java
编程,Netty
是一个不错的学习框架,本篇文章结合面试题开发,整体串起 Netty
的核心知识。