NIO概述
什么是NIO?
Java NIO(New IO)是一个可以替代标准Java IO API的IO API(从Java 1.4开始),Java NIO提供了与标准IO不同的IO工作方式。
Java NIO: Channels and Buffers(通道和缓冲区)
标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
Java NIO: Non-blocking IO(非阻塞IO)
Java NIO可以让你非阻塞的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。
Java NIO: Selectors(选择器)
Java NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。
注意:传统IT是单向。 NIO类似
区别
IO |
NIO |
面向流 |
面向缓冲区 |
阻塞IO |
非阻塞IO |
无 |
选择器 |
Buffer的数据存取
一个用于特定基本数据类行的容器。有java.nio包定义的,所有缓冲区都是抽象类Buffer的子类。
Java NIO中的Buffer主要用于与NIO通道进行交互,数据是从通道读入到缓冲区,从缓冲区写入通道中的。
Buffer就像一个数组,可以保存多个相同类型的数据。根据类型不同(boolean除外),有以下Buffer常用子类:
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
Buffer的概述
1)容量(capacity):表示Buffer最大数据容量,缓冲区容量不能为负,并且建立后不能修改。
2)限制(limit):第一个不应该读取或者写入的数据的索引,即位于limit后的数据不可以读写。缓冲区的限制不能为负,并且不能大于其容量(capacity)。
3)位置(position):下一个要读取或写入的数据的索引。缓冲区的位置不能为负,并且不能大于其限制(limit)。
4)标记(mark)与重置(reset):标记是一个索引,通过Buffer中的mark()方法指定Buffer中一个特定的position,之后可以通过调用reset()方法恢复到这个position。
package com.hongmoshui.sum; import java.nio.ByteBuffer; /** * (缓冲区)buffer 用于NIO存储数据 支持多种不同的数据类型 <br> * 1.byteBuffer <br> * 2.charBuffer <br> * 3.shortBuffer<br> * 4.IntBuffer<br> * 5.LongBuffer<br> * 6.FloatBuffer <br> * 7.DubooBuffer <br> * 上述缓冲区管理的方式 几乎<br> * 通过allocate() 获取缓冲区 <br> * 二、缓冲区核心的方法 put 存入数据到缓冲区 get <br> 获取缓冲区数据 flip 开启读模式 * 三、缓冲区四个核心属性<br> * capacity:缓冲区最大容量,一旦声明不能改变。 limit:界面(缓冲区可以操作的数据大小) limit后面的数据不能读写。 * position:缓冲区正在操作的位置 */ public class Test004 { public static void main(String[] args) { // 1.指定缓冲区大小1024 ByteBuffer buf = ByteBuffer.allocate(1024); System.out.println("--------------------"); System.out.println(buf.position()); System.out.println(buf.limit()); System.out.println(buf.capacity()); // 2.向缓冲区存放5个数据 buf.put("abcd1".getBytes()); System.out.println("--------------------"); System.out.println(buf.position()); System.out.println(buf.limit()); System.out.println(buf.capacity()); // 3.开启读模式 buf.flip(); System.out.println("----------开启读模式...----------"); System.out.println(buf.position()); System.out.println(buf.limit()); System.out.println(buf.capacity()); byte[] bytes = new byte[buf.limit()]; buf.get(bytes); System.out.println(new String(bytes, 0, bytes.length)); System.out.println("----------重复读模式...----------"); // 4.开启重复读模式 buf.rewind(); System.out.println(buf.position()); System.out.println(buf.limit()); System.out.println(buf.capacity()); byte[] bytes2 = new byte[buf.limit()]; buf.get(bytes2); System.out.println(new String(bytes2, 0, bytes2.length)); // 5.clean 清空缓冲区 数据依然存在,只不过数据被遗忘 System.out.println("----------清空缓冲区...----------"); buf.clear(); System.out.println(buf.position()); System.out.println(buf.limit()); System.out.println(buf.capacity()); System.out.println((char)buf.get()); } }
make与rest用法
标记(mark)与重置(reset):标记是一个索引,通过Buffer中的mark()方法指定Buffer中一个特定的position,之后可以通过调用reset()方法恢复到这个position。
package com.hongmoshui.sum; import java.nio.ByteBuffer; public class Test002 { public static void main(String[] args) { ByteBuffer buf = ByteBuffer.allocate(1024); String str = "abcd1"; buf.put(str.getBytes()); // 开启读取模式 buf.flip(); byte[] dst = new byte[buf.limit()]; buf.get(dst, 0, 2); buf.mark(); System.out.println(new String(dst, 0, 2)); System.out.println(buf.position()); buf.get(dst, 2, 2); System.out.println(new String(dst, 2, 2)); System.out.println(buf.position()); buf.reset(); System.out.println("重置恢复到mark位置.."); System.out.println(buf.position()); } }
直接缓冲区与非直接缓冲区别
非直接缓冲区:通过 allocate() 方法分配缓冲区,将缓冲区建立在 JVM 的内存中
直接缓冲区:通过 allocateDirect() 方法分配直接缓冲区,将缓冲区建立在物理内存中。可以提高效率
字节缓冲区要么是直接的,要么是非直接的。如果为直接字节缓冲区,则 Java 虚拟机会尽最大努力直接在此缓冲区上执行本机 I/O 操作。也就是说,在每次调用基础操作系统的一个本机 I/O 操作之前(或之后),虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。
直接字节缓冲区可以通过调用此类的 allocateDirect() 工厂方法来创建。此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。
直接字节缓冲区还可以通过 FileChannel 的 map() 方法 将文件区域直接映射到内存中来创建。该方法返回MappedByteBuffer 。 Java 平台的实现有助于通过 JNI 从本机代码创建直接字节缓冲区。如果以上这些缓冲区中的某个缓冲区实例指的是不可访问的内存区域,则试图访问该区域不会更改该缓冲区的内容,并且将会在访问期间或稍后的某个时间导致抛出不确定的异常。
字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其 isDirect() 方法来确定。提供此方法是为了能够在性能关键型代码中执行显式缓冲区管理。
package com.hongmoshui.sum; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; import java.nio.channels.FileChannel.MapMode; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; public class Test005 { public static void main(String[] args) throws IOException { test1(); test2(); } // 使用直接缓冲区完成文件的复制(内存映射文件) public static void test2() throws IOException { long start = System.currentTimeMillis(); FileChannel inChannel = FileChannel.open(Paths.get("D:/test.txt"), StandardOpenOption.READ); FileChannel outChannel = FileChannel.open(Paths.get("D:/test2.txt"), StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE); // 内存映射文件 MappedByteBuffer inMappedByteBuf = inChannel.map(MapMode.READ_ONLY, 0, inChannel.size()); MappedByteBuffer outMappedByteBuffer = outChannel.map(MapMode.READ_WRITE, 0, inChannel.size()); // 直接对缓冲区进行数据的读写操作 byte[] dsf = new byte[inMappedByteBuf.limit()]; inMappedByteBuf.get(dsf); outMappedByteBuffer.put(dsf); inChannel.close(); outChannel.close(); long end = System.currentTimeMillis(); System.out.println(end - start); } // 1.利用通道完成文件的复制(非直接缓冲区) public static void test1() throws IOException { // 4400 long start = System.currentTimeMillis(); FileInputStream fis = new FileInputStream("D:/test.txt"); FileOutputStream fos = new FileOutputStream("D:/test2.txt"); // ①获取通道 FileChannel inChannel = fis.getChannel(); FileChannel outChannel = fos.getChannel(); // ②分配指定大小的缓冲区 ByteBuffer buf = ByteBuffer.allocate(1024); while (inChannel.read(buf) != -1) { buf.flip();// 切换为读取数据 // ③将缓冲区中的数据写入通道中 outChannel.write(buf); buf.clear(); } outChannel.close(); inChannel.close(); fos.close(); fis.close(); long end = System.currentTimeMillis(); System.out.println(end - start); } }
通道(Channel)的原理获取
通道表示打开到 IO 设备(例如:文件、套接字)的连接。若需要使用 NIO 系统,需要获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。Channel 负责传输, Buffer 负责存储。通道是由 java.nio.channels 包定义的。 Channel 表示 IO 源与目标打开的连接。Channel 类似于传统的“流”。只不过 Channel本身不能直接访问数据, Channel 只能与Buffer 进行交互。
java.nio.channels.Channel 接口:
|--FileChannel
|--SocketChannel
|--ServerSocketChannel
|--DatagramChannel
获取通道
1. Java 针对支持通道的类提供了 getChannel() 方法
本地 IO:
FileInputStream/FileOutputStream
RandomAccessFile
网络IO:
Socket
ServerSocket
DatagramSocket
2. 在 JDK 1.7 中的 NIO.2 针对各个通道提供了静态方法 open()
3. 在 JDK 1.7 中的 NIO.2 的 Files 工具类的 newByteChannel()
package com.hongmoshui.sum; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; import java.nio.channels.FileChannel.MapMode; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import org.junit.Test; public class Test006 { @Test // 使用直接缓冲区完成文件的复制(內存映射文件) public void test2() throws IOException { FileChannel inChannel = FileChannel.open(Paths.get("1.png"), StandardOpenOption.READ); FileChannel outChannel = FileChannel.open(Paths.get("2.png"), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE); // 映射文件 MappedByteBuffer inMapperBuff = inChannel.map(MapMode.READ_ONLY, 0, inChannel.size()); MappedByteBuffer outMapperBuff = outChannel.map(MapMode.READ_WRITE, 0, inChannel.size()); // 直接对缓冲区进行数据读写操作 byte[] dst = new byte[inMapperBuff.limit()]; inMapperBuff.get(dst); outMapperBuff.put(dst); outChannel.close(); inChannel.close(); } @Test // 1.利用通道完成文件复制(非直接缓冲区) public void test1() throws IOException { FileInputStream fis = new FileInputStream("1.png"); FileOutputStream fos = new FileOutputStream("2.png"); // ①获取到通道 FileChannel inChannel = fis.getChannel(); FileChannel outChannel = fos.getChannel(); // ②分配指定大小的缓冲区 ByteBuffer buf = ByteBuffer.allocate(1024); while (inChannel.read(buf) != -1) { buf.flip();// 切换到读取模式 outChannel.write(buf); buf.clear();// 清空缓冲区 } // 关闭连接 outChannel.close(); inChannel.close(); fos.close(); fis.close(); } }
直接缓冲区与非直接缓冲耗时计算
package com.hongmoshui.sum; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; import java.nio.channels.FileChannel.MapMode; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import org.junit.Test; public class Test007 { @Test // 使用直接缓冲区完成文件的复制(內存映射文件) //428、357 public void test2() throws IOException { long startTime = System.currentTimeMillis(); FileChannel inChannel = FileChannel.open(Paths.get("f://1.mp4"), StandardOpenOption.READ); FileChannel outChannel = FileChannel.open(Paths.get("f://2.mp4"), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE); // 映射文件 MappedByteBuffer inMapperBuff = inChannel.map(MapMode.READ_ONLY, 0, inChannel.size()); MappedByteBuffer outMapperBuff = outChannel.map(MapMode.READ_WRITE, 0, inChannel.size()); // 直接对缓冲区进行数据读写操作 byte[] dst = new byte[inMapperBuff.limit()]; inMapperBuff.get(dst); outMapperBuff.put(dst); outChannel.close(); inChannel.close(); long endTime = System.currentTimeMillis(); System.out.println("内存映射文件耗时:" + (endTime - startTime)); } @Test // 1.利用通道完成文件复制(非直接缓冲区) public void test1() throws IOException { // 11953 、3207、3337 long startTime = System.currentTimeMillis(); FileInputStream fis = new FileInputStream("f://1.mp4"); FileOutputStream fos = new FileOutputStream("f://2.mp4"); // ①获取到通道 FileChannel inChannel = fis.getChannel(); FileChannel outChannel = fos.getChannel(); // ②分配指定大小的缓冲区 ByteBuffer buf = ByteBuffer.allocate(1024); while (inChannel.read(buf) != -1) { buf.flip();// 切换到读取模式 outChannel.write(buf); buf.clear();// 清空缓冲区 } // 关闭连接 outChannel.close(); inChannel.close(); fos.close(); fis.close(); long endTime = System.currentTimeMillis(); System.out.println("非缓冲区:" + (endTime - startTime)); } }
分散读取与聚集写入
分散读取(scattering Reads):将通道中的数据分散到多个缓冲区中
聚集写入(gathering Writes):将多个缓冲区的数据聚集到通道中
package com.hongmoshui.sum; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; public class Test008 { public static void main(String[] args) throws IOException { RandomAccessFile raf1 = new RandomAccessFile("test.txt", "rw"); // 1.获取通道 FileChannel channel = raf1.getChannel(); // 2.分配指定大小的指定缓冲区 ByteBuffer buf1 = ByteBuffer.allocate(100); ByteBuffer buf2 = ByteBuffer.allocate(1024); // 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())); // 聚集写入 RandomAccessFile raf2 = new RandomAccessFile("test2.txt", "rw"); FileChannel channel2 = raf2.getChannel(); channel2.write(bufs); } }
字符集 Charset
编码:字符串->字节数组
解码:字节数组 -> 字符串
package com.hongmoshui.sum; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.nio.charset.CharsetEncoder; public class Test009 { public static void main(String[] args) throws CharacterCodingException { // 获取编码器【utf-8编码,中文占3个字节,英文占1个字节】 Charset cs1 = Charset.forName("UTF-8"); // 获取编码器 CharsetEncoder ce = cs1.newEncoder(); // 获取解码器 CharsetDecoder cd = cs1.newDecoder(); CharBuffer cBuf = CharBuffer.allocate(1024); cBuf.put("洪墨水,哈哈哈!"); cBuf.flip(); // 编码 ByteBuffer bBuf = ce.encode(cBuf); for (int i = 0; i < 24; i++) { System.out.println(bBuf.get()); } // 解码 bBuf.flip(); CharBuffer cBuf2 = cd.decode(bBuf); System.out.println(cBuf2.toString()); System.out.println("-------------------------------------"); Charset cs2 = Charset.forName("UTF-8"); bBuf.flip(); CharBuffer cbeef = cs2.decode(bBuf); System.out.println(cbeef.toString()); } }
NIO同步阻塞与同步非阻塞
BIO与NIO
IO(BIO)和NIO区别:其本质就是阻塞和非阻塞的区别
阻塞概念:应用程序在获取网络数据的时候,如果网络传输数据很慢,就会一直等待,直到传输完毕为止。
非阻塞概念:应用程序直接可以获取已经准备就绪好的数据,无需等待。
IO为同步阻塞形式,NIO为同步非阻塞形式,NIO并没有实现异步,在JDK1.7后升级NIO库包,支持异步非阻塞
同学模型NIO2.0(AIO)
BIO:同步阻塞式IO,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
NIO:同步非阻塞式IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
AIO(NIO.2):异步非阻塞式IO,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。
同步时,应用程序会直接参与IO读写操作,并且我们的应用程序会直接阻塞到某一个方法上,直到数据准备就绪:
或者采用轮训的策略实时检查数据的就绪状态,如果就绪则获取数据.
异步时,则所有的IO读写操作交给操作系统,与我们的应用程序没有直接关系,我们程序不需要关系IO读写,当操作
系统完成了IO读写操作时,会给我们应用程序发送通知,我们的应用程序直接拿走数据极即可。
伪异步
由于BIO一个客户端需要一个线程去处理,因此我们进行优化,后端使用线程池来处理多个客户端的请求接入,形成客户端个数M:线程池最大的线程数N的比例关系,其中M可以远远大于N,通过线程池可以灵活的调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。
原理:
当有新的客户端接入时,将客户端的Socket封装成一个Task(该Task任务实现了java的Runnable接口)投递到后端的线程池中进行处理,由于线程池可以设置消息队列的大小以及线程池的最大值,因此,它的资源占用是可控的,无论多少个客户端的并发访问,都不会导致资源的耗尽或宕机。
IO模型关系
什么是阻塞
阻塞概念:应用程序在获取网络数据的时候,如果网络传输很慢,那么程序就一直等着,直接到传输完毕。
什么是非阻塞
应用程序直接可以获取已经准备好的数据,无需等待.
IO为同步阻塞形式,NIO为同步非阻塞形式。NIO没有实现异步,在JDK1.7之后,升级了NIO库包
,支持异步费阻塞通讯模型NIO2.0(AIO)
NIO非阻塞代码
/** * 启动客户端【nio异步非阻塞】 * @param port 端口号 * @author 墨水 */ private static void upClient(int port) throws IOException { // 1.创建通道 SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", port)); // 2.切换异步非阻塞 sChannel.configureBlocking(false); System.out.println("port:" + port + "客户端已经启动...."); // 3.指定缓冲区大小 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); Scanner scanner = new Scanner(System.in); while (scanner.hasNext()) { String str = scanner.next(); byteBuffer.put((new Date().toString() + " " + str).getBytes()); // 4.切换读取模式 byteBuffer.flip(); sChannel.write(byteBuffer); byteBuffer.clear(); } sChannel.close(); } /** * 启动服务端【nio异步非阻塞】 * @param port 端口号 * @author 墨水 */ private static void upServer(int port) throws IOException, ClosedChannelException { // 1.创建通道 ServerSocketChannel sChannel = ServerSocketChannel.open(); // 2.切换读取模式 sChannel.configureBlocking(false); // 3.绑定连接 sChannel.bind(new InetSocketAddress(port)); // 4.获取选择器 Selector selector = Selector.open(); // 5.将通道注册到选择器 "并且指定监听接受事件" sChannel.register(selector, SelectionKey.OP_ACCEPT); System.out.println("port:" + port + "服务器端已经启动...."); // 6. 轮训式 获取选择 "已经准备就绪"的事件 while (selector.select() > 0) { // 7.获取当前选择器所有注册的"选择键(已经就绪的监听事件)" Iterator<SelectionKey> it = selector.selectedKeys().iterator(); while (it.hasNext()) { // 8.获取准备就绪的事件 SelectionKey sk = it.next(); // 9.判断具体是什么事件准备就绪 if (sk.isAcceptable()) { // 10.若"接受就绪",获取客户端连接 SocketChannel socketChannel = sChannel.accept(); // 11.设置阻塞模式 socketChannel.configureBlocking(false); // 12.将该通道注册到服务器上 socketChannel.register(selector, SelectionKey.OP_READ); } else if (sk.isReadable()) { // 13.获取当前选择器"就绪" // 状态的通道 SocketChannel socketChannel = (SocketChannel) sk.channel(); // 14.读取数据 ByteBuffer buf = ByteBuffer.allocate(1024); int len = 0; while ((len = socketChannel.read(buf)) > 0) { buf.flip(); System.out.println("port:" + port + "||||||||" + new String(buf.array(), 0, len)); buf.clear(); } } it.remove(); } } }
选择KEY
1、SelectionKey.OP_CONNECT
2、SelectionKey.OP_ACCEPT
3、SelectionKey.OP_READ
4、SelectionKey.OP_WRITE
如果你对不止一种事件感兴趣,那么可以用“位或”操作符将常量连接起来,如下:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
在SelectionKey类的源码中我们可以看到如下的4中属性,四个变量用来表示四种不同类型的事件:可读、可写、可连接、可接受连接