NIO简介
Java NIO
(New IO Non Blocking IO)可以理解为新IO
或非阻塞IO
,是从Java1.4开始引入的一个新的IO API,与原来的IO有同样的作用和目的,但是使用方式完全不同
NIO支持面向缓冲区、基于通道的IO操作。简单说,NIO将以更高效的方式进行文件的读写
传统IO与NIO的区别?
传统IO是面向流的,且是单向操作的。分别需要输入流跟输出流,流直接面对其中的数据,来进行数据的传输(即把文件通过byte[]数组进行传输),可以简单的将传统的IO理解为日常生活中的水流
NIO中建立连接的是通道,可以将通道简单的理解为日常生活中的铁路,但是铁路本身并不具备传输功能,我们需要通过缓冲区来进行传输数据,缓冲区可以对等的理解为火车。所以我们可以得知,NIO是面向缓冲区的,且是双向操作的
传统IO:面向流,直接传输数据,单向操作,阻塞IO(水流在水管中单向流动)
NIO:面向缓冲区,通道只做连接,传输依靠缓冲区,双向操作,非阻塞IO(火车在铁路上来回运输)
通道与缓冲区
NIO系统的核心在于:通道(Channel
)和缓冲区(Buffer
)
通道: 表示打开到IO设备(文件等)的连接,使用NIO之前,一定要获取连接IO设备的通道【铁路】
缓冲区: 就是数组,负责不同数据类型的数据存储【火车】
可以简单的理解为:Channel
负责传输,Buffer
负责存储
缓冲区的使用
根据数据类型不同,提供对应类型的缓冲区(boolean除外
)
包含如下:ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer
上述缓冲区的管理方式几乎一致,通过allocate()
获取缓冲区
缓冲区存取数据的两个核心方法:
put()
:存入数据到缓冲区中
get()
:获取缓冲区中的数据
缓冲区中四个核心属性:
capacity
:容量,表示缓冲区中最大存储数据的容量,一旦声明不能改变(因为底层是数组
)
limit
:界限,还是缓冲区中可以操作的数据的大小(limit后数据不能进行读写
)
position
:位置,表示缓冲区中正在操作数据的位置
mark
:标记,表示记录当前position
的位置,可以通过reset()
恢复到mark
位置
以上参数的限制:0 <= mark <= position <= limit <= capacity
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 ByteBuffer bf = ByteBuffer.allocate(1024 ); System.out.println("-----------allocate()-----------" ); System.out.println("当前位置:" + bf.position()); System.out.println("当前界限:" + bf.limit()); System.out.println("当前容量:" + bf.capacity()); System.out.println("-----------put()-----------" ); bf.put("abcde" .getBytes()); System.out.println("当前位置:" + bf.position()); System.out.println("当前界限:" + bf.limit()); System.out.println("当前容量:" + bf.capacity()); System.out.println("-----------flip()-----------" ); bf.flip(); System.out.println("当前位置:" + bf.position()); System.out.println("当前界限:" + bf.limit()); System.out.println("当前容量:" + bf.capacity()); System.out.println("-----------get()-----------" ); byte [] dst = new byte [2 ];bf.get(dst); System.out.println("读取数据:" + new String(dst, 0 , dst.length)); System.out.println("当前位置:" + bf.position()); System.out.println("当前界限:" + bf.limit()); System.out.println("当前容量:" + bf.capacity()); System.out.println("-----------get()-----------" ); byte [] dst1 = new byte [3 ];bf.get(dst1); System.out.println("读取数据:" + new String(dst1, 0 , dst1.length)); System.out.println("当前位置:" + bf.position()); System.out.println("当前界限:" + bf.limit()); System.out.println("当前容量:" + bf.capacity()); System.out.println("-----------rewind()-----------" ); bf.rewind(); System.out.println("当前位置:" + bf.position()); System.out.println("当前界限:" + bf.limit()); System.out.println("当前容量:" + bf.capacity()); System.out.println("-----------clear()-----------" ); bf.clear(); System.out.println("当前位置:" + bf.position()); System.out.println("当前界限:" + bf.limit()); System.out.println("当前容量:" + bf.capacity());
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 控制台打印信息 -----------allocate()----------- 当前位置:0 当前界限:1024 当前容量:1024 -----------put()----------- 当前位置:5 当前界限:1024 当前容量:1024 -----------flip()----------- 当前位置:0 当前界限:5 当前容量:1024 -----------get()----------- 读取数据:ab 当前位置:2 当前界限:5 当前容量:1024 -----------get()----------- 读取数据:cde 当前位置:5 当前界限:5 当前容量:1024 -----------rewind()----------- 当前位置:0 当前界限:5 当前容量:1024 -----------clear()----------- 当前位置:0 当前界限:1024 当前容量:1024
这里我相信大家肯定跟我同样有一个疑问,到底什么时候使用flip()
进行切换呢?
下面我们看一下flip()
这个方法里面都干了什么,你就会明白什么时候使用了
1 2 3 4 5 6 7 8 9 10 11 12 13 public final Buffer flip () { limit = position; position = 0 ; mark = -1 ; return this ; }
下面我们重新存储一下数据 进行mark()
和reset()
的演示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 String str = "abcdef" ; ByteBuffer bf = ByteBuffer.allocate(1024 ); bf.put(str.getBytes()); bf.flip(); byte [] b = new byte [bf.limit()];bf.get(b, 0 , 2 ); System.out.println("读取到的字符:" + new String(b, 0 , 2 ) + " 当前位置:" + bf.position()); bf.mark(); System.out.println("使用mark进行标记" ); bf.get(b, 2 , 2 ); System.out.println("读取到的字符:" + new String(b, 2 , 2 ) + " 当前位置:" + bf.position()); bf.reset(); System.out.println("已使用reset恢复 当前位置:" + bf.position()); if (bf.hasRemaining()) { System.out.println("剩余可读取的数量:" + bf.remaining()); }
1 2 3 4 5 6 7 控制台打印信息 读取到的字符:ab 当前位置:2 使用mark进行标记 读取到的字符:cd 当前位置:4 已使用reset恢复 当前位置:2 剩余可读取的数量:4
直接缓冲区&非直接缓冲区
非直接缓冲区:通过allocate()
方法分配缓冲区,将缓冲区建立在JVM
的内存中
直接缓冲区:通过allocateDirect()
方法或通过FileChannel
的map()
方法,直接将缓冲区建立在物理内存中,且只有ByteBuffer
支持直接缓冲区,两种方法原理相同仅获取方式不同
1 2 3 4 5 6 7 ByteBuffer bf1 = ByteBuffer.allocateDirect(1024 ); ByteBuffer bf2 = ByteBuffer.allocate(1024 ); System.out.println("allocateDirect:" + bf1.isDirect()); System.out.println("allocate:" + bf2.isDirect());
1 2 3 4 控制台打印信息 allocateDirect:true allocate:false
建议:一般情况下,直接缓冲区分配和取消所需要消耗的成本都高于非直接缓冲区,所以除非直接缓冲区能在程序性能方面带来明显好处时才进行使用
通道Channel
用于源节点与目标节点的连接,在Java NIO
中负责缓冲区中数据的传输
Channel
本身不存储数据,因此需要配合缓冲区进行传输
通道的主要实现类 java.nio.channels.Channel
接口:
本地:
网络:
SocketChannel
ServerSocketChannel
DatagramChannel
获取通道Channel三种方法
这里需要注意的是,只是获取通道的方式不同,与直接缓冲区或非直接缓冲区无关
Java
针对支持通道的类提供了getChannel()
方法
本地IO:
FileInputStream/FileOutputStream
RandomAccessFile
网络IO:
Socket
ServerSocket
DatagramSocket
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 FileInputStream fis = new FileInputStream("d:\\xx.mp4" ); FileOutputStream fos = new FileOutputStream("d:\\yy.mp4" ); FileChannel inChannel = fis.getChannel(); FileChannel outChannel = fos.getChannel(); ByteBuffer buffer = ByteBuffer.allocate(1024 ); while (inChannel.read(buffer) != -1 ) { buffer.flip(); outChannel.write(buffer); buffer.clear(); } outChannel.close(); inChannel.close(); fos.close(); fis.close();
在JDK1.7
中的NIO.2
针对各个通道提供了静态方法open()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 FileChannel inChannel = FileChannel.open(Paths.get("D:\\xx.mp4" ), StandardOpenOption.READ); FileChannel outChannel = FileChannel.open(Paths.get("D:\\yy.mp4" ), StandardOpenOption.WRITE,StandardOpenOption.READ, StandardOpenOption.CREATE); MappedByteBuffer inMapBuffer = inChannel.map(MapMode.READ_ONLY, 0 , inChannel.size()); MappedByteBuffer outMapBuffer = outChannel.map(MapMode.READ_WRITE, 0 , inChannel.size()); byte [] dst = new byte [inMapBuffer.limit()];inMapBuffer.get(dst); outMapBuffer.put(dst); inChannel.close(); outChannel.close();
StandardOpenOption.CREATE_NEW
:如果文件不存在则创建 存在则报错
StandardOpenOption.CREATE
:如果文件不存在则创建 存在则覆盖
注意:因为MapMode.READ_WRITE
只有读写方式,所以StandardOpenOption
必须同时指定READ
和WRITE
两种模式,否则会出现报错
在JDK1.7
中的NIO.2
的Files
工具类的newByteChannel()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 SeekableByteChannel inChannel = Files.newByteChannel(Paths.get("D:\\xx.mp4" ), StandardOpenOption.READ); SeekableByteChannel outChannel = Files.newByteChannel(Paths.get("D:\\yy.mp4" ), StandardOpenOption.WRITE,StandardOpenOption.READ, StandardOpenOption.CREATE); ByteBuffer buf = ByteBuffer.allocate(1024 ); while (inChannel.read(buf) != -1 ) { buf.flip(); outChannel.write(buf); buf.clear(); } outChannel.close(); inChannel.close();
通道之间的数据传输
使用transferFrom()
和transferTo()
对数据进行直接传输(采用直接缓冲区方式
)
读数据通道使用transferTo()
,写数据通道使用transferFrom()
,任选一种方式即可
1 2 3 4 5 6 7 8 9 10 FileChannel inChannel = FileChannel.open(Paths.get("d:\\xx.mp4" ), StandardOpenOption.READ); FileChannel outChannel = FileChannel.open(Paths.get("d:\\yy.mp4" ), StandardOpenOption.READ,StandardOpenOption.WRITE, StandardOpenOption.CREATE); outChannel.transferFrom(inChannel, 0 , inChannel.size()); inChannel.close(); outChannel.close();
分散(Scatter)与聚集(Gather)
分散读取 :将通道中的数据分散到多个缓冲区中,按照缓冲区顺序,从通道中读取的数据依次将缓冲区填满
聚集写入 :将多个缓冲区中的数据聚集到通道中,按照缓冲区顺序,写入position
和limit
间的数据到通道中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 RandomAccessFile read = new RandomAccessFile(new File("d:\\11.txt" ), "rw" ); FileChannel inChannel = read.getChannel(); ByteBuffer buffer1 = ByteBuffer.allocate(100 ); ByteBuffer buffer2 = ByteBuffer.allocate(1024 ); ByteBuffer[] bufs = { buffer1, buffer2 }; inChannel.read(bufs); RandomAccessFile write = new RandomAccessFile(new File("d:\\22.txt" ), "rw" ); FileChannel outChannel = write.getChannel(); for (ByteBuffer buffer : bufs) { buffer.flip(); } outChannel.write(bufs); outChannel.close(); inChannel.close();
字符集Charset
编码:字符串 -> 字节数组
解码:字节数组 -> 字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Charset charset = Charset.forName("GBK" ); CharsetEncoder encoder = charset.newEncoder(); CharsetDecoder decoder = charset.newDecoder(); CharBuffer charBuffer = CharBuffer.allocate(1024 ); charBuffer.put("一给我哩giao" ); charBuffer.flip(); ByteBuffer byteBuffer = encoder.encode(charBuffer); CharBuffer result = decoder.decode(byteBuffer); System.out.println("解码后的数据:" + result.toString());
那么为什么encode()编码
跟decode()解码
后不需要用flip()
呢?
这里我们需要注意三点:
第一点是flip()
的含义,flip()
是将我们现在移动的位置position
当作界限limit
,然后将position
重置为0
,这就意味着我们只有在flip()
后才可以进行get()
,否则我们获取的位置是新的位置,并不是我们已经放入数据的位置
第二点为什么我们编码或解码前,需要进行flip()
,因为在编码或解码的过程中,是需要读取数据的,否则怎么对你已经put()
存入的数据进行读取呢?所以我们需要保证传入的缓冲区已经执行了flip()
方法,确保编码跟解码的时候可以读取到数据
第三点我们可以查看encode()
跟decode()
源码发现,在原代码最后返回之前,都已经替我们执行了一次flip()
,所以我们无论是编码还是解码后,都可以直接读取里面的数据而无需flip()
阻塞式网络通信
接下来我们用代码模拟阻塞式网络通信
,传输一张图片
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 SocketChannel clientChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1" ,8888 )); ByteBuffer buf = ByteBuffer.allocate(1024 ); FileChannel inChannel = FileChannel.open(Paths.get("d:\\xx.jpg" ),StandardOpenOption.READ); while (inChannel.read(buf) != -1 ) { buf.flip(); clientChannel.write(buf); buf.clear(); } clientChannel.shutdownOutput(); int len = 0 ;while ((len = clientChannel.read(byteBuffer)) != -1 ) { byteBuffer.flip(); System.out.println(new String(byteBuffer.array(), 0 , len)); byteBuffer.clear(); } inChannel.close(); clientChannel.close();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 ServerSocketChannel serverChannel = ServerSocketChannel.open(); serverChannel.bind(new InetSocketAddress(8888 )); SocketChannel clientChannel = serverChannel.accept(); FileChannel outChannel = FileChannel.open(Paths.get("d:\\yy.jpg" ), StandardOpenOption.WRITE,StandardOpenOption.CREATE); ByteBuffer buf = ByteBuffer.allocate(1024 ); while (clientChannel.read(buf) != -1 ) { buf.flip(); outChannel.write(buf); buf.clear(); } byteBuffer.put("服务端数据接收完成!" .getBytes()); byteBuffer.flip(); clientChannel.write(byteBuffer); outChannel.close(); clientChannel.close(); serverChannel.close();
非阻塞式网络通信
当调用register(Selector sel,int ops)
将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数ops
指定
可以监听的事件类型(可以使用SelectionKey
的四个常量表示):
读:SelectionKey.OP_READ
写:SelectionKey.OP_WRITE
连接:SelectionKey.OP_CONNECT
接收:SelectionKey.OP_ACCEPT
1 2 int i = SelectionKey.OP_READ|SelectionKey.OP_WRITE;
接下来我们用代码模拟非阻塞式网络通信
,完成简易聊天室的效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 SocketChannel clientChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1" ,9999 )); clientChannel.configureBlocking(false ); ByteBuffer byteBuffer = ByteBuffer.allocate(1024 ); Scanner scanner = new Scanner(System.in); while (scanner.hasNext()) { byteBuffer.put((new Date().toString() + "\n" + scanner.next()).getBytes()); byteBuffer.flip(); clientChannel.write(byteBuffer); byteBuffer.clear(); } clientChannel.close();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 ServerSocketChannel serverChannel = ServerSocketChannel.open(); serverChannel.configureBlocking(false ); serverChannel.bind(new InetSocketAddress(9999 )); Selector selector = Selector.open(); serverChannel.register(selector, SelectionKey.OP_ACCEPT); while (selector.select() > 0 ) { Iterator<SelectionKey> it = selector.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey key = it.next(); if (key.isAcceptable()) { SocketChannel clientChannel = serverChannel.accept(); clientChannel.configureBlocking(false ); clientChannel.register(selector, SelectionKey.OP_READ); } else if (key.isReadable()) { SocketChannel clientChannel = (SocketChannel) key.channel(); ByteBuffer byteBuffer = ByteBuffer.allocate(1024 ); int len = 0 ; while ((len = clientChannel.read(byteBuffer)) > 0 ) { byteBuffer.flip(); System.out.println(new String(byteBuffer.array(), 0 , len)); byteBuffer.clear(); } } it.remove(); } }
DatagramChannel
Java NIO
中的DatagramChannel
是一个能收发UDP
包的通道
操作步骤:
打开DatagramChannel
接收/发送数据
同样,我们使用这种方式实现一个简易的聊天室
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 DatagramChannel sendChannel = DatagramChannel.open(); sendChannel.configureBlocking(false ); ByteBuffer byteBuffer = ByteBuffer.allocate(1024 ); Scanner scanner = new Scanner(System.in); while (scanner.hasNext()) { byteBuffer.put((new Date().toString() + "\n" + scanner.next()).getBytes()); byteBuffer.flip(); sendChannel.send(byteBuffer, new InetSocketAddress("127.0.0.1" , 9999 )); byteBuffer.clear(); } sendChannel.close();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 DatagramChannel receiveChannel = DatagramChannel.open(); receiveChannel.configureBlocking(false ); receiveChannel.bind(new InetSocketAddress(9999 )); Selector selector = Selector.open(); receiveChannel.register(selector, SelectionKey.OP_READ); while (selector.select() > 0 ) { Iterator<SelectionKey> it = selector.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey key = it.next(); if (key.isReadable()) { ByteBuffer byteBuffer = ByteBuffer.allocate(1024 ); receiveChannel.receive(byteBuffer); byteBuffer.flip(); System.out.println(new String(byteBuffer.array(), 0 , byteBuffer.limit())); byteBuffer.clear(); } } it.remove(); }
管道Pipe
Java NIO
管道是2个线程之间的单向数据连接
Pipe
有一个source
通道和一个sink
通道,数据会被写到sink
通道,从source
通道读取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 Pipe pipe = Pipe.open(); ByteBuffer byteBuffer = ByteBuffer.allocate(1024 ); SinkChannel sinkChannel = pipe.sink(); byteBuffer.put("通过单向管道发送数据" .getBytes()); byteBuffer.flip(); sinkChannel.write(byteBuffer); SourceChannel sourceChannel = pipe.source(); byteBuffer.flip(); int len = sourceChannel.read(byteBuffer);System.out.println(new String(byteBuffer.array(), 0 , len)); sourceChannel.close(); sinkChannel.close();