NIO的知识点总结

参考文档链接:  https://www.zhihu.com/question/29005375

         http://ifeve.com/java-nio-all/

         https://www.yiibai.com/java_nio/java-nio-channels.html 

1.nio定义

  nio是非阻塞IO

2.IO、NIO的区别

  IO是面向流的阻塞IO,NIO是面向缓存的非阻塞IO。面向流的IO可以理解为以字节的处理数据,面向缓存是以块的形式处理数据。

3.三个核心

(1)Buffer - 缓冲

(2)Channel - 通道

(3)Selector - 选择器

4.Channel与Buffer的区别

  Channel管道比作成铁路,buffer缓冲区比作成火车(运载着货物)。而我们的NIO就是通过Channel管道运输着存储数据的Buffer缓冲区的来实现数据的处理!Channel不与数据打交道,它只负责运输数据。与数据打交道的是Buffer缓冲区。

5.Buffer

基本用法

  写入数据到Buffer

  调用flip()方法

  从Buffer中读取数据

  调用clear()方法或者compact()方法

三个属性

(1)capacity:作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”。你只能往里写capacity个byte、long,char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据。

(2)position:当你写数据到Buffer中时,position表示当前的位置。初始的position值为0.当一个byte、long等数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1。

(3)limit:在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0. 当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。

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

常用方法

(1)flip()

  flip方法将Buffer从写模式切换到读模式。调用flip()方法会将position设回0,并将limit设置成之前position的值。

(2)clear() 与 compact()

  如果调用的是clear()方法,position将被设回0,limit被设置成 capacity的值。换句话说,Buffer 被清空了。Buffer中的数据并未清除,只是这些标记告诉我们可以从哪里开始往Buffer里写数据。

  如果Buffer中有一些未读的数据,调用clear()方法,数据将“被遗忘”,意味着不再有任何标记会告诉你哪些数据被读过,哪些还没有。

  如果Buffer中仍有未读的数据,且后续还需要这些数据,但是此时想要先先写些数据,那么使用compact()方法。compact()方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。limit属性依然像clear()方法一样,设置成capacity。现在Buffer准备好写数据了,但是不会覆盖未读的数据。

(3)mark() 与 reset()

  通过调用Buffer.mark()方法,可以标记Buffer中的一个特定position。之后可以通过调用Buffer.reset()方法恢复到这个position

(4)equals() 与 compareTo()

equals() 只是比较Buffer的一部分,不是每一个在它里面的元素都比较。实际上,它只比较Buffer中的剩余元素。当满足下列条件时,表示两个Buffer相等:

(1)有相同的类型(byte、char、int等)。

(2)Buffer中剩余的byte、char等的个数相等。

(3)Buffer中所有剩余的byte、char等都相同。

compareTo()方法比较两个Buffer的剩余元素(byte、char等), 如果满足下列条件,则认为一个Buffer“小于”另一个Buffer:

(1)第一个不相等的元素小于另一个Buffer中对应的元素 。

(2)所有元素都相等,但第一个Buffer比另一个先耗尽(第一个Buffer的元素个数比另一个少)。

6.Channel

常用的通道

  FileChannel

  DatagramChannel

  SocketChannel

  ServerSocketChannel

通道实现

在Java NIO中,主要使用的通道如下:

  (1)FileChannel:文件通道用于从文件读取数据。它只能通过调用getChannel()方法来创建对象。不能直接创建FileChannel对象。注意,FileChannel不能设置为非阻塞模式。下面是一个创建FileChannel对象的例子:

FileInputStream fis = new FileInputStream("D:\\file-read.txt"); // Path of Input text file  

ReadableByteChannel rbc = fis.getChannel();

  (2)DatagramChannel:数据报通道可以通过UDP(用户数据报协议)通过网络读取和写入数据。它使用工厂方法来创建新对象。下面是打开DatagramChannel的语法:

DatagramChannel ch = DatagramChannel.open();

用于关闭DatagramChannel的语法:

DatagramChannel ch = DatagramChannel.close();

  (3)SocketChannel:数据报通道可以通过TCP(传输控制协议)通过网络读取和写入数据。 它还使用工厂方法来创建新对象。

用于打开SocketChannel的语法:

SocketChannel ch = SocketChannel.open();  

ch.connect(new InetSocketAddress("somehost", someport));

用于关闭SocketChannel的语法:

SocketChannel ch = SocketChannel.close();  

ch.connect(new InetSocketAddress("somehost", someport));

  (4)ServerSocketChannelServerSocketChannel允许用户监听传入的TCP连接,与Web服务器相同。对于每个传入连接,都会为连接创建一个SocketChannel

下面是打开ServerSocketChannel的语法:

ServerSocketChannel ch = ServerSocketChannel.open();  

ch.socket().bind (new InetSocketAddress (somelocalport));

下面是关闭ServerSocketChannel的语法:

ServerSocketChannel ch = ServerSocketChannel.close();  

ch.socket().bind (new InetSocketAddress (somelocalport));

7.Selector

定义

  选择器(Selector)用于使用单个线程处理多个通道。 因此,它需要较少的线程来处理这些通道。 线程之间的切换对于操作系统来说是昂贵的。 因此,使用它可以提高系统效率。

Selector的创建

Selector selector = Selector.open();

向Selector注册通道

为了将Channel和Selector配合使用,必须将channel注册到selector上。与Selector一起使用时,Channel必须处于非阻塞模式下。通过SelectableChannel.register()方法来实现,如下:

channel.configureBlocking(false);
SelectionKey key =channel.register(selector,Selectionkey.OP_READ);

SelectionKey

当向Selector注册Channel时,register()方法会返回一个SelectionKey对象。这个对象包含了一些你感兴趣的属性:

  • interest集合
  • ready集合
  • Channel
  • Selector

interest集合

通道触发了一个事件意思是该事件已经就绪。所以,某个channel成功连接到另一个服务器称为“连接就绪”。一个server socket channel准备好接收新进入的连接称为“接收就绪”。一个有数据可读的通道可以说是“读就绪”。等待写数据的通道可以说是“写就绪”。这四种事件用SelectionKey的四个常量来表示:

  1. SelectionKey.OP_CONNECT
  2. SelectionKey.OP_ACCEPT
  3. SelectionKey.OP_READ
  4. SelectionKey.OP_WRITE

可以通过SelectionKey读写interest集合,像这样:

int interestSet = selectionKey.interestOps();

boolean isInterestedInAccept  = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;

boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;

可以看到,用“位与”操作interest 集合和给定的SelectionKey常量,可以确定某个确定的事件是否在interest 集合中。

ready集合

ready 集合是通道已经准备就绪的操作的集合。在一次选择(Selection)之后,你会首先访问这个ready set。Selection将在下一小节进行解释。可以这样访问ready集合:

int readySet = selectionKey.readyOps();

可以用像检测interest集合那样的方法,来检测channel中什么事件或操作已经就绪。但是,也可以使用以下四个方法,它们都会返回一个布尔类型:

selectionKey.isAcceptable();

selectionKey.isConnectable();

selectionKey.isReadable();

selectionKey.isWritable();

通过Selector选择通道

一旦向Selector注册了一或多个通道,就可以调用几个重载的select()方法。这些方法返回你所感兴趣的事件(如连接、接受、读或写)已经准备就绪的那些通道。换句话说,如果你对“读就绪”的通道感兴趣,select()方法会返回读事件已经就绪的那些通道。

下面是select()方法:

  • int select()
  • int select(long timeout)
  • int selectNow()

select()阻塞到至少有一个通道在你注册的事件上就绪了。

select(long timeout)和select()一样,除了最长会阻塞timeout毫秒(参数)。

selectNow()不会阻塞,不管什么通道就绪都立刻返回(译者注:此方法执行非阻塞的选择操作。如果自从前一次选择操作后,没有通道变成可选择的,则此方法直接返回零。)。

select()方法返回的int值表示有多少通道已经就绪。亦即,自上次调用select()方法后有多少通道变成就绪状态。如果调用select()方法,因为有一个通道变成就绪状态,返回了1,若再次调用select()方法,如果另一个通道就绪了,它会再次返回1。如果对第一个就绪的channel没有做任何操作,现在就有两个就绪的通道,但在每次select()方法调用之间,只有一个通道就绪了。

selectedKeys()

可以通过调用selector的selectedKeys()方法,访问“已选择键集(selected key set)”中的就绪通道。如下所示:

Set selectedKeys = selector.selectedKeys();

当像Selector注册Channel时,Channel.register()方法会返回一个SelectionKey 对象。这个对象代表了注册到该Selector的通道。可以通过SelectionKey的selectedKeySet()方法访问这些对象。

可以遍历这个已选择的键集合来访问就绪的通道。如下:

Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {   SelectionKey key = keyIterator.next();
  if(key.isAcceptable()) {     // a connection was accepted by a ServerSocketChannel.   } else if (key.isConnectable())     // a connection was established with a remote server.   } else if (key.isReadable()) {     // a channel is ready for reading   } else if (key.isWritable()) {     // a channel is ready for writing   }   keyIterator.remove(); }

这个循环遍历已选择键集中的每个键,并检测各个键所对应的通道的就绪事件。

注意每次迭代末尾的keyIterator.remove()调用。Selector不会自己从已选择键集中移除SelectionKey实例。必须在处理完通道时自己移除。下次该通道变成就绪时,Selector会再次将其放入已选择键集中。

SelectionKey.channel()方法返回的通道需要转型成你要处理的类型,如ServerSocketChannel或SocketChannel等。

wakeUp()与close()

某个线程调用select()方法后阻塞了,即使没有通道已经就绪,也有办法让其从select()方法返回。只要让其它线程在第一个线程调用select()方法的那个对象上调用Selector.wakeup()方法即可。阻塞在select()方法上的线程会立马返回。

如果有其它线程调用了wakeup()方法,但当前没有线程阻塞在select()方法上,下个调用select()方法的线程会立即“醒来(wake up)”。

用完Selector后调用其close()方法会关闭该Selector,且使注册到该Selector上的所有SelectionKey实例无效。通道本身并不会关闭。

完整的示例

Selector selector = Selector.open();

channel.configureBlocking(false);

SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

while(true) {

  int readyChannels = selector.select();

  if(readyChannels == 0) continue;

  Set selectedKeys = selector.selectedKeys();

  Iterator keyIterator = selectedKeys.iterator();

  while(keyIterator.hasNext()) {

    SelectionKey key = keyIterator.next();

    if(key.isAcceptable()) {

       // a connection was accepted by a ServerSocketChannel.

    } else if (key.isConnectable()) {

      // a connection was established with a remote server.

    } else if (key.isReadable()) {

      // a channel is ready for reading

    } else if (key.isWritable()) {

      // a channel is ready for writing

    }

    keyIterator.remove();

  }

}

8.Scatter/Gathe

  分散读取(scatter):将一个通道中的数据分散读取到多个缓冲区中

  聚集写入(gather):将多个缓冲区中的数据集中写入到一个通道中

9. pipe

创建管道

 通过Pipe.open()方法打开管道。例如:

Pipe pipe = Pipe.open();

向管道写数据

要向管道写数据,需要访问sink通道。像这样:

Pipe.SinkChannel sinkChannel = pipe.sink();

通过调用SinkChannel的write()方法,将数据写入SinkChannel,像这样:

String newData = "New String to write to file..." + System.currentTimeMillis();

ByteBuffer buf = ByteBuffer.allocate(48);

buf.clear();

buf.put(newData.getBytes());

buf.flip();

while(buf.hasRemaining()) {

    sinkChannel.write(buf);

}

从管道读取数据

从读取管道的数据,需要访问source通道,像这样:

Pipe.SourceChannel sourceChannel = pipe.source();

调用source通道的read()方法来读取数据,像这样:

ByteBuffer buf = ByteBuffer.allocate(48);

int bytesRead = sourceChannel.read(buf);

read()方法返回的int值会告诉我们多少字节被读进了缓冲区。

 
上一篇:NIO-Selector


下一篇:参数是SelectionKey类中的一个命名常量,标识所注册的操作