1.概述
Java NIO 全称java non-blocking IO ,是指 JDK 提供的新 API。从 JDK1.4 开始,Java 提供了一系列改进的输入/输出新特性,被统称为 NIO(即 New IO),是同步非阻塞的。NIO采用内存映射文件的方式来处理输入输出,NIO将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样访问文件。NIO与原来的IO有同样的作用,但是使用的方式完全不同, NIO支持面向缓冲区的、基于通道的IO操作。 NIO将以更加高效的方式进行文件的读写操作。
2.NIO 三大核心原理示意图
NIO 有三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器)。NIO是面向缓冲区编程。数据读取到一个buffer中(缓冲区),需要时可在缓冲区内前后移动,增加了处理过程中的灵活性,使用它可提供非阻塞式的伸缩性网络。对于非阻塞式的理解:通俗来说就是一个线程可以处理多个操作。
- 每个 channel 都会对应一个 Buffer;
- Selector 对应一个线程, 一个线程对应多个 channel(连接);
- 每个 channel 都注册到 Selector选择器上;
- Selector不断轮询查看Channel上的事件, 根据不同的事件完成不同的操作;
- Buffer 就是一个内存块 , 底层是一个数组,NIO的Buffer是可以读也是可以写的,channel是双向的。
2.1 缓冲区Buffer
缓冲区实际上是一个容器对象,底层是一个数组,在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,它也是写入到缓冲区中的;任何时候访问 NIO 中的数据,都是将它放到缓冲区中。而在面向流I/O系统中,所有数据都是直接写入或者直接将数据读取到Stream对象中。具体看下面这张图就理解了:
上图描述了从一个客户端向服务端发送数据,然后服务端接收数据的过程。客户端发送数据时,必须先将数据存入Buffer中,然后将Buffer中的数据写入通道。服务端这边接收数据必须通过Channel将数据读入到Buffer中,然后再从Buffer中取出数据来处理。
在NIO中,所有的缓冲区类型都继承于抽象类Buffer,最常用的就是ByteBuffer,对于Java中的基本类型,基本都有一个具体Buffer类型与之相对应,它们之间的继承关系如下图所示:
注:可以看到除了Boolean 类型外,其它都有对应的Buffer。
Buffer四个成员变量的说明
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
一般来说,四个属性的关系应该属于:
0 <= mark <= position <= limit <= capacity
属性 | 说明 |
---|---|
capacity | 容量,即可以容纳的最大数据量;在缓冲区创建时设置并且不能改变 |
limit | 上限,缓冲区中当前的数据量 |
position | 位置,缓冲区中下一个要被读或写的元素的索引 |
mark | 调用mark()方法来设置mark=position,再调用reset()可以让position恢复到mark标记的位置,即position=mark |
由于缓存区是读写共存,所以不同的模式下,这两个变量的值也具有不同的意义。
写模式下,所谓写模式就是将缓存区中的内容写入通道(buffer–>channel)。position 代表下一个字节应该被写出去的字节在缓存区中的位置,limit 表示最后一个待写字节在缓存区的位置。
读模式下,所谓读模式就是从通道读取数据到缓存区(channel–>buffers)。position 代表下一个读出来的字节应当存储在缓存区的位置,limit 等于 capacity。
2.2 通道channel
Channel和传统IO中的Stream很相似。虽然相似,但是有很大的区别,主要区别为:通道是双向的,通过一个Channel既可以读,也可以写;而Stream只能进行单向操作,通过一个Stream只能进行读或者写,比如:InputStream只能进行读取操作,OutputStream只能进行写操作。但是通道和流一样都是需要基于物理文件的,而每个流或者通道都通过文件指针操作文件,这里说的「通道是双向的」也是有前提的,那就是通道基于随机访问文件『RandomAccessFile』的可读可写文件指针。
通道是一个对象,通过它可以读取和写入数据,当然了所有数据都通过Buffer对象来处理。我们永远不会将字节直接写入通道中,相反是将数据写入包含一个或者多个字节的缓冲区。同样不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。
比喻:通常把IO比喻成为水流,管道就是水流的通道;把NIO比喻为火车的轨道,然后缓冲区就是上面的火车。
在NIO中,提供了多种通道对象,而所有的通道对象都实现了Channel接口。它们之间的继承关系如下图所示:
Channel(通道)表示到实体如硬件设备、文件、网络套接字或可以执行一个或多个不同I/O操作的程序组件的开放的连接。所有的Channel都不是通过构造器创建的,而是通过传统的节点InputStream、OutputStream的getChannel方法来返回响应的Channel。
Channel中最常用的三个类方法就是map、read和write,其中map方法用于将Channel对应的部分或全部数据映射成ByteBuffer,而read或write方法有一系列的重载形式,这些方法用于从Buffer中读取数据或向Buffer中写入数据。
2.3 选择器Selector
Selector类是NIO的核心类,**Selector能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理。**这样一来,只是用一个单线程就可以管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销。
与Selector有关的一个关键类是SelectionKey,一个SelectionKey表示一个到达的事件,这2个类构成了服务端处理业务的关键逻辑。
Selector选择器可以理解为一个IO事件的监听与查询器,通过选择器,一个线程可以查询多个通道的IO事件的就绪状态。
什么是IO事件?
表示通道某种IO操作已经就绪或者说已经做好了准备。
例如:如果一个新Channel连接建立成功,就会在Server Socket Channel上发生一个IO事件,代表一个新连接一个准备好,这个IO事件叫做“接收就绪”事件。
NIO定义了四个事件:SelectionKey.OP_ACCEPT、SelectionKey.OP_CONNECT、SelectionKey.OP_READ、SelectionKey.OP_WRITE
3.使用案例
3.1服务端
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;
public class Server {
public static void main(String[] args) throws Exception {
//创建Selector对象,管理多个channel
Selector selector = Selector.open();
//创建ServerSocketChannel对象(绑定主机名和端口号)
ServerSocketChannel ssc = ServerSocketChannel.open().bind(new InetSocketAddress("localhost", 8080));
//设置服务为非阻塞(必须配置,否则报错)
ssc.configureBlocking(false);
// 通道可支持的操作:支持新的连接 监听ACCEPT事件
// ServerSocketChannel仅支持接受新连接,因此此方法返回SelectionKey.OP_ACCEPT 。
int ops = ssc.validOps();
//将通道注册到selector 等待连接
SelectionKey selectKy = ssc.register(selector, ops, null);
for (; ; ) {// 无条件的循环
// 检测当前选择器注册通道是否有就绪事件,如果没有就阻塞,有事件,线程才会恢复运行
int noOfKeys = selector.select();
if (noOfKeys <= 0) {
continue;
}
// 获取就绪的事件,即为选择器键集合
Set selectedKeys = selector.selectedKeys();
Iterator itr = selectedKeys.iterator();
while (itr.hasNext()) {
SelectionKey ky = (SelectionKey) itr.next();
//选择键事件为接收就绪事件
if (ky.isAcceptable()) {
//获取客户端连接通道
SocketChannel client = ssc.accept();
//配置客户端为非阻塞
client.configureBlocking(false);
//重点关注:READ事件
client.register(selector, SelectionKey.OP_READ);
// 选择键为READ事件
} else if (ky.isReadable()) {
//获取客户端SocketChannel通道
SocketChannel client = (SocketChannel) ky.channel();
//设置缓冲区大小
ByteBuffer buffer = ByteBuffer.allocate(256);
//客户端读取缓冲区数据
client.read(buffer);
String output = new String(buffer.array()).trim();
System.out.println("接收客户端信息: " + output);
ByteBuffer buffer1 = ByteBuffer.wrap(("服务端时间戳:"+System.currentTimeMillis()).getBytes(StandardCharsets.UTF_8));
//将数据写回缓冲区
client.write(buffer1);
}
itr.remove();// 将选择键清空,防止下次循环时被重复处理
}
}
}
}
3.2 客户端
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
public class Client {
public static void main(String[] args) throws Exception {
// 创建InetSocketAddress对象,绑定主机名和端口号
InetSocketAddress hA = new InetSocketAddress("localhost", 8080);
//获取客户端SocketChannel通道
SocketChannel client = SocketChannel.open(hA);
System.out.println("The Client is sending messages to server...");
for (; ; ) {
//实例化缓冲区对象
ByteBuffer buffer = ByteBuffer.wrap(("客户端时间戳:" + System.currentTimeMillis()).getBytes(StandardCharsets.UTF_8));
//数据写入缓冲区
client.write(buffer);
//清空缓冲区
buffer.clear();
//设置新缓冲区的大小
ByteBuffer buffer1 = ByteBuffer.allocate(256);
//读取新缓冲区数据
client.read(buffer1);
//打印结果到控制台
System.out.println("接收服务器消息:" + new String(buffer1.array(), StandardCharsets.UTF_8).trim());
TimeUnit.SECONDS.sleep(3);
}
}
}