NIO三大核心
- 在NIO中有三个核心对象
- 缓冲区(Buffer)
- 选择器(Selector)
- 通道(channel)
什么是缓冲区Buffer?
- 缓冲区实际上是一个容器对象,本质上是一个数组
- 在NIO库中,几乎所有的数据都是使用缓冲区处理的
- 在进行读写操作的时候,首先会经过缓冲区
- 在面向流的IO系统中,所有的数据都是 读写 到Stream对象中
类继承关系
- 在NIO库中,顶层抽象类Buffer定义了缓冲区的规范。它提供了各个基本数据类型对应的buffer
工作原理
- 在缓冲区中有三个重要的属性
- position capacity limit
postiton:指定下一个将要被写入或者读取的元素索引,调用put/get方法时更新,初始化为0
limit :指定剩余数据容量/剩余可存储数据空间
capacity:可以存储在缓冲区的最大数据容量
- 一个简单示例
public class BufferPlay {
public static void main(String[] args) throws Exception {
// 文件IO处理 使用文件输入流读取文件 把数据从磁盘读取到内存—>内核缓冲区->进程缓冲区
FileInputStream fis = new FileInputStream("filePath");
// 创建文件的操作管道
FileChannel channel = fis.getChannel();
// 分配一个大小固定的缓冲区,即一个固定长度的byte数组
ByteBuffer buffer = ByteBuffer.allocate(10);
output("初始化", buffer);
// 进行一次读操作
channel.read(buffer);
output("调用channel.read()", buffer);
// 锁定操作范围
buffer.flip();
output("调用buffer.flip()", buffer);
// 判断有无可读数据
while (buffer.remaining() > 0) {
byte b = buffer.get();
}
output("调用buffer.get()", buffer);
// 复位
buffer.clear();
output("调用buffer.clear()", buffer);
// 关闭管道
channel.close();
}
private static void output(String string, ByteBuffer buffer) {
System.out.println(string + " : ");
// 容量,数组大小
System.out.print("capacity: " + buffer.capacity() + ", ");
// 当前操作数据所在的位置,也可以叫做游标
System.out.print("position: " + buffer.position() + ", ");
// 锁定值,flip,数据操作范围索引只能在position - limit 之间
System.out.println("limit: " + buffer.limit());
System.out.println();
}
}
// 运行结果
初始化 :
capacity: 10, position: 0, limit: 10
调用channel.read() :
capacity: 10, position: 8, limit: 10
调用buffer.flip() :
capacity: 10, position: 0, limit: 8
调用buffer.get() :
capacity: 10, position: 8, limit: 8
调用buffer.clear() :
capacity: 10, position: 0, limit: 10
- 图解
缓冲区分配
public class BufferWrap {
public void myMethod() {
/* 调用 allocate(int i) 方法相当于创建了一个指定大小的数组,并封装为缓冲区对象
* 也可以使用 wrap(byte[] arr) 方法,把一个现有的数组封装为缓冲区对象
*
*/
// 分配指定大小的缓冲区
ByteBuffer allocate = ByteBuffer.allocate(10);
// 定义一个 Byte 数组 容量为10
byte[] arr = new byte[10];
ByteBuffer result = ByteBuffer.wrap(arr);
}
}
缓冲区分片
public class BufferSlice {
/*
* 缓冲区分片:在现有缓冲区上切出一片来作为一个新的缓冲区,即子缓冲区
* 现有缓冲区与字缓冲区在底层数组层面是数据共享的
* 子缓冲区相当于现有缓冲区的一个视图窗口
*/
public static void main(String[] args) {
// 分配一个缓冲区大小为10的数组
ByteBuffer buffer = ByteBuffer.allocate(10);
// 向缓冲区中写入数据 0-9
for (int i = 0; i < buffer.capacity(); ++i) {
buffer.put((byte) i);
}
// 创建子缓冲区 设置字缓冲区的位置 为 原缓冲区位置的 3-6 容量为4
buffer.position(3);
buffer.limit(7);
ByteBuffer slice = buffer.slice();
// 改变子缓冲区的内容
for (int i=0; i<slice.capacity(); ++i) {
byte b = slice.get( i );
b *= 10;
slice.put(i, b);
}
// 重新设置 position 和 limit 的位置,方便读取验证数据
buffer.position(0);
buffer.limit(buffer.capacity());
while (buffer.remaining() > 0) {
System.out.print(buffer.get()+" ");
}
}
}
只读缓冲区
- 只读缓冲区不允许写入数据
// 创建一个缓冲区
ByteBuffer buffer = ByteBuffer.allocate(10);
// 创建一个只读缓冲区-从原有缓冲区基础上复制
ByteBuffer readOnly = buffer.asReadOnlyBuffer();
-
只读缓冲区与原缓冲区共享数据,原缓冲区内容发生变化,只读缓冲区内容随之变化
-
只读缓冲区不能转换为常规缓冲区,修改只读缓冲区的内容会报异常
直接缓冲区
- 为加快IO速度,使用一种特殊方式为其分配内存的缓冲区
- 操作系统在进行IO操作之前,会尝试跳过中间缓冲区,直接将内容分配给直接缓冲区
ByteBuffer buffer = ByteBuffer.allocateDirect();
内存映射
- 内存映射是一种读写文件数据的方法
- 速度优于常规基于流或者通道的IO
- 通过使文件中的数据出现在内存数组中来完成,一般只有文件中实际读写的内容部分才会映射到内存中
public class BufferMapped {
public static void main(String[] args) throws IOException {
RandomAccessFile raf = new RandomAccessFile("filePath", "rw");
FileChannel channel = raf.getChannel();
// 把缓冲区和文件系统进行映射关联
MappedByteBuffer map = channel.map(FileChannel.MapMode.READ_WRITE, 0, 8);
// 修改数据——执行完毕在对应文件中查看
map.put(1, (byte) 100);
map.put(7, (byte) 101);
raf.close();
channel.close();
}
}
选择器Selector
传统的Server/Client
- 传统的Server/Client交互基于TPR (Thread per Request)
- 服务器会为每一个客户端请求建立一个线程,由该线程负责处理一个用户请求
- 请求数量很多的情况下,系统难以承受
NIO模型中采用的方式
- NIO中的非阻塞IO,采用了基于Reactor的工作模式,IO调用不会被阻塞
- 通过注册特定的IO事件,如可读数据到达、新的套接字连接等。发生特定事件时,系统返回通知
- 实现非阻塞IO的核心对象就是选择器Selector
- Selector是注册各种IO事件的地方,对不同事件发生做出相对响应
图片引用自咕泡学院Tom老师的课堂笔记
-
当有事件发生时,可以从Selector中获取相应的SelectionKey
-
从Selectionkey中可以找到事件发生的具体SelectableChannel
-
SelecttableChannel处理完毕,可对key重新进行注册
-
代码示例
public class NioServerSocket {
/**
* selector 选择器
*/
private Selector selector;
/**
* 缓冲区
*/
private ByteBuffer buffer = ByteBuffer.allocate(1024);
private int port = 8080;
public NioServerSocket(int port) {
// 初始化 轮询器
try {
this.port = port;
ServerSocketChannel server = ServerSocketChannel.open();
// socket 服务绑定到目标 IP:PORT 默认为本地IP即 localhost
server.bind(new InetSocketAddress(this.port));
//NIO:BIO的升级版本 兼容BIO NIO模型默认采用阻塞式
// 设置false:非阻塞
server.configureBlocking(false);
// 可以接收请求了
this.selector = Selector.open();
// ON_ACCEPT :设置为可接收请求状态
server.register(this.selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
public void listen() {
System.out.println("listen on "+ this.port + "");
//轮询主线程
try {
while (true) {
// 接收所有的请求 select()方法会阻塞,直到拿到一个key
this.selector.select();
Set<SelectionKey> selectionKeys = this.selector.selectedKeys();
// 不断地迭代
Iterator<SelectionKey> iterator = selectionKeys.iterator();
// 同步体现:每次只能拿到一个key,每次只能处理一种状态
while (iterator.hasNext()) {
// 每一个key代表一种状态
SelectionKey key = iterator.next();
iterator.remove();
process(key);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void process(SelectionKey key) throws IOException {
// 针对每一种 key 的状态给一个反应
if(key.isAcceptable()) {
ServerSocketChannel severChannel = (ServerSocketChannel) key.channel();
SocketChannel channel = severChannel.accept();
channel.configureBlocking(false);
// 当数据准备就绪的时候,将状态改为可读
key = channel.register(this.selector, SelectionKey.OP_READ);
} else if (key.isReadable()){
// key.channel 从多路复用器中拿到客户端的引用
// 多路复用体现在,key.channel 每一次拿到的对象都是同一个引用
SocketChannel channel = (SocketChannel) key.channel();
int length = channel.read(this.buffer);
if (length > 0) {
this.buffer.flip();
String content = new String(this.buffer.array(), 0, length);
key = channel.register(this.selector, SelectionKey.OP_WRITE);
key.attach(content);
System.out.println("读取内容:"+content);
}
} else if (key.isWritable()) {
SocketChannel channel = (SocketChannel) key.channel();
String content = (String) key.attachment();
channel.write(ByteBuffer.wrap(("输出"+content).getBytes()));
channel.close();
}
}
public static void main(String[] args) {
new NioServerSocket(8080).listen();
}
}
通道Channel
- 数据通过Buffer对象来处理
- 数据 和 Buffer对象交互中,通过通道Channel来完成
NIO中Channel结构
- NIO中所有的通道对象都实现了Channel接口
Channel的作用
- 使用NIO读写数据
从 InputStream 中获取 Channel
创建Buffer
将数据通过Channle 读/写 到Buffer