Java – 什么是NIO
1. Java NIO 基本介绍
- Java NIO 全称 java non-blocking IO,是指 JDK 提供的新 API。从 JDK1.4 开始,Java 提供了一系列改进的输入/输出的新特性,被统称为 NIO(即 New IO),是同步非阻塞的。
- NIO 相关类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写。
- NIO 有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器)。
- NIO 是 面向缓冲区 ,或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络。
- Java NIO 的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
2. NIO 和 BIO 的比较
- BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多。
- BIO 是阻塞的,NIO 则是非阻塞的。
- BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。
3. NIO 三大核心示意图
- 每一个Channel都会对应一个Buffer,并且Channel是双向的。
- Selector对应一个线程,一个线程对应多个Channel。
- Selector 会根据不同的事件,在各个通道上切换。
- 数据的读取写入是通过 Buffer,Buffer 就是一个内存块 ,底层是有一个数组。
4. 缓冲区Buffer
4.1 基本介绍
缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer完成。
4.2 Buffer常用类
在NIO中,Buffer是一个抽象的父类,一般我们使用其子类来完成读写操作,其子类有:
- ByteBuffer:存储字节数据到缓冲区
- ShortBuffer:存储字符串数据到缓冲区
- CharBuffer:存储字符数据到缓冲区
- IntBuffer:存储整数数据到缓冲区
- LongBuffer:存储长整数数据到缓冲区
- DoubleBuffer:存储小数数据到缓冲区
- FloatBuffer:存储小鼠数据到缓冲区
4.3 Buffer四大属性
属性 | 描述 |
---|---|
Capacity | 容量,即可以容纳的最大数据量。在缓冲区创建时被指定,不能更改。 |
Limit | 相当于一个索引,只能对索引之前的数据进行读写操作,之后的数据无法操作。 |
Position | 相当于一个索引,表示从该位置起进行读写操作,并往后移,但不能超过Limit。 |
Mark | 标记,一般无需关注。 |
5. 通道Channel
5.1 基本介绍
1)NIO的通道类似于流,但与流有所区别:
- 通道可以同时进行读写,而流只能读或者只能写
- 通道可以实现异步读写数据
- 通道可以从缓冲读数据,也可以写数据到缓冲
2)BIO 中的 stream 是单向的,例如 FileInputStream 对象只能进行读取数据的操作,而 NIO 中的通道(Channel)是双向的,可以读操作,也可以写操作。
3)Channel 在 NIO 中是一个接口
public interface Channel extends Closeable{}
5.2 常用的Channel类有:
- FileChannel:用于文件的数据读写。
- DatagramChannel :用于UDP的数据读写。
- ServerSocketChannel :用于TCP的数据读写。
- SocketChannel: 用于TCP的数据读写。
ServerSocketChannel 和 SocketChannel类似于 ServerSocket 和 Socket的关系。
6. Selector选择器
6.1 基本介绍
- Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到 Selector(选择器)。
- Selector 能够检测多个注册的通道上是否有事件发生(注意:多个 Channel 以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
- 只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。
- 避免了多线程之间的上下文切换导致的开销。
6.2 Selector类相关方法
public static Selector open(); // 得到一个选择器对象
public int select(long timeout); // 监控所有注册的通道,当其中有IO操作可以进行时,将对应的SelectionKey加入到内部集合中并发回,参数用来设置超时时间。
public Set<SelectionKey> selectedKeys(); // 从内部集合中得到所有的SelectionKey
7. NIO 非阻塞 原理和步骤
- 当客户端连接时,会通过 ServerSocketChannel 得到 SocketChannel。
- Selector进行监听select方法,返回有事件发生的通道的个数。
- 将SocketChannel注册到Selector上,一个Selector可以注册多个SocketChannel。
- 注册后返回一个 SelectionKey, 会和该 Selector 关联(集合)。
- 进一步得到各个 SelectionKey (有事件发生)。
- 在通过 SelectionKey 反向获取 SocketChannel , 方法 channel()。
- 可以通过得到的 channel , 完成业务处理。
服务端代码:
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.util.Iterator;
import java.util.Set;
public class NIOServer {
public static void main(String[] args) throws Exception {
//创建 ServerSocketChannel -> ServerSocket
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//得到一个 Selecor 对象
Selector selector = Selector.open();
//绑定一个端口 6666, 在服务器端监听
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
//把 serverSocketChannel 注册到 selector 关心 事件为 OP_ACCEPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//循环等待客户端连接
while (true) {
//这里我们等待 1 秒,如果没有事件发生, 返回
if (selector.select(1000) == 0) { //没有事件发生
System.out.println("服务器等待了 1 秒,无连接");
continue;
}
//如果返回的>0, 就获取到相关的 selectionKey 集合
//1.如果返回的>0, 表示已经获取到关注的事件
//2. selector.selectedKeys() 返回关注事件的集合
// 通过 selectionKeys 反向获取通道
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//遍历 Set<SelectionKey>, 使用迭代器遍历
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()) {
//获取到 SelectionKey
SelectionKey key = keyIterator.next();
//根据 key 对应的通道发生的事件做相应处理
if (key.isAcceptable()) { //如果是 OP_ACCEPT, 有新的客户端连接
//该该客户端生成一个 SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println(" 客 户 端 连 接 成 功 生 成 了 一 个 socketChannel " +
socketChannel.hashCode());
//将 SocketChannel 设置为非阻塞
socketChannel.configureBlocking(false);
//将 socketChannel 注册到 selector, 关注事件为 OP_READ, 同时给 socketChannel关联一个 Buffer
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if (key.isReadable()) { //发生 OP_READ
//通过 key 反向获取到对应 channel
SocketChannel channel = (SocketChannel) key.channel();
//获取到该 channel 关联的 buffer
ByteBuffer buffer = (ByteBuffer) key.attachment();
channel.read(buffer);
System.out.println("form 客户端 " + new String(buffer.array()));
}
//手动从集合中移动当前的 selectionKey, 防止重复操作
keyIterator.remove();
}
}
}
}
客户端代码:
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NIOClient {
public static void main(String[] args) throws Exception{
//得到一个网络通道
SocketChannel socketChannel = SocketChannel.open();
//设置非阻塞
socketChannel.configureBlocking(false);
//提供服务器端的 ip 和 端口
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
//连接服务器
if (!socketChannel.connect(inetSocketAddress)) {
while (!socketChannel.finishConnect()) {
System.out.println("因为连接需要时间,客户端不会阻塞,可以做其它工作..");
}
}
//...如果连接成功,就发送数据
String str = "hello, 尚硅谷~";
//Wraps a byte array into a buffer
ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
//发送数据,将 buffer 数据写入 channel
socketChannel.write(buffer);
System.in.read();
}
}
8. 使用NIO实现多人聊天群
要求:实现多人聊天,即一个客户端发送消息,其它连接服务端的客户端能够收到消息。
服务端代码:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
public class GroupChatServer {
// 选择器
private Selector selector;
// 通道
private ServerSocketChannel serverSocketChannel;
// 端口
private static final int PORT = 3000;
// 构造器, 初始化
public GroupChatServer() {
try {
// 得到选择器
selector = Selector.open();
// 得到通道
serverSocketChannel = ServerSocketChannel.open();
// 绑定端口
serverSocketChannel.socket().bind(new InetSocketAddress(PORT));
// 设置非阻塞模式
serverSocketChannel.configureBlocking(false);
// 将通道注册到选择器上
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
// 监听
public void listen() {
// 循环
while (true) {
try {
int count = selector.select(1_000);
// 有事件处理
if (count > 0) {
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
// 去出SelectionKey
SelectionKey key = iterator.next();
// 监听到客户端请求连接
if (key.isAcceptable()) {
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false); // 设为非阻塞
// 将通道注册到选择器
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println(socketChannel.getRemoteAddress() + "上线");
} else if (key.isReadable()) { // 通道发送 read 事件,即通道是可读的状态
// 处理读消息
readData(key);
}
// 删除当前SelectionKey, 防止重读处理
iterator.remove();
}
} else {
System.out.println("等待中----");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 处理客户端发来的消息
private void readData(SelectionKey key) {
// 获取到关联的通道
SocketChannel socketChannel = (SocketChannel) key.channel();
// 创建buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
try {
// 将通道里的数据写入到buffer当中
int read = socketChannel.read(byteBuffer);
if (read > 0) {
// 把缓存区的数据转成字符串
String msg = new String(byteBuffer.array());
// 打印消息
System.out.println("客户端发送消息:" + msg);
// 向其他的客户端转发消息
sendInfoToOtherClients(msg, socketChannel);
}
} catch (IOException e) {
// 取消注册
key.cancel();
// 关闭通道
try {
socketChannel.close();
} catch (IOException ioException) {
ioException.printStackTrace();
}
e.printStackTrace();
}
}
// 转发消息给其它客户(通道)
private void sendInfoToOtherClients(String msg, SocketChannel self) {
// 遍历 所有注册到 selector 上的 SocketChannel,并排除 自身通道
for (SelectionKey key : selector.keys()) {
// 通过 key 取出对应的 SocketChannel
Channel targetChannel = key.channel();
// 排除自己
if (targetChannel instanceof SocketChannel && targetChannel != self) {
// 转型
SocketChannel dest = (SocketChannel) targetChannel;
// 将 msg 存储到 buffer
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
// 将 buffer 的数据写入 通道
try {
dest.write(buffer);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
// 创建服务器对象
GroupChatServer chatServer = new GroupChatServer();
// 启动监听
chatServer.listen();
}
}
客户端代码:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;
public class GroupChatClient {
// 定义相关的属性
private final String HOST = "127.0.0.1"; // 服务器的 ip
private final int PORT = 3000; // 服务器端口
private Selector selector;
private SocketChannel socketChannel;
private String username;
// 构造器, 完成初始化工作
public GroupChatClient() {
try {
selector = Selector.open();
// 连接服务器
socketChannel = SocketChannel.open(new InetSocketAddress(HOST, PORT));
// 设置非阻塞
socketChannel.configureBlocking(false);
// 将 channel 注册到 selector
socketChannel.register(selector, SelectionKey.OP_READ);
// 得到 username
username = socketChannel.getLocalAddress().toString().substring(1);
System.out.println(username + " is ok...");
} catch (Exception e) {
e.printStackTrace();
}
}
// 向服务器发送消息
public void sendInfo(String info) {
info = username + " 说:" + info;
try {
socketChannel.write(ByteBuffer.wrap(info.getBytes()));
} catch (IOException e) {
e.printStackTrace();
}
}
// 读取从服务器端回复的消息
public void readInfo() {
try {
int readChannels = selector.select();
if (readChannels > 0) {// 有可以用的通道
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isReadable()) {
// 得到相关的通道
SocketChannel sc = (SocketChannel) key.channel();
// 得到一个 Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 读取
sc.read(buffer);
// 把读到的缓冲区的数据转成字符串
String msg = new String(buffer.array());
System.out.println(msg.trim());
}
}
iterator.remove(); // 删除当前的 selectionKey, 防止重复操作
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
// 启动我们客户端
GroupChatClient chatClient = new GroupChatClient();
// 启动一个线程, 每个 3 秒,读取从服务器发送数据
new Thread(() -> {
while (true) {
chatClient.readInfo();
try {
Thread.currentThread().sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
// 发送数据给服务器端
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String s = scanner.nextLine();
chatClient.sendInfo(s);
}
}
}