Files
rikako-note/java se/nio.md
2024-08-27 13:31:41 +08:00

254 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# NIO
## 简介
nio库在jdk 1.4时被引入和传统i/o不同的是nio性能高且基于block。在nio中定义了类来持有数据并以block为单位对数据进行处理。
### nio和传统io区别
nio和传统io最大的区别是数据打包和传输的方式。在传统io中会以stream的形式来处理数据而在nio中数据则是以block的形式被处理。
面向stream的io系统在同一时刻只能处理1字节的数据通常较慢。
面向block的io系统则是以block为单位处理数据处理速度较传统io快。
## Channel/Buffer
channel和buffer是nio中的核心对象几乎在每个io操作中被使用。
channel类似于传统io中的stream所有数据的写入或读取都需要通过channel。而buffer本质上则是数据的容器`所有被发送到channel中的数据都要先被放置到buffer中``所有从channel中读取的数据都要先被读取到buffer中`
### Buffer
Buffer是一个类用于存储数据buffer中的数据要么将要被写入到Channel中要么刚从Channel中读取出来。
> Buffer是nio和传统io的重要区别在传统io中数据直接从stream中被读取出来也被直接写入到stream中。
>
> 在nio中数据的读取和写入都需要经过Buffer
buffer的本质是一个字节数组buffer提供了对数据的结构化访问并且buffer还追踪了系统的读/写操作。
#### Buffer类型
最常用的Buffer类型是ByteBuffer其支持对底层的字节数据进行set/get操作初次之外Buffer还有其他类型。
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
上述的每个Buffer类都实现了Buffer接口除了ByteBuffer之外其他每个Buffer类型都拥有相同的操作。由于ByteBuffer被绝大多数标准io操作锁使用故而ByteBuffer除了拥有上述操作外还拥有一些额外的操作。
#### Buffer设计
Buffer是存储指定primitive type数据的容器Long,Byte,Int,Float,Char,Double,Shortbuffer是线性并且有序的。
buffer的主要属性包括capacity、limit和position
- capacitybuffer的capacity是其可以包含的元素数量capacity不可以为负数并且不可以被改变
- limitbuffer的limit代表buffer中第一个不应该被读/写的元素下表,`例如buffer的capacity为5但是其中只包含3个元素那么limit则为3`limit不能为负数并且limit不可以比capacity大
- positionposition代表下一个被读/写元素的下标position部为负数也不应该比limit大
#### 传输数据
buffer的每个子类都定义了get/put类似的操作相应的read或write操作会从position转移一个或多个元素的数据如果请求转移的数据比limit大那么相应的get操作会抛出`BufferUnderflowException`set操作则是会抛出`BufferOverflowException`;在上述两种情况下,没有数据会被传输。
#### mark & reset
buffer的mark操作标识对buffer执行reset操作后position会被重置到的位置。mark并非所有情况下都被定义当mark被定义时其应该不为负数并且不应该比position大。如果position或limit被调整为比mark更小的值之后mark值将会被丢弃。如果在mark没有被定义时调用reset操作那么会抛出`InvalidMarkException`异常。
> #### 属性之间的大小关系
>
> `0<=mark<=position<=limit<=capacity`
#### clear/flip/rewind
除了对buffer定义mark/reset操作之外buffer还定义了如下操作
- `clear()`使buffer可以执行新的sequence channel-read操作或relative put操作
- clear操作会将limit设置为capacity的值并且将position设置为0
- `flip()`使buffer可以执行sequence channel-wirte操作和relative get操作
- flip操作会将limit设置为position并且将position设置为0
- `rewind()`使buffer中的数据可以重新被读取
- rewind操作会将position设置为0limit保持不变
> 在向buffer写入数据之前应该调用`clear()`方法将limit设置为capacity并将position设置为0从而可以向[0, capacity-1]的范围内写入数据
>
> 当写入完成之后想要从buffer中读取数据时需要先调用`flip()`方法将limit设置为position并且将position设置为0从而可以读取[0, position-1]范围内的数据
>
> 如果想要对buffer中的数据进行重新读取可以调用`rewind()`方法其将position设置为0从而可以对[0, limit-1]范围内的数据重新进行读取。
#### Buffer get/put方法
##### get
在ByteBuffer中拥有如下`get`方法:
```java
byte get();
ByteBuffer get( byte dst[] );
ByteBuffer get( byte dst[], int offset, int length );
byte get( int index );
```
其中前3个get方法都是relative方法其基于当前position和limit来获取数据。第4个get方法则是absolute方法其不基于position和limit而是基于index绝对偏移量来获取数据。
> absolute get方法调用不基于position和limit也不会对position和limit造成影响。
##### put
ByteBuffer拥有如下`put`方法:
```java
ByteBuffer put( byte b );
ByteBuffer put( byte src[] );
ByteBuffer put( byte src[], int offset, int length );
ByteBuffer put( ByteBuffer src );
ByteBuffer put( int index, byte b );
```
#### Buffer allocation and warpping
在创建buffer之前必须对buffer进行分配
```java
ByteBuffer buffer = ByteBuffer.allocate(1024);
```
`Buffer.allocate`会创建一个指定大小的底层数组并将数组封装到一个buffer对象中。
除了调用`allocate`之外还可以将一个已经存在的数组分配给buffer
```java
byte array[] = new byte[1024];
ByteBuffer buffer = ByteBuffer.wrap(array);
```
在调用`wrap`方法之后会创建一个buffer对象buffer对象对array进行的包装。此后既可以通过array直接访问数组也可以通过buffer访问数组。
#### Buffer slice
buffer slice会基于buffer对象创建一个sub buffer新建的sub buffer会和原来的buffer共享部分的底层数组数据。
```java
ByteBuffer buffer = ByteBuffer.allocate(10);
for (int i = 0; i < buffer.capacity(); ++i) {
buffer.put((byte) i);
}
buffer.position(3);
buffer.limit(7);
ByteBuffer slice = buffer.slice();
```
通过上述方法可以创建一个包含原始buffer[3,6]范围内元素的sub buffer。但是sub buffer和buffer共享[3,6]范围内的数据,实例如下所示:
```java
// 将[3,6]范围内的元素都 * 11
for (int i = 0; i < slice.capacity(); ++i) {
byte b = slice.get(i);
b *= 11;
slice.put(i, b);
}
// 重置original buffer的position和limit并读取数据
buffer.position(0);
buffer.limit(buffer.capacity());
while (buffer.remaining() > 0) {
System.out.println(buffer.get());
}
```
修改sub buffer中的元素内容后访问original buffer中的内容输出结果如下所示
```shell
$ java SliceBuffer
0
1
2
33
44
55
66
7
8
9
```
易得知在sub buffer修改内容后内容修改对buffer也可见。
#### ReadOnly Buffer
对于readonly buffer可以从其中读取值但是无法向其写入值。对于任何常规buffer可以对其调用`buffer.asReadOnlyBuffer()`来获取一个readonly buffer。
#### Direct & Indirect Buffer
direct buffer的内存通过特定方式来分配从而增加io速度。
对于direct bufferjvm在将尽量避免在调用本地io操作前将buffer中内容写入/读取到中间buffer。
### Channel
Channel为一个对象可以从channel中读取数据或向channel写入数据。将传统io和nio相比较channel类似于stream。
Channel和Stream不同的是channel为双向的而Stream则仅支持单项InputStream或OutputStream。一个开启的channel可以被用于读取、写入或同时读取或写入。
## Channel读写
向channel读取或写入数据十分简单
-仅需创建一个buffer对象并且访问channel将数据读取到buffer对象中
-创建一个buffer对象并将数据填充到buffer对象之后访问channel并将bufffer中的数据写入到channel中
### 读取文件
通过nio读取文件分为如下步骤
1. 从FileInputStream中获取Channel
2. 创建buffer对象
3. 将channel中的数据读取到buffer
示例如下所示:
```java
FileInputStream fin = new FileInputStream( "readandshow.txt" );
// 1. 获取channel
FileChannel fc = fin.getChannel();
// 2. 创建buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 3. 将数据从channel中读取到buffer中
fc.read( buffer );
```
> 如上述示例所示在从channel读取数据时无需指定向buffer中读取多少字节的数据每个buffer内部中都有一个复杂的统计系统会跟踪已经读取了多少字节的数据、buffer中还有多少空间用于读取更多数据。
### 写入文件
通过nio写入文件步骤和读取类似示例如下所示
```java
FileOutputStream fout = new FileOutputStream( "writesomebytes.txt" );
// 1. 获取channel
FileChannel fc = fout.getChannel();
// 2. 创建buffer对象
ByteBuffer buffer = ByteBuffer.allocate( 1024 );
for (int i = 0; i < message.length; ++i) {
buffer.put(message[i]);
}
buffer.flip();
// 3. 将buffer中的数据写入到channel中
fc.write(buffer);
```
在向channel写入数据时同样无需指定需要向channel写入多少字节的数据buffer内部的统计系统同样会追踪buffer中含有多少字节的数据、还有多少数据待写入。
### 同时读取和写入文件
同时读取和写入文件的示例如下所示:
```java
public class CopyFile {
static public void main(String args[]) throws Exception {
if (args.length < 2) {
System.err.println("Usage: java CopyFile infile outfile");
System.exit(1);
}
String infile = args[0];
String outfile = args[1];
try (FileInputStream fin = new FileInputStream(infile);
FileOutputStream fout = new FileOutputStream(outfile)) {
FileChannel fcin = fin.getChannel();
FileChannel fcout = fout.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
buffer.clear();
int r = fcin.read(buffer);
if (r == -1) {
break;
}
buffer.flip();
fcout.write(buffer);
}
}
}
}
```
在上述示例中,通过`channel.read(buffer)`的返回值来判断是否已经读取到文件末尾,如果返回值为-1那么代表文件读取已经完成。