IO流 netty

目录

1、Netty是什么?和Tomcat有什么区别?特点是什么?

2、Netty高性能体现在哪些方面?

3、Netty 有哪些应用场景?

4、Netty 核心组件有哪些?分别有什么作用?

5、EventloopGroup 了解么?和 EventLoop 啥关系?

6、Bootstrap 和 ServerBootstrap 了解么?

7、NioEventLoopGroup 默认的构造函数会起多少线程?

8、Netty 服务端和客户端的启动过程了解么?

10、Netty的多路复用通讯方式

11、什么是 TCP 粘包/拆包?有什么解决办法呢?

12、Netty的长连接、心跳机制了解吗?

13、什么是网络滑动窗口?

14、Netty 的零拷贝了解么?

15、Netty的无锁设计和线程绑定

BIO

NIO

AIO

同步阻塞

同步非阻塞:

NIO三大核心组件讲解

 序列化和反序列化(链接)


1、Netty是什么?和Tomcat有什么区别?特点是什么?


Netty是一个基于NIO的异步网络通信框架,提供了TCP/UDP和文件传输的支持,性能高,封装了原生NIO编码的复杂度,开发者可以直接使用Netty来开发高效率的各种网络服务器,并且编码简单。

Tomcat是一个web服务器,是一个Servlet容器,基本上Tomcat内部只能运行Servlet程序,并处理HTTP请求,而Netty封装的是底层IO模型,关注的是网络数据的传输,而不关心具体的协议,可定制性更高。

Netty的特点:

1、异步、NIO的网络通信框架

2、高性能

3、高扩展,高定制性

4、易用性

2、Netty高性能体现在哪些方面?


1、NIO模型,用最小的资源做更多的事情。

2、内存零拷贝,尽量减少不必要的内存拷贝,实现更高效的传输。(零拷贝

3、内存池设计,申请的内存可以重用,只要指直接内存。内部实现是用一颗二叉树查找管理内存分配情况。

4、串行化处理读写:避免使用锁带来的性能开销。即消息的处理尽可能在同一个线程内完成,期间不进行线程的切换,这样就避免了多线程竞争和同步锁。表面上看,串行化设计似乎CPU利用率不搞,并发程度不够。但是,通过调整NIO线程池的线程参数,可以同时启动多个串行化的线程并行运行,这样局部无锁化的串行程序设计相比一个队里多个工作线程模型性能更优秀。

5、高性能序列化协议:支持protobuf等高性能序列化协议

6、高效并发编程的体现:volatile的大量、正确使用;CAS和原子类的广泛使用;线程安全容器的使用;通过读写锁提升并发性能。

3、Netty 有哪些应用场景?


Netty 主要用来做网络通信 :

作为 RPC 框架的网络通信工具 :我们在分布式系统中,不同服务节点之间经常需要相互调用,这个时候就需要 RPC 框架了。不同服务节点之间的通信是如何做的呢?可以使用 Netty 来做。比如我调用另外一个节点的方法的话,至少是要让对方知道我调用的是哪个类中的哪个方法以及相关参数吧!

实现一个自己的 HTTP 服务器 :通过 Netty 我们可以自己实现一个简单的 HTTP 服务器,这个大家应该不陌生。说到 HTTP 服务器的话,作为 Java 后端开发,我们一般使用 Tomcat 比较多。一个最基本的 HTTP 服务器可要以处理常见的 HTTP Method 的请求,比如 POST 请求、GET 请求等等。

实现一个即时通讯系统 :使用 Netty 我们可以实现一个可以聊天类似微信的即时通讯系统,这方面的开源项目还蛮多的,可以自行去 Github 找一找。

实现消息推送系统 :市面上有很多消息推送系统都是基于 Netty 来做的。......

4、Netty 核心组件有哪些?

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<Void> {

Channel channel();

ChannelFuture addListener(GenericFutureListener<? extends Future<? super Void>> var1); ......

ChannelFuture sync();

 throws InterruptedException;

}

另外,我们还可以通过 ChannelFuture 接口的 sync()方法让异步的操作变成同步的。

4.ChannelHandler 和 ChannelPipeline

ChannelHandler 是消息的具体处理器。他负责处理读写操作、客户端连接等事情。

ChannelPipeline 为 ChannelHandler 的链,提供了一个容器并定义了用于沿着链传播入站和出站事件流的 API 。当 Channel 被创建时,它会被自动地分配到它专属的 ChannelPipeline。

我们可以在 ChannelPipeline 上通过 addLast() 方法添加一个或者多个ChannelHandler ,因为一个数据或者事件可能会被多个 Handler 处理。当一个 ChannelHandler 处理完之后就将数据交给下一个 ChannelHandler 。

5、EventloopGroup 了解么?和 EventLoop 啥关系?

 
EventLoopGroup 包含多个 EventLoop(每一个 EventLoop 通常内部包含一个线程), EventLoop 的主要作用实际就是负责监听网络事件并调用事件处理器进行相关 I/O 操作的处理。

并且 EventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理,即 Thread 和 EventLoop 属于 1 : 1 的关系,从而保证线程安全。

上图是一个服务端对 EventLoopGroup 使用的大致模块图,其中 Boss EventloopGroup 用于接收连接,Worker EventloopGroup 用于具体的处理(消息的读写以及其他逻辑处理)。

从上图可以看出:当客户端通过 connect 方法连接服务端时,bossGroup 处理客户端连接请求。当客户端处理完成后,会将这个连接提交给 workerGroup 来处理,然后 workerGroup 负责处理其 IO 相关操作。

6、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 通常使用 connet() 方法连接到远程的主机和端口,作为一个 Netty TCP 协议通信中的客户端。另外,Bootstrap 也可以通过 bind() 方法绑定本地的一个端口,作为 UDP 协议通信中的一端。ServerBootstrap通常使用 bind() 方法绑定本地的端口上,然后等待客户端的连接。Bootstrap 只需要配置一个线程组— EventLoopGroup ,而 ServerBootstrap需要配置两个线程组—EventLoopGroup ,一个用于接收连接,一个用于具体的处理。

7、NioEventLoopGroup 默认的构造函数会起多少线程?


默认的构造函数会启动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);

 }

8、Netty 服务端和客户端的启动过程了解么?


请查看入门教程中的启动流程:netty服务器和客户端的启动过程

9、Netty的线程模型是怎样的?


Netty同时支持Reactor单线程模型、Reactor多线程模型和Reactor主从线程模型,用户可根据启动参数配置在这三种模式中切换。

服务算启动时,通常会创建两个NioEventLoopGroup实例,对应了两个独立的Reactor线程池,bossGroup负责处理客户端的连接请求,workerGroup负责处理I/O相关操作,执行系统Task、定时任务等。用户可根据服务端引导类ServerBootStrap配置参数选择Reactor线程模型,进而最大限度满足用户的定制化需求。

10、Netty的多路复用通讯方式


Netty 的 IO 线程 NioEventLoop 由于聚合了多路复用器 Selector,可以同时并发处理成百上千个
客户端 Channel,由于读写操作都是非阻塞的,这就可以充分提升 IO 线程的运行效率,避免由于
频繁 IO 阻塞导致的线程挂起。

服务端:

客户端:

11、什么是 TCP 粘包/拆包?有什么解决办法呢?


粘包

现象,发送 abc def,接收 abcdef
原因
应用层:接收方 ByteBuf 设置太大(Netty 默认 1024)
滑动窗口:假设发送方 256 bytes 表示一个完整报文,但由于接收方处理不及时且窗口大小足够大,这 256 bytes 字节就会缓冲在接收方的滑动窗口中,当滑动窗口中缓冲了多个报文就会粘包
Nagle 算法:会造成粘包
半/拆包

现象,发送 abcdef,接收 abc def
原因
应用层:接收方 ByteBuf 小于实际发送数据量
滑动窗口:假设接收方的窗口只剩了 128 bytes,发送方的报文大小是 256 bytes,这时放不下了,只能先发送前 128 bytes,等待 ack 后才能发送剩余部分,这就造成了半包
MSS 限制:当发送的数据超过 MSS 限制后,会将数据切分发送,就会造成半包
本质是因为 TCP 是流式协议,消息无边界

解决思路:

短链接,发一个包建立一次连接,这样连接建立到连接断开之间就是消息的边界,缺点效率太低
每一条消息采用固定长度,缺点浪费空间
每一条消息采用分隔符,例如 \n,缺点需要转义
每一条消息分为 head 和 body,head 中包含 body 的长度
解决方法:

a、使用 Netty 自带的解码器

FixedLengthFrameDecoder: 固定长度解码器,它能够按照指定的长度对消息进行相应的拆包。

DelimiterBasedFrameDecoder : 可以自定义分隔符解码器,

LineBasedFrameDecoder 实际上是一种特殊的 DelimiterBasedFrameDecoder 解码器。发送端发送数据包的时候,每个数据包之间以换行符作为分隔,LineBasedFrameDecoder 的工作原理是它依次遍历 ByteBuf 中的可读字节,判断是否有换行符,然后进行相应的截取。LengthFieldBasedFrameDecoder:预设长度解码器

b、自定义解码器

除了上面的自带解码器之外,如果还有特殊需求,我们可以自定义解码器。

12、Netty的长连接、心跳机制了解吗?


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 。

13、什么是网络滑动窗口?


TCP 以一个段(segment)为单位,每发送一个段就需要进行一次确认应答(ack)处理,但如果这么做,缺点是包的往返时间越长性能就越差


为了解决此问题,引入了窗口概念,窗口大小即决定了无需等待应答而可以继续发送的数据最大值

 


窗口实际就起到一个缓冲区的作用,同时也能起到流量控制的作用
图中深色的部分即要发送的数据,高亮的部分即窗口
窗口内的数据才允许被发送,当应答未到达前,窗口必须停止滑动
如果 1001~2000 这个段的数据 ack 回来了,窗口就可以向前滑动
接收方也会维护一个窗口,只有落在窗口内的数据才能允许接收


14、Netty 的零拷贝了解么?

在 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 方式导致的内存拷贝问题.

15、Netty的无锁设计和线程绑定


Netty 采用了串行无锁化设计,在 IO 线程内部进行串行操作,避免多线程竞争导致的性能下降。表面上看,串行化设计似乎 CPU 利用率不高,并发程度不够。但是,通过调整 NIO 线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列多个工作线程模型性能更优。

Netty 的 NioEventLoop 读取到消息之后,直接调用 ChannelPipeline 的fireChannelRead(Object msg),只要用户不主动切换线程,一直会由 NioEventLoop 调用到用户的 Handler,期间不进行线程切换,这种串行化处理方式避免了多线程操作导致的锁的竞争,从性能角度看是最优的。
 

BIO

传统的网络通讯模型,就是BIO,同步阻塞IO

它其实就是服务端创建一个ServerSocket, 然后就是客户端用一个Socket去连接服务端的那个ServerSocket, ServerSocket接收到了一个的连接请求就创建一个Socket和一个线程去跟那个Socket进行通讯。

接着客户端和服务端就进行阻塞式的通信,客户端发送一个请求,服务端Socket进行处理后返回响应。

在响应返回前,客户端那边就阻塞等待,什么事情也做不了。

这种方式的缺点:每次一个客户端接入,都需要在服务端创建一个线程来服务这个客户端

这样大量客户端来的时候,就会造成服务端的线程数量可能达到了几千甚至几万,这样就可能会造成服务端过载过高,最后崩溃死掉。

BIO模型图:

Acceptor:

传统的IO模型的网络服务的设计模式中有俩种比较经典的设计模式:一个是多线程, 一种是依靠线程池来进行处理。

如果是基于多线程的模式来的话,就是这样的模式,这种也是Acceptor线程模型。

NIO

NIO是一种同步非阻塞IO, 基于Reactor模型来实现的。

其实相当于就是一个线程处理大量的客户端的请求,通过一个线程轮询大量的channel,每次就获取一批有事件的channel,然后对每个请求启动一个线程处理即可。

这里的核心就是非阻塞,就那个selector一个线程就可以不停轮询channel,所有客户端请求都不会阻塞,直接就会进来,大不了就是等待一下排着队而已。

这里面优化BIO的核心就是,一个客户端并不是时时刻刻都有数据进行交互,没有必要死耗着一个线程不放,所以客户端选择了让线程歇一歇,只有客户端有相应的操作的时候才发起通知,创建一个线程来处理请求。

Reactor模型:

AIO

AIO:异步非阻塞IO,基于Proactor模型实现。

每个连接发送过来的请求,都会绑定一个Buffer,然后通知操作系统去完成异步的读,这个时间你就可以去做其他的事情

等到操作系统完成读之后,就会调用你的接口,给你操作系统异步读完的数据。这个时候你就可以拿到数据进行处理,将数据往回写

在往回写的过程,同样是给操作系统一个Buffer,让操作系统去完成写,写完了来通知你。

这俩个过程都有buffer存在,数据都是通过buffer来完成读写。

这里面的主要的区别在于将数据写入的缓冲区后,就不去管它,剩下的去交给操作系统去完成。

操作系统写回数据也是一样,写到Buffer里面,写完后通知客户端来进行读取数据。

AIO:模型图

同步阻塞

为什么说BIO是同步阻塞的呢?

其实这里说的不是针对网络通讯模型而言,而是针对磁盘文件读写IO操作来说的。

因为用BIO的流读写文件,例如FileInputStrem,是说你发起个IO请求直接hang死,卡在那里,必须等着搞完了这次IO才能返回。

同步非阻塞:

为什么说NIO为啥是同步非阻塞?

因为无论多少客户端都可以接入服务端,客户端接入并不会耗费一个线程,只会创建一个连接然后注册到selector上去,这样你就可以去干其他你想干的其他事情了

一个selector线程不断的轮询所有的socket连接,发现有事件了就通知你,然后你就启动一个线程处理一个请求即可,这个过程的话就是非阻塞的。

但是这个处理的过程中,你还是要先读取数据,处理,再返回的,这是个同步的过程。

NIO三大核心组件讲解

 序列化和反序列化(链接


  **对象序列化**:将内存中保存的对象变为二进制数据流的形式进行传输,或者是将其保存在文本中。
  简单来说:
   **序列化**:把对象转换为字节序列的过程
   **反序列化**:把字节序列恢复为对象的过程
  **序列化的用途**:
   1、把对象的字节序列永久的保存在硬盘上(放在一个文件中),要取出的时候再反序列化为对象即可。
   2、网络上传输对象(对象->字节序列->网络传输(字节)->字节序列->对象)    序列化与反序列化的前提
   需要被序列化的类必须实现java.io.Serializable接口。
  **对象输出流**:ObjectOutputStream,使用writeObject(Object obj)方法可对obj对象进行序列化,将得到的字节序列写入一个当前程序的输出流中。对象输出流需要和其他输出流(内存流/文件流…)配合使用。
  **对象输入流**:ObjectInputStream,使用readObject()方法从一个源输入流中读取字节序列,再将该字节序列反序列化为一个对象,并将该对象返回。对象输入流也需要和其他输入流(内存流/文件流…)搭配使用。
  **对象序列化的过程:**
   1、创建一个对象输出流,它可以包装一个其他类型的输出流(eg:文件输出流)
   2、通过对象输出流的writeObject()将对象->字节序列。
  **对象反序列化的过程:**
   1、 创建一个对象输入流,它可以包装一个其他类型的源输入流(eg:文件输入流)
   2、通过对象输入流的readObject()将字节序列->对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值