通道
和 缓冲区
是 NIO 中的核心对象,几乎在每个 I/O 操做中都要使用它们。html
通道是对原 I/O 包中的流的模拟。到任何目的地(或来自任何地方)的全部数据都必须经过一个 Channel 对象。一个 Buffer 实质上是一个容器对象。发送给一个通道的全部对象都必须首先放到缓冲区中;一样地,从通道中读取的任何数据都要读到缓冲区中。java
在本节中,您会了解到 NIO 中通道和缓冲区是如何工做的。数组
Buffer
是一个对象, 它包含一些要写入或者刚读出的数据。 在 NIO 中加入 Buffer
对象,体现了新库与原 I/O 的一个重要区别。在面向流的 I/O 中,您将数据直接写入或者将数据直接读到 Stream
对象中。网络
在 NIO 库中,全部数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的。在写入数据时,它是写入到缓冲区中的。任什么时候候访问 NIO 中的数据,您都是将它放到缓冲区中。app
缓冲区实质上是一个数组。一般它是一个字节数组,可是也可使用其余种类的数组。可是一个缓冲区不 仅仅 是一个数组。缓冲区提供了对数据的结构化访问,并且还能够跟踪系统的读/写进程。dom
最经常使用的缓冲区类型是 ByteBuffer
。一个 ByteBuffer
能够在其底层字节数组上进行 get/set 操做(即字节的获取和设置)。异步
ByteBuffer
不是 NIO 中惟一的缓冲区类型。事实上,对于每一种基本 Java 类型都有一种缓冲区类型:socket
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
每个 Buffer
类都是 Buffer
接口的一个实例。 除了 ByteBuffer
,每个 Buffer 类都有彻底同样的操做,只是它们所处理的数据类型不同。由于大多数标准 I/O 操做都使用 ByteBuffer
,因此它具备全部共享的缓冲区操做以及一些特有的操做。学习
如今您能够花一点时间运行 UseFloatBuffer.java,它包含了类型化的缓冲区的一个应用例子。this
Channel
是一个对象,能够经过它读取和写入数据。拿 NIO 与原来的 I/O 作个比较,通道就像是流。
正如前面提到的,全部数据都经过 Buffer
对象来处理。您永远不会将字节直接写入通道中,相反,您是将数据写入包含一个或者多个字节的缓冲区。一样,您不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。
通道与流的不一样之处在于通道是双向的。而流只是在一个方向上移动(一个流必须是 InputStream
或者OutputStream
的子类), 而 通道
能够用于读、写或者同时用于读写。
由于它们是双向的,因此通道能够比流更好地反映底层操做系统的真实状况。特别是在 UNIX 模型中,底层操做系统通道是双向的。
读和写是 I/O 的基本过程。从一个通道中读取很简单:只需建立一个缓冲区,而后让通道将数据读到这个缓冲区中。写入也至关简单:建立一个缓冲区,用数据填充它,而后让通道用这些数据来执行写入操做。
在本节中,咱们将学习有关在 Java 程序中读取和写入数据的一些知识。咱们将回顾 NIO 的主要组件(缓冲区、通道和一些相关的方法),看看它们是如何交互以进行读写的。在接下来的几节中,咱们将更详细地分析这其中的每一个组件以及其交互。
在咱们第一个练习中,咱们将从一个文件中读取一些数据。若是使用原来的 I/O,那么咱们只需建立一个FileInputStream
并从它那里读取。而在 NIO 中,状况稍有不一样:咱们首先从 FileInputStream
获取一个 Channel
对象,而后使用这个通道来读取数据。
在 NIO 系统中,任什么时候候执行一个读操做,您都是从通道中读取,可是您不是 直接 从通道读取。由于全部数据最终都驻留在缓冲区中,因此您是从通道读到缓冲区中。
所以读取文件涉及三个步骤:(1) 从 FileInputStream
获取 Channel
,(2) 建立 Buffer
,(3) 将数据从 Channel
读到 Buffer
中。
如今,让咱们看一下这个过程。
第一步是获取通道。咱们从 FileInputStream
获取通道:
1
2
|
FileInputStream fin = new FileInputStream( "readandshow.txt" );
FileChannel fc = fin.getChannel();
|
下一步是建立缓冲区:
1
|
ByteBuffer buffer = ByteBuffer.allocate( 1024 );
|
最后,须要将数据从通道读到缓冲区中,以下所示:
1
|
fc.read( buffer );
|
您会注意到,咱们不须要告诉通道要读 多少数据 到缓冲区中。每个缓冲区都有复杂的内部统计机制,它会跟踪已经读了多少数据以及还有多少空间能够容纳更多的数据。咱们将在 缓冲区内部细节 中介绍更多关于缓冲区统计机制的内容。
在 NIO 中写入文件相似于从文件中读取。首先从 FileOutputStream
获取一个通道:
1
2
|
FileOutputStream fout = new FileOutputStream( "writesomebytes.txt" );
FileChannel fc = fout.getChannel();
|
下一步是建立一个缓冲区并在其中放入一些数据 - 在这里,数据将从一个名为 message
的数组中取出,这个数组包含字符串 "Some bytes" 的 ASCII 字节(本教程后面将会解释 buffer.flip()
和buffer.put()
调用)。
1
2
3
4
5
6
|
ByteBuffer buffer = ByteBuffer.allocate( 1024 );
for (int i=0; i<message.length; ++i) {
buffer.put( message[i] );
}
buffer.flip();
|
最后一步是写入缓冲区中:
1
|
fc.write( buffer );
|
注意在这里一样不须要告诉通道要写入多数据。缓冲区的内部统计机制会跟踪它包含多少数据以及还有多少数据要写入。
下面咱们将看一下在结合读和写时会有什么状况。咱们以一个名为 CopyFile.java 的简单程序做为这个练习的基础,它将一个文件的全部内容拷贝到另外一个文件中。CopyFile.java 执行三个基本操做:首先建立一个 Buffer
,而后从源文件中将数据读到这个缓冲区中,而后将缓冲区写入目标文件。这个程序不断重复 ― 读、写、读、写 ― 直到源文件结束。
CopyFile 程序让您看到咱们如何检查操做的状态,以及如何使用 clear()
和 flip()
方法重设缓冲区,并准备缓冲区以便将新读取的数据写到另外一个通道中。
public class CopyFile { static public void main( String args[] ) throws Exception { if (args.length<2) { System.err.println( "Usage: java CopyFile 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.allocate( 1024 ); while (true) { buffer.clear(); int r = fcin.read( buffer ); if (r==-1) { break; } buffer.flip(); fcout.write( buffer ); } } }
使用管道实现读和写 buffer.flip
clear()
方法重设缓冲区,使它能够接受读入的数据。 flip()
方法让缓冲区能够将新读入的数据写入另外一个通道。
缓冲区的三个状态变量:
position limit capacity
如今咱们要将数据写到输出通道中。在这以前,咱们必须调用 flip()
方法。这个方法作两件很是重要的事:
limit
设置为当前 position
。position
设置为 0。前一小节中的图显示了在 flip 以前缓冲区的状况。下面是在 flip 以后的缓冲区:
咱们如今能够将数据从缓冲区写入通道了。 position
被设置为 0,这意味着咱们获得的下一个字节是第一个字节。 limit
已被设置为原来的 position
,这意味着它包括之前读到的全部字节,而且一个字节也很少。
在第一次写入时,咱们从缓冲区中取四个字节并将它们写入输出通道。这使得 position
增长到 4,而 limit
不变,以下所示:
咱们只剩下一个字节可写了。 limit
在咱们调用 flip()
时被设置为 5,而且 position
不能超过 limit
。因此最后一次写入操做从缓冲区取出一个字节并将它写入输出通道。这使得 position
增长到 5,并保持 limit
不变,以下所示:
ByteBuffer中存放不一样的数据类型:
public class TypesInByteBuffer { public static void main(String[] args) { ByteBuffer buffer = ByteBuffer.allocate(64); buffer.putInt(30); buffer.putLong(700000L); buffer.putDouble(Math.PI); buffer.flip(); System.out.println(buffer.getInt()); System.out.println(buffer.getLong()); System.out.println(buffer.getDouble()); }}
在可以读和写以前,必须有一个缓冲区。要建立缓冲区,您必须 分配 它。咱们使用静态方法 allocate()
来分配缓冲区:
1
|
ByteBuffer buffer = ByteBuffer.allocate( 1024 );
|
allocate()
方法分配一个具备指定大小的底层数组,并将它包装到一个缓冲区对象中 ― 在本例中是一个 ByteBuffer
。
您还能够将一个现有的数组转换为缓冲区,以下所示:
1
2
|
byte array[] = new byte[1024];
ByteBuffer buffer = ByteBuffer.wrap( array );
|
本例使用了 wrap()
方法将一个数组包装为缓冲区。必须很是当心地进行这类操做。一旦完成包装,底层数据就能够经过缓冲区或者直接访问。
slice()
方法根据现有的缓冲区建立一种 子缓冲区 。也就是说,它建立一个新的缓冲区,新缓冲区与原来的缓冲区的一部分共享数据。
使用例子能够最好地说明这点。让咱们首先建立一个长度为 10 的 ByteBuffer
:
1
|
ByteBuffer buffer = ByteBuffer.allocate( 10 );
|
而后使用数据来填充这个缓冲区,在第 n 个槽中放入数字 n:
1
2
3
|
for (int i=0; i<buffer.capacity(); ++i) {
buffer.put( (byte)i );
}
|
如今咱们对这个缓冲区 分片 ,以建立一个包含槽 3 到槽 6 的子缓冲区。在某种意义上,子缓冲区就像原来的缓冲区中的一个窗口 。
窗口的起始和结束位置经过设置 position
和 limit
值来指定,而后调用 Buffer
的 slice()
方法:
1
2
3
|
buffer.position( 3 );
buffer.limit( 7 );
ByteBuffer slice = buffer.slice();
|
片
是缓冲区的 子缓冲区
。不过, 片断
和 缓冲区
共享同一个底层数据数组,咱们在下一节将会看到这一点。
咱们已经建立了原缓冲区的子缓冲区,而且咱们知道缓冲区和子缓冲区共享同一个底层数据数组。让咱们看看这意味着什么。
咱们遍历子缓冲区,将每个元素乘以 11 来改变它。例如,5 会变成 55。
1
2
3
4
5
|
for (int i=0; i<slice.capacity(); ++i) {
byte b = slice.get( i );
b *= 11;
slice.put( i, b );
}
|
最后,再看一下原缓冲区中的内容:
1
2
3
4
5
6
|
buffer.position( 0 );
buffer.limit( buffer.capacity() );
while (buffer.remaining()>0) {
System.out.println( buffer.get() );
}
|
只读缓冲区:asReadOnlyBuffer()
直接和间接缓冲区:
ByteBuffer buffer =ByteBuffer.allocateDirect(1024);
直接缓冲区加快IO速度 给定一个直接字节缓冲区,Java 虚拟机将尽最大努力直接对它执行本机 I/O 操做。也就是说,它会在每一次调用底层操做系统的本机 I/O 操做以前(或以后),尝试避免将缓冲区的内容拷贝到一个中间缓冲区中(或者从一个中间缓冲区中拷贝数据)。
内存映射文件 I/O 是一种读和写文件数据的方法,它能够比常规的基于流或者基于通道的 I/O 快得多。
内存映射文件 I/O 是经过使文件中的数据神奇般地出现为内存数组的内容来完成的。这其初听起来彷佛不过就是将整个文件读到内存中,可是事实上并非这样。通常来讲,只有文件中实际读取或者写入的部分才会送入(或者 映射 )到内存中。
内存映射并不真的神奇或者多么不寻常。现代操做系统通常根据须要将文件的部分映射为内存的部分,从而实现文件系统。Java 内存映射机制不过是在底层操做系统中能够采用这种机制时,提供了对该机制的访问。
尽管建立内存映射文件至关简单,可是向它写入多是危险的。仅只是改变数组的单个元素这样的简单操做,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。
了解内存映射的最好方法是使用例子。在下面的例子中,咱们要将一个 FileChannel
(它的所有或者部分)映射到内存中。为此咱们将使用 FileChannel.map()
方法。下面代码行将文件的前 1024 个字节映射到内存中:
|
MappedByteBuffer mbb = fc.map( FileChannel.MapMode.READ_WRITE, 0, 1024 );
|
map()
方法返回一个 MappedByteBuffer
,它是 ByteBuffer
的子类。所以,您能够像使用其余任何 ByteBuffer
同样使用新映射的缓冲区,操做系统会在须要时负责执行行映射。
分散/汇集 I/O 是使用多个而不是单个缓冲区来保存数据的读写方法。
一个分散的读取就像一个常规通道读取,只不过它是将数据读到一个缓冲区数组中而不是读到单个缓冲区中。一样地,一个汇集写入是向缓冲区数组而不是向单个缓冲区写入数据。
分散/汇集 I/O 对于将数据流划分为单独的部分颇有用,这有助于实现复杂的数据格式。
public class UseScatterGather { static private final int firstHeaderLength = 2; static private final int secondHeaderLength = 4; static private final int bodyLength = 6; public static void main(String[] args) throws Exception{ if (args.length!=1) { System.err.println( "Usage: java UseScatterGather port" ); System.exit( 1 ); } int port=Integer.parseInt(args[0]); ServerSocketChannel ssc=ServerSocketChannel.open(); InetSocketAddress address= new InetSocketAddress(port); ssc.socket().bind(address); int messageLength=firstHeaderLength+secondHeaderLength+bodyLength; ByteBuffer buffers[] = new ByteBuffer[3]; buffers[0] = ByteBuffer.allocate( firstHeaderLength ); buffers[1] = ByteBuffer.allocate( secondHeaderLength ); buffers[2] = ByteBuffer.allocate( bodyLength ); SocketChannel sc= ssc.accept(); while (true){ //分散读到 buffers int bytesRead=0; while(bytesRead<messageLength){ long r = sc.read(buffers); bytesRead+=r; System.out.println("r"+r); for(int i=0;i<buffers.length;i++){ ByteBuffer bb= buffers[i]; System.out.println("b"+i+" "+bb.position()+" "+bb.limit()); } } //处理消息 //flip buffers for(int i =0;i<buffers.length;i++){ ByteBuffer bb= buffers[i]; bb.flip(); } //scatter-write back out long bytesWritten = 0; while(bytesWritten<messageLength){ long r= sc.write(buffers); bytesWritten+=r; } //clear buffers for(int i =0;i<buffers.length;i++){ ByteBuffer bb= buffers[i]; bb.clear(); } System.out.println(bytesRead+" "+bytesWritten+" "+messageLength); } } }
文件锁定初看起来可能让人迷惑。它 彷佛 指的是防止程序或者用户访问特定文件。事实上,文件锁就像常规的 Java 对象锁 ― 它们是 劝告式的(advisory) 锁。它们不阻止任何形式的数据访问,相反,它们经过锁的共享和获取赖容许系统的不一样部分相互协调。
您能够锁定整个文件或者文件的一部分。若是您获取一个排它锁,那么其余人就不能得到同一个文件或者文件的一部分上的锁。若是您得到一个共享锁,那么其余人能够得到同一个文件或者文件一部分上的共享锁,可是不能得到排它锁。文件锁定并不老是出于保护数据的目的。例如,您可能临时锁定一个文件以保证特定的写操做成为原子的,而不会有其余程序的干扰。
大多数操做系统提供了文件系统锁,可是它们并不都是采用一样的方式。有些实现提供了共享锁,而另外一些仅提供了排它锁。事实上,有些实现使得文件的锁定部分不可访问,尽管大多数实现不是这样的。
在本节中,您将学习如何在 NIO 中执行简单的文件锁过程,咱们还将探讨一些保证被锁定的文件尽量可移植的方法。
要获取文件的一部分上的锁,您要调用一个打开的 FileChannel
上的 lock()
方法。注意,若是要获取一个排它锁,您必须以写方式打开文件。
1
2
3
|
RandomAccessFile raf = new RandomAccessFile( "usefilelocks.txt", "rw" );
FileChannel fc = raf.getChannel();
FileLock lock = fc.lock( start, end, false );
|
在拥有锁以后,您能够执行须要的任何敏感操做,而后再释放锁:
1
|
lock.release();
|
在释放锁后,尝试得到锁的其余任何程序都有机会得到它。
本小节的例子程序 UseFileLocks.java 必须与它本身并行运行。这个程序获取一个文件上的锁,持有三秒钟,而后释放它。若是同时运行这个程序的多个实例,您会看到每一个实例依次得到锁。
文件锁定多是一个复杂的操做,特别是考虑到不一样的操做系统是以不一样的方式实现锁这一事实。下面的指导原则将帮助您尽量保持代码的可移植性:
public class UseFileLocks { static private final int start=10; static private final int end=20; public static void main(String[] args) throws Exception{ //get file channel RandomAccessFile raf= new RandomAccessFile("test.txt","rw"); FileChannel fc=raf.getChannel(); //get lock System.out.println("trying to get lock"); FileLock lock=fc.lock(start,end,false); System.out.println("got lock"); //pause System.out.println("pausing"); try{ Thread.sleep(3000); }catch (InterruptedException e){ } //release lock System.out.println("going to release lock"); lock.release(); System.out.println("release lock"); raf.close(); } }
连网是学习异步 I/O 的很好基础,而异步 I/O 对于在 Java 语言中执行任何输入/输出过程的人来讲,无疑都是必须具有的知识。NIO 中的连网与 NIO 中的其余任何操做没有什么不一样 ― 它依赖通道和缓冲区,而您一般使用 InputStream
和OutputStream
来得到通道。
本节首先介绍异步 I/O 的基础 ― 它是什么以及它不是什么,而后转向更实用的、程序性的例子。
异步 I/O 是一种 没有阻塞地 读写数据的方法。一般,在代码进行 read()
调用时,代码会阻塞直至有可供读取的数据。一样, write()
调用将会阻塞直至数据可以写入。
另外一方面,异步 I/O 调用不会阻塞。相反,您将注册对特定 I/O 事件的兴趣 ― 可读的数据的到达、新的套接字链接,等等,而在发生这样的事件时,系统将会告诉您。
异步 I/O 的一个优点在于,它容许您同时根据大量的输入和输出执行 I/O。同步程序经常要求助于轮询,或者建立许许多多的线程以处理大量的链接。使用异步 I/O,您能够监放任何数量的通道上的事件,不用轮询,也不用额外的线程。
咱们将经过研究一个名为 MultiPortEcho.java 的例子程序来查看异步 I/O 的实际应用。这个程序就像传统的 echo server,它接受网络链接并向它们回响它们可能发送的数据。不过它有一个附加的特性,就是它能同时监听多个端口,并处理来自全部这些端口的链接。而且它只在单个线程中完成全部这些工做。
public class MultiPortEcho { private int ports[]; private ByteBuffer echoBuffer = ByteBuffer.allocate(1024); public MultiPortEcho(int[] ports) throws IOException { this.ports = ports; go(); } private void go() throws IOException { Selector selector = Selector.open(); //open a listener on each port and register each one with the selector for (int i = 0; i < ports.length; i++) { ServerSocketChannel ssc = ServerSocketChannel.open(); ssc.configureBlocking(false); ServerSocket ss = ssc.socket(); InetSocketAddress address = new InetSocketAddress(ports[i]); ss.bind(address); SelectionKey key = ssc.register(selector, SelectionKey.OP_ACCEPT); System.out.println("going to listen on" + ports[i]); } while (true) { int num = selector.select(); Set selectedKeys = selector.selectedKeys(); Iterator it = selectedKeys.iterator(); while (it.hasNext()) { SelectionKey key = (SelectionKey) it.next(); if ((key.readyOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) { ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); SocketChannel sc = ssc.accept(); sc.configureBlocking(false); //add new connection to the selector SelectionKey newKey = sc.register(selector, SelectionKey.OP_READ); it.remove(); System.out.println("got connection from " + sc); } else if ((key.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) { //read data SocketChannel sc = (SocketChannel) key.channel(); //echo data int bytesEchoed = 0; while (true) { echoBuffer.clear(); int r = sc.read(echoBuffer); if (r <= 0) break; echoBuffer.flip(); sc.write(echoBuffer); bytesEchoed += r; } System.out.println("echoed" + bytesEchoed + "from " + sc); it.remove(); } } } } public static void main(String[] args) throws Exception { if (args.length <= 0) { System.err.println("Usage: java MultiPortEcho port [port port ...]"); System.exit(1); } int ports[] = new int[args.length]; for (int i = 0; i < args.length; i++) { ports[i] = Integer.parseInt(args[i]); } new MultiPortEcho(ports); } }