共计 7614 个字符,预计需要花费 20 分钟才能阅读完成。
很多技术框架都应用 NIO 技术,学习和把握 Java NIO 技术对于高性能、高并发网络的利用是十分要害的 @mikechen
NIO 简介
NIO 中的 N 能够了解为 Non-blocking,不单纯是 New,是解决高并发、I/ O 高性能的无效形式。
Java NIO 是 Java1.4 之后推出来的一套 IO 接口,NIO 提供了一种齐全不同的操作形式,NIO 反对面向缓冲区的、基于通道的 IO 操作。
新增了许多用于解决输入输出的类,这些类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写,新增了满足 NIO 的性能。
NIO VS BIO
BIO
BIO 全称是 Blocking IO,同步阻塞式 IO,是 JDK1.4 之前的传统 IO 模型。
Java BIO:服务器实现模式为一个连贯一个线程,即客户端有连贯申请时服务器端就须要启动一个线程进行解决,如下图所示:
尽管此时服务器具备了高并发能力,即可能同时解决多个客户端申请了,然而却带来了一个问题,随着开启的线程数目增多,将会耗费过多的内存资源,导致服务器变慢甚至解体,NIO 能够肯定水平解决这个问题。
NIO
Java NIO:同步非阻塞,服务器实现模式为一个线程解决多个申请(连贯),即客户端发送的连贯申请都会注册到多路复用器上,多路复用器轮询到连贯有 I / O 申请就进行解决。
一个线程中就能够调用多路复用接口(java 中是 select)阻塞同时监听来自多个客户端的 IO 申请,一旦有收到 IO 申请就调用对应函数解决,NIO 善于 1 个线程治理多条连贯,节约系统资源。
NIO 的外围实现
NIO 蕴含 3 个外围的组件:
- Channel(通道)
- Buffer(缓冲区)
- Selector(选择器)
关系图的阐明:
- 每个 Channel 对应一个 Buffer。
- Selector 对应一个线程,一个线程对应多个 Channel。
- 该图反馈了有三个 Channel 注册到该 Selector。
- 程序切换到那个 Channel 是由事件决定的(Event)。
- Selector 会依据不同的事件,在各个通道上切换。
- Buffer 就是一个内存块,底层是有一个数组。
- 数据的读取和写入是通过 Buffer,然而须要 flip()切换读写模式,而 BIO 是单向的,要么输出流要么输入流。
Channel(通道)
Channel 是 NIO 的外围概念,它示意一个关上的连贯,这个连贯能够连贯到 I/O 设施(例如:磁盘文件,Socket)或者一个反对 I/O 拜访的应用程序,Java NIO 应用缓冲区和通道来进行数据传输。
通道的次要实现类:
FileChannel 类
本地文件 IO 通道,用于读取、写入、映射和操作文件的通道,应用文件通道操作文件的个别流程为:
1)获取通道
文件通道通过 FileChannel 的静态方法 open() 来获取,获取时须要指定文件门路和文件打开方式。
// 获取文件通道
FileChannel.open(Paths.get(fileName), StandardOpenOption.READ);
2)创立字节缓冲区
文件相干的字节缓冲区有两种,一种是基于堆的 HeapByteBuffer,另一种是基于文件映射,放在堆外内存中的 MappedByteBuffer。
// 调配字节缓存
ByteBuffer buf = ByteBuffer.allocate(10);
3)读写操作
读取数据
个别须要一个循环构造来读取数据,读取数据时须要留神切换 ByteBuffer 的读写模式。
while (channel.read(buf) != -1){ // 读取通道中的数据,并写入到 buf 中
buf.flip(); // 缓存区切换到读模式
while (buf.position() < buf.limit()){ // 读取 buf 中的数据
text.append((char)buf.get());
}
buf.clear(); // 清空 buffer,缓存区切换到写模式}
写入数据
for (int i = 0; i < text.length(); i++) {buf.put((byte)text.charAt(i)); // 填充缓冲区,须要将 2 字节的 char 强转为 1 本人的 byte
if (buf.position() == buf.limit() || i == text.length() - 1) { // 缓存区已满或者曾经遍历到最初一个字符
buf.flip(); // 将缓冲区由写模式置为读模式
channel.write(buf); // 将缓冲区的数据写到通道
buf.clear(); // 清空缓存区,将缓冲区置为写模式,下次能力应用}
}
4)将数据刷出到物理磁盘,FileChannel 的 force(boolean metaData) 办法能够确保对文件的操作可能更新到磁盘。
channel.force(false);
5)敞开通道
channel.close();
SocketChannel 类
网络套接字 IO 通道,TCP 协定,针对面向流的连贯套接字的可抉择通道(个别用在客户端)。
TCP 客户端应用 SocketChannel 与服务端进行交互的流程为:
1)关上通道,连贯到服务端。
SocketChannel channel = SocketChannel.open(); // 关上通道,此时还没有关上 TCP 连贯
channel.connect(new InetSocketAddress("localhost", 9090)); // 连贯到服务端
2)调配缓冲区
ByteBuffer buf = ByteBuffer.allocate(10); // 调配一个 10 字节的缓冲区,不实用,容量太小
3)配置是否为阻塞形式。(默认为阻塞形式)
channel.configureBlocking(false); // 配置通道为非阻塞模式
4)与服务端进行数据交互
5)敞开连贯
channel.close(); // 敞开通道
ServerSocketChannel 类
网络通信 IO 操作,TCP 协定,针对面向流的监听套接字的可抉择通道(个别用于服务端),流程如下:
1)关上一个 ServerSocketChannel 通道, 绑定端口。
ServerSocketChannel server = ServerSocketChannel.open(); // 关上通道
2)绑定端口
server.bind(new InetSocketAddress(9090)); // 绑定端口
3)阻塞期待连贯到来,有新连贯时会创立一个 SocketChannel 通道,服务端能够通过这个通道与连贯过去的客户端进行通信。期待连贯到来的代码个别放在一个循环构造中。
SocketChannel client = server.accept(); // 阻塞,直到有连贯过去
4)通过 SocketChannel 与客户端进行数据交互
5)敞开 SocketChannel
client.close();
Buffer(缓冲区)
缓冲区 Buffer 是 Java NIO 中一个外围概念,在 NIO 库中,所有数据都是用缓冲区解决的。
在读取数据时,它是间接读到缓冲区中的, 在写入数据时,它也是写入到缓冲区中的, 任何时候拜访 NIO 中的数据,都是将它放到缓冲区中。
而在面向流 I / O 零碎中,所有数据都是间接写入或者间接将数据读取到 Stream 对象中。
Buffer 数据类型
从类图中能够看到,7 种数据类型对应着 7 种子类,这些名字是 Heap 结尾子类,数据是寄存在 JVM 堆中的。
MappedByteBuffer
而 MappedByteBuffer 则是寄存在堆外的间接内存中,能够映射到文件。
通过 java.nio 包和 MappedByteBuffer 容许 Java 程序间接从内存中读取文件内容,通过将整个或局部文件映射到内存,由操作系统来解决加载申请和写入文件,利用只须要和内存打交道,这使得 IO 操作十分快。
Mmap 内存映射和一般规范 IO 操作的本质区别在于它并不需要将文件中的数据先拷贝至 OS 的内核 IO 缓冲区,而是能够间接将用户过程公有地址空间中的一块区域与文件对象建设映射关系,这样程序就如同能够间接从内存中实现对文件读 / 写操作一样。
只有当缺页中断产生时,间接将文件从磁盘拷贝至用户态的过程空间内,只进行了一次数据拷贝,对于容量较大的文件来说(文件大小个别须要限度在 1.5~2G 以下),采纳 Mmap 的形式其读 / 写的效率和性能都十分高,大家熟知的 RocketMQ 就应用了该技术。
Buffer 数据流程
应用程序能够通过与 I/O 设施建设通道来实现对 I/O 设施的读写操作,操作的数据通过缓冲区 Buffer 来进行交互。
从 I/O 设施读取数据时:
1)应用程序调用通道 Channel 的 read() 办法;
2)通道往缓冲区 Buffer 中填入 I/O 设施中的数据,填充实现之后返回;
3)应用程序从缓冲区 Buffer 中获取数据。
往 I/O 设施写数据时:\
1)应用程序往缓冲区 Buffer 中填入要写到 I/O 设施中的数据;\
2)调用通道 Channel 的 write() 办法,通道将数据传输至 I/O 设施。
缓冲区外围办法
缓冲区存取数据的两个外围办法:
1)put(): 存入数据到缓冲区
- put(byte b):将给定单个字节写入缓冲区的以后地位
- put(byte[] src):将 src 中的字节写入缓冲区的以后地位
- put(int index, byte b):将指定字节写入缓冲区的索引地位(不会挪动 position)
2)get(): 获取缓冲区的数据
- get():读取单个字节
- get(byte[] dst):批量读取多个字节到 dst 中
- get(int index):读取指定索引地位的字节(不会挪动 position)
Selector(选择器)
Selector 类是 NIO 的外围类,Selector(选择器)选择器提供了抉择曾经就绪的工作的能力。
Selector 会一直的轮询注册在下面的所有 channel,如果某个 channel 为读写等事件做好筹备,那么就处于就绪状态,通过 Selector 能够一直轮询发现出就绪的 channel,进行后续的 IO 操作。
一个 Selector 可能同时轮询多个 channel,这样,一个独自的线程就能够治理多个 channel,从而治理多个网络连接,这样就不必为每一个连贯都创立一个线程,同时也防止了多线程之间上下文切换导致的开销。
选择器应用步骤
1 获取选择器
与通道和缓冲区的获取相似,选择器的获取也是通过动态工厂办法 open() 来失去的。
Selector selector = Selector.open(); // 获取一个选择器实例
2 获取可抉择通道
可能被选择器监控的通道必须实现了 SelectableChannel 接口,并且须要将通道配置成非阻塞模式,否则后续的注册步骤会抛出 IllegalBlockingModeException。
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 9090)); // 关上 SocketChannel 并连贯到本机 9090 端口
socketChannel.configureBlocking(false); // 配置通道为非阻塞模式
3 将通道注册到选择器
通道在被指定的选择器监控之前,应该先通知选择器,并且告知监控的事件,即:将通道注册到选择器。
通道的注册通过 SelectableChannel.register(Selector selector, int ops) 来实现,ops 示意关注的事件,如果须要关注该通道的多个 I/O 事件,能够传入这些事件类型或运算之后的后果。这些事件必须是通道所反对的,否则抛出 IllegalArgumentException。
socketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE); // 将套接字通过到注册到选择器,关注 read 和 write 事件
4 轮询 select 就绪事件 \
通过调用选择器的 Selector.select() 办法能够获取就绪事件,该办法会将就绪事件放到一个 SelectionKey 汇合中,而后返回就绪的事件的个数。这个办法映射多路复用 I/O 模型中的 select 零碎调用,它是一个阻塞办法。失常状况下,直到至多有一个就绪事件,或者其它线程调用了以后 Selector 对象的 wakeup() 办法,或者以后线程被中断时返回。
while (selector.select() > 0){ // 轮询,且返回时有就绪事件
Set<SelectionKey> keys = selector.selectedKeys(); // 获取就绪事件汇合
.......
}
有 3 种形式能够 select 就绪事件:
1)select() 阻塞办法,有一个就绪事件,或者其它线程调用了 wakeup() 或者以后线程被中断时返回。
2)select(long timeout) 阻塞办法,有一个就绪事件,或者其它线程调用了 wakeup(),或者以后线程被中断,或者阻塞时长达到了 timeout 时返回。不抛出超时异样。
3)selectNode() 不阻塞,如果无就绪事件,则返回 0;如果有就绪事件,则将就绪事件放到一个汇合,返回就绪事件的数量。
5 解决就绪事件 \
每次能够 select 出一批就绪的事件,所以须要对这些事件进行迭代。
for(SelectionKey key : keys){if(key.isWritable()){ // 可写事件
if("Bye".equals( (line = scanner.nextLine()) )){socketChannel.shutdownOutput();
socketChannel.close();
break;
}
buf.put(line.getBytes());
buf.flip();
socketChannel.write(buf);
buf.compact();}
}
从一个 SelectionKey 对象能够失去:1)就绪事件的对应的通道;2)就绪的事件。通过这些信息,就能够很不便地进行 I/O 操作。
NIO 源码案例
NIOServer
public static void main(String[] args) throws Exception{
// 创立 ServerSocketChannel,-->> ServerSocket
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress(5555);
serverSocketChannel.socket().bind(inetSocketAddress);
serverSocketChannel.configureBlocking(false); // 设置成非阻塞
// 开启 selector, 并注册 accept 事件
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while(true) {selector.select(2000); // 监听所有通道
// 遍历 selectionKeys
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {SelectionKey key = iterator.next();
if(key.isAcceptable()) { // 解决连贯事件
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false); // 设置为非阻塞
System.out.println("client:" + socketChannel.getLocalAddress() + "is connect");
socketChannel.register(selector, SelectionKey.OP_READ); // 注册客户端读取事件到 selector
} else if (key.isReadable()) { // 解决读取事件
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
SocketChannel channel = (SocketChannel) key.channel();
channel.read(byteBuffer);
System.out.println("client:" + channel.getLocalAddress() + "send" + new String(byteBuffer.array()));
}
iterator.remove(); // 事件处理完毕,要记得革除}
}
}
NIOClient
public class NIOClient {public static void main(String[] args) throws Exception{SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 5555);
if(!socketChannel.connect(inetSocketAddress)) {while (!socketChannel.finishConnect()) {System.out.println("客户端正在连接中,请急躁期待");
}
}
ByteBuffer byteBuffer = ByteBuffer.wrap("mikechen 的互联网架构".getBytes());
socketChannel.write(byteBuffer);
socketChannel.close();}
}
以上
作者简介
陈睿 |mikechen,10 年 + 大厂架构教训,《BAT 架构技术 500 期》系列文章作者,分享十余年 BAT 架构教训以及面试心得!
浏览 mikechen 的互联网架构更多技术文章合集
Java 并发 |JVM|MySQL|Spring|Redis| 分布式 | 高并发 | 架构师
关注「mikechen 的互联网架构」公众号,回复 【架构】 支付我原创的《300 期 + BAT 架构技术系列与 1000 + 大厂面试题答案》