BIO_AIO_NIO区别

BIO_AIO_NIO区别

简单对比:

  • BIO 就是传统的 java.io 包,它是基于流模型实现的,交互的方式是同步、阻塞方式,也就是说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调用时可靠的线性顺序。它的有点就是代码比较简单、直观;缺点就是 IO 的效率和扩展性很低,容易成为应用性能瓶颈。
  • NIO 是 Java 1.4 引入的 java.nio 包,提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层高性能的数据操作方式。
  • AIO 是 Java 1.7 之后引入的包,是 NIO 的升级版本,提供了异步非堵塞的 IO 操作方式,所以人们叫它 AIO(Asynchronous IO),异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

前言

问题的关键其实就是理解同步/异步、阻塞/非阻塞的含义。

最初我看了很多blog以及一些所谓的面试考题讲解视频,以为理解了同步/异步、阻塞/非阻塞的含义,但其实越想越经不起推敲

比如下面的一些理解(我认为是不对的):

同步和异步(针对请求),阻塞和非阻塞(针对客户端)

在网络请求中,客户端会发出一个请求到服务端

  1. 客户端发了请求后,就一直等着服务端的返回相应。 客户端:阻塞。 请求:同步
  2. 客户端发了请求后,就去干别的事情了,时不时来检查服务端是否给出了响应。 客户端:非阻塞。 请求: 同步。
  3. 换成异步请求后,客户端发出请求后,就坐在椅子上,等着服务端的返回相应。 客户端:阻塞。 请求:异步。
  4. 客户端发出请求后,就去干别的事情了,等到服务端给出响应后,再来处理业务逻辑。 客户端:阻塞。 请求:异步。

像这样举通俗的例子来说明这几个概念,通常都经不起推敲,原因在于,例子中缺少了一个“操作系统”级别的调度者。

  • 阻塞非阻塞是跟进程/线程严密相关的,而进程/线程又是依赖于操作系统存在的,所以自然不能脱离操作系统来讨论阻塞非阻塞。

  • 同步/异步也是跟任务流相关的,所以举例子必须考虑到并发的任务流,不然,肯定很难举出恰当的例子的。

本文的讨论:

  • 限定Linux环境下的network IO作为背景来讨论同步/异步、阻塞/非阻塞的理解。[1][2][4]

  • 讨论Java中对应的编程模型

  • 对比BIO/NIO的区别[3]

1. Linux下的五种I/O模型

Stevens在文章中一共比较了五种IO Model:

  • blocking IO
  • nonblocking IO
  • IO multiplexing
  • signal driven IO
  • asynchronous IO

由于signal driven IO在实际中并不常用,所以只提及剩下的四种IO Model。

IO发生时涉及的对象和步骤:

  • 对于一个network IO (这里我们以read举例),它会涉及到两个系统对象:
    • 一个是调用这个IO的process (or thread)
    • 另一个就是系统内核(kernel)
  • 当一个read操作发生时,它会经历两个阶段:
    1 等待数据准备 (Waiting for the data to be ready)
    2 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
  • 记住这两点很重要,因为这些IO Model的区别就是在两个阶段上各有不同的情况。

1.1 阻塞I/O(blocking I/O)

在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:

  • 第一阶段:准备数据

    当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据。对于network io来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。

  • 第二阶段:数据拷贝

    当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。

所以,blocking IO的特点就是在IO执行的两个阶段都被block了。以下是java实现的服务器bio代码(需要掌握),注意阻塞方法阻塞的第一个阶段。

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
// BIO阻塞代码
public class ServerTcpSocket {
static byte[] bytes = new byte[1024];

public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
try {
// 1.创建一个ServerSocket连接
final ServerSocket serverSocket = new ServerSocket();
// 2.绑定端口号
serverSocket.bind(new InetSocketAddress(8080));
// 3.当前线程放弃cpu资源等待获取数据
System.out.println("等待获取数据...");
while (true) {
final Socket socket = serverSocket.accept(); // 阻塞方法
executorService.execute(new Runnable() {
public void run() {
try {
System.out.println("获取到数据...");
// 4.读取数据
int read = socket.getInputStream().read(bytes); // 会阻塞
String result = new String(bytes);
System.out.println(result);
} catch (Exception e) {

}
}
});

}
} catch (Exception e) {

}
}
}

应用程序想要读取数据就会调用recvfrom,而recvfrom会通知OS来执行,OS就会判断数据报是否准备好(比如判断是否收到了一个完整的UDP报文,如果收到UDP报文不完整,那么就继续等待)。当数据包准备好了之后,OS就会将数据从内核空间拷贝到用户空间(因为我们的用户程序只能获取用户空间的内存,无法直接获取内核空间的内存)。拷贝完成之后socket.read()就会解除阻塞,并得到read的结果。

1.2 非阻塞(Non-Blocking IO)

linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:

  • 第一阶段:准备数据

    从图中可以看出,当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。

  • 第二阶段:数据拷贝

    一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。

所以,用户进程其实是需要不断的主动询问kernel数据好了没有。一定要注意这个地方,Non-Blocking还是会阻塞的

以下是java实现的服务器bio代码(需要掌握),注意数据准备阶段是非阻塞方法。

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
// NIO非阻塞式代码
public class ServerNioTcpSocket {
static ByteBuffer byteBuffer = ByteBuffer.allocate(512);

public static void main(String[] args) {
try {
// 1.创建一个ServerSocketChannel连接
final ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 2.绑定端口号
serverSocketChannel.bind(new InetSocketAddress(8080));
// 设置为非阻塞式
serverSocketChannel.configureBlocking(false);
// 非阻塞式
SocketChannel socketChannel = serverSocketChannel.accept();
if (socketChannel != null) {
int j = socketChannel.read(byteBuffer); // 此处不会阻塞
if (j > 0) {
byte[] bytes = Arrays.copyOf(byteBuffer.array(), byteBuffer.limit());
System.out.println("获取到数据" + new String(bytes));
}
}
System.out.println("程序执行完毕..");

} catch (Exception e) {
e.printStackTrace();
}
}
}

NIO中提供了集中Channel:ServerSocketChannel;SocketChannel;FileChannel; DatagramChannel只有FileChannel无法设置成非阻塞模式,其他Channel都可以设置为非阻塞模式。

当设置为非阻塞后,我们的socket.read()方法就会立即得到一个返回结果(成功 or 失败),我们可以根据返回结果执行不同的逻辑,比如在失败时,我们可以做一些其他的事情。但事实上这种方式也是低效的,因为我们不得不使用轮询方法区一直问OS:“我的数据好了没啊”。第一个代码是该线程不断的轮询,第二个代码是使用selector实现轮询等待有效请求。

NIO 不会在recvfrom也就是socket.read()时候阻塞,但是还是会在将数据从内核空间拷贝到用户空间阻塞。

1.3 I/O复用 IO multiplexing

IO multiplexing这个词可能有点陌生,但是如果我说select,epoll,大概就都能明白了。

有些地方也称这种IO方式为event driven IO。我们都知道,select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。

它的基本原理就是select/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。它的流程如图:

这里用java的NIO示意select

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
// 服务端代码
public class NIOServer{
public static void main(String[] args) throws Exception {
// 创建一个本地端口进行监听的服务Socket通道,并设置为非阻塞方式
ServerSocketChannel ssc = ServerSocketChannel.open();
// 必须配置为非阻塞才能往selector上注册,否则会报错,selector模式甭说就是非阻塞模式
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress(8888));
// 创建一个选择器selector
Selector selector = selector.open();
// 把ServerSocketChannel注册到selector上,并且selector对客户端accept连接操作感兴趣
ssc.register(selector, SelectionKey.OP_ACCEPT);

while(true){
System.out.println("等待事件发生...");
// 轮询监听channel里面的key,select是阻塞的,accept也是阻塞的
int select = selector.select();

System.out.println("事件发生了...");
// 有客户端请求,被轮询监听到
Iterator<SelectionKey> it = selector.selectionKeys().iterator();
while(it.hasNext()){
SelectionKey key = it.next();
// 删除本次已处理的key, 防止下次select重复处理
it.remove();
handle(key);
}
}
}

private static void handle(SelectionKey key) throws IOException{
if(key.isAcceptable()){
System.out.println("有客户端连接事件发生了...");
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
// NIO非阻塞体现:此处accept方法是阻塞的,但是这里因为发生了连接事件,所以这个方法会马上执行完,不会阻塞
// 处理完连接请求不会继续等待客户端的数据发送
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
// 通过Selector监听Channel时对读事件感兴趣
sc.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()){
System.out.println("有客户端数据可读事件发生了...");
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
// NIO非阻塞体现:首先read方法不会阻塞,其次这种事件响应模型,当调用到read方法时肯定时发生了客户端发送数据的事件
int len = sc.read(buffer);
if(len != -1){
System.out.println("读取到客户端发送的数据:" + new String(buffer.array(), 0, len));
}
ByteBuffer bufferToWrite = ByteBuffer.wrap("HelloClient".getBytes());
sc.write(bufferToWrite);
key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
} else if(key.isWritable) {
SocketChannel sc = (SocketChannel) key.channel();
System.out.println("write事件...");
// NIO事件触发是水平触发
// 使用Java的NIO编程时,在没有数据可以往外写的时候要取消写事件
// 在有数据往外写的时候在注册写事件
key.interestOps(SelectionKey.OP_READ);
// sc.close();
}
}
}

当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。

这个图和blocking IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。

多说一句。所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。

在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。

1.4 异步I/O(Asynchronous IO)

linux下的asynchronous IO其实用得很少(所以这里就不贴AIO的java代码了,一般不要求掌握)。先看一下它的流程:

用户进程发起read操作之后,立刻就可以开始去做其它的事。

而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。

Asynchronous IO调用中是真正的无阻塞,其他IO model中多少会有点阻塞。

1.5 基于四个模型,对blocking和non-blocking,synchronous IO和asynchronous IO的理解

其实是POSIX的定义是这样子的:

  • A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;

  • An asynchronous I/O operation does not cause the requesting process to be blocked;

核心区别是:synchronous IO做”IO operation”的时候会将process阻塞

  1. blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO。

  2. 而asynchronous IO,当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被block。

有人可能会说,non-blocking IO并没有被block啊。这里有个非常“狡猾”的地方,定义中所指的”IO operation”是指真实的IO操作,就是例子中的recvfrom这个system call。non-blocking IO在执行recvfrom这个system call的时候,如果kernel的数据没有准备好,这时候不会block进程。但是,当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了,在这段时间内,进程是被block的。

经过上面的介绍,会发现non-blocking IO和asynchronous IO的区别还是很明显的。在non-blocking IO中,虽然进程大部分时间都不会被block,但是它仍然要求进程去主动的check,并且当数据准备完成以后,也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。而asynchronous IO则完全不同。它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据。

1.6 select,poll,epoll区别

知乎这篇文章把底层讲得很清楚(一共三篇):https://zhuanlan.zhihu.com/p/64746509

他们是NIO中多路复用的三种实用机制(底层是三个C++的API),是由Linux操作系统提供的。

Unix,Linux操作系统的一些基本知识:

  • 用户空间和内核空间:操作系统为了保护系统安全,将内核划分为两部分,一个是用户空间,一个是内核空间。用户空间不能直接访问底层的硬件设备,必须通过内核空间

  • 文件描述符File Descriptor(FD):是一个抽象概念,形式上是一个整数,实际上是一个索引值。指向内核中为每一个进程维护进程所打开的文件的记录表。当程序打开一个文件或者创建一个文件时,内核就会向进程返回一个FD。

select机制:会维护一个FD的集合fd_set。将fd_set从用户空间复制到内核空间,激活socket。(fd_set是一个数组结构,拷贝大小受限制 x64最大2048)

poll机制:和select机制差不多的。将fd_set结构进行了优化,FD集合大小突破了操作系统限制。(pollfd结构来代替fd_set,通过链表实现的)

Epoll机制:Event Poll不在扫描所有的FD,只将用户关心的FD的事件存放到内核的一个事件表当中。可以减少用户空间与内核空间之间需要拷贝的数据。

操作方式 底层实现 最大连接数 IO效率
select(1984) 遍历 数组 受限于内核 一般
poll(1997) 遍历 链表 无上限 一般
epoll(2002) 事件回调 红黑树 无上限

Java NIO当中是使用哪一种机制?

基于内核版本,可以查看DefaultSelectorProvider源码:

  • windows下,WindowsSelectorProvider。

  • Linux下,内核2.6版本以上,就是EpollSelectorProvider,否则就是默认的PollSelectorProvider。

2. Java的IO编程模型

BIO 同步阻塞IO。 可靠性差,吞吐量低,适用于连接比较少且比较固定的场景。JDK1.4之前唯一的选择。

编程模型最简单。

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
// 复习黑马文件上传的服务端BIO代码
public class FileUpload_Server {
public static void main(String[] args) throws IOException {
System.out.println("服务器 启动..... ");
// 1. 创建服务端ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);
// 2. 循环接收,建立连接
while (true) {
Socket accept = serverSocket.accept(); // 阻塞方法
/*
3. socket对象交给子线程处理,进行读写操作
Runnable接口中,只有一个run方法,使用lambda表达式简化格式
*/
new Thread(() ‐> {
try (
//3.1 获取输入流对象
BufferedInputStream bis = new BufferedInputStream(accept.getInputStream());
//3.2 创建输出流对象, 保存到本地 .
FileOutputStream fis = new FileOutputStream(System.currentTimeMillis() +
".jpg");
BufferedOutputStream bos = new BufferedOutputStream(fis);) {
// 3.3 读写数据
byte[] b = new byte[1024 * 8];
int len;
while ((len = bis.read(b)) != ‐1) {
bos.write(b, 0, len);
}
//4. 关闭 资源
bos.close();
bis.close();
accept.close();
System.out.println("文件上传已保存");
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
}

但是如果不活跃的连接逐渐增多,线程池里的线程慢慢的也都开始阻塞等待IO,线程池里真正在运行的线程数会越来越少,当线程池处理不过来时,会放置到线程池配置的BlockingQueue中,队列塞满后,慢慢的线程池中线程的数目会逐渐达到线程池配置的maximumPoolSize,如果再处理不过来,执行拒绝策略。也就是说,最终会导致请求无法及时处理。

这样带来的问题是,假设有线程池corePoolSize设置为100,只要有100个不活跃的连接正在阻塞读写IO,就会把前corePoolSize线程都阻塞住,后续的请求就无法及时处理。

NIO 同步非阻塞IO。 可靠性比较好,吞吐量也比较高,适用于连接比较多且连接比较短(轻操作)。例如聊天室。JDK1.4开始支持。(应用最广)

编程模型最复杂。

NIO非阻塞体现:

  • accept方法是阻塞的,但是这里因为发生了连接事件,所以这个方法会马上执行完,不会阻塞
  • 首先read方法不会阻塞,其次这种事件响应模型,当调用到read方法时肯定时发生了客户端发送数据的事件

(我的一些看法,java的NIO就是Linux中IO复用和NIO两个模型的结合)

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
// 服务端代码
public class NIOServer{
public static void main(String[] args) throws Exception {
// 创建一个本地端口进行监听的服务Socket通道,并设置为非阻塞方式
ServerSocketChannel ssc = ServerSocketChannel.open();
// 必须配置为非阻塞才能往selector上注册,否则会报错,selector模式甭说就是非阻塞模式
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress(8888));
// 创建一个选择器selector
Selector selector = selector.open();
// 把ServerSocketChannel注册到selector上,并且selector对客户端accept连接操作感兴趣
ssc.register(selector, SelectionKey.OP_ACCEPT);

while(true){
System.out.println("等待事件发生...");
// 轮询监听channel里面的key,select是阻塞的,accept也是阻塞的
int select = selector.select();

System.out.println("事件发生了...");
// 有客户端请求,被轮询监听到
Iterator<SelectionKey> it = selector.selectionKeys().iterator();
while(it.hasNext()){
SelectionKey key = it.next();
// 删除本次已处理的key, 防止下次select重复处理
it.remove();
handle(key);
}
}
}

private static void handle(SelectionKey key) throws IOException{
if(key.isAcceptable()){
System.out.println("有客户端连接事件发生了...");
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
// NIO非阻塞体现:此处accept方法是阻塞的,但是这里因为发生了连接事件,所以这个方法会马上执行完,不会阻塞
// 处理完连接请求不会继续等待客户端的数据发送
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
// 通过Selector监听Channel时对读事件感兴趣
sc.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()){
System.out.println("有客户端数据可读事件发生了...");
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
// NIO非阻塞体现:首先read方法不会阻塞,其次这种事件响应模型,当调用到read方法时肯定时发生了客户端发送数据的事件
int len = sc.read(buffer);
if(len != -1){
System.out.println("读取到客户端发送的数据:" + new String(buffer.array(), 0, len));
}
ByteBuffer bufferToWrite = ByteBuffer.wrap("HelloClient".getBytes());
sc.write(bufferToWrite);
key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
} else if(key.isWritable) {
SocketChannel sc = (SocketChannel) key.channel();
System.out.println("write事件...");
// NIO事件触发是水平触发
// 使用Java的NIO编程时,在没有数据可以往外写的时候要取消写事件
// 在有数据往外写的时候在注册写事件
key.interestOps(SelectionKey.OP_READ);
// sc.close();
}
}
}

AIO 异步非阻塞IO。解决了服务端需要一直守着线程的问题,可靠性是最好的,吞吐量也是非常高的,适用于连接比较多且比较长(重操作)。 例如相册服务器。 JDK7开始支持。

编程模型比较简单,但需要操作系统来支持异步服务。

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
// 服务端代码(了解)
public class AIOServer{
public static void main(String[] args) throws Exception{
final AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open().bind(new InewSocketAddress(9000));

serverChannel.accept(null, new CompletionHander<AsynchronousServerSocketChannel, Object>()){
@Override
public void completed(AsynchronousServerSocketChannel socketChannel, Object attachment){
try{
// 再次接受客户端连接,如果不写这一行代码后面的客户端连接不上服务器端
serverChannel.accept(attachment, this);
System.out.println(socketChannel.getRemoteAddress());
ByteBuffer buffer = ByteBuffer.allocate(1024);
socketChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>(){
@Override
public void completed(Integer result, ByteBuffer buffer){
buffer.flip();
System.out.println(new String(buffer.array(), 0, result));
socketChannel.write(ByteBuffer.wrap("HelloClient".getBytes()));
}

@Override
public void failed(Throwable exc, ByteBuffer buffer) {exc.printStackTrace();}
});
} catch (IOException e) {
e.printStackTrace();
}
}

@Override
public void failed(Throwable exc, ByteBuffer buffer) {exc.printStackTrace();}
});

Thread.sleep(Integer.MAX_VALUE);
}
}

3. BIO/NIO区别

4. BIO、NIO、AIO适用场景

  • BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择。
  • NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂。
  • AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。

5. Java NIO的3个核心组件

NIO重点:

Channel(通道),Buffer(缓冲区),Selector(选择器)三个类之间的关系

简单解释:

  • 每一个channel对应一个buffer缓冲区。

  • channel会注册到selector。

  • selector会根据channel上发生的读写事件,将请求交由某一个空闲线程处理。

  • selector对应一个或者多个线程。

  • buffer和channel都是可读可写的。

5.1 缓冲区Buffer

Buffer是一个对象。它包含一些要写入或者读出的数据。在面向流的I/O中,可以将数据写入或者将数据直接读到Stream对象中。

在NIO中,所有的数据都是用缓冲区处理。这也就本文上面谈到的IO是面向流的,NIO是面向缓冲区的。

缓冲区实质是一个数组,通常它是一个字节数组(ByteBuffer),也可以使用其他类的数组。但是一个缓冲区不仅仅是一个数组,缓冲区提供了对数据的结构化访问以及维护读写位置(limit)等信息。

最常用的缓冲区是ByteBuffer,一个ByteBuffer提供了一组功能于操作byte数组。除了ByteBuffer,还有其他的一些缓冲区,事实上,每一种Java基本类型(除了Boolean)都对应一种缓冲区,具体如下:

  • ByteBuffer:字节缓冲区
  • CharBuffer:字符缓冲区
  • ShortBuffer:短整型缓冲区
  • IntBuffer:整型缓冲区
  • LongBuffer:长整型缓冲区
  • FloatBuffer:浮点型缓冲区
  • DoubleBuffer:双精度浮点型缓冲区

5.2 通道Channel

Channel是一个通道,可以通过它读取和写入数据,他就像自来水管一样,网络数据通过Channel读取和写入。

通道和流不同之处在于通道是双向的,流只是在一个方向移动,而且通道可以用于读,写或者同时用于读写。

因为Channel是全双工的,所以它比流更好地映射底层操作系统的API,特别是在UNIX网络编程中,底层操作系统的通道都是全双工的,同时支持读和写。

Channel有四种实现:

  • FileChannel:是从文件中读取数据。
  • DatagramChannel:从UDP网络中读取或者写入数据。
  • SocketChannel:从TCP网络中读取或者写入数据。
  • ServerSocketChannel:允许你监听来自TCP的连接,就像服务器一样。每一个连接都会有一个SocketChannel产生。

5.3 多路复用器Selector

Selector选择器可以监听多个Channel通道感兴趣的事情(read、write、accept(服务端接收)、connect,实现一个线程管理多个Channel,节省线程切换上下文的资源消耗。Selector只能管理非阻塞的通道,FileChannel是阻塞的,无法管理。

关键对象

  • Selector:选择器对象,通道注册、通道监听对象和Selector相关。
  • SelectorKey:通道监听关键字,通过它来监听通道状态。

监听注册

监听注册在Selector

socketChannel.register(selector, SelectionKey.OP_READ);

监听的事件有

  • OP_ACCEPT: 接收就绪,serviceSocketChannel使用的
  • OP_READ: 读取就绪,socketChannel使用
  • OP_WRITE: 写入就绪,socketChannel使用
  • OP_CONNECT: 连接就绪,socketChannel使用

5.4 NIO的一些应用和框架

例如:Dubbo(服务框架),就默认使用Netty作为基础通信组件,用于实现各进程节点之间的内部通信。

Jetty、Mina、Netty、Dubbo、ZooKeeper等都是基于NIO方式实现

参考资料

  1. UNIX® Network Programming Volume 1, Third Edition: The Sockets Networking,Richard Stevens,6.2节 I/O Models
  2. https://blog.csdn.net/historyasamirror/article/details/5778378
  3. https://zhuanlan.zhihu.com/p/83597838
  4. https://zhuanlan.zhihu.com/p/112810033

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!