NIO基础教程之channel、buffer(一)

 一、什么是NIO? 

Java NIO (New IO) 是一个替代接口(从Java 1.4开始),相对于Java传统IO接口和网络接口而言。NIO处理IO的方式和传统IO会有很大区别。标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。NIO可以理解为非阻塞IO,传统的IO的read和write只能阻塞执行,线程在读写IO期间不能干其他事情,比如调用socket.read()时,如果服务器一直没有数据传输过来,线程就一直阻塞,而NIO中可以配置socket为非阻塞模式。NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。注意:传统IO是单向。

1.1、IO与NIO的区别

IO NIO
面向流 面向缓冲区
阻塞IO 非阻塞IO
选择器

1.1.1、面向流与面向缓冲

     Java IO和NIO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

1.1.2、阻塞与非阻塞IO

     Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

1.1.3、选择器(Selectors)

     Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。

1.1.4、文件传输

传统的IO通信:磁盘→系统→程序→socket→协议引擎

NIO:磁盘→系统→socket→协议引擎

1.2、 Channel(通道)

通道表示打开到 IO 设备(例如:文件、套接字)的连接。若需要使用 NIO 系统,需要获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。 Channel相比IO中的Stream更加高效,可以异步双向传输,但是必须和buffer一起使用。

主要实现类

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

主要获取方式

1、java针对支持通道的类提供了getChannel()方法

本地io: 
   FileInputStreanm/FileOutputStream 
   RandomAccessFile 
网络io: 
  Socket 
  ServerSocket 
  DatagramSocket

        FileInputStream fis = new FileInputStream("D:\\1.jpg");
        FileChannel inChannel = fis.getChannel();

2、 jdk1.7的nio2只对各个通道提供了一个静态方法open()

        FileChannel inChannel = FileChannel.open(Paths.get("D:\\1.jpg"), StandardOpenOption.READ);

通道之间的数据传输

1、read&write

//将 Buffer 中数据写入 Channel
outChannel.write(buff)
//从 Channel 读取数据到 Buffer
inChannel.read(buff)

2、transferFrom

从源信道读取字节到这个通道的文件中。如果源通道的剩余空间小于 count 个字节,则所传输的字节数要小于请求的字节数。这种方法可能比从源通道读取并写入此通道的简单循环更有效率。 
@param SRC 源通道 
@param position 调动开始的文件内的位置,必须是非负的 
@param count 要传输的最大字节数,必须是非负 
@return 传输文件的大小(单位字节),可能为零, 
public abstract long transferFrom(ReadableByteChannel src, long position, long count) throws IOException;

    //复制图片,利用直接缓存区
    public void test() throws Exception{
        FileChannel inChannel = FileChannel.open(Paths.get("D:\\1.jpg"), StandardOpenOption.READ);
        FileChannel outChannel = FileChannel.open(Paths.get("D:\\2.jpg"), StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE);
        outChannel.transferFrom(inChannel,0, inChannel.size()); 
        inChannel.close();
        outChannel.close();
    }

3、transferTo

将字节从这个通道的文件传输到给定的可写字节通道。 
@param position 调动开始的文件内的位置,必须是非负的 
@param count 要传输的最大字节数,必须是非负 
@param target 目标通道 
@return 传输文件的大小(单位字节),可能为零, 
public abstract long transferTo(long position, long count, WritableByteChannel target) throws IOException;

 //复制图片,利用直接缓存区
    public void test2() throws Exception{
        FileChannel inChannel = FileChannel.open(Paths.get("D:\\1.jpg"), StandardOpenOption.READ);
        FileChannel outChannel = FileChannel.open(Paths.get("D:\\3.jpg"), StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE);
        inChannel.transferTo(0, inChannel.size(), outChannel);  
        inChannel.close();
        outChannel.close();
    }
    

1.3buffer(缓冲区)

Buffer 是一个对象, 它包含一些要写入或者刚读出的数据。 在 NIO 中加入 Buffer 对象,体现了新库与原 I/O 的一个重要区别。在面向流的 I/O 中,您将数据直接写入或者将数据直接读到 Stream 对象中。在 NIO 库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的。在写入数据时,它是写入到缓冲区中的。任何时候访问 NIO 中的数据,您都是将它放到缓冲区中。缓冲区实质上是一个数组。通常它是一个字节数组,但是也可以使用其他种类的数组。但是一个缓冲区不 仅仅 是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。

NIO基础教程之channel、buffer(一)_第1张图片

这就是Buffer的大致继承体系,很多都没有罗列出来,因为主要讲解的是ByteBuffer.这里大概说一下,Buffer是一个抽象类,包括一个Buffer的最基本属性,比如,容量,位置,上界,标记.子类通过调用父类构造方法来实例化这几个参数,子类也都有各自的容量实现.比如ByteBuffer类用字节数组当缓冲区.旗下又有两个自己的实现类.
在jdk中,buffer有很多中实现,例如intBuffer,LongBuffer,ByteBuffer等,但是在NIO中实现的大多使用的是ByteBuffer,重点来看一下ByteBuffer.

Buffer的属性

容量(capacity):缓冲区能够容纳的数据元素的最大数量。这一容量在缓冲区创建时被设定,并且永远不能被改变 
上界(limit):缓冲区的第一个不能被读或写的元素。或者说,缓冲区中现存元素的计数 
位置(position):下一个要被读或写的元素的索引。位置会自动由相应的 get( )和 put( )函数更新 
标记(mark):下一个要被读或写的元素的索引。位置会自动由相应的 get( )和 put( )函数更新一个备忘位置。调用 mark( )来设定 mark = postion。调用 reset( )设定 position =mark。标记在设定前是未定义的(undefined)。这四个属性之间总是遵循以下关系:0 <= mark <= position <= limit <= capacity 

我们先看一个Demo,根据demo在细说

@Test
public void test006(){
	try{
		//1.新建缓冲区
		ByteBuffer byteBuffer =ByteBuffer.allocate(10);
		System.out.println("1.新建缓冲区【position:"+byteBuffer.position()+" Limit:"+byteBuffer.limit()+" Capacity:"+byteBuffer.capacity()+"】");
		//2.第一次存放HELLO 5个字节到缓冲区
		String str1 = "HELLO";
		byteBuffer.put(str1.getBytes());
		System.out.println("2.第一次输入HELLO,缓冲区【position:"+byteBuffer.position()+" Limit:"+byteBuffer.limit()+" Capacity:"+byteBuffer.capacity()+"】");
		//3.第二次存放JM 2个字节到缓冲区
		String str2 = "JM";
		byteBuffer.put(str2.getBytes());
		System.out.println("3.第二次输入JM,缓冲区【position:"+byteBuffer.position()+" Limit:"+byteBuffer.limit()+" Capacity:"+byteBuffer.capacity()+"】");
		//4.开启读的模式
		byteBuffer.flip();
		System.out.println("4.开启读模式之后,缓冲区【position:"+byteBuffer.position()+" Limit:"+byteBuffer.limit()+" Capacity:"+byteBuffer.capacity()+"】");
		//5.从缓冲区取4个字节,并标记
		byte[] bytes = new byte[byteBuffer.limit()];
		byteBuffer.get(bytes,0,4);//获取缓冲区数据
		byteBuffer.mark();//mark是一个索引,通过此方法指定Buffer中一个特定的position
		System.out.println("5.从缓冲区取4个字节写入到通道,缓冲区【position:"+byteBuffer.position()+" Limit:"+byteBuffer.limit()+" Capacity:"+byteBuffer.capacity()+"】");
		//6.将缓冲区剩余的字节取出
		byteBuffer.get(bytes,byteBuffer.position(),byteBuffer.limit()-byteBuffer.position());
		System.out.println("6.将缓冲区剩余的字节取出写入到通道,缓冲区【position:"+byteBuffer.position()+" Limit:"+byteBuffer.limit()+" Capacity:"+byteBuffer.capacity()+"】");
		//7.调用reset()方法恢复到mark标记的position位置
		byteBuffer.reset();
		System.out.println("7.调用reset()方法恢复到mark标记的位置,缓冲区【position:"+byteBuffer.position()+" Limit:"+byteBuffer.limit()+" Capacity:"+byteBuffer.capacity()+"】");
		//8.重设缓冲区以便接收更多的字节
		byteBuffer.clear();
		System.out.println("8.调用缓冲区的clear(),缓冲区【position:"+byteBuffer.position()+" Limit:"+byteBuffer.limit()+" Capacity:"+byteBuffer.capacity()+"】");
	}catch(Exception e){
		e.printStackTrace();
	}
}


输出结果:
1.新建缓冲区【position:0 Limit:10 Capacity:10】
2.第一次输入HELLO,缓冲区【position:5 Limit:10 Capacity:10】
3.第二次输入JM,缓冲区【position:7 Limit:10 Capacity:10】
4.开启读模式之后,缓冲区【position:0 Limit:7 Capacity:10】
5.从缓冲区取4个字节写入到通道,缓冲区【position:4 Limit:7 Capacity:10】
6.将缓冲区剩余的字节取出写入到通道,缓冲区【position:7 Limit:7 Capacity:10】
7.调用reset()方法恢复到mark标记的位置,缓冲区【position:4 Limit:7 Capacity:10】
8.调用缓冲区的clear(),缓冲区【position:0 Limit:10 Capacity:10】

我们首先新创建的缓冲区。出于本例子的需要,我们假设这个缓冲区的 总容量 为10个字节。 Buffer 的状态如下所示:

//1.新建缓冲区
ByteBuffer byteBuffer =ByteBuffer.allocate(10);
System.out.println("1.新建缓冲区【position:"+byteBuffer.position()+" Limit:"+byteBuffer.limit()+" Capacity:"+byteBuffer.capacity()+"】");

1.新建缓冲区【position:0 Limit:10 Capacity:10】

回想一下 ,limit 决不能大于 capacity,此例中这两个值都被设置为 10。我们通过将它们指向数组的尾部之后(如果有第10个槽,则是第10个槽所在的位置)来说明这点。

NIO基础教程之channel、buffer(一)_第2张图片

position 设置为0。如果我们读一些数据到缓冲区中,那么下一个读取的数据就进入 slot 0 。如果我们从缓冲区写一些数据,从缓冲区读取的下一个字节就来自 slot 0 

第一次读取

现在我们可以开始在新创建的缓冲区上进行读/写操作。首先从输入通道中读一些数据到缓冲区中。第一次读取得到五个字节。它们被放到数组中从 position 开始的位置,这时 position 被设置为 0。读完之后,position 就增加到 5,如下所示:

//2.第一次存放HELLO 5个字节到缓冲区
String str1 = "HELLO";
byteBuffer.put(str1.getBytes());
System.out.println("2.第一次输入HELLO,缓冲区【position:"+byteBuffer.position()+" Limit:"+byteBuffer.limit()+" Capacity:"+byteBuffer.capacity()+"】");

NIO基础教程之channel、buffer(一)_第3张图片

2.第一次输入HELLO,缓冲区【position:5 Limit:10 Capacity:10】

我们可以看出Limit和Capacity没有改变,Position的值从0变为5。

第二次读取

在第二次读取时,我们从输入通道读取另外两个字节到缓冲区中。这两个字节储存在由 position 所指定的位置上, position 因而增加 2:

//3.第二次存放JM 2个字节到缓冲区
String str2 = "JM";
byteBuffer.put(str2.getBytes());
System.out.println("3.第二次输入JM,缓冲区【position:"+byteBuffer.position()+" Limit:"+byteBuffer.limit()+" Capacity:"+byteBuffer.capacity()+"】");
		

NIO基础教程之channel、buffer(一)_第4张图片

3.第二次输入JM,缓冲区【position:7 Limit:10 Capacity:10】

我们可以看出Limit和Capacity没有改变,Position的值从5变为7。

开启读模式

现在我们要将数据写到输出通道中。在这之前,我们必须调用 flip() 方法。这个方法做两件非常重要的事:

  1. 它将 limit 设置为当前 position
  2. 它将 position 设置为 0。

下面是在 flip 之后的缓冲区:

//4.开启读的模式
byteBuffer.flip();
System.out.println("4.开启读模式之后,缓冲区【position:"+byteBuffer.position()+" Limit:"+byteBuffer.limit()+" Capacity:"+byteBuffer.capacity()+"】");
		

NIO基础教程之channel、buffer(一)_第5张图片

4.开启读模式之后,缓冲区【position:0 Limit:7 Capacity:10】

我们现在可以将数据从缓冲区写入通道了。 position 被设置为 0,这意味着我们得到的下一个字节是第一个字节。 limit 已被设置为原来的 position,这意味着它包括以前读到的所有字节,并且一个字节也不多。

第一次写入并标记

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

//5.从缓冲区取4个字节,并标记
byte[] bytes = new byte[byteBuffer.limit()];
byteBuffer.get(bytes,0,4);//获取缓冲区数据
byteBuffer.mark();//mark是一个索引,通过此方法指定Buffer中一个特定的position
System.out.println("5.从缓冲区取4个字节写入到通道,缓冲区【position:"+byteBuffer.position()+" Limit:"+byteBuffer.limit()+" Capacity:"+byteBuffer.capacity()+"】");
		

NIO基础教程之channel、buffer(一)_第6张图片

5.从缓冲区取4个字节写入到通道,缓冲区【position:4 Limit:7 Capacity:10】

第二次写入

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

//6.将缓冲区剩余的字节取出
byteBuffer.get(bytes,byteBuffer.position(),byteBuffer.limit()-byteBuffer.position());
System.out.println("6.将缓冲区剩余的字节取出写入到通道,缓冲区【position:"+byteBuffer.position()+" Limit:"+byteBuffer.limit()+" Capacity:"+byteBuffer.capacity()+"】");
		

NIO基础教程之channel、buffer(一)_第7张图片

6.将缓冲区剩余的字节取出写入到通道,缓冲区【position:7 Limit:7 Capacity:10】

重置  

 //7.调用reset()方法恢复到mark标记的position位置
byteBuffer.reset();
System.out.println("7.调用reset()方法恢复到mark标记的位置,缓冲区【position:"+byteBuffer.position()+" Limit:"+byteBuffer.limit()+" Capacity:"+byteBuffer.capacity()+"】");

NIO基础教程之channel、buffer(一)_第8张图片

7.调用reset()方法恢复到mark标记的位置,缓冲区【position:4 Limit:7 Capacity:10】

clear

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

  1. 它将 limit 设置为与 capacity 相同。
  2. 它设置 position 为 0。

下图显示了在调用 clear() 后缓冲区的状态:

//8.重设缓冲区以便接收更多的字节
byteBuffer.clear();
System.out.println("8.调用缓冲区的clear(),缓冲区【position:"+byteBuffer.position()+" Limit:"+byteBuffer.limit()+" Capacity:"+byteBuffer.capacity()+"】");

NIO基础教程之channel、buffer(一)_第9张图片

8.调用缓冲区的clear(),缓冲区【position:0 Limit:10 Capacity:10】

缓冲区现在可以接收新的数据了。

 

你可能感兴趣的:(NIO)