关于NIO Buffer基本原理详解

关于NIO Buffer中4个重要状态属性的含义:position、limit、capacity与mark
Buffer本身是一个容器,称作缓冲区,里面包装了特定的一种原生类型,其子类包括ByteBuffer、CharBuffer、LongBuffer、IntBuffer、DoubleBuffer、ShortBuffer、FloatBuffer. 注意是没有Boolean类型的Buffer
Buffer是一种特定原生类型线性的有限的元素序列,Buffer中比较重要的4个属性:position、limit、capacity、mark. 在使用 Buffer 时,我们实际操作的就是这四个属性的值.
具体介绍下4个属性:

  • capacity(容量):一个buffer能够容纳数据元素的最大数量,capacity不会为负数,且永远不能被改变.
    假设:IntBuffer.allocate(1024), 分配了大小为1024的元素个数,则capacity就等于1024.
  • limit(上界):一个buffer的limit指的是第一个不能在读也不能在写的元素索引. limit不会为负数,并且一定是小于capacity的.
    假设:IntBuffer.allocate(1024), 我们在程序中设置limit=512,说明Buffer的容量是1024,但是从512之后既不能读也不能写了,进一步说明该Buffer的实际可用大小是512.
  • position(位置):一个buffer的position指的是下一个将要读或者写的元素的索引.position不会为负数,并且一定是小于limit的. position的位置主要由get()和put()方法的调用来更新.
  • mark(标记):一个备忘地址,作为临时标记位置使用,标记在设定前是未定义的.
    mark的使用场景:
    假设IntBuffer.allocate(1024),现在position位置为10,现在只想发送512到1024之间的缓冲数据,此时我们可以buffer.mark(buffer.position())既将position记入mark位置,然后buffer.postion(512),此时发送的数据就是512到1024之间的数据。发送完成后,调用buffer.reset()将mark临时标记赋值给position使得position=mark。注意如果未设定mark,而调用了buffer.reset()方法则会抛出InvalidMarkException.

不变式:
0 <= mark <= position <= limit <= capacity

传输数据:
Buffer中的每个子类中都有get()和put()方法.
带参数的put和get方法称作绝对存入/取出,位置是通过参数指定的.
绝对操作不影响position位置,但是如果索引位置超出limit,则会抛出IndexOutOfBoundsException;
不带参数的put和get称作相对存入/取出,即position位置自动前进.
对于get相对操作,如果位置超过了limit,则会抛出BufferUnderflowException;
对于put相对操作,如果位置超过了limit,则会抛出BufferOverflowException;

线程安全性:
buffer在多线程并发下并不是安全的。如果一个buffer会在多个线程使用,那么需要使用恰当的同步操作来访问buffer。也就是buffer本身并不是线程安全的。

常用方法:
这部分我们以实际代码为例来说明:
clear()方法,清除,将buffer重置为空状态,它并不会更改缓冲区内的任何数据元素. 如果此时还没有读取的数据,则就无法读取到了.

public final Buffer clear() {
        position = 0; // 位置重置为0
        limit = capacity; // limit重置为capacity
        mark = -1; // 丢弃标记
        return this;
}

flip()方法,翻转,使buffer从写模式转换到读模式.

public final Buffer flip() {
        limit = position; // 将limit设置为position
        position = 0; // position重置为0
        mark = -1; // 丢弃标记
        return this;
}

rewind()方法,重绕,重置position为0,limit保持不变,此时调用rewind前buffer已处于读模式下了, 可以重新读取buffer中的数据.

public final Buffer rewind() {
        position = 0; // 重置position为0
        mark = -1; // 丢弃标记 
        return this;
}

compact()方法,将所有未读的数据拷贝到Buffer起始处. 然后将position设到最后一个未读元素正后面. limit属性依然像clear()方法一样,设置成capacity. 现在Buffer准备好写数据了,但是不会覆盖未读的数据.
注意compact方法的实现是由原生类型的子类实现,比如ByteBuffer则由HeapByteBuffer中实现.

public ByteBuffer compact() {
        System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
        position(remaining());
        limit(capacity());
        discardMark();
        return this;
 }

hasRemaining()方法,会在读取缓冲区时告诉你是否已经达到缓冲区的上界. 可以通过remaining()高效读取buffer数据.

int count = buffer.remaining();  
for (int i = 0; i < count; i++) {  
    myByteArray[i] = buffer.get();  
} 

slice()方法,是对原有数据的一个快照,共享相同的底层数据元素. 调用slice方法后,会得到大于等于position且小于limit之间的数据,对于改变slice方法获得大buffer数据,也能够反映到原buffer上。
举个例子:

public class NioTest6_slice {
    public static void main(String[] args) {
        ByteBuffer byteBuffer = ByteBuffer.allocate(10);
        int capacity = byteBuffer.capacity();
        for (int i = 0 ; i < capacity; i++) {
            byteBuffer.put((byte)i);
        }

        // 设置position、limit
        byteBuffer.position(2);
        byteBuffer.limit(6);

        //slice方法是前闭后开的 大于等于position,小于limit
        ByteBuffer subByteBuffer = byteBuffer.slice();

        // 改变subByteBuffer内容,也能反映到byteBuffer上
        for (int j = 0; j < subByteBuffer.capacity(); j++) {
            // 2到5位置的数*2
            subByteBuffer.put((byte)(2 * subByteBuffer.get(j)));
        }

        // 设置回原来的值,打印输出看下byteBuffer数据变化
        byteBuffer.position(0);
        byteBuffer.limit(capacity);
        while (byteBuffer.hasRemaining()) {
            System.out.println(byteBuffer.get());
        }
    }
}

输出结果中看到第3个位置到第5个位置的数据都✖️2了,返回4,6,8.

0
1
4
6 
8 
10
6
7
8
9

duplicate()方法,创建了一个与原始缓冲区相似的新缓冲区,与调用slice方法一样也是共享相同的底层数据元素, 拥有同样的容量, 但每个缓冲区拥有各自的 position、limit 和 mark 属性.

public class NioTest6_duplicate {
    public static void main(String[] args) {
        ByteBuffer byteBuffer = ByteBuffer.allocate(10);
        int capacity = byteBuffer.capacity();
        for (int i = 0 ; i < capacity; i++) {
            byteBuffer.put((byte)i);
        }

        ByteBuffer subByteBuffer = byteBuffer.duplicate();
        subByteBuffer.position(0); // 单独给duplicate出来的buffer设置position
        System.out.println("byteBuffer position:" + byteBuffer.position() + "--subByteBuffer position:" + subByteBuffer.position());

        // 改变subByteBuffer内容,也能反映到byteBuffer上
        for (int j = 0; j < subByteBuffer.capacity(); j++) {
            // 2到5位置的数*2
            subByteBuffer.put((byte)(2 * subByteBuffer.get(j)));
        }

        // 切换到读模式
        byteBuffer.flip();
        while (byteBuffer.hasRemaining()) {
            System.out.println(byteBuffer.get());
        }
    }
}

输出结果能看到原buffer数据元素变化

byteBuffer position:10--subByteBuffer position:0
0
2
4
6
8
10
12
14
16
18

asReadOnlyBuffer()方法,我们可以随时将一个普通的Buffer调用asReadOnlyBuffer方法返回一个只读Buffer. 但是,不能将一个只读Buffer转换为读写Buffer.

关于Buffer的Scattering 和 Gathering
scatter / gather经常用于需要将传输的数据分开处理的场合,例如传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头分散到不同的buffer中,这样你可以方便的处理消息头和消息体.
Scattering:
Scattering Reads在移动下一个buffer前,必须填满当前的buffer,这也意味着它不适用于动态消息(译者注:消息大小不固定). 换句话说,如果存在消息头和消息体,消息头必须完成填充(例如 128byte),Scattering Reads才能正常工作。
Gattering:
buffer数组是write()方法的入参,write()方法会按照buffer在数组中的顺序,将数据写入到channel,注意只有position和limit之间的数据才会被写入。
因此,如果一个buffer的容量为128byte,但是仅仅包含58byte的数据,那么这58byte的数据将被写入到channel中。因此与Scattering Reads相反,Gathering Writes能较好的处理动态消息.
举个例子:

public class NioTest10_scatteringandgathering {

    public static void main(String[] args) throws  Exception {
        try(ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();) {
            InetSocketAddress inetSocketAddress = new InetSocketAddress(8899);
            serverSocketChannel.socket().bind(inetSocketAddress); // 绑定到8899端口


            int messageLength = 2 + 3 + 4;

            ByteBuffer[] byteBuffers = new ByteBuffer[3];
            byteBuffers[0] = ByteBuffer.allocate(2);
            byteBuffers[1] = ByteBuffer.allocate(3);
            byteBuffers[2] = ByteBuffer.allocate(4);

            SocketChannel socketChannel = serverSocketChannel.accept();

            do {
                int byteRead = 0;
                while (byteRead < messageLength) {
                    long r = socketChannel.read(byteBuffers);
                    System.out.println("--------------r:" + r);
                    byteRead += r;

                    System.out.println("byteRead:" + byteRead);

                    Arrays.asList(byteBuffers).stream().map(buffer -> "position:" + buffer.position() + ", limit:" + buffer.limit())
                            .forEach(System.out::println);
                }

                Arrays.asList(byteBuffers).forEach(Buffer::flip);

                // 实际写的个数
                int byteWrites = 0;
                while (byteWrites < messageLength) {
                    long r = socketChannel.write(byteBuffers);
                    byteWrites += r;
                }

                Arrays.asList(byteBuffers).forEach(Buffer::clear);

                System.out.println("byteRead:" + byteRead + ", byteWrite:" + byteWrites + ", messageLength:" + messageLength);
            } while (true);
        }
    }
}

以上服务启动后,我们通过nc localhost 8899或者telecom localhost 8899,然后输入Hello world字符串,在控制台上能看到输出:

--------------r:9
byteRead:9
position:2, limit:2
position:3, limit:3
position:4, limit:4
byteRead:9, byteWrite:9, messageLength:9

参考文章:
http://zachary-guo.iteye.com/blog/1457542
http://www.jianshu.com/p/1af407c043cb

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java I...
    JackChen1024阅读 7,533评论 1 143
  • 转自 http://www.ibm.com/developerworks/cn/education/java/j-...
    抓兔子的猫阅读 2,272评论 0 22
  • Java NIO中的Buffer用于和NIO通道进行交互。如你所知,数据是从通道读入缓冲区,从缓冲区写入到通道中的...
    AFinalStone阅读 276评论 0 0
  • Buffer java NIO库是在jdk1.4中引入的,NIO与IO之间的第一个区别在于,IO是面向流的,而NI...
    德彪阅读 2,187评论 0 3
  • 美国前总统吉米·卡特,90岁高龄,于8月20日承认自己脑部有四处黑色素瘤病灶,正在进行放疗治疗,这次亲口承认证实了...
    许远山阅读 655评论 0 1