继上一篇文章《网络编程之IO与NIO阻塞分析》的讲解,已经知道了网络编程的基本方式,今天将继续进行网络编程相关概念的深入讲解。
1.基本概念
1.1 IO(BIO)
Blocking IO,同步阻塞式IO(传统的网络编程模型),完全依靠网络,具有以下特点:
- Client/Server模型,两个进程直接进行相互通信,其中服务端提供配置信息(绑定的IP和端口),客户端通过连接操作向服务端监听的地址发送请求,通过三次握手建立连接。连接成功,双方就可以进行通信
- 采用线程池和任务队列可以实现伪异步的IO通信框架
- 传统IO缺点:占用资源多,耗时长
1.2 NIO
Non-Blocking IO 同步非阻塞,多了一个管道的概念,以空间换时间的概念。
- 1.0版本:只实现非阻塞,未实现异步
- 2.0版本:JDK1.7之后异步非阻塞(AIO)
- NIO与BIO一个最大的区别就是,NIO中的selector采用了epoll代替了传统的select实现,性能大大提升
1.2 AIO
NIO2.0,异步+非阻塞 (JDK1.7以后实现)
1.3 Socket
套接字,应用程序间的TCP通信通常通过Socket向网络发出请求或者应答网络请求
- Socket和ServerSocket为java.net包中,ServerSocket用于服务端
- Socket是在建立网络连接时候用的。在连接成功时,应用程序的两端(客户端和服务端)都会产生一个Socket实例,操作这个实例来完成所需的会话。
- 对于一个网络连接来说,Socket和ServerSocket是平等的,不管是在服务端还是在客户端,他们的工作都是通过SocketImpl类及其子类完成的。
2.阻塞和非阻塞
程序在等待调用结果(消息,返回值等)时的状态(具体的技术,接收数据的方式、状态),针对网络传输而言。
- 阻塞:调用结果返回前,当前的线程会被挂起,直到得到结果之后才会返回。(也就是说,应用程序在获取网络数据的时候,如果网络传输数据的时候很慢,那么程序就一直等着,知道传输完毕为止)
- 非阻塞:在不能立刻得到结果之前,该调用不会阻塞当前线程。(应用程序可以直接获取已经准备就绪好的数据,无需等待)
可参见上一篇文章《网络编程之IO与NIO阻塞分析》的详细讲解。
3.同步和异步
关注的是消息通信机制(synchronous communication/asynchronous communication),一般是面向操作系统与应用程序对IO操作的层面上来区分(server端应用程序的执行方式),针对程序层面而言。
- 同步:应用程序发出了一个“调用”时,在没有得到结果之前,该“调用”就不会返回。直到调用返回。(应用程序会直接参与IO读写操作,并且我们的应用程序会直接阻塞到某个方法上,直到数据准备好了,或者采用轮询的策略检查数据的就绪状态,如果就绪则获取数据)
- 异步:一个异步过程调用发出之后,调用者不会立即得到结果,而是“被调用者”通过状态、通知等来通知调用者,或者通过回调函数来通知应用程序。(所有的IO读写操作交给操作系统处理,与我们的应用程序没有直接关联,我们的程序不关心IO读写,当操作系统完成了IO读写操作时,会给我们的应用程序发送通知,我们的应用程序直接拿走数据即可)
典型的异步编程模型Node.js
4.Socket的连接过程
Socket的连接过程分为四个步骤:服务器监听、客户端请求服务器、服务器确认、进行通信。
- 服务器监听:服务器端的套接字并不定位具体的客户端套接字,而是处理等待连接的状态(阻塞),实时监控网络状态
- 客户端请求:客户端提示网络请求,连接的目标就是服务端的Socket。所以,客户端要连接的Socket必须首先描述出它要连接的服务器的Socket,指出服务器Socket的地址和端口,然后才向服务器提示网络连接的请求
- 服务端连接确认:服务端套接字监听到客户端的连接请求,就会响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的情况发送给客户端
- 客户端连接确认:一旦客户端确认了服务端的描述,连接就建立好了。双方就可以开始通信了。并且服务器端的套接字进行处于监听状态,继续监听其他的客户端套接字的请求
5.NIO详解
一般称作Non-Blocking IO,同步非阻塞,在传统的TCP直接建立连接的基础,做了一层抽象,把客户端(SocketChannel)和服务端(ServerSocketChannel )注册到多路复用器上,然后Selector回去轮询所有注册到服务器上的SocketChannel的通道,根据通道的状态(connect/连接、Accept/阻塞、Read/可读、Write/可写)执行相关操作。(概念好理解,编程不好实现)。
这边涉及几个核心概念:Buffer(缓冲区,填充数据的结构)、Channel(管道)、Selector(选择器,多路复用器),如下图所示:
5.1 Buffer
Buffer是一个包含一些要写入或者读取的数据的对象,在NIO类库中加入Buffer对象,体检了新库与原IO的一个重要的区别:
- 在面向流的IO中,可以直接将数据写入或者读取到Stream对象中
- 在NIO库中,所有的数据都用缓冲区处理读写操作
- 缓冲区本质上是一个数组,通常是一个字节数组(ByteBuffer),为缓冲区提供了数据访问的读写操作,如位置、容量、上限等概念
5.2 Channel
网络数据通过Channel进行读写操作,通道与流不同之处在于,通道是双向的(可以用于读写或者二者同时进行,最关键的可以与Selector结合起来),而流是单方向的(一个流必须是InputStream或者OutputStream的子类)。通道分为两大类
- 网络读写(SelectableChannel),子类:
- SocketChannel(客户端)
- ServerSocketChannel(服务端)
- 文件操作(FileChannel)
5.3 Selector
NIO编程的基础,核心。提供了已经选择就绪的任务的能力,Selector会不断的轮询注册到其上的Channel,如果某个通道发生了读写操作,这个通道就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以取得就绪的Channel的集合(可能有多个channel),从而进行后续的IO操作
- 一个Selector理论上可以负责无数个Channel通道,因为JDK使用了epoll代替了传统的select实现,获得的连接句柄没有限制。
- Selector线程就类似一个管理者,只不过是轮询哪个管道的数据已经准备好,然后通知CPU去读写数据
- 当Channel注册到Selector后,Selector会分配给Channel一个Key值,Selector是以轮询的方式进行查找注册的Channel,当Channel准备好了,Selector就会识别,通过KEY值找到相应的Channel进行相关的数据处理操作。
- Selector只负责轮询注册到其上的Channel的状态位,不负责具体操作,轮询出来的SocketChannel一定是准备就绪好的数据(缓冲好的数据,非阻塞状态),而在BIO中是一个字节一个字节的传输(阻塞状态)
NIO编程需要注意的问题:TCP拆包粘包的问题(解码的时候才会出现这个问题)
6.AIO(NIO2.0)
异步非阻塞,在NIO的基础上,引入了异步通道的概念,并提供了异步文件和异步套接字通道的实现,从而在真正意义上实现了异步非阻塞。AIO不需要通过Selector注册Channel进行轮询操作即可实现异步读写,从而简化了NIO编程模型。(概念不好理解,编程好实现)
- AsynchronousSocketChannel
- AsynchronousServerSocketChannel
7.IO多路复用的系统调用使用epoll代替了select,主要有以下几方面原因(整理自《netty权威指南》):
- 支持一个进程打开的socket描述符(FD)不受限制(仅受限于操作系统的最大文件句柄数)
- I/O效率不会随着FD数目的增加而线性下降
- 使用mmap加速内核与用户空间的消息传递
- epoll的API更简单