Java NIO(上)

NIO是JDK 1.4中开始引入的新的IO库。它提供了高速的、面向块的I/O。NIO和IO的不同之处在于:

1)通道和缓冲区(Channels and Buffers)

标准的IO基于字节流和字符流进行操作,而NIO基于通道(Channel)和缓冲区(Buffer)。NIO的数据总是从通道读取到缓冲区,或者从缓冲区写入到通道。

2)非阻塞IO(Non-blocking IO)

NIO可以让你以非阻塞的方式使用IO。比如,线程从通道读取数据到缓冲区时,线程还可以干其他事。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。

3)选择器(Selectors)

NIO引入了选择器的概念,它用于监听多个通道的事件(如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。

在java编程中,在NIO引入之前,使用流(Stream)的方式完成IO。所有I/O均被视为单个字节的移动。通过一个称为流的对象一次移动一个字节。流IO用于将对象转换为字节,然后再转换为对象。

缓冲区(Buffer)

buffer是一个对象,它包含一些要写入或刚读出的数据。在面向流的IO中,数据直接写入或读到stream对象中。而在NIO中,所有数据都用缓冲区处理。在读取数据时,它是直接读到缓冲区的。在写入数据时,它是写入到缓冲区的。缓冲区的实质是一个数组,通常是一个字节数组,但也有其他类型的数组。但缓冲区也不仅是数组,它提供了对数组的结构化访问,而且还可以跟踪系统的读/写进程。

最常用的缓冲区是ByteBuffer。一个ByteBuffer可以在其底层字节数组上进行get/set操作。常用的缓冲区类型有:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

这些buffer覆盖了你能通过IO发送的基本数据类型。

通道(Channel)

通道是对原IO包中流的模拟。所有数据可以从通道读到buffer,也可以从buffer写到通道。所有数据都是通过buffer对象来处理,永远不会将字节直接写入通道中。通道与流的不同在于它是双向的。而流只是在一个方向上移动(一个流必须是InputStream或OutputStream的子类)。而通道可以用于读、写或同时读写。java NIO中最重要的通道实现如下:

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

FileChannel从文件中读写数据。DatagramChannel通过UDP读写网络中的数据。SocketChannel通过TCP读写网络中的数据。ServerSocketChannel可以监听新进来的TCP连接,像web服务器一样,对每个新进来的连接都创建一个SocketChannel。

选择器(Selector)

选择器允许单线程处理多个通道。如果应用打开了多个连接(通道),但每个连接流量都很低,使用选择器就会很方便。一个单线程使用一个选择器处理三个通道的例子如下:

Java NIO(上)_第1张图片

要使用选择器,需要向选择器注册通道,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件(如有新连接,数据接收等)。

NIO中的读和写

读和写是IO的基本过程。从通道中读取数据很简单,只需要创建一个缓冲区,然后从通道中将数据读取到缓冲区。写入则恰好相反,将缓冲区的数据写入通道中。

读取文件的三步如下:

1)从FileInputStream获取channel;

FileInputStream fin = new FileInputStream( "test.txt" );
FileChannel fc = fin.getChannel();

2)创建Buffer;

ByteBuffer buffer = ByteBuffer.allocate( 1024 );

3)将数据从Channel读到Buffer中。

fc.read( buffer );

写入文件的三步如下:

1)从FileOutputStream获取channel;

FileOutputStream fout = new FileOutputStream( "test.txt" );
FileChannel fc = fout.getChannel();
2)创建一个缓冲区,并放入一些数据。
ByteBuffer buffer = ByteBuffer.allocate( 1024 );

for (int i=0; i<message.length; ++i) {
     buffer.put( message[i] );
}
buffer.flip();
3)将缓冲区的内容写入通道中。

fc.write( buffer );

读写混合

以一个CopyFile.java的程序作为例子,它用来实现将一个文件的内容拷贝到另一个文件中。该程序执行三个基本操作:1)创建一个buffer;2)将源文件的数据读入这个缓冲区中;3)重复读,直到源文件结束。

public class CopyFile {

    public static void main(String[] args) {
        try {
            FileInputStream fileInputStream = new FileInputStream("/export/src.txt");
            FileOutputStream fileOutputStream = new FileOutputStream("/export/des.txt");

            FileChannel fInChannel = fileInputStream.getChannel();
            FileChannel fOutChannel =fileOutputStream.getChannel();
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

            while (true) {
                byteBuffer.clear();
                int result = fInChannel.read(byteBuffer);
                if (result == -1) {
                    break;
                }
                byteBuffer.flip();
                fOutChannel.write(byteBuffer);
            }

            fileInputStream.close();
            fileOutputStream.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
clear()方法重设缓冲区,使它可以接收读入的数据。flip()方法让缓冲区可以将新读入的数据写入另一个通道。

缓冲区细节(capacity、position和limit)

缓冲区本质上是一块可写入数据,然后可从中读数据的内存。这块数据被包装成NIO Buffer对象,并且提供了一组方法,用来方便访问这块内存。

理解了buffer的下面三个属性,就可以很好的理解它的工作原理了。

  • capacity
  • position
  • limit

position和limit的含义取决于buffer处于读模式还是写模式。不过,不论buffer处于什么模式,capacity的含义是一样的。

Java NIO(上)_第2张图片

capacity

buffer有固定的大小值,称作capacity。只能往buffer中写capacity个byte、long、char等类型。一旦Buffer满了,需要将其清空(读数据或清除数据)才能继续往buffer中写数据。

position

当写数据到buffer中时,position表示当前的位置。初始position值为0。当一个byte、char等数据写入buffer后,position会向前移动到下一个可插入数据的buffer单元。position最大可为capacity-1。

当读数据时,也是从某个特定位置读。当buffer从写模式切换到读模式,position会被重置为0。当从buffer的position处读取数据时,position向前移动到下一个可读的位置。

position总是小于或等于limit。

limit

写模式下,buffer的limit表示最多能往buffer里写入多少数据。写模式下,limit等于buffer的capacity。

当切换到读模式时,limit表示最多能读到多少数据。因此,当切换buffer到读模式时,limit会被设置成写模式下的position值。也就是说,你只能读到之前写入的所有数据(limit被设置成已写入的数据量,也就是写模式下的position)。

limit决不能大于capacity。

接下来我们以一个实例来更详细的了解一下这些buffer的内部细节。

创建一个大小为8个字节的buffer。有8个槽,分别为(slot-0,...,slot-7)


limit不能大于capacity。所以初始的时候,它们的位置如下:

Java NIO(上)_第3张图片

position的初始值为0。如果读一些数据到缓冲区中,那么下一个读取的数据被放入slot-0。

Java NIO(上)_第4张图片

第一次读取数据。假设第一次获取3个字节。读完之后,position就会增加到3。如下:

Java NIO(上)_第5张图片

因为capacity一直不会改变,所以省略了capacity标记。第二次再读取两个字节数据后,position增加到5。如下:


limit一直都没有改变。现在我们需要将数据写入到通道中。我们必须调用flip()方法。它会做两件很重要的事情:

1.将limit设置为当前position;

2.将position设置为0。

flip()方法调用之后,如下:


现在就可以将数据从缓冲区写入通道了。position被设置为0。也意味着我们得到的下一个字节是第一个字节。limit被设置成了原来的position。意味着可以读到原来的所有字节。

第一次写入时,我们从缓冲区取4个字节将它们写入通道。这使得position增加到4,而limit不变。如下:


第二次就只剩下一个字节可写,limit在我们调用flip()之后被设置为5,且position不能超过limit,所以最后一次写入操作从缓冲区取出一个字节并将它写入输出通道。使得position增加到5,而limit保持不变。如下:

Java NIO(上)_第6张图片

最后一步是调用缓冲区的clear()方法。该方法重设缓冲区,以便接收更多字节。clear做两件非常重要的事情:

1.将limit设置为与capacity相同;

2.设置position为0。

调用clear()方法后的状态如下:

Java NIO(上)_第7张图片

缓冲区此时就可以接收新的数据了。

缓冲区分配和包装

读写缓冲区之前,必须先创建一个缓冲区,常用的创建方法是用静态方法allocate()来分配:

ByteBuffer buffer = ByteBuffer.allocate( 1024 );
该方法分配一个指定大小的底层数组,并将其包装到一个缓冲区对象中。也可以将一个现有数组转换成缓冲区:

byte array[] = new byte[1024];
ByteBuffer buffer = ByteBuffer.wrap( array );
缓冲区分片

使用slice()方法可以根据现有缓冲区创建一个子缓冲区。新缓冲区与原缓冲区共享一部分数据。

创建一个缓冲区,并在第n个槽放入数字n,如下:

ByteBuffer buffer = ByteBuffer.allocate( 10 );
for (int i=0; i<buffer.capacity(); ++i) {
     buffer.put( (byte)i );
}
现在我们该缓冲区分片,以创建一个包含槽3到槽6的子缓冲区。
buffer.position( 3 );
buffer.limit( 7 );
ByteBuffer slice = buffer.slice();
缓冲区数据共享

我们遍历子缓冲区,将每个元素乘以10来改变它。例如,5会变成50。

for (int i=0; i<slice.capacity(); ++i) {
      byte b = slice.get( i );
      b *= 10;
      slice.put( i, b );
}
最后再查看原缓冲区的内容:

buffer.position( 0 );
buffer.limit( buffer.capacity() );

while (buffer.remaining()>0) {
     System.out.println( buffer.get() );
}
结果如下:

0
1
2
30
40
50
60
7
8
9








你可能感兴趣的:(Java NIO(上))