Netty 常见面试题总结
很多小伙伴搞不清楚为啥要学习 Netty ,正式今天这篇文章开始之前,简单说一下自己的看法:
- Netty 基于 NIO (NIO 是一种同步非阻塞的 I/O 模型,在 Java 1.4 中引入了 NIO),使用 Netty 可以极大地简化 TCP 和 UDP 套接字服务器等网络编程,并且性能以及安全性等很多方面都非常优秀。
- 我们平常经常接触 的 Dubbo、RocketMQ、Elasticsearch、gRPC、Spark 等等热门开源项目都用到了 Netty。
- 大部分微服务框架底层涉及到网络通信的部分都是基于 Netty 来做的,比如说 Spring Cloud 生态系统中的网关 Spring Cloud Gateway。
简单总结一下和 Netty 相关问题。
BIO,NIO 和 AIO 有啥区别?
👨💻面试官 :先来简单介绍一下 BIO,NIO 和 AIO 3 者的区别吧!
🙋 我 :好的!
- BIO (Blocking I/O): 同步阻塞 I/O 模式,数据的读取写入必须阻塞在一个线程内等待其完成。在客户端连接数量不高的情况下,是没问题的。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
- NIO (Non-blocking/New I/O): NIO 是一种同步非阻塞的 I/O 模型,于 Java 1.4 中引入,对应
java.nio
包,提供了Channel
,Selector
,Buffer
等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它支持面向缓冲的,基于通道的 I/O 操作方法。 NIO 提供了与传统 BIO 模型中的Socket
和ServerSocket
相对应的SocketChannel
和ServerSocketChannel
两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模 式。对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发 - AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的 IO 模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步 IO 的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO 操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。
关于 IO 模型更详细的介绍,你可以看这篇文章:《常见的 IO 模型有哪些?Java 中的 BIO、NIO、AIO 有啥区别?》 这篇文章。
Netty 是什么?
👨💻面试官 :那你再来介绍一下自己对 Netty 的认识吧!小伙子。
🙋 我 :好的!那我就简单用 3 点来概括一下 Netty 吧!
- Netty 是一个 基于 NIO 的 client-server(客户端服务器)框架,使用它可以快速简单地开发网络应用程序。
- 它极大地简化并优化了 TCP 和 UDP 套接字服务器等网络编程,并且性能以及安全性等很多方面甚至都要更好。
- 支持多种协议 如 FTP,SMTP,HTTP 以及各种二进制和基于文本的传统协议。
用官方的总结就是:Netty 成功地找到了一种在不妥协可维护性和性能的情况下实现易于开发,性能,稳定性和灵活性的方法。
网络编程我愿意称中 Netty 为王 。
为啥不直接用 NIO 呢?
👨💻面试官 :你上面也说了 Netty 基于 NIO,那为啥不直接用 NIO 呢?。
不用 NIO 主要是因为 NIO 的编程模型复杂而且存在一些 BUG,并且对编程功底要求比较高。下图就是一个典型的使用 NIO 进行编程的案例:
而且,NIO 在面对断连重连、包丢失、粘包等问题时处理过程非常复杂。Netty 的出现正是为了解决这些问题,更多关于 Netty 的特点可以看下面的内容。
为什么要用 Netty?
👨💻面试官 :为什么要用 Netty 呢?能不能说一下自己的看法。
🙋 我 :因为 Netty 具有下面这些优点,并且相比于直接使用 JDK 自带的 NIO 相关的 API 来说更加易用。
- 统一的 API,支持多种传输类型,阻塞和非阻塞的。
- 简单而强大的线程模型。
- 自带编解码器解决 TCP 粘包/拆包问题。
- 自带各 种协议栈。
- 真正的无连接数据包套接字支持。
- 比直接使用 Java 核心 API 有更高的吞吐量、更低的延迟、更低的资源消耗和更少的内存复制。
- 安全性不错,有完整的 SSL/TLS 以及 StartTLS 支持。
- 社区活跃
- 成熟稳定,经历了大型项目的使用和考验,而且很多开源项目都使用到了 Netty, 比如我们经常接触的 Dubbo、RocketMQ 等等。
- ......
Netty 应用场景了解么?
👨💻面试官 :能不能通俗地说一下使用 Netty 可以做什么事情?
🙋 我 :凭借自己的了解,简单说一下吧!理论上来说,NIO 可以做的事情 ,使用 Netty 都可以做并且更好。Netty 主要用来做网络通信 :
- 作为 RPC 框架的网络通信工具 : 我们在分布式系统中,不同服务节点之间经常需要相互调用,这个时候就需要 RPC 框架了。不同服务节点的通信是如何做的呢?可以使用 Netty 来做。比如我调用另外一个节点的方法的话,至少是要让对方知道我调用的是哪个类中的哪个方法以及相关参数吧!
- 实现一个自己的 HTTP 服务器 :通过 Netty 我们可以自己实现一个简单的 HTTP 服务器,这个大家应该不陌生。说到 HTTP 服务器的话,作为 Java 后端开发,我们一般使用 Tomcat 比较多。一个最基本的 HTTP 服务器可要以处理常见的 HTTP Method 的请求,比如 POST 请求、GET 请求等等。
- 实现一个即时通讯系统 : 使用 Netty 我们可以实现一个可以聊天类似微信的即时通 讯系统,这方面的开源项目还蛮多的,可以自行去 Github 找一找。
- 实现消息推送系统 :市面上有很多消息推送系统都是基于 Netty 来做的。
- ......
哪些开源项目用到了 Netty?
我们平常经常接触的 Dubbo、RocketMQ、Elasticsearch、gRPC 等等都用到了 Netty。
可以说大量的开源项目都用到了 Netty,所以掌握 Netty 有助于你更好的使用这些开源项目并且让你有能力对其进行二次开发。
实际上还有很多很多优秀的项目用到了 Netty,Netty 官方也做了统计,统计结果在这里:https://netty.io/wiki/related-projects.html 。
介绍一下 Netty 的核心组件?
👨💻面试官 :Netty 核心组件有哪些?分别有什么作用?
🙋 我 :表面上,嘴上开始说起 Netty 的核心组件有哪些,实则,内心已经开始 mmp 了,深度怀疑这面试官是存心搞我啊!
简单介绍 Netty 最核心的一些组件(对于每一个组件这里不详细 介绍)。通过下面这张图你可以将我提到的这些 Netty 核心组件串联起来。
Bytebuf(字节容器)
网络通信最终都是通过字节流进行传输的。 **ByteBuf**
就是 Netty 提供的一个字节容器,其内部是一个字节数组。 当我们通过 Netty 传输数据的时候,就是通过 ByteBuf
进行的。
我们可以将 ByteBuf
看作是 Netty 对 Java NIO 提供了 ByteBuffer
字节容器的封装和抽象。
有很多小伙伴可能就要问了 : 为什么不直接使用 Java NIO 提供的 **ByteBuffer**
呢?
因为 ByteBuffer
这个类使用起来过于复杂和繁琐。
Bootstrap 和 ServerBootstrap(启动引导类)
**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
通常使用connect()
方法连接到远程的主机和端口,作为一个 Netty TCP 协议通信中的客户端。另外,Bootstrap
也可以通过bind()
方法绑定本地的一个端口,作为 UDP 协议通信中的一端。ServerBootstrap
通常使用bind()
方法绑定本地的端口上,然后等待客户端的连接。Bootstrap
只需要配置一个线程组—EventLoopGroup
,而ServerBootstrap
需要配置两个线程组—EventLoopGroup
,一个用于接收连接,一个用于具体的 IO 处理。
Channel(网络操作抽象类)
Channel
接口是 Netty 对网络操作抽象类。通过 Channel
我们可以进行 I/O 操作。
一旦客户端成功连接服务端,就会新建一个 Channel
同该用户端进行绑定,示例代码如下:
// 通过 Bootstrap 的 connect 方法连接到服务端
public Channel doConnect(InetSocketAddress inetSocketAddress) {
CompletableFuture<Channel> completableFuture = new CompletableFuture<>();
bootstrap.connect(inetSocketAddress).addListener((ChannelFutureListener) future -> {
if (future.isSuccess()) {
completableFuture.complete(future.channel());
} else {
throw new IllegalStateException();
}
});
return completableFuture.get();
}
比较常用的Channel
接口实现类是 :
NioServerSocketChannel
(服务端)NioSocketChannel
(客户端)
这两个 Channel
可以和 BIO 编程模型中的ServerSocket
以及Socket
两个概念对应上。
EventLoop(事件循环)
EventLoop 介绍
这么说吧!EventLoop
(事件循环)接口可以说是 Netty 中最核心的概念了!
《Netty 实战》这本书是这样介绍它的:
EventLoop
定义了 Netty 的核心抽象,用于处理连接的生命周期中所发生的事件。
是不是很难理解?说实话,我学习 Netty 的时候看到这句话是没太能理解的。
说白了,**EventLoop**
** 的主要作用实际就是责监听网络事件并调用事件处理器进行相关 I/O 操作(读写)的处理。**
Channel 和 EventLoop 的关系
那 Channel
和 EventLoop
直接有啥联系呢?
**Channel**
** 为 Netty 网络操作(读写等操作)抽象类,**EventLoop**
负责处理注册到其上的****Channel**
** 的 I/O 操作,两者配合进行 I/O 操作。**
EventloopGroup 和 EventLoop 的关系
EventLoopGroup
包含多个 EventLoop
(每一个 EventLoop
通常内部包含一个线程),它管理着所有的 EventLoop
的生命周期。
并且,**EventLoop**
** 处理的 I/O 事件都将在它专有的 **Thread**
上被处理,即 **Thread**
和 **EventLoop**
属于 1 : 1 的关系,从而保证线程安全。**
下图是 Netty NIO 模型对应的 EventLoop
模型。通过这个图应该可以将EventloopGroup
、EventLoop
、 Channel
三者联系起来。
https://www.jianshu.com/p/128ddc36e713
ChannelHandler(消息处理器) 和 ChannelPipeline(ChannelHandler 对象链表)
下面这段代码使用过 Netty 的小伙伴应该不会陌生,我们指定了序列化编解码器以及自定义的 ChannelHandler
处理消息。
b.group(eventLoopGroup)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new NettyKryoDecoder(kryoSerializer, RpcResponse.class));
ch.pipeline().addLast(new NettyKryoEncoder(kryoSerializer, RpcRequest.class));
ch.pipeline().addLast(new KryoClientHandler());
}
});
**ChannelHandler**
** 是消息的具体处理器,主要负责处理客户端/服务端接收和发送的数据。**
当 Channel
被创建时,它会被自动地分配到它专属的 ChannelPipeline
。 一个Channel
包含一个 ChannelPipeline
。 ChannelPipeline
为 ChannelHandler
的链,一个 pipeline 上可以有多个 ChannelHandler
。
我们可以在 ChannelPipeline
上通过 addLast()
方法添加一个或者多个ChannelHandler
(一个数据或者事件可能会被多个 Handler 处理) 。当一个 ChannelHandler
处理完之后就将数据交给下一个 ChannelHandler
。
当 ChannelHandler
被添加到的 ChannelPipeline
它得到一个 ChannelHandlerContext
,它代表一个 ChannelHandler
和 ChannelPipeline
之间的“绑定”。 ChannelPipeline
通过 ChannelHandlerContext
来间接管理 ChannelHandler
。
https://www.javadoop.com/post/netty-part-4
ChannelFuture(操作执行结果)
public interface ChannelFuture extends Future<Void> {
Channel channel();
ChannelFuture addListener(GenericFutureListener<? extends Future<? super Void>> var1);
......
ChannelFuture sync() throws InterruptedException;
}
Netty 中所有的 I/O 操作都为异步的,我们不能立刻得到操作是否执行成功。
不过,你可以通过 ChannelFuture
接口的 addListener()
方法注册一个 ChannelFutureListener
,当操作执行成功或者失败时,监听就会自动触发返回结果。
ChannelFuture f = b.connect(host, port).addListener(future -> {
if (future.isSuccess()) {
System.out.println("连接成功!");
} else {
System.err.println("连接失败!");
}
}).sync();
并且,你还可以通过ChannelFuture
的 channel()
方法获取连接相关联的Channel
。
Channel channel = f.channel();
另外,我们还可以通过 ChannelFuture
接口的 sync()
方法让异步的操作变成同步的。
//bind()是异步的,但是,你可以通过 sync()方法将其变为同步。
ChannelFuture f = b.bind(port).sync();
NioEventLoopGroup 默认的构造函数会起多少线程?
👨💻面试官 :看过 Netty 的源码了么?NioEventLoopGroup
默认的构造函数会起多少线程呢?
🙋 我 :嗯嗯!看过部分。
回顾我们在上面写的服务器端的代码:
// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
为了搞清楚NioEventLoopGroup
默认的构造函数 到底创建了多少个线程,我们来看一下它的源码。
/**
* 无参构造函数。
* nThreads:0
*/
public NioEventLoopGroup() {
//调用下一个构造方法
this(0);
}
/**
* Executor:null
*/
public NioEventLoopGroup(int nThreads) {
//继续调用下一个构造方法
this(nThreads, (Executor) null);
}
//中间省略部分构造函数
/**
* RejectedExecutionHandler():RejectedExecutionHandlers.reject()
*/
public NioEventLoopGroup(int nThreads, Executor executor, final SelectorProvider selectorProvider,final SelectStrategyFactory selectStrategyFactory) {
//开始调用父类的构造函数
super(nThreads, executor, selectorProvider, selectStrategyFactory, RejectedExecutionHandlers.reject());
}
一直向下走下去的话,你会发现在 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);
}
综上,我们发现 NioEventLoopGroup
默认的构造函数实际会起的线程数为 **CPU核心数*2**
。
另外,如果你继续深入下去看构造函数的话,你会发现每个NioEventLoopGroup
对象内部都会分配一组NioEventLoop
,其大小是 nThreads
, 这样就构成了一个线程池, 一个NIOEventLoop
和一个线程相对应,这和我们上面说的 EventloopGroup
和 EventLoop
关系这部分内容相对应。
Reactor 线程模型
👨💻面试官 :大部分网络框架都是基于 Reactor 模式设计开发的。你先聊聊 Reactor 线程模型吧!
🙋 我 :好的呀!
Reactor 是一种经典的线程模型,Reactor 模式基于事件驱动,特别适合处理海量的 I/O 事件。
Reactor 线程模型分为单线程模型、多线程模型以及主从多线程模型。
以下图片来源于网络,原出处不明,如有侵权请联系我。
单线程 Reactor
所有的 IO 操作都由同一个 NIO 线程处理。
单线程 Reactor 的优点是对系统资源消耗特别小,但是,没办法支撑大量请求的应用场景并且处理请求的时间可能非常慢,毕竟只有一个线程在工作嘛!所以,一般实际项目中不会使用单线程 Reactor 。
为了解决这些问题,演进出了 Reactor 多线程模型。
多线程 Reactor
一个线程负责接受请求,一组 NIO 线程处理 IO 操作。
大部分场景下多线程 Reactor 模型是没有问题的,但是在一些并发连接数比较多(如百万并发)的场景下,一个线程负责接受客户端请求就存在性能问题了。
为了解决这些问题,演进出了主从多线程 Reactor 模型。