Netty快速入门(03)Java NIO 介绍-Buffer

NIO 介绍

NIO,能够说是New IO,也能够说是non-blocking IO,具体怎么解释均可以。java

NIO 1是在JSR51里面定义的,在JDK1.4中引入,由于BolckingIO不支持高并发网络编程,这也是Java1.4之前被人诟病的缘由。NIO 2是在JSR203中定义的,在JDK1.7中引入,这是JavaNIO整个的发展历程。NIO 1和NIO 2并非一个新旧替代的关系,而是一个补充的关系,NIO 2补充了1中缺乏的一些东西。咱们能够看一下两个的内容:git

NIO 1(本系列文章只介绍NIO 1):编程

Buffers

Channelssegmentfault

Selectors数组

NIO 2:缓存

Update

New File System API(引入文件系统API,以前的NIO 1当中,在Linux中操做文件都须要有读写权限,操做各类属性等等,在Java中是没办法操做的,因此这里引入了一个文件API)网络

Asynchronous IO(即 常说的 AIO)并发

BIO与NIO的比较

BIO:函数

面向流(Stream Oriented)(链接创建的时候就能够得到InputStream和OutputStream,就能够经过流来进行操做,流是没有缓冲的,效率比较低)

阻塞IO(Blocking IO)高并发

NIO:

面向缓冲区(Buffer Oriented)(读写都是经过Buffer来进行的,要读数据就得先把数据读到Buffer中去,写数据也要先把数据写到Buffer中,再从Buffer写到网络中去)

非阻塞IO(Non Blocking IO)(IO复用模型)

选择器(Selectors)

NIO Buffer 学习

上面说了,NIO三个最重要的概念就是Buffer,Channel,Selector,Buffer缓冲区是用来放数据,Channel就是通道,能够把数据写到别的地方,Selector就是一个多路复用器,用来实现线程复用。下面会一个一个说。NIO不像BIO那么简单,甚至比起来要复杂的多。因此要一块一块的学习。下面先学习Buffer。

一个Buffer本质上是内存中的一块,能够将数据写入这块内存,也能够从这块内存中读取数据。JavaNIO中定义了七种类型的Buffer:

file

能够看到几种基本类型对应的都有Buffer(除了布尔类型),能够存放不一样类型的原始数据。

Buffer有三大核心概念:position,limit,capacity。

file

最好理解的是capacity,表明Buffer的容量,申请一个容量为1024的Buffer,那么capacity就是1024。没有特殊状况,capacity永远不会变。一旦Buffer的数据大小达到了capacity,须要清空Buffer,才能重新写入值。

position就是表示下一个能够操做(读或者写)的位置。JavaNIO不少人诟病比较复杂的一个地方就是,把读操做和写操做混在一块儿,同一种操做能够分红读模式和写模式,从读模式到写模式须要本身手动去切换(执行flip),就是说没有分开的读指针和写指针,就一个操做指针position,在写模式下,position表示下一个能够写入的位置,在读模式下,position表示下一个能够读的位置。两种模式在切换的时候,position都会归零,这样就能够从头开始操做。好比在写模式下,position从0写到了5,那么切换到读模式,position会变成0,从第一位开始读。

limit表示一个限制的最大位置,在写模式下,limit表明的是最大能写入的数据的位置,这个时候limit等于capacity。写结束后切换到读模式,此时的limit等于Buffer中实际的数据大小,由于Buffer不必定被写满,好比写模式下在capacity为10的Buffer中写入了五个数据,那么切换到读模式,capacity仍是10,limit变为5,position天然归0,值变为0。

Buffer的建立

Buffer大体分为两种类型,一种是Direct Buffer,一种是non-direct Buffer,也叫HeapBuffer。下面看一下比较:

Non-direct ByteBuffer

HeapByteBuffer,标准的java类(表示在堆上建立了一个Buffer,就是一个普通的Java类,在堆上申请内存存放Buffer实例)

维护一份byte[] 在JVM堆上

建立开销小(heap申请内存是很快的,因此建立开销很小)

拷贝到临时DirectByteBuffer,但临时缓冲区使用缓存。汇集写/发散读时没有缓存临时缓冲区 (也就是前面的线程模型提到的当作数据拷贝的时候,并不能直接从堆上直接写到内核态缓冲区发送出去,必需要在native中申请一块内存,先把数据拷贝到native中去,而后再发送)

能够自动GC(垃圾回收)

Direct ByteBuffe

底层存储在非JVM堆上,经过native代码操做(数据存储在堆以外,也就是JVM以外的普通内存空间,Java经过JNI调用c函数malloc申请的nvtive内存,也就表明JVM是没法回收的)

-XX:MaxDirectMemorySize=<size>

建立开销大(须要调用c函数申请,建立开销大)

无需临时缓冲区作拷贝(数据原本就在native中,能够直接发送)

须要本身GC,每次建立或者释放都须要调用一次System.gc()

咱们来看一下代码,建立Buffer有两种方法,allocate/allocateDirect方法,从名字就能看出各自建立的是上面哪一种Buffer。或者借助数组建立(使用warp)。咱们先用allocate建立:

ByteBuffer buffer0 = ByteBuffer.allocate(10);

能够看到,很简单,而后用allocateDirect方法建立:

ByteBuffer buffer1 = ByteBuffer.allocateDirect(10);

而后用第三种,根据一个数组去建立:

byte[] bytes = new byte[10];

ByteBuffer buffer2 = ByteBuffer.wrap(bytes);

根据数组建立的时候,还能够设置偏移量(新Buffer的位置)和长度:

byte[] bytes2 = new byte[10];

//指定范围

ByteBuffer buffer3 = ByteBuffer.wrap(bytes2, 2, 3);

上面一共用四种方法建立了Buffer,咱们来打印出四种buffer的信息,包括数组的信息,position,limit,capacity三个信息,以及剩余可操做的空间(每一个buffer都打印):

if (buffer0.hasArray()) {

System.out.println("buffer0 array: " + buffer0.array());

    System.out.println("Buffer0 array offset: " + buffer0.arrayOffset());

}

System.out.println("Position: " + buffer0.position());

System.out.println("Limit: " + buffer0.limit());

System.out.println("Capacity: " + buffer0.capacity());

System.out.println("Remaining: " + buffer0.remaining());

hasArray()判断表示的是,buffer底层是不是以数组的形式存储的,是就为true,对于HeapByteBuffer,底层都是数组,因此weitrue,DirectByteBuffer底层不是以数组形式存储的,因此为false,使用warp建立的Buffer实际上返回的也是HeapByteBuffer,也在堆上,咱们先来看buffer0的打印结果

file

前两行打印出了数组和数组的偏移量,后面打印出一些属性,由于尚未作任何操做,因此position是0,capacity理所固然是10,可操做的最后位置limit这时候等于capacity,也是10,可操做的剩余空间大小也是10,因此打印出了上面的结果。根据上面建立出的实际状况,buffer0,buffer1,buffer2打印的结果应该是同样的,实际也是如此:

file

buffer3有些不一样,咱们建立的时候设置了前提条件,由于偏移量为2,因此position的起始位置是从2开始的,长度设置成了3,因此可操做的最大位置limit是5(2加3),由于是根据数据byte2建立的,因此总的容量仍是10,因此capacity仍是10,剩余可操做的空间天然就是长度3,因此buffer3的打印结果以下:

file

Buffer的访问

上面写了代码说了怎么建立Buffer,下面看怎么访问Buffer。先建立一个buffer:

ByteBuffer buffer = ByteBuffer.allocate(10);

而后看一个打印Buffer信息的方法:

file

而后咱们打印一下刚刚建立的Buffer:

printBuffer(buffer);

打印结果:

file

上面打印的信息很简单,再也不解释,下面看第一个操做,开始向Buffer中写入数据,而后打印(注意写入的操做):

file

咱们向Buffer中写入了五个数据,来看看Buffer的信息如何变化:

file

position变成了5,其它信息并未改变,下一步,咱们转换buffer的状态,由写模式改成读模式,而后打印:

file

打印结果:

file

能够看到,模式转换后,position归零,limit到了已写入数据的末尾,capacity天然不变,如今读取两个元素(注意读操做):

file

打印结果:

file

注意上面position的变化,下面咱们来标记一下当前buffer的位置,这样进行屡次操做后,还能够回到标记时的状态:

file

打印结果天然没什么变化:

file

再次读取两个数据:

file

查看打印结果:

file

position再次变化,下面恢复到mark以前的位置:

file

打印结果:

file

下面来对buffer进行压缩操做,也就是将 position 与 limit之间的数据复制到buffer的开始位置,复制后 position = limit -position,limit = capacity,但若是position 与limit 之间没有数据的话发,就不会进行复制:

file

打印结果:

file

能够看到,压缩后,将未读过的数据直接移到了开始位置,position直接移到了这些数据的末尾,而且切换到写模式,因此position等于未操做的数据空间长度,也就是limit减去原来的position,capacity回到了空间最后的位置,下面咱们状况buffer:

file

打印结果:

file

能够看到,clear直接把buffer回到了初始状态。经过上面几种操做,你们能够对ByteBuffer几种指针变化的流程有所了解。

Slice切片复制Buffer操做

这种复制操做有点相似视图的概念,是一种浅复制,调用该方法获得的新缓冲区所操做的数组仍是原始缓冲区中的那个数组,不过,经过slice建立的新缓冲区只能操做原始缓冲区中数组剩余的数据,即索引为调用slice方法时原始缓冲区的position到limit索引之间的数据,超出这个范围的数据经过slice建立的新缓冲区没法操做到。咱们给定一个Buffer,放满数据,而后打印:

file

打印结果:

file

而后手动指定position和limit的位置:

file

打印结果:

file

而后根据定位到的位置,切分buffer,切分后的新buffer 的position为0,limit和capacity都为原来的position和limit之间的长度,打印新buffer:

file

打印结果:

file

循环切分出来的新buffer,一个一个读取出来,而后乘以11,而后放到原位置:

file

打印结果:

file

能够看到由于读操做因此position发生变化,手动定位原来buffer的开始和结束位置,循环读取打印原来buffer中的每一个数据,发现切分的buffer修改的同时,原来的buffer也修改了,说明slice也是浅拷贝:

file

打印结果:

file

Duplicate复制Buffer操做

Duplicate表示的也是浅拷贝,也就是只复制饮用,对象实例仍是指向一个。咱们建立一个buffer,放慢数据,而后打印:

file

打印结果:

file

转换读写状态,而后打印:

file

打印结果:

file

手动定位位置到3和6,mark一次,而后又单独定位position到5:

file

打印结果:

file

浅复制一份,包括原来的position和limit的位置也一块儿复制了过来,原来的buffer清空,position和limit的位置复位(注意clear只是指针复位,数据还在):

file

拷贝出来的position和limit的位置仍是最后清空前的位置,打印结果:

file

拷贝出来的也清空,position和limit的位置也回到最左和最右:

file

打印结果:

file

asReadOnlyBuffer操做与Duplicate同样,只是前者是只读的。

缺点

NIO编程负责的一个缘由就是Buffer复杂,Buffer指针变来变去仍是比较复杂的,并且原本就一个指针,读写模式还有互相转换,这种要本身当心的控制才行。模式搞错了会出大问题。

代码地址:https://gitee.com/blueses/net... 02

相关文章
相关标签/搜索