IO、NIO简述

前言

操作系统中,大多数应用运行在用户空间里。在用户空间中,执行的代码并不能之间操控硬件。因此,用户空间中的代码需要用过调用内核代码来读写存储设备上的数据。

io

内核代码将磁盘上的数据,存储到内核空间的缓冲区,再被用户空间读取到用户空间的缓冲区。数据来往与用户空间与存储设备的过程中,内核充当着中间人的角色。

但是,当用户空间有多个缓冲区进行读写时,多次调用内核代码的代价不小。对于这种情况,用户进程可以选择利用一次系统调用,将多个缓冲区地址传递给内核,有内核进行发散、汇聚,同时满足多个缓冲的需求。

发散、汇聚

导航

IO

java io 相关的类比较多,一眼看下来可能有些崩溃。因此,我先很粗糙的切分了一下:字节流与字符流,也就是 Stream 与 Reader/Writer。然后输入、输出流往往也是对应的,因此,任选其一后(如 InputStream)后,大体剩下了这些类:

  • ByteArrayInputStream、StringBufferInputStream、FileInputStream、PipedInputStream、ObjectInputStream
  • FilterInputStream
    • BufferedInputStream
    • DataInputStream
    • LineNumberInputStream
    • PushBackInputStream

可以看到,以上的类被分为了两类:一类是各种数据类型的输入(数组、字符串、文件输入等等),一类则继承了 FilterInputStream 类:将基本数据进行转换或者提供其他功能的类,也就是装饰者模式的主要对象。

总体来说,io 包下,首先划分成了 stream 与 Reader/Writer,其下有分成了输入、输出两种,其下再次分为了基本数据传输流与功能性装饰流。

stream 与 Reader/Writer 的区别显而易见,一个按字节读取,一个按字符读取:

1
2
3
4
5
6
7
8
9
// OutputStream,按字节数组写
public void write(byte b[]) throws IOException {
write(b, 0, b.length);
}

// Writer,按字符数组写
public void write(char cbuf[]) throws IOException {
write(cbuf, 0, cbuf.length);
}

根据不同数据类型而区分的数据流更是直观:

1
2
3
4
5
6
7
// FileInputStream,从file读取
public FileInputStream(File file) throws FileNotFoundException {
}

// ByteArrayInputStream,从字节数组读取
public ByteArrayInputStream(byte buf[]) {
}

我们来以常见的装饰流 BufferedInputStream 举例:

1
2
3
4
5
6
7
8
9
10
11
// BufferedInputStream
protected volatile byte[] buf;

public synchronized int read() throws IOException {
if (pos >= count) {
fill(); // 读入更多的数据到buf
if (pos >= count)
return -1;
}
return getBufIfOpen()[pos++] & 0xff;
}

fill()方法会读取更多的数据到 buf,也就是缓存之中。将下次读取数据的时候,优先从缓存中读取,因此减少了本地 io api 的调用。

而基本的数据传输流,是没有 fill 与 buf 的,每次读将会直接调用本地 api,如 FileInputStream

1
2
3
4
5
public int read() throws IOException {
return read0();
}

private native int read0() throws IOException;

NIO

java nio与io相比,多了几个概念:buffer(缓冲)、channel(通道)、selector(选择器)。相信有了多缓冲io的那张图,理解这些概念并不困难。简而言之,从buffer中读写数据,channel传输数据,selector控制channel。

发散、汇聚

与io相比,nio无疑复杂了许多,那么nio的优势在哪里呢?这里,我们得先提一下io模型:

iomodel

关于io模型,推荐阅读:漫话:如何给女朋友解释什么是Linux的五种IO模型?

io是阻塞式,而nio则是I/O复用形式。通过将多个channel注册到一个selector上,等待数据的时间会被分摊,空等数据的时间将会减少。

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
public static void main(String[] args) throws  Exception{
//创建ServerSocketChannel,-->> ServerSocket
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress(5555);
serverSocketChannel.socket().bind(inetSocketAddress);
serverSocketChannel.configureBlocking(false); //设置成非阻塞

//开启selector,并注册accept事件
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

while(true) {
selector.select(2000); //监听所有通道
//遍历selectionKeys
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if(key.isAcceptable()) { //处理连接事件
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false); //设置为非阻塞
System.out.println("client:" + socketChannel.getLocalAddress() + " is connect");
socketChannel.register(selector, SelectionKey.OP_READ); //注册客户端读取事件到selector
} else if (key.isReadable()) { //处理读取事件
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
SocketChannel channel = (SocketChannel) key.channel();
channel.read(byteBuffer);
System.out.println("client:" + channel.getLocalAddress() + " send " + new String(byteBuffer.array()));
}
iterator.remove(); //事件处理完毕,要记得清除
}
}

}

至于异步IO(AIO)模型,则是java nio 2了:

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
AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(80));

server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
final ByteBuffer buffer = ByteBuffer.allocate(1024);

@Override
public void completed(AsynchronousSocketChannel result, Object attachment) {
Future<Integer> writeResult = null;
try {
buffer.clear();
result.read(buffer).get(100, TimeUnit.SECONDS);
buffer.flip();
writeResult = result.write(buffer);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
server.accept(null, this);
writeResult.get();
result.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}

@Override
public void failed(Throwable exc, Object attachment) {
System.out.println("failed: " + exc);
}
});