概述
NIO是Java中一种新的IO,API,提供了与传统IO不同的操作,NIO是一种同步非阻塞、基于事件的IO API
核心组件
- Buffer:代表NIO中数据读、写的中转池,无论是收到写数据还是读数据,都会通过Buffer
- Channel:代表数据的源头或目的源,为Buffer提供(写)数据和从Buffer获取(读)数据
- Selector:用于监听一个或多个通道(Channel)的事件,当通道发生事件时,选择器可以对被发生事件的通道进行事件处理,例如像通道被连接、通道有可读/写数据等
关系图
Buffer缓冲区组件
概述:这个缓冲区主要是储存通道所使用(用来读、写)的数据,这个通道的基本接口是Channel,其实现这个接口的实现类有基于各种基本类型的Buffer,例如IntBuffer
重要的属性
- capacity:这个缓冲区的最大大小
- limit:这个缓冲区的可访问到的最大位置,即极限距离,这个值应该小于等于capacity
- position:这个缓冲区当前读/写的位置,例如如果position为5则获取position缓冲区中第5个元素
- mark:一个标记,用于reset()时恢复位置的标记
Channel通道组件
概述:这个组件可以理解为与客户的连接,一个连接就是一个通道,客户通过通道写消息到缓冲区后再将缓冲区(如果客户端有用到缓冲区)数据写入通道,服务器通过通道读消息到缓冲区,再从缓冲区得到具体数据,这就是通道的一个工作流程
通道类型
- FileChannel:用于文件读/写的通道,可以通过FileInputStream、FileOutputStream、RandomAccessFile的getChannel()方法得到可读/写的通道
- DataChannel:通过UDP读写网络数据的通道
- SocketChannel:通过TCP读写网络数据的通道
- ServerSocketChannel:可监听TCP连接,当有TCP客户端请求连接且连接成功时,它对每一个连接都产生一个SocketChannel通道
- 基于通道的写文件案例
import sun.nio.ch.FileChannelImpl;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
public class Demo1 {
public static void main(String[] args) throws Exception {
String filename = "C:\\Users\\Administrator\\Desktop\\file1.txt";
FileOutputStream fileOutputStream = new FileOutputStream(filename);
FileChannel fileChannel = fileOutputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put("杀戮都市:O".getBytes());
byteBuffer.flip();
fileChannel.write(byteBuffer);
fileOutputStream.close();
}
}
- 基于通道的读文件案例
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
public class Demo2 {
public static void main(String[] args) throws Exception {
String filename = "C:\\Users\\Administrator\\Desktop\\file1.txt";
FileInputStream fileInputStream = new FileInputStream(filename);
FileChannel fileChannel = fileInputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
fileChannel.read(byteBuffer);
byteBuffer.flip();
String str = new String(byteBuffer.array(), 0, byteBuffer.limit(), Charset.forName("UTF-8"));
System.out.println(str);
}
}
选择器
概述:异步IO的核心类,允许一个选择器线程管理&处理多个通道,当通道发送事件时会被选择器的select()捕获,此时我们对发生事件的通道进行处理
基于TCP的单线程多用户案例
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Set;
public class Demo3 {
public static void main(String[] args) throws Exception {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 必须设置为非阻塞才能被注册上选择器
Selector selector = Selector.open();
// 将TCP socket绑定在指定端口上
serverSocketChannel.socket().bind(new InetSocketAddress("127.0.0.1", 9000));
serverSocketChannel.configureBlocking(false);
// 将这个TCP通道和有客户准备连接事件绑定并注册在选择器上
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// 循环监听选择器是否有事件发生
while (selector.select(5000) == 0) {
System.out.println("5s 过去了在选择器上注册的通道没有任何一通道有绑定事件发生");
continue;
}
// 得到有事件发生的这些事件
Set<SelectionKey> be_selected_key = selector.selectedKeys();
be_selected_key.forEach((key) -> {
// 迭代这些发生了事件的Key,并对这些事件作出反应
if (key.isAcceptable()) {
try {
// 如果发送事件是客户准备连接的事件,我们就调用服务器accept方法接收连接
SocketChannel connected_client = serverSocketChannel.accept();
// 与客户连接的通道一定也要是非阻塞的
connected_client.configureBlocking(false);
// 得到与连接用户关联的这个通道并注册到选择器中,并绑定读事件,和一个Buffer缓冲区
connected_client.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
System.out.println("与一个客户取得连接");
} catch (IOException e) {
System.err.println("无法与客户进行连接...");
}
}
// 如果发生的是读取事件,也就代表某个客户发送消息到此,准备读数据
if(key.isReadable()){
// 已知我们在这个选择器中注册读事件的全是SocketChannel类型通道,所以直接强转
SocketChannel socket_channel = (SocketChannel) key.channel();
// 对客户发送的消息我们进行回显
// key的attachment()方法返回的是注册时的第三个参数对象,如果没有使用第三个参数,那么应该是NUll
ByteBuffer buffer = (ByteBuffer) key.attachment();
if(buffer == null){
buffer = ByteBuffer.allocate(1024);
buffer.flip(); // 翻转为读模式,因为刚创建默认为写模式
}
try {
// 将通道对应缓冲区初始化
buffer.clear();
socket_channel.read(buffer);
buffer.flip();
// 反转这个buffer,使其作为写模式,且缓冲区capacity、limit初始为position位置,position归0
String message = new String(buffer.array(), 0,buffer.limit());
// 我们将指定缓冲区的内容写入管道中
System.out.println("收到客户端消息... msg:" + message);
buffer = ByteBuffer.allocate(1024);
buffer.put((message + " ---收到你的消息了哦").getBytes("UTF-8"));
buffer.flip(); // 设置limit和capacity为position,且posistion=0
socket_channel.write(buffer); // 发送回显给客户端
}catch (Exception e){
System.err.println("用户主动断开连接");
// 当用户断开连接时我们必须将这个通道从selector中移除
try {
key.channel().close(); // 关闭通道
} catch (IOException ex) {
System.err.println("无法关闭通道...");
}
key.cancel(); // 从selector中移除这个事件key,和对应的通道信息
}
}
/* 移除在已触发事件Key集合中的这个事件Key,移除这个Key不代表清除了Selector与通道的信息,而只是
* 在"已触发事件Key集合中移除Key"以免重复遍历事件,我们可以理解为selectedKeys()每次不会重新拿
* select()也不会重新计数,而这些方法是追加的方式进行的,所以确保每次遍历时selectedKeys()为空
*/
be_selected_key.remove(key);
});
}
}
}