Buffer是一个对象,它包含一些要写入或者要读出的数据。在NIO类库中加入Buffer对象,体现了新库与原I/O的一个重要区别。在面向流的I/O中,可以将数据直接写入或者将数据直接读到Stream对象中。
Java NIO中数据的读写操作始终是与缓冲区相关联的.数据是从通道读入缓冲区,从缓冲区写入到通道中的。
缓冲区实质上是一个数组。通常它是一个字节数组(ByteBuffer),也可以使用其他种类的数组。但是一个缓冲区不仅仅是一个数组,缓冲区提供了对数据的结构化访问以及维护读写位置(limit)等信息。
最常用的缓冲区是ByteBuffer,一个ByteBuffer提供了一组功能用于操作byte数组。除了ByteBuffer,还有其他的一些缓冲区,事实上,每一种Java基本类型(除了Boolean类型)都对应有一种缓冲区,具体如下。
- ByteBuffer:字节缓冲区
- CharBuffer:字符缓冲区
- ShortBuffer:短整型缓冲区
- IntBuffer:整形缓冲区
- LongBuffer:长整形缓冲区
- FloatBuffer:浮点型缓冲区
- DoubleBuffer:双精度浮点型缓冲区
每一个Buffer类都是Buffer接口的一个子实例。除了ByteBuffer,每一个 Buffer类都有完全一样的操作,只是它们所处理的数据类型不一样。因为大多数标准I/O操作都使用ByteBuffer,所以它除了具有一般缓冲区的操作之外还提供一些特有的操作,方便网络读写。
ByteBuffer是最常用的缓冲区,它提供了读写其他数据类型的方法,且信道的读写方法只接收ByteBuffer.因此ByteBuffer的用法是有必要牢固掌握的.
1.创建ByteBuffer
1.1 使用allocate()静态方法
ByteBuffer buffer=ByteBuffer.allocate(1024);
以上方法将创建一个容量为1024字节的ByteBuffer,如果发现创建的缓冲区容量太小,唯一的选择就是重新创建一个大小合适的缓冲区.
1.2使用put()方法来填充ByteBuffer
通过put()方法可以给ByteBuffer填充一个或者多个字节或者基本数据类型的值.
public void readAllData(byte[] bytes){
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put(bytes);
}
1.3 通过包装一个已有的数组来创建
如下,通过包装的方法创建的缓冲区保留了被包装数组内保存的数据.
ByteBuffer buffer=ByteBuffer.wrap(byteArray);
如果要将一个字符串存入ByteBuffer,可以如下操作:
ByetBuffer buffer = ByteBuffer.warp(“Hello World NIO”.getBytes());
2.回绕缓冲区与清除缓冲区
buffer.flip();
buffer.clear();
一旦调用read()来告知FileChannel向ByteBuffer存储字节,就必须调用缓存器上的flip()方法,这个方法用来将缓冲区准备为数据传出状态,执行以上方法后,输出通道会从数据的开头而不是末尾开始.回绕保持缓冲区中的数据不变,只是准备写入而不是读取。通俗的理解就是让它做好被读取的准备(看起来很麻烦,但是为了最大的传输速度)。如果我们打算使用缓冲器执行进一步的read()操作,我们也必须得调用clear()来为每个read()做准备。
在前面的一片文章中我们只是使用ByteBuffer来读取指定字节的大小的文件,现在我们通过ByteBuffer来读取一个文本的全部内容并写入到另一个文件中:
try {
FileChannel inputFileChannel = new FileInputStream("/home/wang/bigchat.sql").getChannel();
//newBigChat.sql可以不存在。系统会自动创建一个
FileChannel outputFileChannel = new FileOutputStream("/home/wang/newBigchat.sql").getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int len = 0;
//当read返回-1时表示读到了文件末尾
while ((len=inputFileChannel.read(byteBuffer))!=-1){
byteBuffer.flip();//准备好写
outputFileChannel.write(byteBuffer);
byteBuffer.clear();//准备好读
}
} catch (IOException e) {
e.printStackTrace();
}
flip()则是准备缓冲器以便它的信息可以由write()提取。write()之后数据依然在缓冲器中,接着clear()方法则对所有内部指针重新安排,以便缓存器在另一个read()操作期间能够做好接受数据的准备。
其实上面的代码并不是最理想,我们可以使用transferTo()和transferFrom()则允许我们将一个通道和另一个通道直接相连:
try {
FileChannel inputFileChannel = new FileInputStream("/home/wang/bigchat.sql").getChannel();
//newBigChat.sql可以不存在。系统会自动创建一个
FileChannel outputFileChannel = new FileOutputStream("/home/wang/newBigchat1.sql").getChannel();
inputFileChannel.transferTo(0,inputFileChannel.size(),outputFileChannel);
outputFileChannel.transferFrom(inputFileChannel,0,inputFileChannel.size());
}catch (IOException e){}
这种方法了解了解即可。
ByteBuffer俗称缓冲器, 是将数据移进移出通道的唯一方式,并且我们只能创建一个独立的基本类型缓冲器。ByteBuffer 中存放的是字节,如果要将它们转换成字符流则需要使用 Charset , Charset 是字符编码,它提供了把字节流转换成字符流( 解码 ) 和将字符流转换成字节流 ( 编码) 的方法。
private byte[] getBytes (char[] chars) {//将字符转为字节(编码)
Charset cs = Charset.forName ("UTF-8");
CharBuffer cb = CharBuffer.allocate (chars.length);
cb.put (chars);
cb.flip ();
ByteBuffer bb = cs.encode (cb)
return bb.array();
}
private char[] getChars (byte[] bytes) {//将字节转为字符(解码)
Charset cs = Charset.forName ("UTF-8");
ByteBuffer bb = ByteBuffer.allocate (bytes.length);
bb.put (bytes);
bb.flip ();
CharBuffer cb = cs.decode (bb);
return cb.array();
}
通过ByteBuffer读取文件后转化为CharBuffer并输出:
FileChannel fc = new FileInputStream(PATH).getChannel();
ByteBuffer buffer = ByteBuffer.allocate(SIZE);
fc.read(buffer);
//重值ByteBuffer中的数组,调用方法后输出通道会从数据的开头而不是末尾开始
buffer.flip();
//使用UTF-8编码为CharBuffer
Charset charset = Charset.forName("UTF-8");
CharBuffer cb = charset.decode(buffer);
System.out.println(cb);//正确输出数据
Buffer的组成中除了数据之外还有四个索引。如果对RandomAccessFile比较了解的话这四个索引很容易理解。
- capacity:表示Buffer的最大数据容量,即最多可以存储多少个数据,创建后无法改变。
- limit:第一个不应该被读或者写的缓冲区位置的索引。即位于limit后的数据不能被读也不能被写。
- position:指向下一个可以被读或者被写的缓冲区位置的索引。
- mark:定义一个标记。可以将position直接定位到mark处。不是很常用
有了上述四个索引的概念我们来思考之前我们使用的flip()与clear()方法
flip方法其实就是将limit设置为当前position的位置,然后将position设置为0。然后Buffer就为输出数据write()做好准备了
clear方法不是把Buffer中的清空数据,其实是将limit设置为capactiy,position设置为0。然后Buffer就为输入数据read()做好准备了
Buffer还有如下几个常用方法:
- boolean hasRemaining(): 判断position与limit之间是否有元素可供处理。
- int remaining():返回当前位置和界限之间的元素个数
- Buffer rewind():将位置设置为0,取消设置的mark。
Buffer除了提供操作postion,limit和mark方法之外。还有两个很重要的方法:put()和get()用于向Buffer中放入数据和从Buffer中取出数据。Buffer即支持对单个数据的访问,也支持对批量数据的访问(数组为参数)。
Java NIO的通道比缓冲区好理解多了,它就类似于流,但又有些不同:
- 既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。
- 通道可以异步地读写。
- 通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。
正如之前所说,从通道读取数据到缓冲区,从缓冲区写入数据到通道。因为Channel是全双工的,所以它可以比流更好地映射底层操作系统的API。特别是在UNIX网络编程模型中,底层操作系统的通道都是全双工的,同时支持读写操作。
前面我们通过三个基础流调用getChannel()方法获取了一个Channel的实现FileChannel。下面我们来看NIO中的其它Channel的实现:
- FileChannel:从文件中读写数据。
- DatagramChannel:能通过UDP读写网络中的数据。
- SocketChannel:能通过TCP读写网络中的数据。
- ServerSocketChannel:可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。
Channel其实只有两大类:分别是用于网络读写的SelectableChannel和用于文件操作的FileChannel。上述的DatagramChannel,SocketChannel,ServerSocketChannel都是SelectableChannel的子类