1. NIO简介
NIO:Non-blocking I/O 或 New I/O
- 从特性出发:非阻塞IO
- 从年龄出发:新IO,在原始IO之后出现的
诞生:从 JDK 1.4 引入的全新的输入输出标准库,重新设计的一套IO标准
职务:高并发网络服务器支持岗。作为原始IO的补充,为了应对高性能高并发的应用场景。
2. 编程模型
模型:对事物共性的抽象(个人理解:用一个统一的方法解决一系列相似的问题)
编程模型:对编程共性的抽象(个人理解:用一个统一的编程方法解决一系列相似的问题)
2.1 BIO网络模型
BIO:Blocking I/O,阻塞IO。
传统的 BIO 通过socket.read()读取服务端的数据,如果TCP RecvBuffer里没有数据,函数会一直阻塞,直到收到数据,返回读到的数据。
当有很多客户端访问服务端,但客户端未发送数据,服务端线程处于线程阻塞,等待客户端响应,这样可能会导致服务端崩溃。
在大量并发的情况下,接入的客户端过多,会出现问题。
BIO网络模型的缺点:
- 阻塞式I/O模型。(服务端必须等待客户端的再次请求)
- 弹性伸缩能力差。(客户端与服务端的线程数时 1:1 的关系)
- 多线程耗资源(线程创建、销毁,或大量线程处于服务器端都会使得服务端的CPU调度资源受到影响)
2.2 NIO网络模型
selector:负责管理与客户端建立的多个连接,负责监听注册到上面的一些事件,如有新连接接入、当前连接上有可读消息或可写消息。一旦事件被其监听到,就会调用对应的事件处理器来完成对事件的响应。
主要功能:接收所有与客户端socket的连接,并且监听它们关心的事件,当这个事件发生之后,就会调用相应的事件处理器来处理这个事件。
2.3 NIO网络模型改进
- 非阻塞式I/O模型。(服务器端提供一个单线程的Selector来统一管理客户端socket接入的所有连接,并负责每个连接所关心的事件)
- 弹性伸缩能力强。(服务器端不再是通过多个线程来处理请求,而是通过一个线程来处理所有请求,对应关系变为 N:1)
- 单线程节省资源。(线程频繁创建、销毁,线程之间的上下文切换等带来的资源消耗得要缓解)
3. NIO网络编程详解
NIO核心类:
- Channel:通道
- Buffer:缓冲区
- Selector:选择器 或 多路复用器
3.1 Channel简介
3.1.1 Channe特性
(1)双向性
Channel是信息传输的通道,是 JDK NIO中对输入输出方式的另一种抽象,可以类比BIO中的流的概念。但与流不同的是,流是单向传输,有InputStream()、OutputStream(),而Channel是双向传输,即可读,又可写。
(2)非阻塞性
Channel可以工作在非阻塞模式下,正是由于这个特性,构成了NIO的基础。
(3)操作唯一性
操作Channel的唯一方式是使用Buffer,通过Buffer操作Channel实现数据块的读写。Buffer本质上就是一个字节数组。
3.1.2 Channel核心实现类
- 文件类:FileChannel
- UDP类:DatagramChannel
- TCP类:ServerSocketChannel / SocketChannel
Socket回顾:
/********** 服务端 **********/
// 1. 监听端口
ServerSocket serverSocket = new ServerSocket(8000);
while (true) {
// 2. 接收请求,建立连接
Socket socket = serverSocket.accept();
// 3. 数据交换
new Thread(new BIOServerHandler(socket)).start();
}
// 4. 关闭资源
serverSocket.close();
/********** 客户端 **********/
// 1. 建立连接
Socket socket = new Socket("127.0.0.1", 8000);
// 2. 获取输入输出流
InputStream inputStream = socket.getInputStream();
OutputStream outStream = socket.getOutputStream();
Channel使用:
// 代码片段 1:服务器端通过服务器socket创建channel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 代码片段 2:服务器端绑定端口
serverSocketChannel.bind(new InetSocketAddress(8000));
// 代码片段 3:服务器端监听客户端连接,建立socketChannel连接
SocketChannel socketChannel = serverSocketChannel.accept();
// 代买片段 4:客户端连接远程主机及端口
SocketChannel socketChannel = SocketChannel.open(
new InetSocketAddress("127.0.0.1", 8000))
3.2 Buffer简介
提供唯一与Channel进行交互的方式。
- 作用:读写Channel中的数据
- 本质:一块可以从中读取或写入的内存区域。被NIO包装成一个NIO Buffer对象,并提供一组便于操作内存的方法。
3.2.1 Buffer属性
(1)Capacity:容量。
标明数组可以容纳的最大字节长度,若超出容量,则需要将其清空后才能重新写入。
(2)Position:位置
写模式:表示当前的位置,初始位置为0,最大值为 Capacity-1
读模式:重置为0
(3)Limit:上限
写模式:最多能往Buffer中写的数据数量,此时等于Capacity
读模式:此时等于写模式下的Position值。
(4)Mark:标记
标记一个特定Position位置,可通过Buffer的Reset()方法恢复到标记位置。
3.2.2 Buffer使用
// 初始化长度为10的byte类型Buffer
ByteBuffer.allocate(10);
// 向byteBuffer中写入三个字节
byteBuffer.put("abc".getBytes(Charset.forName("UTF-8")));
// 将byteBuffer从写模式切换成读模式
byteBuffer.flip();
// 从byteBuffer中读取一个字节
byteBuffer.get();
// 调用mark方法记录下当前position的位置
byteBuffer.mark();
// 先调用get方法读取下一个字节
byteBuffer.get()
// 再调用reset()方法将position重置到mark位置
byteBuffer.reset()
// 调用clear方法,将所有属性重置
byteBuffer.clear()
3.3 Selector简介
Java NIO中能够检测1到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。由Selector就能通过一个单独的线程来管理多个Channel,从而管理多个网络连接。
3.3.1 Selector使用
// 代码片段 1:创建Selector
Selector selector = Selector.open();
// 代码片段 2:将channel注册到selector上,监听读就绪事件
SelectionKey selectionKey = channel.register(selector, SelectionKey.OP_READ);
// 代码片段 3:阻塞等待channel的就绪事件发生
int selectNum = selector.select();
// 代码片段 4:获取发生就绪事件的channel集合
Set<SelectionKey> selectedKeys = selector.selectedKeys();
3.3.2 SelectionKey简介
(1)四种就绪状态常量
SelectionKey常量提供了四个可监听事件的静态常量值。
- OP_CONNECT:连接就绪。连接操作,Client端支持的一种操作
- OP_ACCEPT:接收就绪。连接可接受操作,仅ServerSocketChannel支持
- OP_READ:读就绪。
- OP_WRITE:写就绪。
(2)可以获取有价值的属性
在调用Selector对象的SelectedKey方法时,会返回一个SelectionKey的集合。可以通过这个SelectionKey的集合获取当前的Channel、当前Selector的对象、Channel已就绪事件集合和所关心事件集合。