Java NIO详解一[Netty系列]

Java NIO详解一(Netty系列)

IO与NIO的区别

java.io:以阻塞的方式处理输入输出。
java.nio :以非阻塞的方式处理IO操作。
.
java.io 中最为核心的概念是流(Stream),面向流的编程。Java中,一个流要么是输入流,要么是输出流,不可能同时及是输入流又是输出流。
java.nio中拥有3个核心概念:Selector、Channel与Buffer。在java.nio中,我们是面向块(block)或是缓冲区(buffer)编程的。Buffer本身就是一块内存,底层实现上,它实际上是个数组,数据的读、写都是通过Buffer来实现的。
.
io中数据直接从stream流读到我们的程序中。
nio中是从channel中读到buffer中,之后我们再读取buffer(数组)中的内容,读到到我们的程序中。我们还可以将数据写回到buffer中。buffer不仅可以读还可以回写。 进行读写切换之前必须调用filp()方法。

Java NIO详解一[Netty系列]_第1张图片

如图:一个线程对应了一个selector,而一个selector对应了三个channel,每个channel又分别对应了一个Buffer。一个线程可以不断的在三个channel上来回切换,具体某一时刻切换到哪?通过事件决定的,在NIO中Event(事件)是一个非常重要的概念。【类似于我们客户端通过Ajax编程的时候,通过事件来判断并执行一个一个事情。】

什么是Channel? 可以将nio中的Channel理解为io中的stream【通道】


除了数组之外,Buffer还提供了对于数据的结构化访问方式,并且可以追踪到系统的读写过程。

Java中的7种原生数据类型都有各自对应的Buffer类型,如IntBuffer、LongBuffer、ByteBuffer及CharBuffer等等。并没有BooleanBuffer

Channel指的是可以向其写入数据或是从中读取数据的对象,它类似于java.io中的Stream。

所有数据的读写都是通过Buffer来进行的,永远不会出现直接向Channel写入数据的情况,或是直接从Channel读取数据的情况。

与Stream不同的是,Channel是双向的,一个流只可能是InputStream或是OutputStream,

Channel打开后则可以进行读取、写入或是读写。

由于Channel是双向的,因此它能更好地反映出底层操作系统的真实情况;在Linux系统中,底层操作系统的通道就是双向的。

关于NIO Buffer中的3个重要状态属性的含义:position,limit与capacity。

0 <= mark <= position <= limit <= capacity

通过NIO读取文件设计到3个步骤

  1. 从FileInputStream获取FileChannel对象
  2. 创建Buffer
  3. 将数据从Channel读取到Buffer中

让我们来看看Buffer源码中给出的定义

Buffer是一个抽象类不能通过new创建。
Java NIO详解一[Netty系列]_第2张图片

  1. 由此我们可以看出,Buffer其实是一个容器,它用来包装原生的数据类型,比如说int类型包装过后就是IntBuffer。
  2. Buffer本质的属性是capacity、limit、position
  3. capacity的定义:是Buffer包含的元素的数量。缓冲区的容量永远不会为负,也永远不会改变。
  4. limit的定义:不应该被读取或写入的第一个元素的索引。limit永远不会是负的,也永远不会大于Buffer的容量。
  5. position的定义:是下一个要读取或写入的元素的索引。position永远不会是负的,也永远不会大于Buffer的限制。
#相对操作
相对操作:从当前位置开始读取或写入一个或多个元素,然后按所转移的元素数量增加位置。如果请求的传输超过了限制,那么相对get操作抛出{@link BufferUnderflowException},相对put操作抛出{@link BufferOverflowException};在这两种情况下,都不会传输数据。
#绝对操作
绝对操作:采用显式元素索引,不影响位置。绝对get和put操作抛出一个
IndexOutOfBoundsException}如果索引参数超过限制。
当然,数据也可以通过通道的I/O操作(总是相对于当前位置)被传送到缓冲区或从缓冲区中传送出去。

mark与reset

mark和reset:当调用reset()方法时,缓冲区的mark是将其位置重置到的索引。mark并不总是被定义的,但当它被定义时,它永远不会为负,也不会比position大。如果marker已定义,则在将位置或限制调整为小于标记的值时将丢弃标记。如果标记没有定义,那么调用reset()方法会导致抛出InvalidMarkException。
#不变性
mark、position、limit和capacity值不变:

新创建的缓冲区的position总是为零,mark为undefined。初始limit可以是零,也可以是其他一些值,这取决于缓冲区的类型和构造方式。新分配的缓冲区中的每个元素都被初始化为0。

clear、flip、rewind

除了postion、limit和capacity以及mark和reset的方法,这个类还定义了对缓冲区的以下操作:
#clear
用于清空数据,其实就是将指针恢复初始值
将 limit 设置为 capacity ,position 设置为 0
#flip
反转,用于读写切换
将 limit 设置为当前的 position 将 position 设置为 0
#rewind
用于为重新读取数据做准备
将 position 设置为 0 ,limit 不变,mark 丢弃

线程安全性

对于多个并发线程使用缓冲区是不安全的。如果一个缓冲区被多个线程使用,那么对缓冲区的访问应该由synchronization进行控制

首选我们来看一下由于创建Buffer的静态方法 allocate

ByteBuffer byteBuffer = ByteBuffer.allocate(10);
 /**
 * 分配一个新的Buffer
 * 默认position为0,limit为我们传入的capacity,到目前位置没有定义mark,
 * 每个元素都将被初始化为0,(limit和capacity不包括).
 * 数据其实存放在数组中,数组的偏移量为0
 * 返回的对象是 HeapByteBuffer(堆字节缓冲区)。 
 */
public static ByteBuffer allocate(int capacity) {
    if (capacity < 0)
        throw new IllegalArgumentException();
    return new HeapByteBuffer(capacity, capacity);
}

我们发现它返回的对象不是自身 ByteBuffer 而是 HeapByteBuffer,因为 ByteBuffer 是一个abstract类。抽象类不能被实例化

哈哈哈这时候会不会有人说你这里不是用的 ByteBuffer类型的引用嘛,为什么还说还是不能被实例化呢? 这时候就需要去了解一下面向接口的编程了。

让我们来看一下 HeapByteBuffer

/**
* read/write HeapByteBuffer. 是一个可读可写的Buffer
* HeapByteBuffer继承自ByteBuffer,并且没有权限修饰符(不能通过new关键字实例化), * 所以这里是通过return返回的
*/
class HeapByteBuffer
    extends ByteBuffer{
}

HeapByteBuffer 在构造方法中通过super调用父类(ByteBuffer)的构造,然后 ByteBuffer又通过super调用父类(Buffer)的构造 -> 最终就是完成一些列的初始化。 很容易理解看一遍就懂了。

说了这么多,那Buffer是如何放至元素的呢?

//当我们看完put的逻辑就知道Buffer是如何放置元素的啦。。。
//注意是HeapByteBuffer中的put,因为我们实际使用的是HeapByteBuffer
//put调用
byteBuffer.put(new byte[1]);
----------------------------------分割线---------------------------------
//put源码
//hb是一个数组,里面存放的是Buffer的数据
//
public ByteBuffer put(byte x) {
    hb[ix(nextPutIndex())] = x;
    return this;
}

//nextPutIndex()方法和ix方法就是找位置 就像我们平常使用的 arr[index] = 0
final int nextPutIndex() {                          
    if (position >= limit)
        throw new BufferOverflowException();
    return position++;
}
//offset 偏移量
protected int ix(int i) {
    return i + offset;
}

图解

Java NIO详解一[Netty系列]_第3张图片

下面来看一下Buffer中常用的方法。

Mark

Java NIO详解一[Netty系列]_第4张图片

Reset

Java NIO详解一[Netty系列]_第5张图片

Clear

Java NIO详解一[Netty系列]_第6张图片

Flip

Java NIO详解一[Netty系列]_第7张图片

我们来看一段程序

光看源码还是不太明白怎么办?下面我们来看一段程序

public static void main(String[] args) throws IOException {
    FileInputStream fileInputStream = new FileInputStream("2021_fc.txt");
    FileChannel fileChannel = fileInputStream.getChannel();

    ByteBuffer byteBuffer = ByteBuffer.allocate(10);
    fileChannel.read(byteBuffer);

    byteBuffer.flip();
    while (byteBuffer.remaining()>0){
        byte b = byteBuffer.get();
        System.out.println("Character: " + (char)b);
    }
    fileInputStream.close();
}

图解

Java NIO详解一[Netty系列]_第8张图片

fileChannel.read(byteBuffer); 往buffer中读取数据,假设我们的2021_fc.txt中只有五个字节。那么如图 ->

Java NIO详解一[Netty系列]_第9张图片

byteBuffer.flip(); 调用flip,进行数据翻转。各属性指向如图 ->

Java NIO详解一[Netty系列]_第10张图片

将limt指向position,将position置为0,而Capacity不会发生改变,永远为初始化时的容量大小。

那么如果不调用flip()方法,会发生什么呢?

我的猜想是position会从索引5开始继续往后写,但是因为已经没有数据了所以都是为空

让我们来实际看看程序的输出是不是如我猜想的那样吧 ->

public static void main(String[] args) throws IOException {
    FileInputStream fileInputStream = new FileInputStream("2021_fc.txt");
    FileChannel fileChannel = fileInputStream.getChannel();
    ByteBuffer byteBuffer = ByteBuffer.allocate(10);
    fileChannel.read(byteBuffer);
    System.out.println("flip之前 limit值:" + byteBuffer.limit());
    //byteBuffer.flip();
    System.out.println("flop之后 limit值:" + byteBuffer.limit());
    while (byteBuffer.remaining()>0){
        System.out.println("limit: " + byteBuffer.limit());
        System.out.println("position: " + byteBuffer.position());
        System.out.println("capacity: " + byteBuffer.capacity());
        byte b = byteBuffer.get();
        System.out.println("Character: " + (char)b);
    }
    fileInputStream.close();
}
--------------------------------输出结果---------------------------------
limit: 10
position: 5
capacity: 10
Character:  
......
limit: 10
position: 9
capacity: 10
Character:  

正如我们猜想的那样, 调用flip的输出结果 ->

limit: 5
position: 0
capacity: 10
Character: A
limit: 5
position: 1
capacity: 10
Character: B
limit: 5
position: 2
capacity: 10
Character: C
limit: 5
position: 3
capacity: 10
Character: D
limit: 5
position: 4
capacity: 10
Character: E

Rewind

Java NIO详解一[Netty系列]_第11张图片
我想懂了flip之后其他的方法执行逻辑就很简单啦。
Java NIO详解一[Netty系列]_第12张图片

你可能感兴趣的:(Java,Netty)