网络通信的基本模型是 Client/Server 模型,也就是两个进程间相互通信,通过三次握手建立连接,连接成功就可以通过 Socket 进行通信。
BIO通信模型
采用 BIO 通信模型的服务端 Server,通常由一个 Acceptor 线程负责监听客户端 Clinet 的连接,它接收到客户端连接请求后为每个客户端创建一个新的线程进行链路处理。处理完后,通过输出流返回给客户端。
问题:该模型缺乏弹性伸缩的能力,服务端的线程数和客户端的并发访问数呈 1:1 的关系。线程是Java虚拟机非常宝贵的系统资源,随着并发数增大,系统会发生线程堆栈溢出、创建新线程失败等问题。
伪异步I/O模型
为了解决 BIO同步阻塞模型面临的一个链路需要一个线程处理的问题,进行了优化:服务端通过一个线程池来处理多个客户端的请求接入。通过线程池可以灵活地调配线程资源,防止由于海量并发接入导致线程耗尽。
采用线程池和任务队列可以实现一种叫做伪异步I/O通信框架。
伪异步I/O通信框架虽然避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题,但是它底层的通信依然采用同步阻塞模型。
读写操作仍然是同步阻塞的,阻塞时间取决于I/O线程的处理速度和网络I/O的传输速度。
伪异步I/O模型的所有可用线程都被故障服务器阻塞,那后续的所有I/O消息都将在队列中排队,如果队列积满则后续入队列操作将会阻塞,还会造成客户端发生大量的连接超时。
NIO模型
与Socket
和ServerSocket
相对应,NIO (JDK 1.4) 也提供了 SocketChannel
和ServerSocketChannel
两种不同的套接字通道实现。这两种通道都支持阻塞和非阻塞模式。
严格的按照Linux/Unix网络编程模型,NIO模型实际上只能被称为非阻塞I/O,不能叫异步非阻塞I/O。
缓冲区 Buffer
在面向流的 I/O 中,可以将数据直接写入或读到 Stream 对象中。
在 NIO 中,所有数据都是用缓冲区处理的(读入或写出),任何时候访问 NIO 中的数据,都是通过缓冲区进行操作的。例如:ByteBuffer
、CharBuffer
、ShortBuffer
、IntBuffer
、LongBuffer
、FloatBuffer
、DoubleBuffer
。
通道 Channel
通道与流的不同之处在于通道是双向的,流只是在一个方向上移动(InputStream或者OutputStream的子类),而通道可以用于读、写或者同时进行。
Channle
可以分为两大类:用于网络读写的SelectableChannel
和用于文件操作的FileChannel
。
多路复用器(Selector)
多路复用器:提供选择已经就绪的任务的能力。
Selector会不断地轮询注册在其上的Channel,如果某个Channel上发生读或写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪的Channel集合,进行后续的I/O操作。
仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道。对于操作系统来说,线程之间上下文切换的开销很大,而且每个线程都要占用系统的一些资源(如内存)。因此,使用的线程越少越好。
JDK 中使用
epoll
代替传统的select
实现,所以它没有最大连接句柄1024/2048的限制,也就是接入的客户端没有限制。
AIO模型
NIO 2.0 引入了新的异步通道概念,是真正的异步非阻塞I/O,并提供了异步文件通道和异步套接字通道的实现。它不需要通过多路复用器Selector进行轮询通道即可实现异步读写。
异步通道提供两种方式获取操作结果:
- 通过
java.util.concurrent.Future
类来表示异步操作的结果。 - 在执行异步操作的时候传入一个
java.nio.channels
。
CompletionHandler
接口的实现类作为操作完成的回调。
参见:Linux网络I/O模型