深入 Buffer :
下面,我们看下NIO中buffer的两个重要的组成部分:
buffer的状态变量和buffer的访问方法;
状态变量是buffer内部计数系统的关键,在每一次的read/write过程中,buffer的状态变量都是变化的。通过记录和跟踪这些状态变化,buffer就可以在内部完成操作资源的控制;
当你从channel中读取数据的时候,数据被存放到buffer中;有的时候,你直接把获得的buffer数据集传递给另外一个channel;
但是大部分情况下,你需要检查buffer中的数据,此时你可以使用get()方法;如果你需要把原数据存放到buffer中,可以使用put()方法;
虽然初看起来NIO的内部计数系统比较复杂,但是你很快会发现很多工作实际上是为你做的;那些你本应该自己用byte数组和index来实现的动作,被NIO内部完成了;
状态变量:
buffer的状态变量有三个:position,limit,capacity;
position:
我们已经说过,buffer实际上一个byte数组的封装,当你从channel中读入数据时,你实际上将数据放置在底层的array中。position记录了你已经写入数组的数据量.更准确的说,是指向数组中下一个数据将要放置的位置。所以,如果你读入3个byte,buffer的position的值为3(数组的第四个元素的位置);
同时,写入数据时,postion记录了你已经写过的数据量,确切地说,是指向数组中下一个将写的数据的位置.例如,如果你已经写了5个byte,position的值为5(数组的第6个元素的位置)。
Limit:
limit变量表明写数据时剩余将要获取的数据个数(bufferàwrite),或者读数据时还可以存放数据的空间量(readàbuffer)。不管写出还是读入数据,
状态变量满足以下关系:position<=limit;
Capacity(容量)
底层数组的大小(size);
满足:limit<=capacity;
变量变化过程观察:
1)capacity=8
2)设置limit的值 limit=8;
3)set position=0;
读过程:
读取3数据:limit不变,position=3
再读取2个数据:
limit不变;position+=2;
写过程:
1)flip完成2个操作(limit置于当前position位置后,position置0)
limit = position;
position = 0;
2)第一次读操作,读4数据:
limit不变,position=4;
第二次读取,读1数据;
limit不变,position+=1;
读数据的clear操作:limit=capacity, position=0; (准备新的数据读取)
buffer的访问方法:
当你需要直接处理buffer中的数据时,使用buffer的访问方法,包括get()以获取buffer中的数据,put()以向buffer中添加数据;
get的4个相关的方法:
byte get();
ByteBuffer get(byte[] dst); 效果同(get(byte[] dest,0,length))
Bytebuffer get(byte[] dst, int offset,int length)
byte get(int index);
对方法2,3是将ByteBuffer中的数据从buffer.position开始,读取length个数据,放置在dest数组中以偏移量offset位置开始的地方之后的空间里;如果length>buffer.remaining()(limit-position),抛出BufferUnderflowException运行时异常;
put的5个相关方法:
ByteBuffer put(byte b);
ByteBuffer put(byte[] src);
ByteBuffer put(byte[] src,int offset,int length);
ByteBuffer put(ByteBuffer src);
ByteBuffer put(int index,byte b);
方法5将byte b放置到ByteBuffer的指定位置;方法2,3从数组中获取数据放置到buffer中;方法4是ByteBuffer之间数据的复制;
以上方法是ByteBuffer的相关方法;其他类型都有相似的处理行为;
Buffer中使用put和get方法的例子:
CreateBuffer.java CreateArrayBuffer.java
ReadAndShow.java WriteSomeBytes.java
get()和put()方法可以指定类型,如下:
getByte();getChar();getShort();getInt();getLong();getFloat();
getDouble();
putByte();putChar();putShort();putInt();putLong();putFloat();
putDouble();
TypesInByteBuffer.java
不同Buffer的get()和put()例子;
UseFloatBuffer.java
---------------------------------------------------------------
static public void main( String args[] ) throws Exception {
FloatBuffer buffer = FloatBuffer.allocate( 10 );
for (int i=0; i<buffer.capacity(); ++i) {
float f = (float)Math.sin( (((float)i)/10)*(2*Math.PI) );
//System.out.println("-------------"+f);
buffer.put( f );
}
buffer.flip();
while (buffer.hasRemaining()) {
float f = buffer.get();
System.out.println( f );
}
}
---------------------------------------------------------------
重新回顾下buffer工作的循环过程:
---------------------------------------------------------------------
while (true) {
buffer.clear();
int r = fcin.read( buffer );
if (r==-1) {
break;
}
buffer.flip();
fcout.write( buffer );
}
---------------------------------------------------------------------
read()和write()封装了大部分的操作细节,所以操作起来非常方便,而clear和flip分别对应buffer的读操作和写操作;
更多的Buffer细节:
有了前面的探讨,你可以尽可能简单地用NIO中的方法实现原来的IO方法;下面将要讨论使用buffers中的复杂的方面,譬如buffer的allocation,wrapping,slicing;同时我们将会探讨一些nio中的新特性;还有如何创建不同的buffer对象来满足不同的需求:read-only的buffer,可以保护数据不被修改;direct的buffer,可以直接和底层的OS进行映射;最后,还有memory-buffer;
Buffer的allocation和wrapping:
NIO中的读写之前,你需要用一个buffer,为了创建buffer,你需要先allocation这个buffer。为了达成这个目的,我们需要用一个静态方法allocate();
ByteBuffer buffer = ByteBuffer.allocation(1024);
也可以把一个存在的array转变为一个buffer,例如:
-------------------------------------------------------------------
byte array[] = new byte[1024];
ByteBuffer buffer = ByteBuffer.wrap(array);
eg: CreateArrayBuffer.java
public class CreateArrayBuffer
{
static public void main( String args[] ) throws Exception {
byte array[] = new byte[1024];
ByteBuffer buffer = ByteBuffer.wrap( array );
buffer.put( (byte)'a' );
buffer.put( (byte)'b' );
buffer.put( (byte)'c' );
buffer.flip();
System.out.println( (char)buffer.get() );
System.out.println( (char)buffer.get() );
System.out.println( (char)buffer.get() );
}
}
-------------------------------------------------------------------
Buffer的slicing:
slice()方法为一个存在的buffer对象创建了一个sub-buffer;也即:它创建了一个新的和原来buffer共享部分数据的buffer;
slice()是源buffer的一个子buffer,他们共享底层的数组;slice buffer是一个很棒的抽象特性,当你需要处理buffer的一部分时,你可以使用slice buffer,而不需要传入起止位置的参数;slice buffer就像是buffer的一个窗口;
buffer.position( 3 );
buffer.limit( 7 );
ByteBuffer slice = buffer.slice();
for (int i=0; i<slice.capacity(); ++i) {
byte b = slice.get( i );
b *= 11;
slice.put( i, b );
}
buffer.position( 0 );
buffer.limit( buffer.capacity() );//buffer.clear();
while (buffer.remaining()>0) {
System.out.println( buffer.get() );
}
--------------------------------------------------------------------
只读buffer:
调用buffer的asReadOnlyBuffer();将创建一个新的只读的buffer对象;
无法把一个只读的buffer对象转为可写的buffer对象;
直接和间接的buffers对象:
direct buffer是一种被特定的方式allocate的buffer,可以提升I/O的速度;
direct buffer的定义可以是独立的,jdk文档中关于direct buffer的说明如下:
Given a direct byte buffer, the Java virtual machine will make a best effort to perform native I/O operations directly upon it. That is, it will attempt to avoid copying the buffer's content to (or from) an intermediate buffer before (or after) each invocation of one of the underlying operating system's native I/O operations.
你还可以用memory-mapped files的方式来使用direct buffer;
定义code:
public class FastCopyFile
{
public static void main( String args[] ) throws Exception {
if (args.length<2) {
System.err.println( "Usage: java FastCopyFile infile outfile" );
System.exit( 1 );
}
String infile = args[0];
String outfile = args[1];
FileInputStream fin = new FileInputStream( infile );
FileOutputStream fout = new FileOutputStream( outfile );
FileChannel fcin = fin.getChannel();
FileChannel fcout = fout.getChannel();
ByteBuffer buffer = ByteBuffer.allocateDirect( 1024 );
while (true) {
buffer.clear();
int r = fcin.read( buffer );
if (r==-1) {
break;
}
buffer.flip();
fcout.write( buffer );
}
}
}
---------------------------------------------------------------------
Memory-mapped file I/O
Memory-mapped file I/O 是一种比I/O stream和 channel-based-I/O还要快得多的处理读写的方式;
Memory-mapped file I/O的实现,是通过把file中的数据处理地看起来好像memory array中的内容的方式;实际上,Memory-mapped file并没有把整个文件一次性的读进memory,它只把实际读写的部分map到内存中;
现代的OS在需要的时候,通过把部分的文件和部分的memory相映射来实现文件系统;java的memory-mapping系统仅仅在需要的情况下为底层的os提供访问权限;
向memory-mapping files中进行写动作时危险的;
将一个文件映射到memory中的方法:
-----------------------------------------------------------------
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE,0,1024);
把文件中0~1024个字节映射到memory中;
public class UseMappedFile
{
static private final int start = 0;
static private final int size = 1024;
static public void main( String args[] ) throws Exception {
RandomAccessFile raf = new RandomAccessFile( "usemappedfile.txt", "rw" );
FileChannel fc = raf.getChannel();
MappedByteBuffer mbb = fc.map( FileChannel.MapMode.READ_WRITE,
start, size );
mbb.put( 0, (byte)97 );
mbb.put( 1023, (byte)122 );
raf.close();
}
}
-----------------------------------------------------------------
map的方法会返回一个MappedByteBuffer的对象,MappedByteBuffer是ByteBuffer的子集;你尽可以使用能够在buffer上使用的一切方法对buffer进行操作,同时,os会在你需要的时候在底层替你照顾好mapping的动作;