Java NIO

Java NIO (New IO)是从Java1.4版本开始引入的一个新的IO API,可以替代次奥准的Java IO API。NIO支持面向缓冲区的,基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。这篇文章是根据课件做的学习笔记,比较长。可以根据选择查看。

比较NIO与IO

特性对比
NIO和IO

面向流与面向缓冲区的区别以及对通道与缓冲区的理解

面向流是单向的,文件与程序之间建立数据流,输入流和输出流都需要建立不同的“管道”。抽象的理解为自来水管和下水道吧,水就是传输的数据。

面向缓冲区,文件与程序之间建立通道,里面存在缓冲区。抽象的理解可以把通道认为是铁路,缓冲区认为是一辆火车,而载着的货物也就是所要传输的数据了。

简单认为:通道负责传输,缓冲区负责存储。

缓冲区(Buffer)

Buffer在Java NIO 中负责数据的存取,缓冲区就是数组,用于存储不同数据类型的数据。

缓冲区类型

根据数据类型的不同(boolean除外),提供了相应类型的缓冲区。

ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
上述缓冲区的管理方式几乎一致,通过allocate()获取缓冲区。
ByteBuffer最为常用
缓冲区存取数据的两个核心方法
put():存入数据到缓冲区中

get():获取缓冲区中的数据

缓冲区的四个核心属性

capacity: 容量,表示缓冲区中最大存储数据的容量,一但声明不能改变。(因为底层是数组,数组一但被创建就不能被改变)

limit: 界限,表示缓冲区中可以操作数据的大小。(limit后数据不能进行读写)

position: 位置,表示缓冲区中正在操作数据的位置

position <= limit <= capacity
mark:标记,表示记录当前position的位置,可以通过reset()恢复到mark的位置。

几个常用方法

allocate():分配缓冲区:

1
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

put():将数据存入缓冲区

1
2
String str = "abcdefg";
byteBuffer.put(str.getBytes());

flip():切换到读取数据的模式

get():读取数据

1
2
3
byte[] bytes = new byte[byteBuffer.limit()];
byteBuffer.get(bytes);
System.out.println(new String(bytes,0,bytes.length));

rewind():重复读,使position归0

clear():清空缓冲区,但是缓冲区中的数据依然存在,只是处于一种“被遗忘“的状态。只是不知道位置界限等,读取会有困难。

mark():标记。mark会记录当前的position,limit,capacity

reset():position,limit,capacity恢复到mark记录的位置

直接缓冲区与非直接缓冲区

非直接缓冲区:通过allocate()方法分配缓冲区,将缓冲区建立在JVM的内存中。在每次调用基础操作系统的一个本机IO之前或者之后,虚拟机都会将缓冲区的内容复制到中间缓冲区(或者从中间缓冲区复制内容),缓冲区的内容驻留在JVM内,因此销毁容易,但是占用JVM内存开销,处理过程中有复制操作。

非直接缓冲区的写入步骤

创建一个临时的ByteBuffer对象。

将非直接缓冲区的内容复制到临时缓冲中。

使用临时缓冲区执行低层次I/O操作。

临时缓冲区对象离开作用域,并最终成为被回收的无用数据。

直接缓冲区:通过allocateDirect()方法分配直接缓冲区,将缓冲区建立在物理内存中,可以提高效率。

直接缓冲区在JVM内存外开辟内存,在每次调用基础操作系统的一个本机IO之前或者之后,虚拟机都会避免将缓冲区的内容复制到中间缓冲区(或者从中间缓冲区复制内容),缓冲区的内容驻留在物理内存内,会少一次复制过程,如果需要循环使用缓冲区,用直接缓冲区可以很大地提高性能。虽然直接缓冲区使JVM可以进行高效的I/O操作,但它使用的内存是操作系统分配的,绕过了JVM堆栈,建立和销毁比堆栈上的缓冲区要更大的开销

通道

通道(Channel)表示IO源与目标打开的连接。Channel类似于传统的”流“,只不过Channel本身不能直接访问数据,Channel只能与Buffer进行交互。

  • Channel是一个独立的处理器,专门用于IO操作,附属于CPU。
  • 在提出IO请求的时候,CPU不需要进行干预,也就提高了效率。

作用

用于源节点与目标节点的连接。在Java NIO中负责缓冲区中数据的传输。Channel本身并不存储数据,因此需要配合Buffer一起使用

主要实现类

java.nio.channels.Channel接口:

用于本地数据传输:
FileChannel
**
用于网络数据传输**:
SocketChannel
ServerSocketChannel
DatagramChannel

获取通道
Java 针对支持通道的类提供了一个 getChannel() 方法。

本地IO操作
FileInputStream/FileOutputStream
RandomAccessFile

网络IO
Socket
ServerSocket
DatagramSocket
在JDK1.7中的NIO.2 针对各个通道提供了静态方法 open();
在JDK1.7中的NIO.2 的Files工具类的 newByteChannel();

利用通道完成文件的复制

非直接缓冲区

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
@Test
public void testChannel1(){
FileInputStream inputStream = null;
FileOutputStream outputStream = null;
FileChannel inputChannel = null;
FileChannel outputChannel = null;
try {
inputStream = new FileInputStream(new File("H:\\img\\1.jpg"));
outputStream = new FileOutputStream(new File("H:\\img\\2.jpg"));

// 获取通道
inputChannel = inputStream.getChannel();
outputChannel = outputStream.getChannel();

// 分配缓冲区
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);

// 将通道中数据存入缓冲区
while(inputChannel.read(byteBuffer) != -1){
// 切换成读取数据的模式
byteBuffer.flip();
//缓冲区中数据写到通道中区
outputChannel.write(byteBuffer);
// 清空缓冲区
byteBuffer.clear();
}
System.out.println("读写成功");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
//关闭通道
...略(如果不为null,执行close方法)
System.out.println("数据关闭成功");
}
}

直接缓冲区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void channelTest2() throws IOException {
FileChannel inputChannel = FileChannel.open(Paths.get("H:\\img\\9.jpg"), StandardOpenOption.READ);
FileChannel outputChannel = FileChannel.open(Paths.get("H:\\img\\10.jpg"), StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE);
// 内存映射文件
MappedByteBuffer inputBuffer = inputChannel.map(FileChannel.MapMode.READ_ONLY,0,inputChannel.size());
MappedByteBuffer outputBuffer = outputChannel.map(FileChannel.MapMode.READ_WRITE,0,inputChannel.size());

byte [] bytes = new byte[inputBuffer.limit()];
inputBuffer.get(bytes);
outputBuffer.put(bytes);

inputChannel.close();
outputChannel.close();
}
使用直接缓冲区对文件的存储性能会有极大的提升,但是直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。

### 字符集Charset

设置字符集,解决乱码问题

编码:字符串->字节数组

解码:字节数组->字符串

思路

用Charset.forName(String)构造一个编码器或解码器,利用编码器和解码器来对CharBuffer编码,对ByteBuffer解码。
需要注意的是,在对CharBuffer编码之前、对ByteBuffer解码之前,请记得对CharBuffer、ByteBuffer进行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
@Test
public void CharacterEncodingTest() throws CharacterCodingException {
Charset charset = Charset.forName("utf-8");
Charset charset1 = Charset.forName("gbk");

// 获取编码器 utf-8
CharsetEncoder encoder = charset.newEncoder();

// 获得解码器 gbk
CharsetDecoder decoder = charset1.newDecoder();

CharBuffer buffer = CharBuffer.allocate(1024);
buffer.put("绝不敷衍,从不懈怠!");
buffer.flip();

// 编码
ByteBuffer byteBuffer = encoder.encode(buffer);
for (int i = 0; i < 20; i++) {
System.out.println(byteBuffer.get());
}

// 解码
byteBuffer.flip();
CharBuffer charBuffer = decoder.decode(byteBuffer);
System.out.println(charBuffer.toString());
}

网络阻塞IO与非阻塞IO

传统IO是阻塞式的,也就是说,当一个线程调用 read() 或 write()时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务。因此,在完成网络通信进行 IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量客户端时,性能急剧下降。
NIO是非阻塞式的,当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO操作,所以单独的线程可以管理多个输入和输出通道。因此, NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。

阻塞模式与非阻塞模式

传统阻塞IO方式:客户端向服务器端发送请求,服务器端便开始进行监听客户端的数据是否传过来。这时候客户端在准备自己的数据,而服务器端就需要干等着。即使服务器端是多线程的,但有时一味增加线程数,只会让阻塞的线程越来越多。
NIO的非阻塞方式:将用于传输的通道全部注册到选择器上。选择器的作用是监控这些通道的IO状况(读,写,连接,接收数据的情况等状况)。
选择器与通道之间的联系:1.通道注册到选择器上2.选择器监控通道 当某一通道,某一个事件就绪之后,选择器才会将这个通道分配到服务器端的一个或多个线程上,再继续运行。例如客户端需要发送数据给服务器端,只当客户端所有的数据都准备完毕后,选择器才会将这个注册的通道分配到服务器端的一个或多个线程上。而在客户端准备数据的这段时间,服务器端的线程可以执行别的任务。

使用NIO完成网络通信的三个核心

1.通道(Channel):负责连接

java.mio.channels.Channel 接口:
SelectableChannel
SocketChannel
ServerSocketChannel
-DatagramChannel
Pipe.SinkChannel
Pipe.sourceChannel

2.缓冲区(Buffer):负责数据的存取

3.选择器(Select):是SelectableChannel的多路复用器。用于监控SelectableChannel的IO状况。

非阻塞模式完成客户端向服务器端传输数据

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
@Test
public void client() throws IOException {
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",7498));
// 切换成非 阻塞模式
socketChannel.configureBlocking(false);
FileChannel inputChannel = FileChannel.open(Paths.get("G:\\notes\\nio\\01_简介\\学习使用NIO.md"), StandardOpenOption.READ);

ByteBuffer clientBuffer = ByteBuffer.allocate(1024);

while (inputChannel.read(clientBuffer) != -1){
clientBuffer.flip();
socketChannel.write(clientBuffer);
clientBuffer.clear();
}

socketChannel.close();
inputChannel.close();
}


@Test
public void server() throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

// 非阻塞
serverSocketChannel.configureBlocking(false);

serverSocketChannel.bind(new InetSocketAddress(7498));

FileChannel outputChannel = FileChannel.open(Paths.get("C:\\Users\\admin\\Desktop\\test.md"),StandardOpenOption.WRITE,StandardOpenOption.CREATE);


// 选择器
Selector selector = Selector.open();

// 将通道注册到选择器上,并制定监听事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

// 轮巡式获得选择器里的已经准备就绪的事件
while (selector.select() > 0 ){

// 获取已经就绪的监听事件
Iterator<SelectionKey> selectorIterator = selector.selectedKeys().iterator();

// 迭代获取
while (selectorIterator.hasNext()){
// 获取准备就绪的事件

SelectionKey key = selectorIterator.next();

SocketChannel socketChannel = null;
// 判断是什么事件
if (key.isAcceptable()){
// 或接受就绪,,则获取客户端连接
socketChannel = serverSocketChannel.accept();

//切换非阻塞方式
socketChannel.configureBlocking(false);
// 注册到选择器上
socketChannel.register(selector,SelectionKey.OP_READ);
} else if (key.isReadable()){
// 获取读就绪通道
SocketChannel readChannel = (SocketChannel) key.channel();

readChannel.configureBlocking(false);
ByteBuffer readBuffer = ByteBuffer.allocate(1024);

int len = 0;
while ( (len = readChannel.read(readBuffer)) != -1){
readBuffer.flip();
System.out.println(new String(readBuffer.array(),0,len));
outputChannel.write(readBuffer);
readBuffer.clear();
}
readChannel.close();
outputChannel.close();

}
}

// 取消选择键
selectorIterator.remove();

/**
* 这里就像评论中所说的那样,
* “serverSocketChannel.close();不用关闭的,这是服务器端”
* 没有及时更正,抱歉
*/
// serverSocketChannel.close();

}
}