写在之前
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的通道类似于流,但有些区别
- 通道可以同时进行读写,而流只能读或者只能写
- 通道可以实现异步读写数据
- 通道可以从缓存读数据,也可以写数据到缓存
channel与Buffer关系
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
(固定四个字节)类型的数据内容长度。
就算客户端同时发送两个数据包到服务端,当服务端接受时,也可以先读取四个字节的长度,然后根据长度获取消息的内容,这样就不会出现多读取或者少读取的情况了。
Netty解决拆包和粘包问题示意图
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
的核心知识。