主要内容
1.Java NIO 简介
2.Java NIO 与 IO 的主要区别
3.缓冲区(Buffer)和通道(Channel)
4.NIO 的非阻塞式网络通信选择器(Selector),SocketChannel、ServerSocketChannel、DatagramChannel
5.管道(Pipe)
6.Java NIO2 (Path、Paths 与 Files )
思维导图
一、Java NIO 简介
Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API。
NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。
二、Java NIO 与 IO 的主要区别
IO | NOI |
---|---|
面向流(Stream Oriented) | 面向缓冲区(Buffer Oriented) |
阻塞IO(Blocking IO) | 非阻塞IO(Non Blocking IO) |
(无) | 选择器(Selectors) |
我们可以通过画一个简图来进一步来理解NIO和IO的区别
- IO模型
我们可以把流理解水管里的水流,所以它是单向的。
- NIO模型
三、通道(Channel)与缓冲区(Buffer)
java NIO系统的核心在于:通道(Channel)和缓冲区(Buffer)。通道表示打开到 IO 设备(例如:文件、套接字)的连接。若需要使用 NIO 系统,需要获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。简而言之,Channel 负责传输, Buffer 负责存储
**我们可以根据上面的模型图这样理解:**通道可以类比成我们生活中的铁轨,而缓冲区可以类比成火车。缓冲区里的数据类比成乘客。通道不存储数据,所有的数据都在缓冲区里存取,并且它是可以双向流动的。
缓冲区
Buffer 就像一个数组,可以保存多个相同类型的数据。根据数据类型不同(boolean 除外) ,有以下 Buffer 常用子类:
ByteBuffer,CharBuffer,ShortBuffer,IntBuffer,LongBuffer,FloatBuffer,DoubleBuffer。
- 缓冲区的使用
- 初始化缓冲区大小
//分配指定大小
ByteBuffer b=ByteBuffer.allocate(1024);
Buffer 中的重要概念:
容量 (capacity) :表示 Buffer 最大数据容量,缓冲区容量不能为负,并且创建后不能更改。
限制 (limit):第一个不应该读取或写入的数据的索引,即位于 limit 后的数据不可读写。缓冲区的限制不能为负,并且不能大于其容量。
位置 (position):下一个要读取或写入的数据的索引。缓冲区的位置不能为负,并且不能大于其限制
标记 (mark)与重置 (reset):标记是一个索引,通过 Buffer 中的 mark() 方法指定 Buffer 中一个特定的 position,之后可以通过调用 reset() 方法恢复到这个 position.
- 代码测试
/**
* capacity,limit,position测试
*/
@Test
public void test() {
// 分配指定大小
ByteBuffer b = ByteBuffer.allocate(1024);
System.out.println("----------put----------");
// 1.容量
System.out.println(b.capacity());
// 2.可以操作数据的界限后面的操作不了
System.out.println(b.limit());
// 起始位置capacity>=limit>=position
System.out.println(b.position());
System.out.println("----------flip切换读写数据模式----------");
String str = "abcd";
// 写数据模式
b.put(str.getBytes());
// 切换读写数据模式
b.flip();
System.out.println(b.capacity());
// 2.可以操作数据的界限后面的操作不了
System.out.println(b.limit());
// 起始位置capacity>=limit>=position
System.out.println(b.position());
System.out.println("------------get-------------");
// 读数据模式
byte[] bt = new byte[b.limit()];
// 读取到字节数组
b.get(bt);
System.out.println("capacity " + b.capacity());
System.out.println("limit " + b.limit());
System.out.println("position " + b.position());
System.out.println("------------rewind可重复读-------------");
b.rewind();
System.out.println("capacity " + b.capacity());
System.out.println("limit " + b.limit());
System.out.println("position " + b.position());
System.out.println("------------clear清空缓冲区,数据依然存在但处于被遗忘状态-------------");
b.clear();
System.out.println("capacity " + b.capacity());
System.out.println("limit " + b.limit());
System.out.println("position " + b.position());
;
/**
* 执行结果 ----------put---------- 1024 1024 0
* ----------flip切换读写数据模式---------- 1024 4 0
* ------------get------------- capacity 1024 limit 4 position 4
* ------------rewind可重复读------------- capacity 1024 limit 4 position 0
* ------------clear清空缓冲区,数据依然存在但处于被遗忘状态------------- capacity 1024
* limit 1024 position 0
*/
}
/**
* mark,reset测试
*/
@Test
public void test2() {
ByteBuffer bf = ByteBuffer.allocate(1024);
bf.put("abcd".getBytes());
bf.flip();
byte[] b = new byte[bf.limit()];
// 从0开始读读两个
bf.get(b, 0, 2);
System.out.println("----------flip切换读写数据模式----------");
System.out.println(new String(b, 0, 2));
System.out.println("position " + bf.position());
System.out.println("-----------mark标记--------------");
bf.mark();
bf.get(b, 2, 2);
System.out.println(new String(b, 2, 2));
System.out.println("position " + bf.position());
System.out.println("-----------reset--------------");
bf.reset();
System.out.println("position " + bf.position());
/*执行结果
* ----------flip切换读写数据模式----------
* ab position 2
* -----------mark标记--------------
* cd position 4
* -----------reset--------------
* position 2
*/
}
直接缓冲区和非直接缓冲区
字节缓冲区要么是直接的,要么是非直接的。如果为直接字节缓冲区,则 Java 虚拟机会尽最大努力直接在此缓冲区上执行本机 I/O 操作。也就是说,在每次调用基础操作系统的一个本机 I/O 操作之前(或之后), 虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。
直接字节缓冲区可以通过调用此类的 allocateDirect() 工厂方法来创建。此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。
直接字节缓冲区还可以通过 FileChannel 的 map() 方法 将文件区域直接映射到内存中来创建。该方法返回
MappedByteBuffer 。Java 平台的实现有助于通过 JNI 从本机代码创建直接字节缓冲区。如果以上这些缓冲区中的某个缓冲区实例指的是不可访问的内存区域,则试图访问该区域不会更改该缓冲区的内容,并且将会在访问期间或稍后的某个时间导致抛出不确定的异常。
字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其 isDirect() 方法来确定。提供此方法是为了能够在性能关键型代码中执行显式缓冲区管理
- 直接缓冲区和非直接缓冲区图解。左边缓存为操作系统,右边缓存为JVM。中间断开的为非直接缓冲区,下面的为直接缓冲区。
- 创建直接缓冲区
@Test
public void test5(){
ByteBuffer b=ByteBuffer.allocateDirect(1024);
System.out.println(b.isDirect());//true
}
通道(Channel)
由 java.nio.channels 包定义的。Channel 表示 IO 源与目标打开的连接。Channel 类似于传统的“流”。只不过 Channel本身不能直接访问数据,Channel 只能与Buffer 进行交互。
传统的方式是由cup直接执行IO操作,后来发展为DMA的方式,最后通道与DMA并用,通道相当于一个小的cpu属于cup的一部分(个人理解)。
Java 为 Channel 接口提供的最主要实现类如下:
•FileChannel:用于读取、写入、映射和操作文件的通道。
•DatagramChannel:通过 UDP 读写网络中的数据通道。
•SocketChannel:通过 TCP 读写网络中的数据。
•ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。
获取通道
获取通道的一种方式是对支持通道的对象调用getChannel() 方法。支持通道的类如下:
本地:FileInputStream,FileOutputStream,RandomAccessFile
网络:DatagramSocket,Socket,ServerSocket
获取通道的其他方式是使用 Files 类的静态方法 newByteChannel() 获取字节通道。或者通过通道的静态方法 open() 打开并返回指定通道。
- 利用通道完成文件的复制(非直接缓冲区)
@Test
public void test6() throws IOException{
FileInputStream fis=new FileInputStream("NIO.jpg");
FileOutputStream fos=new FileOutputStream("b.jpg");
//获取通道
FileChannel inChanel=fis.getChannel();
FileChannel outChanel=fos.getChannel();
//将通道中的数据存入缓冲区
ByteBuffer bb=ByteBuffer.allocate(1024);
while(inChanel.read(bb)!=-1){
//切换成写模式
bb.flip();
//讲数据写入通道
outChanel.write(bb);
bb.clear();
}
fis.close();
fos.close();
inChanel.close();
outChanel.close();
}
- 更简单的复制写法
@Test
public void test8() throws IOException{
FileChannel inChannel=FileChannel.open(Paths.get("NIO.jpg"), StandardOpenOption.READ);
FileChannel outChannel=FileChannel.open(Paths.get("d.jpg"), StandardOpenOption.WRITE,StandardOpenOption.READ);
inChannel.transferTo(0,inChannel.size(),outChannel);
inChannel.close();
outChannel.close();
}
- 使用直接缓冲区完成文件的复制
@Test
public void test7() throws IOException{
Instant start=Instant.now();
FileChannel inChannel=FileChannel.open(Paths.get("NIO.jpg"),StandardOpenOption.READ);
FileChannel outChannel=FileChannel.open(Paths.get("c.jpg"), StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE_NEW);
//内存映射文件
MappedByteBuffer inmbb=inChannel.map(MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outmbb=inChannel.map(MapMode.READ_WRITE, 0, inChannel.size());
//直接对缓冲区进行数据的读写操作
byte[] b=new byte[inmbb.limit()];
inmbb.get(b);
outmbb.put(b);
inChannel.close();
outChannel.close();
Instant end=Instant.now();
Duration d=Duration.between(start, end);
System.out.println("复制所花时间"+d.toMillis());
}
分散(Scatter)和聚集(Gather)
分散读取(Scattering Reads)是指从 Channel 中读取的数据“分散”到多个 Buffer 中。
@Test
public void test9() throws IOException{
//这个文件类既可以读又可以写
RandomAccessFile raf=new RandomAccessFile("NIO.txt", "rw");
FileChannel fc=raf.getChannel();
//创建多个缓冲区
ByteBuffer bf1=ByteBuffer.allocate(10);
ByteBuffer bf2=ByteBuffer.allocate(1024);
ByteBuffer[] bfs={bf1,bf2};
fc.read(bfs);
for(ByteBuffer bf:bfs){
bf.flip();
}
System.out.println(new String(bfs[0].array(),0,bfs[0].limit()));
System.out.println(new String(bfs[1].array(),0,bfs[1].limit()));
}
聚集写入(Gathering Writes)是指将多个 Buffer 中的数据“聚集” 到 Channel。
//聚集写入
RandomAccessFile wr=new RandomAccessFile("b.txt", "rw");
FileChannel outChannel=wr.getChannel();
outChannel.write(bfs);
四、阻塞与非阻塞
传统的 IO 流都是阻塞式的。也就是说,当一个线程调用 read() 或 write() 时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务。因此,在完成网络通信进行 IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理, 当服务器端需要处理大量客户端时,性能急剧下降。
Java NIO 是非阻塞模式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。因此,NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。
选择器(Selector)
选择器(Selector) 是 SelectableChannle 对象的多路复用器,Selector 可以同时监控多个 SelectableChannel 的 IO 状况,也就是说,利用 Selector 可使一个单独的线程管理多个 Channel。Selector 是非阻塞 IO 的核心。
SelectableChannle 的结构如下图:
- 使用NIO完成网络通信(阻塞式)
//获取通道
ServerSocketChannel server=ServerSocketChannel.open();
FileChannel local=FileChannel.open(Paths.get("c.jpg"), StandardOpenOption.WRITE,StandardOpenOption.CREATE);
//绑定连接
server.bind(new InetSocketAddress(9099));
//获取客户端的连接通道
SocketChannel client=server.accept();
ByteBuffer bf=ByteBuffer.allocate(1024);
while(client.read(bf)!=-1){
bf.flip();
client.write(bf);
bf.clear();
}
server.close();
local.close();
}
//客户端
@Test
public void test3() throws IOException{
//获取网络通道
SocketChannel client=SocketChannel.open(new InetSocketAddress("127.0.0.1", 8888));
//切换到非阻塞模式
client.configureBlocking(false);
ByteBuffer bb=ByteBuffer.allocate(1024);
bb.put(LocalDateTime.now().toString().getBytes());
bb.flip();
client.write(bb);
bb.clear();
client.close();
}
selector原理
- 使用用NIO完成网络通信(非阻塞式)
//客户端
@Test
public void test3() throws IOException{
//获取网络通道
SocketChannel client=SocketChannel.open(new InetSocketAddress("127.0.0.1", 8888));
//切换到非阻塞模式
client.configureBlocking(false);
ByteBuffer bb=ByteBuffer.allocate(1024);
bb.put(LocalDateTime.now().toString().getBytes());
bb.flip();
client.write(bb);
bb.clear();
client.close();
}
//服务端
@Test
public void test4() throws IOException{
//获取通道
ServerSocketChannel server=ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(8888));
//获取选择器
Selector selector=Selector.open();
//将通道注册到选择器
server.register(selector, SelectionKey.OP_ACCEPT);
//获取选择器上已经准备就绪的事件
while(selector.select()>0){
//获取当前选择器中所有注册的监听事件
Set<SelectionKey> keys=selector.selectedKeys();
List<SelectionKey> ks=keys.stream().collect(Collectors.toList());
for(SelectionKey key:ks){
if(key.isAcceptable()){
//若为接收状态就绪获取客户端连接
SocketChannel sChannel=server.accept();
sChannel.configureBlocking(false);
//将通道注册到选择器上
sChannel.register(selector, SelectionKey.OP_ACCEPT);
}else if(key.isReadable()){
//获取当前选择器上读就绪状态的通道
SocketChannel sc=(SocketChannel) key.channel();
ByteBuffer bf=ByteBuffer.allocate(1024);
int len=0;
while((len=sc.read(bf))>0){
bf.flip();
System.out.println(new String(bf.array(),0,len));
bf.clear();
}
}
}
}
}