NIO
是什么
- New IO,提供与标准IO不同的IO工作方式,标准IO的工作方式是基于字节流和字符流进行操作的,而NIO是基于通道和缓存区进行操作的
- Non-blocking IO,可以非阻塞的使用IO,当线程从通道读取数据到数缓冲区时,线程还是可以进行其他的事情,当数据写入到缓冲区时,线程可以继续处理
- NIO还引入了选择器的概念,选择器用于监听多个通道的事件,如连接打开和数据到达,因此单个线程可以监听多个数据通道
与IO比较
比较对象 | IO | NIO |
---|---|---|
面向对象 | 面向流 | 面向缓冲 |
IO方式 | 阻塞IO | 非阻塞IO |
管理者 | 无 | 选择器 |
- 面向流与面向缓冲
- IO是面向流的,NIO是面向缓冲区的,IO面向流意味着每次从流中读一个或多个字节,直至读取所有的字节,他们没有被缓存在任何地方,此外它不能前后移动流中的数据,如果需要,得先将它们存到一个缓冲区。NIO的缓冲可以在缓冲区直接进行前后移动
- 阻塞与非阻塞
- IO的各种流是阻塞的,当调用read或write时,该线程被阻塞,这时的线程不能再做其他事情了。NIO的非阻塞式模式,使一个线程从某通道发送请求数据读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他事情,非阻塞写也是如此,所以一个单独的线程现在可以管理多个输入和输出通道
- 选择器
- NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或选择已准备写入的通道,这种选择机制,使得一个单独的线程很容易来管理多个通道
核心组件
- Channels
- Buffers
- Selectors
- 注:Channel、Butter和Selector构成了核心的API,虽然Java NIO中除此之外还有很多类和组件,例如Piple和FileLock,它们是与三个核心组件共同使用的工具类
Channel
- 通道,类似流,但有些不同
- 既可以从通道中读数据,又可以写数据到通道,但流的读写通常是单向的
- 通道可以异步的读写
- 通道中的数据总是先要读到一个Buffer,或者总要从一个Buffer中写入
- Channel的主要类型
- FileChannel【文件IO】
- DatagramChannel【UDP IO】
- SocketChannel【TCP IO】
- ServerSocketChannel【TCP IO】
-
Channel例子
//文件对象 RandomAccessFile raf = new RandomAccessFile("test.txt", "rw"); //获取通道 FileChannel fc = raf.getChannel(); //缓冲区 ByteBuffer buf = ByteBuffer.allocate(48); int len = fc.read(buf); while (len != -1) { //转为读模式【limit=position;position=0】 buf.flip(); //hasRemaining()判断position是否小于limit while(buf.hasRemaining()){ System.out.print((char) buf.get()); } //清空缓存区 buf.clear(); //继续读取通道内容 len = fc.read(buf); } raf.close();
-
Channel之间的传输
- 如果两个Channel中有一个是FileChannel,那么可以直接从一个Channel传输到另外一个通道
-
transferFrom方法可以将数据从源通道传输到FileChannel中
-
transferTo方法,调用对象与transferFrom相对
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw"); FileChannel fromChannel = fromFile.getChannel(); RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw"); FileChannel toChannel = toFile.getChannel(); long position = 0; long count = fromChannel.size(); fromChannel.transferTo(position, count, toChannel);
-
FileChannel
- 是一个连接到文件的通道,可以通过文件通道读写文件,FileChannel无法设置为非阻塞模式,它总是运行在阻塞模式
- 打开FileChannel
- 在使用FileChannel之前,必须先打开它,但是无法直接打开一个FileChannel,需要通过一个InputStream、OutputStream或RandomAccessFile来获取一个FileChannel实例
-
向File Channel写入数据
ByteBuffer buf = ByteBuffer.allocate(48); buf.clear(); buf.put("hello world".getBytes()); buf.flip(); while(buf.hasRemaining()) { channel.write(buf); }
- position()方法
- 获取当前的位置
- positio(long pos)
- 设置当前的位置
- size()方法
- 返回所关联文件的大小
- truncate(长度)
- 截取前“长度”个字节
- force()方法
- 将通道里尚未写入磁盘的数据强制写到磁盘上
- 参数 boolean:是否同时将文件元数据(权限信息等)写到磁盘
-
SocketChannel
- SocketChannel是一个连接到TCP网络套接字的通道,可以通过以下2中方式创建SocketChannel
- 打开SocketChannel,并连接到服务器
- 一个新的连接到达ServerSocketChannel时,会创建一个SocketChannel
-
SocketChannel相关操作
SocketChannel socketChannel = SocketChannel.open(); socketChannel.connect(new InetSocketAddress("localhost", 8000)); ByteBuffer buf = ByteBuffer.allocate(48); buf.clear(); buf.put("hello world!".getBytes()); buf.flip(); while(buf.hasRemaining()) { channel.write(buf); }
SocketChannel socketChannel = SocketChannel.open(); socketChannel.configureBlocking(false);//设置为非阻塞式 socketChannel.connect(new InetSocketAddress("localhost", 8000)); while(! socketChannel.finishConnect() ){ }
- SocketChannel是一个连接到TCP网络套接字的通道,可以通过以下2中方式创建SocketChannel
- ServerSocketChannel
- 可以监听新进来的TCP连接的通道,就像标准IO的ServerSocket一样
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.socket().bind(new InetSocketAddress(8000)); serverSocketChannel.configureBlocking(false);//设置为非阻塞式 while(true){ //监听新进来的连接 SocketChannel socketChannel = serverSocketChannel.accept(); //todo }
- 可以监听新进来的TCP连接的通道,就像标准IO的ServerSocket一样
-
DataGramChannel
- 是一个能收发UDP包的通道,因为UDP是无连接的网络协议,所以不能像其他通道那样读取和写入,它发送和接收的是数据包
-
例
DatagramChannel channel = DatagramChannel.open(); channel.socket().bind(new InetSocketAddress(8001)); //接收数据 ByteBuffer buf = ByteBuffer.allocate(48); buf.clear(); channel.receive(buf);//将数据包的内容复制到指定的Buffer,容不下的数据将丢弃
//发送数据 ByteBuffer buf = ByteBuffer.allocate(48); buf.clear(); buf.put("hello world!".getBytes()); buf.flip(); //下面将数据发送到本地的8000端口,因为服务端没有监控这个端口, //所以可能什么也不会发生,也不会通知你发出的数据包是否已经收到,因为UDP在数据传送方面没有任何保证 //因为UDP是无连接的,所以不会像tcp通道那样创建一条真正的连接,而是锁住DataGramChannel,让其从特定的位置收发数据 int bytesSent = channel.send(buf, new InetSocketAddress("localhost", 8001));
- 也可以使用read和write函数,只是在数据传送方面没有任何保证
Buffer
- NIO中的Buffer用于和通道Channel进行交互,数据可以从Channel读到Buffer中,也可以从Buffer写到Channel中
- 缓冲区本质上是一块可以进入数据,然后可以从中读取数据的内存,这块内存被包装成NIO Buffer对象,并提供方法方便使用内存
- Bufer的主要类型
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
- MappedByteBuffer【内存映射文件】
- Buffer的基本用法
- 写入数据到Buffer
- 调用flip方法
- 从Buffer中读取数据
- 调用clear或compact方法
- 当向Buffer写入数据时,Buffer会记录下写了多少数据,当读取数据时,需要flip方法将Buffer从写模式换到读模式,在读模式下,可以读取之前写入到Buffer的数据,一旦读完所有的数据,就需要清空缓冲区,让它可以再次被写入,有两种方式能清空缓冲区,调用clear或compact方法,clear会清空整个缓冲区,而compact方法只会清除已经读过的数据,任何未读的数据都被移到缓冲区的起始处,新的数据将放到缓冲区未读数据的后面
-
例子
//文件对象 RandomAccessFile raf = new RandomAccessFile("test.txt", "rw"); //获取通道 FileChannel fc = raf.getChannel(); //缓冲区 ByteBuffer buf = ByteBuffer.allocate(48); int len = fc.read(buf); while (len != -1) { //转为读模式【limit=position;position=0】 buf.flip(); //hasRemaining()判断position是否小于limit while(buf.hasRemaining()){ System.out.print((char) buf.get()); } //清空缓存区 buf.clear(); //继续读取通道内容 len = fc.read(buf); } raf.close();
- Buffer的capacity,position和limit
- capacity的含义是不变的,即缓冲区的容量,position和limit的含义取决于Buffer的读写模式
- 写模式:position表示当前的位置,初始的position为0,当写入一个数据后,position会向前移动到下一个可插入数据的Buffer单元,position的最大值为capacity-1;limit表示最多往里面写多少数据,limit=capacity
- 读模式:position通过flip函数将position重置为0,从Buffer开始位置进行读取数据;limit由写模式转为读模式时,会将limit的值重置为position写模式时的最后的位置
- Buffer的分配
- 要想获得一个Buffer对象,首先要进行分配,allocate方法
CharBuffer buf = CharBuffer.allocate(1024);
- 要想获得一个Buffer对象,首先要进行分配,allocate方法
- 向Buffer写
- int len = inChannel.read(buf);//通道->Buffer
- buf.put(666);
- 向Buffer读
- fc.write(buf);//Buffer->通道
- buf.get();
- 方法
- flip方法
- 将Buffer的写模式切换为读模式,调用flip函数,会将limit置为position,position置为0
- rewind方法
- 将position置为0,limit不变
- mark方法
- 标记Buffer的一个特定的position
- reset方法
- 恢复到标记的特定的position
- equals方法
- 元素类型+元素个数+元素相同
- compareTo方法
- 第一个不相等的元素相减
- 第一个元素耗尽
- flip方法
Selector
- Selector允许单线程处理多个Channel,如果你的应用打开了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便,例如聊天室。
- 要使用Selector得向Selector注册Channel,然后调用它的select()方法, 这个方法会一直阻塞到某个注册的通道事件就绪,一旦这个方法返回,线程就可以处理这些事件【新连接、数据接收等】
- 选择器能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件,这样,一个单独的线程可以管理多个Channel,从而管理多个网络连接,对于操作系统来说,线程之间的上下文切换的开销很大,而且每个线程都要占用系统的资源,因此,线程越少越好
- 例
//创建Selector Selector selector = Selector.open(); //将Channel设置为非阻塞 channel.configureBlocking(false); //注册(选择器,感兴趣事件【Connect,Accept,Read,Write】) SelectionKey key = channel.register(selector,Selectionkey.OP_READ);
- SelectionKey常量
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
- 多个事件组合
- int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
- SelectionKey对象
- interest集合
- ready集合
- Channel
- Selector
- 附加对象
-
interest集合
- 所选择的感兴趣的事件集合
-
例
int interestSet = selectionKey.interestOps(); boolean isInterestedInAccept = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT; boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT; boolean isInterestedInRead = interestSet & SelectionKey.OP_READ; boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
- ready集合
- 已经准备好的操作的集合
- 例
int readySet = selectionKey.readyOps();
- 也可以使用下面的方法
selectionKey.isAcceptable(); selectionKey.isConnectable(); selectionKey.isReadable(); selectionKey.isWritable();
- 访问Channel和Selector
Channel channel = selectionKey.channel(); Selector selector = selectionKey.selector();
- 附加对象
//selectionKey对象添加 selectionKey.attach(theObject); Object attachedObj = selectionKey.attachment();
//注册时进行添加 SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
-
通过Selector选择通道
- select();阻塞到至少有一个通道在注册的事件上就绪
- select()方法返回的int值表示有多少通道已经就绪,即自上次调用select方法后有多少通道变成就绪状态
- Set selectedKeys = selector.selectedKeys();//已选择的键集
- 例
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(); }
- wakeUp方法
- 某个线程调用select()方法后阻塞了,即使没有通道已经就绪,也有办法让其从select()方法返回。只要让其它线程在第一个线程调用select()方法的那个对象上调用Selector.wakeup()方法即可。阻塞在select()方法上的线程会立马返回。如果有其它线程调用了wakeup()方法,但当前没有线程阻塞在select()方法上,下个调用select()方法的线程会立即“醒来(wake up)”。
- close方法
- 用完Selector后调用其close()方法会关闭该Selector,且使注册到该Selector上的所有SelectionKey实例无效。通道本身并不会关闭。
- select(long timeout);最长会阻塞timeout毫秒,剩下的和select一样
- selectNow();不会阻塞,不管什么通道就绪就立刻返回
-
例-服务端
import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Iterator; import java.util.Set; public class SelectorServer { public static void main(String[] args) { try { ServerSocketChannel ssc = ServerSocketChannel.open(); ssc.socket().bind(new InetSocketAddress("127.0.0.1", 8000)); ssc.configureBlocking(false); Selector selector = Selector.open(); // 注册 channel,并且指定感兴趣的事件是 Accept ssc.register(selector, SelectionKey.OP_ACCEPT); ByteBuffer readBuff = ByteBuffer.allocate(1024); ByteBuffer writeBuff = ByteBuffer.allocate(128); writeBuff.put("received".getBytes()); writeBuff.flip(); while (true) { int nReady = selector.select(); Set<SelectionKey> keys = selector.selectedKeys(); Iterator<SelectionKey> it = keys.iterator(); while (it.hasNext()) { SelectionKey key = it.next(); it.remove(); if (key.isAcceptable()) { // 创建新的连接,并且把连接注册到selector上,而且, // 声明这个channel只对读操作感兴趣。 SocketChannel socketChannel = ssc.accept(); socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ); } else if (key.isReadable()) { SocketChannel socketChannel = (SocketChannel) key.channel(); readBuff.clear(); socketChannel.read(readBuff); readBuff.flip(); System.out.println("客户端发来:" + new String(readBuff.array())); key.interestOps(SelectionKey.OP_WRITE); } else if (key.isWritable()) { writeBuff.rewind(); SocketChannel socketChannel = (SocketChannel) key.channel(); socketChannel.write(writeBuff); key.interestOps(SelectionKey.OP_READ); } } } } catch (IOException e) { e.printStackTrace(); } } }
-
例-客户端
import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; public class SelectorClient { public static void main(String[] args) throws IOException { try { SocketChannel socketChannel = SocketChannel.open(); socketChannel.connect(new InetSocketAddress("127.0.0.1", 8000)); ByteBuffer writeBuffer = ByteBuffer.allocate(32); ByteBuffer readBuffer = ByteBuffer.allocate(32); writeBuffer.put("hello".getBytes()); writeBuffer.flip(); while (true) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } writeBuffer.rewind(); socketChannel.write(writeBuffer); readBuffer.clear(); socketChannel.read(readBuffer); System.out.println("服务端发来:" + new String(readBuffer.array())); } } catch (IOException e) { } } }
- select();阻塞到至少有一个通道在注册的事件上就绪
Scatter【分散】和Gather【聚集】
-
Scatter,从Channel中读取写到多个Buffer中
-
例如
ByteBuffer header = ByteBuffer.allocate(128); ByteBuffer body = ByteBuffer.allocate(1024); ByteBuffer[] bufferArray = { header, body }; channel.read(bufferArray);
-
-
Gather,将多个Buffer写入同一个Channel
-
例如
ByteBuffer header = ByteBuffer.allocate(128); ByteBuffer body = ByteBuffer.allocate(1024); ByteBuffer[] bufferArray = { header, body }; channel.write(bufferArray);
-
- 使用场景
- 将传输数据分开处理的场合【例如消息头和消息体】
Pipe
- Channel是两个线程之间的单向数据连接,Pipe有一个source通道和一个sink通道,数据会被写到sink通道,从source通道读取
-
例
Pipe pipe = Pipe.open(); //写入数据 Pipe.SinkChannel sinkChannel = pipe.sink(); ByteBuffer buf = ByteBuffer.allocate(48); buf.clear(); buf.put("hello world!".getBytes()); buf.flip(); while(buf.hasRemaining()) { sinkChannel.write(buf); } //读取数据 Pipe.SourceChannel sourceChannel = pipe.source(); ByteBuffer buf = ByteBuffer.allocate(48); int bytesRead = sourceChannel.read(buf);