NIO

本地传输

参考:NIO 入门
以往的IO的数据传输方式是以流来进行的。类似两个位置间又一条水渠,数据就是流水,会从一点流向另一点,也正因如此,流的方向都是单向的,并且要想缓存数据流就要通过BufferedInputStream类(相当于水箱)。而NIO则通过引入channel和buffer的概念,改变了传输方式。在NIO中,两点的数据传输首先是修路,修好的路就是channel,然后是通车,这个车就是buffer。因此在NIO中,同一个channel可以进行双向通信,buffer承担数据载体在channel中传递。buffer的本质是数组,争对不同的数据类型构建不同的数组。
从另一个方面看,IO的通信方式是:先和对方打好招呼,等待对方有空可以接收数据了,然后我方开始发送数据。发送的方式也许也是一定量的单位,但是要注意的是这个量的大小以及什么时候发送是由Java类自己决定的,程序员没法干预。而NIO的通信方式是:这辆车我可以先装,到底要不要装满再发送是由程序员决定的。因此可以看出,NIO将原先的buffer部分暴露给了程序员,由此给了程序员更多的权限,更灵活的操作方式,因此编写相较IO更复杂,但是也更高效。
上面反复提到了NIO由两个关键点channel和buffer,因此要了解NIO就要依次讲channel和buffer的使用。

Buffer

参考:What is the difference between buffers in Java.io and buffers in Java.nio?

Buffer类通过4个属性域进行对Buffer的控制:

  • capacity:缓冲区数组的总长度,一般为数组长度。
  • position:下一个要操作的数据元素的位置,会依据读写操作动态变化。
  • limit:缓冲区数组中不可操作的下一个元素的位置,limit<=capacity,通常和position一起构成了可操作区间
  • mark:用于记录当前 position 的前一个位置或者默认是 0,类似书签功能。

在实际操作数据时它们有如下关系图:

NIO

我们通过 ByteBuffer.allocate(11) 方法创建一个 11 个 byte 的数组缓冲区,初始状态如上图所示,position 的位置为 0,capacity 和 limit 默认都是数组长度。当我们写入 5 个字节时位置变化如下图所示:

NIO

这时我们需要将缓冲区的 5 个字节数据写入 Channel 通信信道,所以我们需要调用 byteBuffer.flip() 方法,数组的状态又发生如下变化:

NIO

这时底层操作系统就可以从缓冲区中正确读取这 5 个字节数据发送出去了。在下一次写数据之前我们在调一下 clear() 方法。缓冲区的索引状态又回到初始位置。

这里还要说明一下 mark,当我们调用 mark() 时,它将记录当前 position 的前一个位置,当我们调用 reset 时,position 将恢复 mark 记录下来的值。

深入分析 Java I/O 的工作机制中Buffer的部分。

关于Buffer的更详细的介绍,如缓冲区分片,直接缓冲区和普通缓冲区,内存映射,缓冲区的分散聚集,Buffer的编解码等操作可以参考NIO 入门以及其他相关文章。

Channel

Channel是和Buffer配合使用的,一般情况下,数据先进入Buffer中,然后通过Channel进行传输。反过来也是先通过从Channel中读取数据进入Buffer,然后从Buffer中得到数据。因此,我们可以认为,Buffer作为一个传输工具,其作用是作为两个Channel的连接点,从而进行中转。如下图所示:

NIO

channel的获取可以直接通过对应流的.getChannel()方法获取,也可以通过工厂方法直接得到。在Java1.7之后还可以用channel.open()方法获得。
另一方面,对于FileChannel来说,它还可以直接调用transferTo()transerFrom()方法进行直接传输,并且,使用该方法时,其使用的直接缓冲区。
最后,channel的打开要对应关闭才能释放资源。

网络传输

在仅仅使用channel和buffer的情况下,在网络传输时,NIO和传统IO一样,都是阻塞式的,具体的NIO的实现流程可以见channel小节的示意图。阻塞式的意思是说,Server会为每一个TCP连接分配一个线程,用于保持连接,因此,一旦客户端数量增加,相应的线程数就必须增加。但是如果每个客户端的数据传输频率很低,那么将会阻塞对应的线程,使之已知等待Client发来数据。为解决在大量Client低活跃度的情况,我们提出了非阻塞式IO也就是NIO,其原理是通过设置一个Selector来担任观察者,仅仅对有数据传输进来的channel进行线程分配。而这就是观察者模式。

阻塞传输

NIO

如上图所示,阻塞网络NIO传输和传统网络IO传输很像,通过一个ServerSocketChannel进行监听,当有新Client进来时,就为其分配一个线程去处理。可以发现,客户端越多,线程也越多。

非阻塞传输

NIO

通过以上流程图展示了非阻塞传输的过程。一步步来看。
客户端同网络阻塞NIO相同。主要看服务器端,这一次,主线程只有一个选择器Selector负责调度。其余所有通道都要向选择器进行注册。首先进行注册的时ServerSocketChannel。然后一旦ServerSocketChannel收到accpet消息(假设客户端1发来请求),就会通知选择器,选择器会采取相应动作。在这里就是指派ServerSocketChannel生成一个SocketChannel1与客户端1的SocketChannel建立连接,此时,如果客户端1发送了数据过来,则SocketChannel1会通知选择器,选择器则指派读取通道(eg. FileChannel)和Buffer进行IO读取。同时,客户端2也同样发来建立请求连接,则ServerSocketChannel通知选择器,并接受选择器的指派进行新建SocketChannel2与客户端2的SocketChannel进行连接。接着就是选择器等待SocketChannel1SocketChannel2发送通知,谁通知选择器有消息可读,选择器就指派读取通道(eg. FileChannel)和Buffer对其进行IO读取。这样可以看到仅用少量线程就可以完成多客户端的传输过程。因此将等待IO读取的时间节省了出来。
注意,选择器一般只注册在SocketChannelServerSocketChannel这类网络Channel中(即SelectableChannel),如图中黄色的Channel。
详细的代码描述可以参看:Java 网络IO编程总结(BIO、NIO、AIO均含完整实例代码)

观察者模式

参考:《JAVA与模式》之观察者模式
观察者模式所涉及的角色有:

  • 抽象主题(Subject)角色:
    抽象主题角色把所有对观察者对象的引用保存在一个聚集(比如ArrayList对象)里,每个主题都可以有任何数量的观察者。抽象主题提供一个接口,可以增加和删除观察者对象,抽象主题角色又叫做抽象被观察者(Observable)角色。
  • 具体主题(ConcreteSubject)角色:
    将有关状态存入具体观察者对象;在具体主题的内部状态改变时,给所有登记过的观察者发出通知。具体主题角色又叫做具体被观察者(Concrete Observable)角色。
  • 抽象观察者(Observer)角色:
    为所有的具体观察者定义一个接口,在得到主题的通知时更新自己,这个接口叫做更新接口。
  • 具体观察者(ConcreteObserver)角色:
    存储与主题的状态自恰的状态。具体观察者角色实现抽象观察者角色所要求的更新接口,以便使本身的状态与主题的状态 像协调。如果需要,具体观察者角色可以保持一个指向具体主题对象的引用。

注意,NIO中选择器虽然采用了观察者模式,但是和原始的定义还是有一定出入的,在原始的观察者模式中,主体是被观察对象,他会注册一系列的观察对象,告知其自身状态。而在NIO中,主体是选择器(观察者),被观察对象有很多个,并且选择器在轮询完后会将以及进行IO操作的Socket的Key删除,以防止下次再次被轮询到。虽然有出入,但是其思想还是相同的。