面试宝典到手,搞定面试,不再是难题,系列文章传送地址,请点击本链接。
目录
1、Netty是什么?和Tomcat有什么区别?特点是什么?
2、Netty高性能体现在哪些方面?
3、Netty 有哪些应用场景?
4、Netty 核心组件有哪些?分别有什么作用?
5、EventloopGroup 了解么?和 EventLoop 啥关系?
6、Bootstrap 和 ServerBootstrap 了解么?
7、NioEventLoopGroup 默认的构造函数会起多少线程?
8、Netty 服务端和客户端的启动过程了解么?
9、Netty的线程模型是怎样的?
10、Netty的多路复用通讯方式
11、什么是 TCP 粘包/拆包?有什么解决办法呢?
12、Netty的长连接、心跳机制了解吗?
13、什么是网络滑动窗口?
14、Netty 的零拷贝了解么?
15、Netty的无锁设计和线程绑定
16、BIO、NIO和AIO的区别?
Netty是一个基于NIO的异步网络通信框架,提供了TCP/UDP和文件传输的支持,性能高,封装了原生NIO编码的复杂度,开发者可以直接使用Netty来开发高效率的各种网络服务器,并且编码简单。
Tomcat是一个web服务器,是一个Servlet容器,基本上Tomcat内部只能运行Servlet程序,并处理HTTP请求,而Netty封装的是底层IO模型,关注的是网络数据的传输,而不关心具体的协议,可定制性更高。
Netty的特点:
1、异步、NIO的网络通信框架
2、高性能
3、高扩展,高定制性
4、易用性
1、NIO模型,用最小的资源做更多的事情。
2、内存零拷贝,尽量减少不必要的内存拷贝,实现更高效的传输。(零拷贝)
3、内存池设计,申请的内存可以重用,只要指直接内存。内部实现是用一颗二叉树查找管理内存分配情况。
4、串行化处理读写:避免使用锁带来的性能开销。即消息的处理尽可能在同一个线程内完成,期间不进行线程的切换,这样就避免了多线程竞争和同步锁。表面上看,串行化设计似乎CPU利用率不搞,并发程度不够。但是,通过调整NIO线程池的线程参数,可以同时启动多个串行化的线程并行运行,这样局部无锁化的串行程序设计相比一个队里多个工作线程模型性能更优秀。
5、高性能序列化协议:支持protobuf等高性能序列化协议
6、高效并发编程的体现:volatile的大量、正确使用;CAS和原子类的广泛使用;线程安全容器的使用;通过读写锁提升并发性能。
Netty 主要用来做网络通信 :
作为 RPC 框架的网络通信工具 :我们在分布式系统中,不同服务节点之间经常需要相互调用,这个时候就需要 RPC 框架了。不同服务节点之间的通信是如何做的呢?可以使用 Netty 来做。比如我调用另外一个节点的方法的话,至少是要让对方知道我调用的是哪个类中的哪个方法以及相关参数吧!实现一个自己的 HTTP 服务器 :通过 Netty 我们可以自己实现一个简单的 HTTP 服务器,这个大家应该不陌生。说到 HTTP 服务器的话,作为 Java 后端开发,我们一般使用 Tomcat 比较多。一个最基本的 HTTP 服务器可要以处理常见的 HTTP Method 的请求,比如 POST 请求、GET 请求等等。
实现一个即时通讯系统 :使用 Netty 我们可以实现一个可以聊天类似微信的即时通讯系统,这方面的开源项目还蛮多的,可以自行去 Github 找一找。
实现消息推送系统 :市面上有很多消息推送系统都是基于 Netty 来做的。......
Netty核心组件包括:channel、
1.Channel
Channel 接口是 Netty 对网络操作抽象类,它除了包括基本的 I/O 操作,如 bind()、connect()、read()、write() 等。
比较常用的Channel接口实现类是NioServerSocketChannel(服务端)和NioSocketChannel(客户端),这两个 Channel 可以和 BIO 编程模型中的ServerSocket以及Socket两个概念对应上。Netty 的 Channel 接口所提供的 API,大大地降低了直接使用 Socket 类的复杂性。
2.EventLoop
“EventLoop 定义了 Netty 的核心抽象,用于处理连接的生命周期中所发生的事件”,是不是很难理解?说白了,EventLoop 的主要作用实际就是负责监听网络事件并调用事件处理器进行相关 I/O 操作的处理。
Channel 为 Netty 网络操作(读写等操作)抽象类,EventLoop 负责处理注册到其上的Channel 处理 I/O 操作,两者配合参与 I/O 操作。
3.ChannelFuture
Netty 是异步非阻塞的,所有的 I/O 操作都为异步的。因此,我们不能立刻得到操作是否执行成功,但是,你可以通过 ChannelFuture 接口的 addListener() 方法注册一个 ChannelFutureListener,当操作执行成功或者失败时,监听就会自动触发返回结果。并且,你还可以通过ChannelFuture 的 channel() 方法获取关联的Channel。
public interface ChannelFuture extends Future Channel channel(); ChannelFuture addListener(GenericFutureListener> var1); ...... ChannelFuture sync(); throws InterruptedException; } |
另外,我们还可以通过 ChannelFuture 接口的 sync()方法让异步的操作变成同步的。
4.ChannelHandler 和 ChannelPipeline
ChannelHandler 是消息的具体处理器。他负责处理读写操作、客户端连接等事情。
ChannelPipeline 为 ChannelHandler 的链,提供了一个容器并定义了用于沿着链传播入站和出站事件流的 API 。当 Channel 被创建时,它会被自动地分配到它专属的 ChannelPipeline。
我们可以在 ChannelPipeline 上通过 addLast() 方法添加一个或者多个ChannelHandler ,因为一个数据或者事件可能会被多个 Handler 处理。当一个 ChannelHandler 处理完之后就将数据交给下一个 ChannelHandler 。
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 是客户端的启动引导类/辅助类,具体使用方法如下:
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 通常使用 connet() 方法连接到远程的主机和端口,作为一个 Netty TCP 协议通信中的客户端。另外,Bootstrap 也可以通过 bind() 方法绑定本地的一个端口,作为 UDP 协议通信中的一端。ServerBootstrap通常使用 bind() 方法绑定本地的端口上,然后等待客户端的连接。Bootstrap 只需要配置一个线程组— EventLoopGroup ,而 ServerBootstrap需要配置两个线程组—EventLoopGroup ,一个用于接收连接,一个用于具体的处理。
默认的构造函数会启动CPU核心数*2个线程。我们可以从下面内容分析得出:
回顾我们在上面写的服务器端的代码:
// 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); } |
请查看入门教程中的启动流程:netty服务器和客户端的启动过程
Netty同时支持Reactor单线程模型、Reactor多线程模型和Reactor主从线程模型,用户可根据启动参数配置在这三种模式中切换。
服务算启动时,通常会创建两个NioEventLoopGroup实例,对应了两个独立的Reactor线程池,bossGroup负责处理客户端的连接请求,workerGroup负责处理I/O相关操作,执行系统Task、定时任务等。用户可根据服务端引导类ServerBootStrap配置参数选择Reactor线程模型,进而最大限度满足用户的定制化需求。
Netty 的 IO 线程 NioEventLoop 由于聚合了多路复用器 Selector,可以同时并发处理成百上千个
客户端 Channel,由于读写操作都是非阻塞的,这就可以充分提升 IO 线程的运行效率,避免由于
频繁 IO 阻塞导致的线程挂起。
服务端:
客户端:
粘包
半/拆包
本质是因为 TCP 是流式协议,消息无边界
解决思路:
解决方法:
a、使用 Netty 自带的解码器
FixedLengthFrameDecoder: 固定长度解码器,它能够按照指定的长度对消息进行相应的拆包。
DelimiterBasedFrameDecoder : 可以自定义分隔符解码器,
LineBasedFrameDecoder 实际上是一种特殊的 DelimiterBasedFrameDecoder 解码器。发送端发送数据包的时候,每个数据包之间以换行符作为分隔,LineBasedFrameDecoder 的工作原理是它依次遍历 ByteBuf 中的可读字节,判断是否有换行符,然后进行相应的截取。LengthFieldBasedFrameDecoder:预设长度解码器
b、自定义解码器
除了上面的自带解码器之外,如果还有特殊需求,我们可以自定义解码器。
Netty的长短连接:
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 。
维基百科是这样介绍零拷贝的:
“零复制(英语:Zero-copy;也译零拷贝)技术是指计算机执行操作时,CPU 不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省 CPU 周期和内存带宽。 |
在 OS 层面上的 Zero-copy 通常指避免在 用户态(User-space) 与 内核态(Kernel-space) 之间来回拷贝数据。而在 Netty 层面 ,零拷贝主要体现在对于数据操作的优化。
1、使用 Netty 提供的 CompositeByteBuf 类, 可以将多个ByteBuf 合并为一个逻辑上的 ByteBuf, 避免了各个 ByteBuf 之间的拷贝。
2、ByteBuf 支持 slice 操作, 因此可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf, 避免了内存的拷贝。
3、通过 FileRegion 包装的FileChannel.tranferTo 实现文件传输, 可以直接将文件缓冲区的数据发送到目标 Channel, 避免了传统通过循环 write 方式导致的内存拷贝问题.
Netty 采用了串行无锁化设计,在 IO 线程内部进行串行操作,避免多线程竞争导致的性能下降。表面上看,串行化设计似乎 CPU 利用率不高,并发程度不够。但是,通过调整 NIO 线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列多个工作线程模型性能更优。
Netty 的 NioEventLoop 读取到消息之后,直接调用 ChannelPipeline 的fireChannelRead(Object msg),只要用户不主动切换线程,一直会由 NioEventLoop 调用到用户的 Handler,期间不进行线程切换,这种串行化处理方式避免了多线程操作导致的锁的竞争,从性能角度看是最优的。
详细见:https://blog.csdn.net/wanghaiping1993/article/details/124907695
面试宝典在手,搞定大厂面试,不再是难题,系列文章传送地址,请点击本链接。https://blog.csdn.net/wanghaiping1993/article/details/125075785