• Java NIO Buffer(netty源码死磕1.2)


    【基础篇】netty源码死磕1.2: 

    NIO Buffer


    1. Java NIO Buffer

    Buffer是一个抽象类,位于java.nio包中,主要用作缓冲区。Buffer缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。

    注意:Buffer是非线程安全类。

    1.1. Buffer类型的标记属性

    Buffer在内部也是利用byte[]作为内存缓冲区,只不过多提供了一些标记变量属性而已。当多线程访问的时候,可以清楚的知道当前数据的位置。

    有三个重要的标记属性:capacity、position、limit。

    除此之外,还有一个标记属性:mark,可以临时保持一个特定的position,需要的时候,可以恢复到这个位置。

    1.1.1. capacity

    作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”。你只能往里写capacity个数据。一旦Buffer满了,就不能再写入。

    capacity与缓存的数据类型相关。指的不是内存的字节的数量,而是写入的对象的数量。比如使用的是一个保存double类型的Buffer(DoubleBuffer),写入的数据是double类型, 如果其 capacity 是100,那么我们最多可以写入100个 double 数据.

    capacity一旦初始化,就不能不会改变。

    原因是什么呢?

    Buffer对象在初始化时,会按照capacity分配内部的内存。内存分配好后,大小就不能变了。分配内存时,一般使用Buffer的抽象子类ByteBuffer.allocate()方法,实际上是生成ByteArrayBuffer类。

    1.1.2. position

    position表示当前的位置。position在Buffer的两种模式下的值是不同的。

    读模式下的position的值为:

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

    写模式下的position的值为:

    在写模式下,当写数据到Buffer中时,position表示当前的写入位置。初始的position值为0,position最大可为capacity – 1。

    每当一个数据(byte、long等)写到Buffer后, position会向后移动到下一个可插入数据的可写的位置。

    1.1.3. limit

    limit表示最大的限制。在Buffer的两种模式下,limit的值是不同的。

    读模式下的limit的值为:

    读模式下,Buffer的limit表示最多能从Buffer里读多少数据。当Buffer从写切换到读模式时,limit的值,设置成写模式的position 值,也是是写模式下之前写入的数量值。

    举一个简单的例子,说明一下读模式下的limit值:

    先向Buffer写数据,Buffer在写模式。每写入一个数据,position向后面移动一个位置,值加一。假定写入了5个数,当写入完成后,position的值为5。这时,就可以读取数据了。当开始读取数据时,Buffer切换到读模式。limit的值,先会被设置成写入数据时的position值。这里是5,表示可以读取的最大限制是5个数。

    写模式下的limit的值为:

    limit表示表示可以写入的数据最大限制。在切换成写模式时,limit的值会被更改,设置成Buffer的capacity,为Buffer的容量。

    1.1.4. 总结:

    在Buffer的四个属性之间,有一个简单的数量关系,如下:

    capacity>=limit>=position>=mark>=0

    用一个表格,对着4个属性的进行一下对比:

    属性

    描述

    capacity

    容量,即可以容纳的最大数据量;在缓冲区创建时被设定并且不能改变

    limit

    上界,缓冲区中当前数据量

    position

    位置,下一个要被读或写的元素的索引

    mark(位置标记)

    调用mark(pos)来设置mark=pos,再调用reset()可以让position恢复到标记的位置即position=mark

    1.2. Buffer 类型

    在NIO中主要有八种缓冲区类,分别如下:

    ByteBuffer

    CharBuffer

    DoubleBuffer

    FloatBuffer

    IntBuffer

    LongBuffer

    ShortBuffer

    MappedByteBuffer

    wps6DEB.tmp



    这些 Buffer 覆盖了能从 IO 中传输的所有的 Java 基本数据类型。其中MappedByteBuffer是专门用于内存映射的一种ByteBuffer)。

    1.3. Buffer中的方法

    本节结合Buffer的几个方法,做了一个完整的实例,包含了从Buffer实例的获取、写入、读取、重复读、标记和重置等一个系列操作的完整流程。

    1.3.1. 获取allocate()方法

    为了获取一个 Buffer 对象,我们首先需要分配内存空间。分配内存空间使用allocate()方法。

    public static void allocatTest()
    {
    byteBuffer = IntBuffer.allocate(20);
    Logger.info("------------after allocate------------------");
    Logger.info("position=" + byteBuffer.position());
    Logger.info("limit=" + byteBuffer.limit());
    Logger.info("capacity=" + byteBuffer.capacity());
    }
    
    byteBuffer = IntBuffer.allocate(20);
    

    这里我们分配了20* sizeof(int)字节的内存空间.

    输出的结果如下:

           main |>  分配内存
    
             allocatTest |>  ------------after allocate------------------
    
             allocatTest |>  position=0
    
             allocatTest |>  limit=20
    
             allocatTest |>  capacity=20
    

    通过结果,可以看到Buffer属性的值。

    1.3.2. 写put()方法

    调用allocate分配内存后,buffer处于写模式。可以通过buffer的put方法写入数据。put方法有一个要求,需要写入的数据类型与Buffer的类型一致。

    接着前面的例子,继续上写入的实例代码:

    public static void putTest()
    {
    for (int i = 0; i < 5; i++)
        {
    byteBuffer.put(i);
    }
        Logger.info("------------after put------------------");
    Logger.info("position=" + byteBuffer.position());
    Logger.info("limit=" + byteBuffer.limit());
    Logger.info("capacity=" + byteBuffer.capacity());
    }

    写入5个元素后,输出的结果为:

      main |>  写入
    
                 putTest |>  ------------after putTest------------------
    
                 putTest |>  position=5
    
                 putTest |>  limit=20
    
                 putTest |>  capacity=20
    

    调用了put方法后,buffer处于写模式。写入5个数据后,可以看到,position 变成了5,指向了第6个可以写入的元素位置。

    除了在新建的buffer之后,如何将buffer切换成写模式呢?

    调用 Buffer.clear() 清空或 Buffer.compact()压缩方法,可以将 Buffer 转换为写模式。

    1.3.3. 读切换flip()方法

    put方法写入数据之后,可以直接从buffer中读吗?

    呵呵,不能。

    还需要调用filp()走一个转换的工作。flip()方法是Buffer的一个模式转变的重要方法。简单的说,是写模式翻转成读模式——写转读。

    接着前面的例子,继续上flip()方法的例子代码:

    public static void flipTest()
    {
    byteBuffer.flip();
    Logger.info("------------after flip ------------------");
    Logger.info("position=" + byteBuffer.position());
    Logger.info("limit=" + byteBuffer.limit());
    Logger.info("capacity=" + byteBuffer.capacity());
    }

    接着上一步的写入,在调用flip之后,buffer的属性有一些奇妙的变化。

    运行上面的程序,输出如下:

     main |>  翻转
    
                flipTest |>  ------------after flipTest ------------------
    
                flipTest |>  position=0
    
                flipTest |>  limit=5
    
                flipTest |>  capacity=20
    

    注意到没有,position从前一个小节的5,变成了0。而limit的保存了之前的position,从20变成5。

    这是为什么呢? 先看其源码,Buffer.flip()方法的源码如下:

    public final Buffer flip() {
    
        limit = position;
    
        position = 0;
    
        mark = UNSET_MARK;
    
        return this;
    
    }
    

    解释一下啊,flip()方法主要是从读模式切换成写模式,调整的规则是:

    (1)首先设置可读的长度limit。将写模式下的Buffer中内容的最后位置position值变为读模式下的limit位置值,新的limit值作为读越界位置;

    (2)其次设置读的起始位置。将当position值置为0,表示从0位置开始读。转换后重头开始读。

    (3)如果之前有mark保存的标记位置,还要消除。因为那是写模式下的mark标记。

    1.3.4. 读get() 方法

    get()读数据很简单,每次从postion的位置读取一个数据,并且进行相应的buffer属性的调整。

    接着前面的例子,继续上读取buffer的例子代码:

    public static void getTest()
    {
        Logger.info("------------after &getTest 2------------------");
        for (int i = 0; i < 2; i++)
        {
    int j = byteBuffer.get();
    Logger.info("j = " + j);
    }
        Logger.info("position=" + byteBuffer.position());
    Logger.info("limit=" + byteBuffer.limit());
    Logger.info("capacity=" + byteBuffer.capacity());
    Logger.info("------------after &getTest 3------------------");
        for (int i = 0; i < 3; i++)
        {
    int j = byteBuffer.get();
    Logger.info("j = " + j);
    }
        Logger.info("position=" + byteBuffer.position());
    Logger.info("limit=" + byteBuffer.limit());
    Logger.info("capacity=" + byteBuffer.capacity());
    }

    先读2个,再读3个,输出的Buffer属性值如下:

     main |>  读取 
    
                 getTest |>  ------------after &getTest 2------------------ 
    
                 getTest |>  j = 0 
    
                 getTest |>  j = 1 
    
                 getTest |>  position=2 
    
                 getTest |>  limit=5 
    
                 getTest |>  capacity=20 
    
                 getTest |>  ------------after &getTest 3------------------ 
    
                 getTest |>  j = 2 
    
                 getTest |>  j = 3 
    
                 getTest |>  j = 4 
    
                 getTest |>  position=5 
    
                 getTest |>  limit=5 
    
                 getTest |>  capacity=20 
    

    读完之后,缓存的position 值变成了一个没有数据的元素位置,和limit的值相等,已经不能在读了。

    读完之后,是否可以直接写数据呢?

    不能。一旦读取了所有的 Buffer 数据,那么我们必须清理 Buffer,让其重新可写,可以调用 Buffer.clear() 或 Buffer.compact()。

    1.3.5. 倒带rewind()方法

    已经读完的数据,需要再读一遍,可以直接使用get方法吗?

    答案是,不能。怎么办呢?

    使用rewind() 方法,可以进重复读的设置。rewind()也叫倒带,就像播放磁带一样,倒回去,重新播放。

    接着前面的例子,继续上重复读的例子代码:

    public static void rewindTest()
    {
    byteBuffer.rewind();
    Logger.info("------------after flipTest ------------------");
    Logger.info("position=" + byteBuffer.position());
    Logger.info("limit=" + byteBuffer.limit());
    Logger.info("capacity=" + byteBuffer.capacity());
    }

    实例的结果如下:

     main |>  重复读
    
              rewindTest |>  ------------after flipTest ------------------
    
              rewindTest |>  position=0
    
              rewindTest |>  limit=5
    
              rewindTest |>  capacity=20
    

    flip()方法主要是调整Buffer的 position 属性,调整的规则是:

    (1)position设回0,所以你可以重读Buffer中的所有数据;

    (2)limit保持不变,数据量还是一样的,仍然表示能从Buffer中读取多少个元素。

    Buffer.rewind()方法的源码如下:

    public final Buffer rewind() {
    
    position = 0;
    
    mark = -1;
    
    return this;
    
    }
    

    看到了实现的源码应该就会清楚flip()的作用了。rewind()方法与flip()很相似,区别在于rewind()不会影响limit,而flip()会重设limit属性值。

    1.3.6. mark( )和reset( )

    Buffer.mark()方法将当前的 position 的值保存起来,放在mark属性中,让mark属性记住当前位置,之后可以调用Buffer.reset()方法将 position 的值恢复回来。

    Buffer.mark()和Buffer.reset()方法是一一配套使用的。都是需要操作mark属性。

    在重复读的实例代码中,读到第3个元素,使用mark()方法,设置一下mark 属性,保存为第3个元素的位置。

    下面上实例,演示一下mark和reset的结合使用。

    实例继续接着上面的rewind倒带后的buffer 状态,开始reRead重复读,实例代码如下:

    public static void reRead()
    {
        Logger.info("------------after reRead------------------");
        for (int i = 0; i < 5; i++)
        {
    int j = byteBuffer.get();
    Logger.info("j = " + j);
            if (i == 2)
            {
    byteBuffer.mark();
    }
        }
        Logger.info("position=" + byteBuffer.position());
    Logger.info("limit=" + byteBuffer.limit());
    Logger.info("capacity=" + byteBuffer.capacity());
    }

    然后接着上一段reset()实例代码,如下:

    public static void afterReset()
    {
        Logger.info("------------after reset------------------");
    byteBuffer.reset();
    Logger.info("position=" + byteBuffer.position());
    Logger.info("limit=" + byteBuffer.limit());
    Logger.info("capacity=" + byteBuffer.capacity());
    }

    上面我们调用 mark() 方法将当前的 position 保存起来(在读模式,因此保存的是读的 position)。接着使用 reset() 恢复原来的读 position,因此读 position 就为3,可以再次开始从第2个元素读取数据.

    输出的结果是:

     afterReset |>  ------------after reset------------------
    
              afterReset |>  position=3
    
              afterReset |>  limit=5
    
              afterReset |>  capacity=20
    

    调用reset之后,position的值为3,表示可以从第三个元素开始读。

    Buffer.mark()和Buffer.reset()其实很简答,其源码如下:

    public final Buffer mark() {
    
    mark = position;
    
    return this;
    
    }
    
    public final Buffer reset() {
    
    int m = mark;
    
    if (m < 0)
    
    throw new InvalidMarkException();
    
    position = m;
    
    return this;
    
    }
    
    1.3.7. clear()清空

    clear()方法的作用有两种:

    (1)写模式下,当一个 buffer 已经写满数据时,调用 clear()方法,切换成读模式,可以从头读取 buffer 的数据;

    (2)读模式下,调用 clear()方法,将buffer切换为写模式,将postion为清零,limit设置为capacity最大容量值,可以一直写入,直到buffer写满。

    接着上面的实例,使用实例代码,演示一下clear方法。

    代码如下:

    public static void clearDemo()
    {
        Logger.info("------------after clear------------------");
    byteBuffer.clear();
    Logger.info("position=" + byteBuffer.position());
    Logger.info("limit=" + byteBuffer.limit());
    Logger.info("capacity=" + byteBuffer.capacity());
    }

    运行之后,结果如下:

    main |>  清空
    
               clearDemo |>  ------------after clear------------------
    
               clearDemo |>  position=0
    
               clearDemo |>  limit=20
    
               clearDemo |>  capacity=20
    

    在clear()之前,buffer是在读模式下。clear()之后,可以看到,清空了position 的值,设置为起始位置。

    clear 方法源码:

    public final Buffer clear() {

        position = 0;

        limit = capacity;

        mark = -1;

        return this;

    }

    根据源码我们可以知道,clear 将 positin 设置为0,将 limit 设置为 capacity。

    1.4. Buffer 的使用

    1.4.1. 使用的基本步骤

    总结一下,使用 NIO Buffer 的步骤如下:

    一:将数据写入到 Buffer 中;

    二:调用 Buffer.flip()方法,将 NIO Buffer 转换为读模式;

    三:从 Buffer 中读取数据;

    四:调用 Buffer.clear() 或 Buffer.compact()方法,将 Buffer 转换为写模式。

    当我们将数据写入到 Buffer 中时,Buffer 会记录我们已经写了多少的数据;当我们需要从 Buffer 中读取数据时,必须调用 Buffer.flip()将 Buffer 切换为读模式。

    1.4.2. 完整的实例代码
    package com.crazymakercircle.iodemo.base;

    import com.crazymakercircle.util.Logger;

    import java.nio.IntBuffer;

    public class BufferDemo
    {
    static IntBuffer byteBuffer = null;

    public static void allocatTest()
    {
    byteBuffer = IntBuffer.allocate(20);

    Logger.info("------------after allocate------------------");
    Logger.info("position=" + byteBuffer.position());
    Logger.info("limit=" + byteBuffer.limit());
    Logger.info("capacity=" + byteBuffer.capacity());
    }

    public static void putTest()
    {
    for (int i = 0; i < 5; i++)
    {
    byteBuffer.put(i);

    }

    Logger.info("------------after putTest------------------");
    Logger.info("position=" + byteBuffer.position());
    Logger.info("limit=" + byteBuffer.limit());
    Logger.info("capacity=" + byteBuffer.capacity());

    }

    public static void flipTest()
    {

    byteBuffer.flip();
    Logger.info("------------after flipTest ------------------");

    Logger.info("position=" + byteBuffer.position());
    Logger.info("limit=" + byteBuffer.limit());
    Logger.info("capacity=" + byteBuffer.capacity());
    }

    public static void rewindTest()
    {

    byteBuffer.rewind();
    Logger.info("------------after flipTest ------------------");

    Logger.info("position=" + byteBuffer.position());
    Logger.info("limit=" + byteBuffer.limit());
    Logger.info("capacity=" + byteBuffer.capacity());
    }

    public static void getTest()
    {


    Logger.info("------------after &getTest 2------------------");
    for (int i = 0; i < 2; i++)
    {
    int j = byteBuffer.get();
    Logger.info("j = " + j);
    }


    Logger.info("position=" + byteBuffer.position());
    Logger.info("limit=" + byteBuffer.limit());
    Logger.info("capacity=" + byteBuffer.capacity());

    Logger.info("------------after &getTest 3------------------");

    for (int i = 0; i < 3; i++)
    {
    int j = byteBuffer.get();
    Logger.info("j = " + j);
    }

    Logger.info("position=" + byteBuffer.position());
    Logger.info("limit=" + byteBuffer.limit());
    Logger.info("capacity=" + byteBuffer.capacity());

    }

    public static void reRead()
    {


    Logger.info("------------after reRead------------------");
    for (int i = 0; i < 5; i++)
    {
    int j = byteBuffer.get();
    Logger.info("j = " + j);

    if (i == 2)
    {
    byteBuffer.mark();
    }
    }


    Logger.info("position=" + byteBuffer.position());
    Logger.info("limit=" + byteBuffer.limit());
    Logger.info("capacity=" + byteBuffer.capacity());

    }

    public static void afterReset()
    {


    Logger.info("------------after reset------------------");

    byteBuffer.reset();
    Logger.info("position=" + byteBuffer.position());
    Logger.info("limit=" + byteBuffer.limit());
    Logger.info("capacity=" + byteBuffer.capacity());

    }

    public static void clearDemo()
    {


    Logger.info("------------after clear------------------");

    byteBuffer.clear();
    Logger.info("position=" + byteBuffer.position());
    Logger.info("limit=" + byteBuffer.limit());
    Logger.info("capacity=" + byteBuffer.capacity());

    }

    public static void main(String[] args)
    {
    Logger.info("分配内存");

    allocatTest();

    Logger.info("写入");
    putTest();

    Logger.info("翻转");

    flipTest();

    Logger.info("读取");
    getTest();

    Logger.info("重复读");
    rewindTest();
    reRead();

    Logger.info("make&reset写读");

    afterReset();
    Logger.info("清空");

    clearDemo();


    }
    }




    源码:


    代码工程:  JavaNioDemo.zip

    下载地址:在疯狂创客圈QQ群文件共享。



    无编程不创客,无案例不学习。疯狂创客圈,一大波高手正在交流、学习中!

    疯狂创客圈 Netty 死磕系列 10多篇深度文章博客园 总入口】  QQ群:104131248


  • 相关阅读:
    DHCP全局配置文件解析
    DHCP介绍
    使用Samba服务程序,让linux系统之间共享文件
    操作系统
    XML基础、 webservice
    JDBC编程--JDBC进阶
    JDBC编程--JDBC基础
    JDBC编程--SQL基础
    Java web--web编程原理
    Java web--web编程进阶(二)
  • 原文地址:https://www.cnblogs.com/crazymakercircle/p/9826798.html
Copyright © 2020-2023  润新知