老生常谈NIO,我们再来聊一聊关于NIO的故事和一些用法!

Posted wx61cd7f82847ee

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了老生常谈NIO,我们再来聊一聊关于NIO的故事和一些用法!相关的知识,希望对你有一定的参考价值。




一 . 前言

NIO 是老生常谈了 , 由于最近准备开 Netty 的新坑了 , 不再局限于使用 , 初期先把前置的知识点回顾一下

二 . NIO 的概念

2.1 NIO 是什么 ?

NIO 可以从2个维度说 ,它既可以是一种设计模型 ,也可以说是 Java 中的一个包 (NIO 包是在 java1.4中引入的).

IO 是计算机与其他部分之间的接口 , IO 可以分为多种 : BIO ,NIO , AIO


NIO 模型


2.2 NIO , AIO , BIO 的区别

BIO : 基于流( Stream ) 的阻塞 IO , 也是我们最常见的 IO

NIO : 通过 Selector 实现的基于缓冲区 (Buffer) 的非阻塞式 IO

AIO : 在 NIO 基础上引入了异步的概念


IO 流向和特点:


BIO : 基于 Stream , 单向流转
NIO : 基于 Channel , 双向流通

// PS : Stream 流的特点
- 每次处理一个字节的数据。输入流产生一个字节的数据,输出流消耗一个字节的数据
- 为流式数据创建过滤器非常容易 , 可以方便的创建过滤器链
- 流程更加简洁 , 容易处理复杂的逻辑 , 相对教慢

// PS : NIO的特点
- 事件驱动模型、单线程处理多任务、非阻塞 I/O 、IO 多路复用
- 每个操作在一个步骤中生成或消耗一个数据块
- 基于 block(Buffer) 的传输比基于流的传输更高效
- IO 函数 zero-copy
- 流程相对更复杂

PS : 这里的单线程处理是指只会在一个线程里面通过 selector 来处理逻辑 , 然后由 select 指定具体的 Handler


线程的模式


BIO :一个连接一个线程,客户端的连接请求时服务器端就需要启动一个线程进行处理
NIO :一个请求一个线程,客户端的连接请求都会注册到多路复用器上 , 多路复用轮询到该请求时创建线程
AIO : 一个请求一个线程, 在创建线程时 , 会创建异步线程

2.3 NIO 的组成部分

​Channel​​​ s and ​​Buffer​​ s , 是 NIO 的中心对象,几乎用于每一个 I/O 操作 .


Channels : 频道


  • Channel 是一个对象,可以从中读取数据并向其写入数据
  • 类似于 Stream , 去任何地方(或来自任何地方)的数据都必须通过 Channel 对象
  • 数据可以从 channel 写入buffers ,也可以从 buffers 读取到 channels , channels 的 读写均为单向
  • 通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入
  • 通道可以异步地读写


Buffers : 缓冲区


  • Buffer 是一个 Java 对象
  • 缓冲区本质上是一个数组 , 通常是字节数组 , 也可以是其他数组
  • 缓冲区提供对数据的结构化访问,并且跟踪系统的读/写进程。
  • 发送到通道的所有数据必须首先放置在缓冲区中; 同样,从通道读取的任何数据都被读入缓冲区


Selectors :


  • Selector允许单线程处理多个 Channel , 当打开了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便 .
  • 要使用Selector,得向Selector注册Channel,然后调用它的select()方法。
  • 这个方法会一直阻塞到某个注册的通道有事件就绪。
  • 一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新连接进来,数据接收

2.4 Channel 的主要实现

Channel的类型 :

  • FileChannel — 从文件中读写数据
  • DatagramChannel — 能通过UDP读写网络中的数据
  • SocketChannel — 能通过TCP读写网络中的数据
  • ServerSocketChannel — 可以监听新进来的TCP连接,像Web服务器那样

[图片上传失败…(image-e7cd6e-1628236117379)]

2.5 Buffer 的主要实现

Buffer 的主要实现 : ByteBuffer / CharBuffer / DoubleBuffer / FloatBuffer / IntBuffer / LongBuffer / ShortBuffer

每个 Buffer 类都是 Buffer 接口的一个实例。除了 ByteBuffer 之外(基础对象),每一个都具有完全相同的操作,只是所处理的数据类型不同。


Buffer 方法简述


- array() : 返回返回该缓冲区的数组
- arrayOffset() : 返回该缓冲区第一个元素在该缓冲区的支持数组中的偏移量
- capacity() : 返回该缓冲区的容量

- clear() : 清除这个缓冲区
- clear : position将被设回0,limit被设置成 capacity的值

- flip() : 翻转这个缓冲区
- 将Buffer从写模式切换到读模式。调用flip()方法会将position设回0,并将limit设置成之前position的值

- hasArray() : 指示该缓冲区是否由可访问数组支持
- hasRemaining() : 告诉当前位置和极限之间是否有任何元素
- isReadOnly() : 只读
- limit() : 返回该缓冲区的限制
- limit(int newLimit)
- mark() : 将该缓冲区的标记设置在其位置 , reset() 配合使用
- position() : 返回缓冲区的位置
- position(int newPosition)
- remaining() : 返回当前位置和限制之间的元素数
- reset() : 将该缓冲区的位置重置为先前标记的位置

- rewind() : 倒带这个缓冲区
- Buffer.rewind()将position设回0,所以你可以重读Buffer中的所有数据。
- limit保持不变,仍然表示能从Buffer中读取多少个元素

// 补充 : mark()与reset()方法
- 通过调用Buffer.mark()方法,可以标记Buffer中的一个特定position。
- 通过调用Buffer.reset()方法恢复到这个position


要点 :


当向buffer写入数据时,buffer会记录下写了多少数据。

一旦要读取数据,需要通过flip()方法将Buffer从写模式切换到读模式。

在读模式下,可以读取之前写入到buffer的所有数据


读取完成后 需要清空缓存区 , 任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面 , 此时可以使用如下方法

clear() : 清空整个缓存区

compact() : 清除已经读过的数据


capacity,position 和 limit 三属性


- capacity (总容量)

作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”.你只能往里写capacity个byte、long,char等类型

- position(指针当前位置)

当你写数据到Buffer中时,position表示当前的位置。初始的position值为0

当将Buffer从写模式切换到读模式,position会被重置为0 当从Buffer的position处读取数据时,position向前移动到下一个可读的位置

- limit (读/写边界位置)

在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity 当切换Buffer到读模式时, limit表示你最多能读到多少数据

- mark

用于记录当前 position 的前一个位置或者默认是 0在实际操作数据时它们有如下关系图

参考自 @ ​​一文让你彻底理解 Java NIO 核心组件 - SegmentFault 思否​​ [图片上传失败…(image-65f611-1628236117379)]

在对Buffer进行读/写的过程中,position会往后移动,而 limit 就是 position 移动的边界。

  • 在对Buffer进行写入操作时,limit应当设置为capacity的大小
  • 对Buffer进行读取操作时,limit应当设置为数据的实际结束位置

2.6 Select 的主要实现

三 . Java NIO 使用

3.1 一个 NIO 的简单案例


File 读取


//**使用 buffer 步骤**
1. 写入数据到Buffer
2. 调用flip()方法
3. 从Buffer中读取数据
4. 调用clear()方法或者compact()方法

public void templateRead() throws Exception

logger.info("------> Step 1 : 开启基本案例 , 从 Stream 转换为 <-------");
FileInputStream fin = new FileInputStream("src/main/resources/data/data.txt");
FileChannel fc = fin.getChannel();

logger.info("------> Step 2 : 构建一个缓冲区 <-------");
ByteBuffer buffer = ByteBuffer.allocate(1024);

logger.info("------> Step 3 : 从缓冲区读取数据 <-------");
int bytesRead = fc.read(buffer);
System.out.println("buffer = " + buffer);

logger.info("------> Step 4: 单个字符读取 <-------");
while (bytesRead != -1)
buffer.flip(); //缓冲区准备读取
while (buffer.hasRemaining())
System.out.print((char) buffer.get());// 每次读取一个字节

buffer.clear(); //准备缓冲区写入

// 可以通过屏蔽该语句看效果
bytesRead = fc.read(buffer);


fin.close();


File 写入


public void templateWrite() throws Exception 

logger.info("------> Step 1 : 开启基本案例 , 从 Stream 转换为 <-------");
RandomAccessFile randomAccessFile = new RandomAccessFile("src/main/resources/data/data2.txt", "rw");

FileChannel fc = randomAccessFile.getChannel();

logger.info("------> Step 2 : 构建一个缓冲区 <-------");
ByteBuffer buffer = ByteBuffer.allocate(1024);
byte[] message = new String("Hello World !").getBytes();
for (int i = 0; i < gRPC来聊一聊gRPC认证

我们来聊一聊Https

面向未来,我们来聊一聊什么是现代化数据架构

面向未来,我们来聊一聊什么是现代化数据架构

不吹不黑!我们来聊一聊云原生和容器技术

面试官:我们来聊一聊Redis吧,你了解多少就答多少