一文了解什么是NIO

认识Java中的NIO

nio(non-blocking io)非阻塞IO,在jdk1.4后加入,也有称为new io

1 三大组件

1.1 Channel和Buffer

Channel(通道)是双向的数据通道,比如InputStream和OutputStream是写入和写出数据,是单向的,而channel既可以输入数据也可以输出数据。

Buffer(缓冲区)用来暂存channel的数据,或者写出数据时,也是需要写到buffer然后写出通道

1.1.1 常见的Channel和Buffer

Channel

  • FileChannel:文件的数据传输通道
  • DatagramChannel:UDP网络数据传输通道
  • SocketChannel:TCP数据传输通道
  • ServerSocketChannel:TCP数据传输通道(用于服务器端)

Buffer

  • ByteBuffer
    • MappedByteBuffer
    • DirectByteBuffer
    • HeapByteBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer
  • CharBuffer

1.2 Selector

Selectord的作用就是配合一个线程去管理多个通道,获取到channel的事件以后,通知线程来处理。

我们可以通过三个版本的设计来更加的清晰的了解Selector

  • 多线程版:相当于一个线程和一个socket连接一一对应,把socket比做客人的话,那线程就是服务员

    缺点:如果客流量大的话,那么需要的服务员太多,相当于一个可以一个服务员,很明显我们饭店时放不下这么多服务员

  • 线程池版:那么上面的服务员多了,我们可以使用线程池来进行管理,相当于不管多少个客人,我们只有固定工位的服务员,这样就解决了上面的缺点

    缺点:一个服务员还是会负责一个客人,即使客人什么的不做,也会等待客人走了以后才会服务下一个客人,我们可以称之为==阻塞式==,适合像http请求的短连接

  • Selector版:那么基于上面的线程池版,我们selector就相当于时给每个服务员的一个工具,可以第一时间感知到客人事件的工具,那么我们的客人也是进行了升级成了==channel==,我们的客人是工作早非阻塞模式下,不会让我们的服务员吊死在一个客人身上

2 ByteBuffer

2.1 使用ByteBuffer

ByteBuffer的使用是和Channel息息相关的,下面是使用ByteBuffer读取数据的步骤

  • 获取Channel
  • 分配缓冲区==ByteBuffer.allocate(10)==
  • 循环读channel,直到返回-1,标识读完
  • 在每次循环中,读取到BytenBuffer中就需要写出来,如下
    • 切换ByteBuffer为写读模式==buffer.flip()==
    • 调用get方法循环写出,可以使用==buffer.hasRemaining()==来判断是否写完了
    • 切换ByteBuffer为写模式==buffer.clear()或buffer.compact()==

具体案例代码如下:

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
public static void main(String[] args) {
/*
* 获取fileChannel
* 1、通过输入流/输出流获取
* 2、RandomAccessFile获取
*/
try (FileChannel channel = new FileInputStream("data.txt").getChannel()) {
// 准备缓冲区 ByteBuffer.allocate(10),分配缓冲区
ByteBuffer buffer = ByteBuffer.allocate(10);
// 从channel读,那就是像buffer写 返回值是读到的字节数,读完了则为-1
int readLen = channel.read(buffer);
while (readLen != -1) {
log.debug("读取到的字节数{}", readLen);
// 切换buffer到读模式
buffer.flip();
// buffer.hasRemaining()是否还有未读的数据
while (buffer.hasRemaining()) {
// 读取数据,get(),无参数默认读一个字节
byte b = buffer.get();
System.out.println((char) b);
}
// buffer切换为写模式 clear()或compact();
buffer.clear();
readLen = channel.read(buffer);
}
} catch (IOException e) {
log.error("读取失败",e);
}
}

2.2 ByteBuffer结构

重要属性

  • capacity:容量,ByteBuffer可以容纳多少数据
  • positioon:读写指针,读到哪了,写到那了
  • limit:读写限制

执行时结构变化

  • 初始化

    image-20221125183226801

    写模式下,poosition时写入位置,limit等于容量,下面是写入了4个字节后的状态

    image-20221125183348948

    flip动作后,positioon切换为初始位置,limit切换为position位置

image-20221125183348948

读取4个字节后

image-20221125183348948
clear发生后

image-20221125183348948

compact方法

image-20221125184240561

2.3 ByteBuffer方法

2.3.1 ByteBuffer工具类

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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
public class ByteBufferUtils {
private static final char[] BYTE2CHAR = new char[256];
private static final char[] HEXDUMP_TABLE = new char[256 * 4];
private static final String[] HEXPADDING = new String[16];
private static final String[] HEXDUMP_ROWPREFIXES = new String[65536 >>> 4];
private static final String[] BYTE2HEX = new String[256];
private static final String[] BYTEPADDING = new String[16];

static {
final char[] DIGITS = "0123456789abcdef".toCharArray();
for (int i = 0; i < 256; i++) {
HEXDUMP_TABLE[i << 1] = DIGITS[i >>> 4 & 0x0F];
HEXDUMP_TABLE[(i << 1) + 1] = DIGITS[i & 0x0F];
}

int i;

// Generate the lookup table for hex dump paddings
for (i = 0; i < HEXPADDING.length; i++) {
int padding = HEXPADDING.length - i;
StringBuilder buf = new StringBuilder(padding * 3);
for (int j = 0; j < padding; j++) {
buf.append(" ");
}
HEXPADDING[i] = buf.toString();
}

// Generate the lookup table for the start-offset header in each row (up to 64KiB).
for (i = 0; i < HEXDUMP_ROWPREFIXES.length; i++) {
StringBuilder buf = new StringBuilder(12);
buf.append(StringUtil.NEWLINE);
buf.append(Long.toHexString((long) i << 4 & 0xFFFFFFFFL | 0x100000000L));
buf.setCharAt(buf.length() - 9, '|');
buf.append('|');
HEXDUMP_ROWPREFIXES[i] = buf.toString();
}

// Generate the lookup table for byte-to-hex-dump conversion
for (i = 0; i < BYTE2HEX.length; i++) {
BYTE2HEX[i] = ' ' + StringUtil.byteToHexStringPadded(i);
}

// Generate the lookup table for byte dump paddings
for (i = 0; i < BYTEPADDING.length; i++) {
int padding = BYTEPADDING.length - i;
StringBuilder buf = new StringBuilder(padding);
for (int j = 0; j < padding; j++) {
buf.append(' ');
}
BYTEPADDING[i] = buf.toString();
}

// Generate the lookup table for byte-to-char conversion
for (i = 0; i < BYTE2CHAR.length; i++) {
if (i <= 0x1f || i >= 0x7f) {
BYTE2CHAR[i] = '.';
} else {
BYTE2CHAR[i] = (char) i;
}
}
}

/**
* 打印所有内容
* @param buffer
*/
public static void debugAll(ByteBuffer buffer) {
int oldlimit = buffer.limit();
buffer.limit(buffer.capacity());
StringBuilder origin = new StringBuilder(256);
appendPrettyHexDump(origin, buffer, 0, buffer.capacity());
System.out.println("+--------+-------------------- all ------------------------+----------------+");
System.out.printf("position: [%d], limit: [%d]\n", buffer.position(), oldlimit);
System.out.println(origin);
buffer.limit(oldlimit);
}

/**
* 打印可读取内容
* @param buffer
*/
public static void debugRead(ByteBuffer buffer) {
StringBuilder builder = new StringBuilder(256);
appendPrettyHexDump(builder, buffer, buffer.position(), buffer.limit() - buffer.position());
System.out.println("+--------+-------------------- read -----------------------+----------------+");
System.out.printf("position: [%d], limit: [%d]\n", buffer.position(), buffer.limit());
System.out.println(builder);
}

private static void appendPrettyHexDump(StringBuilder dump, ByteBuffer buf, int offset, int length) {
if (MathUtil.isOutOfBounds(offset, length, buf.capacity())) {
throw new IndexOutOfBoundsException(
"expected: " + "0 <= offset(" + offset + ") <= offset + length(" + length
+ ") <= " + "buf.capacity(" + buf.capacity() + ')');
}
if (length == 0) {
return;
}
dump.append(
" +-------------------------------------------------+" +
StringUtil.NEWLINE + " | 0 1 2 3 4 5 6 7 8 9 a b c d e f |" +
StringUtil.NEWLINE + "+--------+-------------------------------------------------+----------------+");

final int startIndex = offset;
final int fullRows = length >>> 4;
final int remainder = length & 0xF;

// Dump the rows which have 16 bytes.
for (int row = 0; row < fullRows; row++) {
int rowStartIndex = (row << 4) + startIndex;

// Per-row prefix.
appendHexDumpRowPrefix(dump, row, rowStartIndex);

// Hex dump
int rowEndIndex = rowStartIndex + 16;
for (int j = rowStartIndex; j < rowEndIndex; j++) {
dump.append(BYTE2HEX[getUnsignedByte(buf, j)]);
}
dump.append(" |");

// ASCII dump
for (int j = rowStartIndex; j < rowEndIndex; j++) {
dump.append(BYTE2CHAR[getUnsignedByte(buf, j)]);
}
dump.append('|');
}

// Dump the last row which has less than 16 bytes.
if (remainder != 0) {
int rowStartIndex = (fullRows << 4) + startIndex;
appendHexDumpRowPrefix(dump, fullRows, rowStartIndex);

// Hex dump
int rowEndIndex = rowStartIndex + remainder;
for (int j = rowStartIndex; j < rowEndIndex; j++) {
dump.append(BYTE2HEX[getUnsignedByte(buf, j)]);
}
dump.append(HEXPADDING[remainder]);
dump.append(" |");

// Ascii dump
for (int j = rowStartIndex; j < rowEndIndex; j++) {
dump.append(BYTE2CHAR[getUnsignedByte(buf, j)]);
}
dump.append(BYTEPADDING[remainder]);
dump.append('|');
}

dump.append(StringUtil.NEWLINE +
"+--------+-------------------------------------------------+----------------+");
}

private static void appendHexDumpRowPrefix(StringBuilder dump, int row, int rowStartIndex) {
if (row < HEXDUMP_ROWPREFIXES.length) {
dump.append(HEXDUMP_ROWPREFIXES[row]);
} else {
dump.append(StringUtil.NEWLINE);
dump.append(Long.toHexString(rowStartIndex & 0xFFFFFFFFL | 0x100000000L));
dump.setCharAt(dump.length() - 9, '|');
dump.append('|');
}
}

public static short getUnsignedByte(ByteBuffer buffer, int index) {
return (short) (buffer.get(index) & 0xFF);
}
}

2.3.2 分配方法

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
System.out.println(ByteBuffer.allocate(16).getClass());
System.out.println(ByteBuffer.allocateDirect(16).getClass());
/*
打印结果:
class java.nio.HeapByteBuffer - java堆内存,读写效率较低,收到gc的影响(可能因为垃圾回收导致数据的迁移)
class java.nio.DirectByteBuffer - 直接内存,读写效率高些(少一次数据的拷贝),不会收到gc的影响,但是他的分配的效率比较低,而且使用不当,会造成内存泄漏
*/
}

2.3.3 写入和读取

写入:

  • 调用channel的read方法
  • 调用buffer的put方法

读取:

  • 调用channel的write方法

  • 调用buffer的get方法

    注意:buffer的get方法会让指针往后走,如果要重复读数据,可以调用rewind方法将指针重置到0或者使用get(int i)方法获取指针的内容,该方法不会产生指针的位移

2.3.4 mark和reset

mark 做一个标记,记录position位置,reset 是将position回到mark位置

2.3.5 字符串和ByteBuffer的转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(16);
// String -> ByteBuffer
// 字符串的getBytes()方法
buffer.put("hello".getBytes());
// 使用编码,会自动切换为读模式
ByteBuffer hello = StandardCharsets.UTF_8.encode("hello");
// wrap,和上一种方法一样,会自动切换为读模式
ByteBuffer wrap = ByteBuffer.wrap("hello".getBytes());

// ByteBuffer -> String
buffer.flip();
System.out.println(StandardCharsets.UTF_8.decode(buffer));
System.out.println(StandardCharsets.UTF_8.decode(hello));
System.out.println(StandardCharsets.UTF_8.decode(wrap));
}