参考文档链接: 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)ServerSocketChannel:ServerSocketChannel
允许用户监听传入的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的四个常量来表示:
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- 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值会告诉我们多少字节被读进了缓冲区。