Java NIO模型

NIO

BIO

问题

通过前面两个章节的介绍,相信大家已经对网络BIO模型已经有了不少了解了。那么BIO主要的问题点在哪里呢?我们来回顾下BIO模型的架构图:
Java NIO模型
client与service经历三次握手之后,service在内核中开辟空间,分配FD,socket主线程bind到该FD后,clone出子线程来接收client传输的数据以及业务处理。

如果简单的使用此模型,我们可以看到每一次接收到client连接,都需要创建出新线程,而我们也知道计算机创建新线程的代价是很大的,内存分配(每个线程约需几MB),以及线程调度等,这一块会极大的占用系统资源,并且子线程的IO读取,如果线程量很大,会极大降低IO读取的效率。

既然在创建线程这块是个极大的影响点,我们将clone线程的操作换成线程池会如何呢?
Java NIO模型
使用线程池,虽然解决了clone线程带来的同步消耗资源的问题,但是对IO读取,以及线程切换,资源实际消耗等问题,并没有带来实质性的改善。

究其根本原因,是因为kernel的系统调用是阻塞性的,不论是内核的read(FD(0)),write(FD(1))等,都是blocking的操作,导致socket为了不影响其他client的连接,只能new出新线程才异步执行这些blocking操作。

NIO

相信大家在编写Java代码时都听说过NIO,也都清楚IO操作是blocking的,那NIO就是非blocking的。
其实这话对,也不全对。
通过上面对BIO的分析,我们其实可以看到blocking的根本原因是在kernel,所以要实现NIO,根本点也是在kernel。

我们用man命令来看下kernel对socket的描述:
Java NIO模型
这便是kernel层面的NIO。当然我们Java代码里面也有NIO:
Java NIO模型

相信大家看了上一章在Java代码中对系统调用的分析,一定可以得出结论:Java代码中的NIO最终是基于kernel的NIO实现的。Java代码的NIO是基于kernel NIO的一层封装。

所以对于NIO,在Java代码中指的是New IO,这是相较于Java中的老的IO方式、
在kernel中,NIO指的是nonBlocking IO,也即非阻塞IO。

代码

现在我们在Java代码中使用NIO方式来实现Sevice:

Java NIO模型
我们在Java代码中使用NIO方式定义ServerSocket,使用nonBlocking形式accept client,在一个线程中同时负责监听连接和accept连接,并且负责读取数据。

我们实际运行起来看下效果:

Java NIO模型
我们可以看到在client与service监理连接之后,发送“send message”给service。
service端用一个线程accept了client连接,同时也读取出了发送的数据

我们来看分析下代码,我们一定可以发现这么一行代码:

channel.configureBlocking(false);

我们手动将channel的是否Blocking设置为了false,也就是不阻塞。

追踪

前面我们也已经讲过,想要实现NIO,必须kernel支持,那么kernel是如何做的呢?还是老规矩,我们来追中下service的系统调用:
Java NIO模型
结合下代码逻辑:

while (true) {
                SocketChannel socket = channel.accept();

在主线程的while无限循环中,channel对象一直在尝试accept客户端的连接,上一章节的BIO模型中追中系统调用我们可以发现一旦线程运行到accept处时,线程会一直卡在accept处,一直等到有client连接之后才会往下继续执行,也即accept()方法获取到结果才会往下走。

现在再来看下NIO模式下,kernel是怎么做的:
在没有client建立连接之前,accept()不是一直阻塞在那边,而是直接获得了结果"-1",
在有一个client建立连接之后,accept获取到这个连接,根据socket四要素分配资源创建socket,同时分配描述符FD(16),后续再通过buffer从FD(16)中读取到message。

综述

到这里,我们可以总结下Java中NIO是如何工作的:
Java NIO模型
与BIO模式不同,NIO模式下只需要一个线程在不停的无限循环,同时处理accept和读取数据、业务处理。

问题

通过NIO模式,我们可以解决BIO模式下为了解决accept Blocking的问题,从而需要创建大量子线程以及维护成本的问题。那NIO是否也有问题呢?
以下讨论基于socket四元组中service的ip,port固定,且service端只是用单线程处理请求:

  1. 当有大量的client建立socket请求之后,kernel会为每一个socket分配一个FD,并且将该FD与service主线程bind,但是我们需要知道,一个Thread能够分配的FD是有数量限制的,一旦超过了上限,即意味着后续无法处理后续client的数据
  2. 当同时有大量的client到达,我们用一个Thread去遍历所有已经accept了的FD去获取数据时,每一次循环都是一次系统调用,需要不停的调用kernel的write()和read(),这必然造成用户态和内核态的频繁切换。

这些问题,我们留到后续的多路复用章节再介绍

上一篇:iptables 防火墙(三)- 规则的导出 / 导入、使用防火墙脚本程序 |(附体系思维导图)


下一篇:java的TCP通信