Java之IO与NIO
NIO简介
-
起源
Java NIO(New IO)可以理解为非阻塞IO(Non Blocking IO),是从java 1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API
-
使用方式
NIO支持面向缓冲区,是基于通道的IO操作,文件读取操作更加高效
NIO与IO区别
面向对象不同
IO是面向流(Stream Oriented)——流是单向的
NIO是面向缓冲区(Buffer Oriented)——缓冲区是双向的
阻塞方式不同
IO是阻塞IO(Blocking IO)
NIO是非阻塞IO(Non Blocking IO)
其他
NIO引入了Selectors
缓冲区(Buffer)和通道(Channel)
简而言之,Channel负责传输,Buffer负责存储
缓冲区(Buffer)
-
简介
一个用于特定基本数据类型的容器,实现对数据进行处理,由java.nio包定义,所有缓冲区都是Buffer抽象类的子类
-
主要作用
Java NIO中的Buffer主要用于NIO通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的
-
实现方式
-
缓冲区底层是数组,并根据数据类型不同(boolean除外),提供了相应类型的缓冲区
ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer
-
都是通过allocate()获取缓冲区
-
-
四大核心属性
-
直接缓冲区和非直接缓冲区
可以通过isDirect()判断缓冲区类型
非直接缓冲区:通过allocate()方法分配缓冲区,将缓冲区建立在JVM的内存中
直接缓冲区:通过allocateDirect()方法分配直接缓冲区,将缓冲区建立在物理内存中
- 可以提高效率(测试发现大概提高10倍);
- 但不安全,且写入物理内存中后由操作系统主导控制
- 且引用只能有垃圾回收器进行断开引用,造成文件IO操作实际结束,但程序却停顿10秒左右才结束
-
案例代码
public class BufferTest { /** * 四大核心属性: * capacity:容量,表示缓冲区中最大存储数据的容量。一旦声明不能改变 * limit:界限,表示缓冲区中可以操作数据的大小(limit后数据不能读写) * position:位置,表示缓冲区中正在操作数据的位置 * * mark:标记,表示记录当前position的位置,可以通过reset()恢复到mark的位置 * * 0 <= mark <= position <= limit <= capacity **/ @Test public void test1(){ //1.利用allocate() —— 初始化缓冲区 ByteBuffer buf = ByteBuffer.allocate(1024); System.out.println(buf.capacity()); System.out.println("初始化操作数据位置:" + buf.position()); System.out.println(buf.limit()); //2.利用put() —— 存入数据到缓冲区 String str = "charles"; System.out.println("------------------put-------------------------"); buf.put(str.getBytes()); System.out.println("写模式buffer操作数据位置:" + buf.position()); //3.利用flip() —— 切换至读取数据模式 buf.flip(); System.out.println("------------------flip-------------------------"); System.out.println("读模式buffer操作数据位置:" + buf.position()); System.out.println(buf.limit()); //4.利用get() —— 读取缓冲区中的数据 byte[] bst = new byte[buf.limit()]; buf.get(bst);//将缓冲区中的数据读到字节数组中 System.out.println("------------------get-------------------------"); System.out.println(new String(bst,0,bst.length)); System.out.println(buf.position());//读取结束后缓冲区中操作数据库中的位置为读取数据长度 System.out.println(buf.limit()); System.out.println(buf.capacity()); //5.利用rewind() —— 恢复到可重复读模式 buf.rewind(); System.out.println("------------------rewind-------------------------"); System.out.println(buf.position()); System.out.println(buf.limit()); System.out.println(buf.capacity()); //6.利用clear() —— 清空缓冲区(缓冲区中数据依然存在,但是处于被遗忘状态,无法获取位置和状态) buf.clear(); System.out.println("-----------------clear--------------------------"); System.out.println(buf.position()); System.out.println(buf.limit()); System.out.println(buf.capacity()); System.out.println((char)buf.get());//默认读取是ASCII码 } @Test public void test2(){ String str = "charles"; ByteBuffer buffer = ByteBuffer.allocate(1024); buffer.put(str.getBytes()); buffer.flip();//切换为读模式 byte[] dst = new byte[buffer.limit()]; buffer.get(dst,0,3); System.out.println(new String(dst,0,3)); System.out.println("当前操作数据位置:" + buffer.position()); buffer.mark();//标记 buffer.get(dst,5,2); System.out.println(new String(dst,0,7));//思考为什么输出是:cha rle System.out.println(buffer.position()); buffer.reset(); System.out.println(buffer.position()); //判断缓冲区是否还有剩余数据 if (buffer.hasRemaining()) { //获取缓冲区中可以操作的数量 System.out.println(buffer.remaining()); } } }
通道(Channel)
-
例图
-
简介
通道表示打开到IO设备(列如:文件、套接字)的连接。类似于传统的"流",只不过Channel本身不能直接访问数据,Channel只能与Buffer进行交互。另外,Channel是一个完全独立的处理器,附属于CPU,替代以往的DMA总线,直接负责与IO接口交互
-
常用实现类
java.nio.channels.Channel接口
|–FileChannel(用于本地)
|–SocketChannnel(用于网络)
|–ServerSocketChannel(用于网络)
|–DatagramChannel(用于网络)FileChannel不能切换为非阻塞模式,非阻塞模式是针对于网络传输
文件通道(FileChannel)
获取通道方式
-
Java针对支持通道的类提供了getChannel()方法
本地IO:
FileInputStream/FileOutputStream
RandomAccessFile网络IO: Socket ServerSocket DatagramSocket
-
在JDK1.7中的NIO.2针对各个通道提供了静态方法open()
-
在JDK1.7中的NIO.2的Files工具类的newByteChannel()
通道之间的数据传输
transferFrom()
transferTo()
分散与聚集
-
分散读取(Scattering Reads):将通道中的数据分散到多个缓冲区中
-
聚集写入(Gathering Writes):将多个缓冲区中的数据聚集到通道中
-
例图
![分散(Scatter)]与聚集(Gather)](https://img-blog.csdnimg.cn/20200229121058690.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MjU4NjcyMw==,size_16,color_FFFFFF,t_70)
字符集(Charset)
编码:字符串 ——> 字节数组
解码:字节数组 ——> 字符串
案例代码
-
案例
public class TestChannel { /** * @Author charlesYan * @Description 使用非直接缓冲区利用通道完成文件的复制 * @Date 11:26 2020/2/27 * @Param [] * @return void **/ @Test public void test1(){ FileInputStream fis = null; FileOutputStream fos = null; FileChannel inChannel = null; FileChannel outChannel = null; try { fis = new FileInputStream("D:\\temp\\SynchronizedDemo.java"); fos = new FileOutputStream("D:\\temp\\SynchronizedDemo2.java"); //1.获取通道 inChannel = fis.getChannel(); outChannel = fos.getChannel(); //2.分配指定大小的缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); //3.将通道中的数据存入缓冲区中 while (inChannel.read(buffer) != -1){ buffer.flip();//切换读取数据的模式 //4.将缓冲区中的数据写入通道中 outChannel.write(buffer); buffer.clear();//清空缓冲区 } } catch (IOException e) { e.printStackTrace(); } finally { if (outChannel != null) { try { outChannel.close(); } catch (IOException e) { e.printStackTrace(); } } if (inChannel != null) { try { inChannel.close(); } catch (IOException e) { e.printStackTrace(); } } if (fos != null) { try { fos.close(); } catch (IOException e) { e.printStackTrace(); } } if (fis != null) { try { fis.close(); } catch (IOException e) { e.printStackTrace(); } } } } /** * @Author charlesYan * @Description 使用直接缓冲区完成文件的复制(内存映射文件) * @Date 11:51 2020/2/27 * @Param [] * @return void **/ @Test public void test2() throws IOException { FileChannel inChannel = FileChannel.open(Paths.get("D:\\temp\\SynchronizedDemo.java"), StandardOpenOption.READ); //StandardOpenOption.CREATE_NEW若存在,则创建报错 FileChannel outChannel = FileChannel.open(Paths.get("D:\\temp\\SynchronizedDemo2.java"), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW); //内存映射文件(只有ByteBuffer支持) MappedByteBuffer inMappedBuf = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size()); MappedByteBuffer outMappedBuf = outChannel.map(FileChannel.MapMode.READ_WRITE, 0, inChannel.size()); //直接对缓冲区进行数据的读写操作,无需通过Channel操作 byte[] dst = new byte[inMappedBuf.limit()]; inMappedBuf.get(dst); outMappedBuf.put(dst); inChannel.close(); outChannel.close(); } /** * @Author charlesYan * @Description 通道之间的数据传输 * @Date 12:28 2020/2/27 * @Param [] * @return void **/ @Test public void test3() throws IOException { FileChannel inChannel = FileChannel.open(Paths.get("D:\\temp\\SynchronizedDemo.java"), StandardOpenOption.READ); //StandardOpenOption.CREATE_NEW若存在,则创建报错 FileChannel outChannel = FileChannel.open(Paths.get("D:\\temp\\SynchronizedDemo2.java"), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW); // inChannel.transferTo(0,inChannel.size(),outChannel); outChannel.transferFrom(inChannel,0,inChannel.size()); inChannel.close(); outChannel.close(); } } /** * @Author charlesYan * @Description 分散读取 * @Date 14:25 2020/2/27 * @Param [] * @return void **/ @Test public void test4() throws IOException { RandomAccessFile raf = new RandomAccessFile("D:\\temp\\如何将UTC时间转换为CST时间格式.md", "rw"); //1.获取通道 FileChannel channel = raf.getChannel(); //2.分配指定大小的缓冲区 ByteBuffer buf1 = ByteBuffer.allocate(100); ByteBuffer buf2 = ByteBuffer.allocate(10240); //3.分散读取 ByteBuffer[] bufs = {buf1,buf2}; channel.read(bufs); for (ByteBuffer byteBuffer : bufs) { byteBuffer.flip(); } System.out.println(new String(bufs[0].array(), 0, bufs[0].limit())); System.out.println("-----------------------"); System.out.println(new String(bufs[1].array(),0,bufs[1].limit())); } /** * @Author charlesYan * @Description 聚集写入与分散读取 * @Date 14:27 2020/2/27 * @Param [] * @return void **/ @Test public void test5() throws IOException { RandomAccessFile inRaf = new RandomAccessFile("D:\\temp\\如何将UTC时间转换为CST时间格式.md", "rw"); RandomAccessFile outRaf = new RandomAccessFile("D:\\temp\\如何将UTC时间转换为CST时间格式1.md", "rw"); //获取通道 FileChannel channel = inRaf.getChannel(); //2.分配指定大小的缓冲区 ByteBuffer buf1 = ByteBuffer.allocate(100); ByteBuffer buf2 = ByteBuffer.allocate(10240); //3.分散读取 ByteBuffer[] bufs = {buf1,buf2}; channel.read(bufs); for (ByteBuffer byteBuffer : bufs) { byteBuffer.flip(); } //获取通道 FileChannel outChannel = outRaf.getChannel(); outChannel.write(bufs); } /** * @Author charlesYan * @Description 编码与解码 * @Date 16:07 2020/2/27 * @Param [] * @return void **/ @Test public void test6() throws CharacterCodingException { Map<String, Charset> map = Charset.availableCharsets(); Charset gbk = Charset.forName("GBK"); //获取编码器 CharsetEncoder gbkEncoder = gbk.newEncoder(); //获取解码器 CharsetDecoder gbkDecoder = gbk.newDecoder(); //初始化Buffer CharBuffer charBuffer = CharBuffer.allocate(2048); charBuffer.put("我是谁,我在哪"); charBuffer.flip(); //编码 ByteBuffer byteBuffer = gbkEncoder.encode(charBuffer); for (int i = 0; i < byteBuffer.limit(); i++) { System.out.println(byteBuffer.get()); } //解码 byteBuffer.flip(); CharBuffer newCharBuffer = gbkDecoder.decode(byteBuffer); System.out.println(newCharBuffer.toString()); }
NIO非阻塞式网络通信
-
阻塞与非阻塞
IO阻塞原因
用户从客户端网络发送数据到服务端,先到服务端内核地址空间,服务端判断在内核地址空间中数据是否存在,若没有数据,则服务端该线程就等待;若内核地址空间存在数据,则将数据copy到用户地址空间
选择器(Selector)
SelectionKey
-
概念
表示SelectableChannel和Selector之间的注册关系。每次向选择器注册通道时,就会选择一个事件(选择键)。
-
类型
选择键包含两个表示为整数值的操作集,操作集的每一位都表示该键的通道所支持的一类可选择操作。
-
作用
选择器会将每一个用于传输数据的通道都注册到自身,从而监控这些通道的状况,当某一个通道的某一个请求的数据完全准备就绪时,选择器才会将该任务分配到服务端一个或多个线程中运行
-
使用NIO完成网络通信的3个核心
-
通道(Channel):负责连接
-
缓冲区(Buffer):负责数据的存取
-
选择器(Selector):是SelectableChannel的多路复用器。用于监控SelectableChannel的IO状况
-
通道(Channel)
-
常用接口
java.nio.channels.Channel接口:
|–SelectableChannel
|–SocketChannel(是一个连接到TCP网络套接字的通道)
|–ServerSocketChannel
|–DatagramChannel|--Pipe.SinkChannel |--Pipe.SourceChannel
NIO_阻塞式
-
案例代码
//阻塞式IO public class TestBlockingNIO { /** * @Author charlesYan * @Description 服务端接收数据 * @Date 21:55 2020/2/27 * @Param [] * @return void **/ @Test public void server() throws IOException { //1.获取通道 ServerSocketChannel serverChannel = ServerSocketChannel.open(); //创建本地FileChannel FileChannel fileChannel = FileChannel.open(Paths.get("D:\\temp\\如何将UTC时间转换为CST时间格式2.md"), StandardOpenOption.WRITE, StandardOpenOption.CREATE); //2.绑定连接 serverChannel.bind(new InetSocketAddress(9898)); //3.获取客户端连接的通道 SocketChannel clientChannel = serverChannel.accept(); //4.分配指定大小的缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); //5.接收客户端的数据 while (clientChannel.read(buffer) != -1) { buffer.flip(); fileChannel.write(buffer); buffer.clear(); } //6.发送反馈给客户端 buffer.put("服务端接收数据成功".getBytes()); buffer.flip(); clientChannel.write(buffer); //7.关闭通道 clientChannel.close(); fileChannel.close(); serverChannel.close(); } /** * @Author charlesYan * @Description //客户端发送数据 * @Date 21:55 2020/2/27 * @Param [] * @return void **/ @Test public void client() throws IOException { //1.获取通道 SocketChannel clientChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898)); //本地FileChannel FileChannel fileChannel = FileChannel.open(Paths.get("D:\\temp\\如何将UTC时间转换为CST时间格式.md"), StandardOpenOption.READ); //2.分配指定大小缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); //3.读取本地文件,并发送到客户端 while (fileChannel.read(buffer) != -1) { buffer.flip(); clientChannel.write(buffer); buffer.clear(); } //由于服务端不知道客户端数据是否传输结束,进入阻塞模式;可以选择将客户端通道关闭,或者将线程切换为非阻塞模式 //此处选择将客户端通道关闭 clientChannel.shutdownOutput(); //4.接收服务端反馈 int len = 0; while ((len = clientChannel.read(buffer)) != -1) { buffer.flip(); System.out.println(new String(buffer.array(),0,len)); buffer.clear(); } //5.关闭通道 fileChannel.close(); clientChannel.close(); } }
NIO_非阻塞式
-
案例代码
public class TestNonBlockingNIO { /** * @Author charlesYan * @Description //非阻塞模式客户端 * @Date 18:39 2020/2/28 * @Param [] * @return void **/ @Test public void client() throws IOException { //1.获取客户端通道 SocketChannel clientChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",8989)); //2.切换为非阻塞模式 clientChannel.configureBlocking(false); //3.分配指定大小缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); //4.发送数据给服务端 Scanner scanner = new Scanner(System.in); while (scanner.hasNext()) { String next = scanner.next(); //Java1.8日期API LocalDateTime now = LocalDateTime.now(); String nowStr = now.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); buffer.put((nowStr + "\n" + next).getBytes()); buffer.flip(); clientChannel.write(buffer); buffer.clear(); } //5.关闭通道 clientChannel.close(); } @Test public void server() throws IOException { //1.获取服务端通道 ServerSocketChannel serverChannel = ServerSocketChannel.open(); //2.切换为非阻塞模式 serverChannel.configureBlocking(false); //3.绑定连接 serverChannel.bind(new InetSocketAddress(8989)); //4.获取选择器 Selector selector = Selector.open(); //5.将通道注册到选择器上,并且指定"监听接收事件" serverChannel.register(selector, SelectionKey.OP_ACCEPT); //6.轮询式获取选择器上已经"准备就绪"的事件 while (selector.select() > 0) { //7.获取当前选择器中所有注册的"选择键(已经就绪的监听事件)" Iterator<SelectionKey> it = selector.selectedKeys().iterator(); while (it.hasNext()) { //8.获取准备"就绪"的事件 SelectionKey key = it.next(); //9.判断具体什么事件准备就绪 if (key.isAcceptable()) { //10.若"接收就绪"则获取客户端连接 SocketChannel clientChannel = serverChannel.accept(); //11.切换每个客户端通道为非阻塞 clientChannel.configureBlocking(false); //12.将该通道注册到选择器上 clientChannel.register(selector,SelectionKey.OP_READ); }else if(key.isReadable()){ //13.获取当前选择器上"读就绪"状态的通道 SocketChannel clientChannel = (SocketChannel) key.channel(); //14.读取数据 ByteBuffer buffer = ByteBuffer.allocate(1024); int len = 0; while ((len = clientChannel.read(buffer)) > 0) { buffer.flip(); System.out.println((new String(buffer.array(),0,len))); buffer.clear(); } } //15.取消选择键SelectionKey it.remove(); } } } }
NIO_DatagramChannel
-
概念
Java NIO中的DatagramChannel是一个能收发UDP包的通道 -
案例代码
@Test public void send() throws IOException { //1.创建客户端通道 DatagramChannel clientChannel = DatagramChannel.open(); //2.创建缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); //3.输入数据 Scanner in = new Scanner(System.in); while (in.hasNext()) { String str = in.next(); buffer.put((new Date().toString() + "\n" + str.getBytes()).getBytes()); buffer.flip(); //4.发送数据 clientChannel.send(buffer,new InetSocketAddress("127.0.0.1",8080)); buffer.clear(); } //5.关闭客户端通道 clientChannel.close(); } @Test public void receive() throws IOException { //1.打开服务端通道 DatagramChannel serverChannel = DatagramChannel.open(); //2.切换为非阻塞模式 serverChannel.configureBlocking(false); //3.绑定端口号 serverChannel.bind(new InetSocketAddress(8080)); //4.创建选择器 Selector selector = Selector.open(); //5.将服务端通道注册到选择器 serverChannel.register(selector, SelectionKey.OP_READ); //6.轮询选择器已经准备就绪事件 while (selector.select() > 0) { Iterator<SelectionKey> it = selector.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey sk = it.next(); if (sk.isReadable()) { ByteBuffer buffer = ByteBuffer.allocate(1024); serverChannel.receive(buffer); buffer.flip(); System.out.println(new String(buffer.array(),0,buffer.limit())); buffer.clear(); } } //7.取消选择键SelectionKey it.remove(); } }
管道(Pipe)
-
概念
Java NIO管道是2个线程之间的单向数据连接。 -
作用
Pipe有一个source通道和一个sink通道,数据会被写到sink通道,从source通道读取 -
案例代码
@Test public void pipe() throws IOException { //1.获取管道 Pipe pipe = Pipe.open(); //2.将缓冲区中的数据写入管道 ByteBuffer buffer = ByteBuffer.allocate(1024); Pipe.SinkChannel sink = pipe.sink(); buffer.put("通过单向管道发送数据".getBytes()); buffer.flip(); sink.write(buffer); //3.读取缓冲区中的数据 Pipe.SourceChannel source = pipe.source(); buffer.flip(); int len = source.read(buffer); System.out.println(new String(buffer.array(),0,len)); source.close(); sink.close(); }
参考链接
https://www.cnblogs.com/aspirant/p/8630283.html