引:可能很多同学一看Java Java 网络IO,心里肯定会觉得这个有什么好讲的,不就是Socket吗,说对也对,因为他讲到了BIO(同步阻塞IO),但是却不知还有NIO(同步非阻塞IO),AIO(异步非阻塞IO)!
项目源码
关于BIO、NIO以及AIO的代码都放在github上了,注释应该很详细了,大家有需要的可以看一下:你不知道的那些Java网络IO(BIO、NIO、AIO)
同步or异步 阻塞or非阻塞
在谈到IO的时候,避免不了听到同步、异步、阻塞、非阻塞这几个名词或者是他们的组合词,我们经常会感到迷惑不解。
同步or异步
同步和异步是相对于IO事件(读写操作)而言的
- 同步:在进行IO操作时,程序不能干别的事情,等着IO事件完成之后才能去做别的事情
- 异步:不关心IO处理操作(因为把IO操作交给操作系统干了),在处理IO的时候,可以去做别的事情,然后等待IO事件处理完成的通知
阻塞or非阻塞
阻塞和非阻塞是相对于数据而言的
- 阻塞:如果数据没有准备好,程序就一直等待,直到数据准备好了才往下执行
- 非阻塞:不管数据有没有准备好,程序都往下进行
这里是自己看到的一个例子,可以结合起来理解:
如果你想吃一份宫保鸡丁盖饭:
同步阻塞:你到饭馆点餐,然后在那等着,还要一边喊:好了没啊!(IO操作[拿饭]没有干好,预想之外的事都不能干,数据[饭]没有好,就一直等着)
同步非阻塞:在饭馆点完餐,就去遛狗了。不过溜一会儿,就回饭馆喊一声:好了没啊!(IO操作[拿饭]没有干好,预想之外的事都不能干,数据[饭]没有好,但是不用一直等着,可以继续干预先安排的事[遛狗],但是需要时不时去问一下饭有没有好)
异步阻塞:遛狗的时候,接到饭馆电话,说饭做好了,让您亲自去拿。(IO操作[拿饭]没有干好,但是我想干啥就干啥,等着通知就好,数据[饭]没有好,就一直等着去拿饭)
异步非阻塞:饭馆打电话说,我们知道您的位置,一会给你送过来,安心遛狗就可以了。(IO操作[拿饭]没有干好,但是我想干啥就干啥,等着通知就好,数据[饭]没有好,但是不用一直等着,可以继续干预先安排的事)
I/O模型
Unix定义了五种I/O模型,如下:
- 阻塞I/O
- 非阻塞I/O
- I/O复用(select、poll、linux 2.6种改进的epoll)
- 信号驱动IO(SIGIO)
- 异步I/O(POSIX的aio_系列函数)
下图是五种I/O模型的比较:
POSIX把I/O操作划分成两类:
- 同步I/O: 同步I/O操作导致请求阻塞,直至操作完成
- 异步I/O: 异步I/O操作不导致请求阻塞
从上面的图中可以看出Unix的前四种I/O模型都是同步I/O, 只有最后一种才是异步I/O。
我们在看看Java中的IO分类:
- 传统的Java BIO (blocking I/O)是Unix I/O模型中的第一种。
- Java NIO中如果不使用select模式,而只把channel配置成nonblocking则是第二种模型。
- Java NIO select实现的是一种多路复用I/O。底层使用epoll或者相应的poll系统调用。
- 第四种模型JDK应该是没有实现。
- Java NIO2增加了对第五种模型的支持,也就是AIO。
BIO网络编程
传统的同步阻塞模型(BIO)开发中,ServerSocket负责绑定IP地址,启动监听端口;Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。
简单的描述一下BIO的服务端通信模型:采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理没处理完成后,通过输出流返回应答给客户端,线程销毁。即典型的一请求一应答通信模型。如下图:
该模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系,Java中的线程也是比较宝贵的系统资源,线程数量快速膨胀后,系统的性能将急剧下降,随着访问量的继续增大,系统最终就死掉了。
为了改进这种一连接一线程的模型,我们可以使用线程池来管理这些线程(需要了解更多请参考前面提供的文章),实现1个或多个线程处理N个客户端的模型(但是底层还是使用的同步阻塞I/O),通常被称为“伪异步I/O模型“。伪异步I/O模型图如下:
我们知道,如果使用CachedThreadPool线程池,其实除了能自动帮我们管理线程(复用),看起来也就像是1:1的客户端:线程数模型,而使用FixedThreadPool我们就有效的控制了线程的最大数量,保证了系统有限的资源的控制,实现了N:M的伪异步I/O模型。但是,正因为限制了线程数量,如果发生大量并发请求,超过最大数量的线程就只能等待,直到线程池中的有空闲的线程可以被复用。而对Socket的输入流就行读取时,会一直阻塞。 所以在读取数据较慢时(比如数据量大、网络传输慢等),大量并发的情况下,其他接入的消息,只能一直等待,这就是最大的弊端。
PS: 源码都在github上。
NIO网络编程
NIO(同步非阻塞IO)提供了与传统BIO模型中的Socket和ServerSocket相对应的SocketChannel和ServerSocketChannel两种不同的套接字通道实现。新增的着两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用NIO的非阻塞模式来开发。
在用NIO进行开发时,我们需要先了解一下NIO的几个核心概念:
缓冲区 Buffer
Buffer是一个对象,包含一些要写入或者读出的数据。在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的;在写入数据时,也是写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。
缓冲区实际上是一个数组,并提供了对数据结构化访问以及维护读写位置等信息。具体的缓存区有这些:ByteBuffe、CharBuffer、 ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。他们实现了相同的接口:Buffer。
通道 Channel
我们对数据的读取和写入要通过Channel,它就像水管一样,是一个通道。通道不同于流的地方就是通道是双向的,可以用于读、写和同时读写操作。底层的操作系统的通道一般都是全双工的,所以全双工的Channel比流能更好的映射底层操作系统的API。
Channel主要分两大类:
- SelectableChannel:用户网络读写
- FileChannel:用于文件操作
网络编程中涉及的ServerSocketChannel和SocketChannel都是SelectableChannel的子类。
多路复用器 Selector
Selector提供选择已经就绪的任务的能力:Selector会不断轮询注册在其上的Channel,如果某个Channel上面发生读或者写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。
一个Selector可以同时轮询多个Channel(所以是非阻塞的),因为JDK使用了epoll()代替传统的select实现,所以没有最大连接句柄1024/2048的限制。所以,只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端。
创建NIO服务端的通信流程
我们可以看下面这种通信序列图:
PS: 源码都在github上。
AIO网络编程
异步的套接字通道是真正的异步非阻塞I/O,对应于UNIX网络编程中的事件驱动I/O(AIO)。他不需要过多的Selector对注册的通道进行轮询即可实现异步读写,从而简化了NIO的编程模型。
AIO(异步非阻塞IO)提供了与传统BIO模型中的Socket和ServerSocket相对应的AsynchronousSocketChannel和AsynchronousServerSocketChannel两种不同的套接字通道实现。
异步的处理
异步无非是通知系统做一件事情。然后忘掉它,自己做其他事情去了。很多时候系统做完某一件事情后需要一些后续的操作。怎么办?这时候就是告诉异步调用如何做后续处理。通常有两种方式:
- 将来式: 当你希望主线程发起异步调用,并轮询等待结果的时候使用将来式;
- 回调式: 常说的异步回调就是它。
PS: AIO的代码主要基于回调式,源码都在github上。
BIO、NIO、AIO适用场景分析:
- BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
- NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
- AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。