NIO简单介绍
- 三大重要概念:
通道
、缓冲区
、选择器
1. 缓冲区
1.1 简单介绍
-
基本概念
:本质上是一块可以存储数据的内存,被封装成了buffer对象! -
作用
: 主要用于与 NIO 通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的。
1.2 Buffer的详细说明:
常用API
:
ByteBuffer
MappedByteBuffer
(大文件读取)CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
常用方法:
- 1. `allocate()` -- 分配一块缓冲区
- 2. `put()` -- 向缓冲区写数据
- 3. `get()` -- 向缓冲区读数据
- 4. `filp()` -- 将缓冲区从写模式切换到读模式
- 5. `clear()` -- 从读模式切换到写模式,不会清空数据,但后续写数据会覆盖原来的数据,即使有部分数据没有读,也会被遗忘;
- 6. `compact()` -- 从读数据切换到写模式,数据不会被清空,会将所有未读的数据copy到缓冲区头部,后续写数据不会覆盖,
而是在这些数据之后写数据
...
重要属性
:
- 1. `capacity`(容量) -- 表示'Buffer'的最大数据容量。
`注意`: - 缓冲区容量不能为负;
- 创建后不可修改。
- 2. `limit`(限制) --- 第一个不应该读取或写入的数据的索引,即位于 limit 后的数据不可读写。
缓冲区的限制不能为负,并且不能大于其容量。
`注意`: 'capacity':相当于`写模式`的最大容量;
'limit':相当于`读模式`的最大容量。
- 3. `position`(位置)- 下一个要读取或写入的数据的索引。缓冲区的位置不能为负,并且不能大于其限制
- 记录`读写模式`的下一个位置。
- 4. `mark`(标记)和`reset`(重置) --- 标记是一个索引,通过 Buffer 中的 mark() 方法指定 Buffer
中一个特定的 position,之后可以通过调用 reset() 方法恢复到这个 position。
`使用说明(白话文)`:
- 1. 在调用'flip()'方法(`切换成读模式`)之后,'limit'才生效。
- 2. 'mark()':标记当前的'position',当需要'reset()'的时候,就可以恢复到`被标记的位置`。
直接与非直接缓冲区
:
字节缓冲区要么是直接的,要么是非直接的。如果为
直接字节缓冲区
,则 Java 虚拟机会尽最大努力直接在此缓冲区上执行本机 I/O 操作。也就是说,在每次调用基础操作系统的一个本机 I/O 操作之前(或之后),虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。
直接字节缓冲区
:可以通过调用此类的allocateDirect()
工厂方法来创建。此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。
直接字节缓冲区
还可以通过FileChannel
的map()
方法 将文件区域直接映射到内存中来创建。该方法返回MappedByteBuffer
。Java 平台的实现有助于通过 JNI 从本机代码创建直接字节缓冲区。如果以上这些缓冲区中的某个缓冲区实例指的是不可访问的内存区域,则试图访问该区域不会更改该缓冲区的内容,并且将会在访问期间或稍后的某个时间导致抛出不确定的异常。
字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其
isDirect()
方法来确定。提供此方法是为了能够在
性能关键型代码中执行显式缓冲区管理
注意:
- 1. 使用'直接字节缓冲区'时,JVM不能直接操作其中的'内容',如不能打印该内容,
否则,会报错:`java.lang.UnsupportedOperationException`。
1.3 使用案例:
public class BufferDemo01 {
@Test
public void test01() {
// 1. 准备好Buffer -- Buffer的容量
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 2. 准备数据
String str = "6212261001100\n" +
"36042519980\n" +
"1. 基于Java EE技术的学校的问卷调查系统的设计与实现\n" +
"2. 基于B/S架构的工厂物料管理系统的设计与实现";
byteBuffer.put(str.getBytes());
// 理论:str.getBytes().length = position
System.out.println("验证写入的数据大小和position的关系:" + (str.getBytes().length) + " -- " + (byteBuffer.position()));
System.out.println(byteBuffer.limit());
// 3. 测试:开始读模式1
byteBuffer.flip();
byte[] bytes = new byte[20];
// 第一次读取
byteBuffer.get(bytes);
System.out.println("读取结果:" + new String(bytes));
System.out.println(byteBuffer.limit());
System.out.println(byteBuffer.position());
// 第二次读取
byteBuffer.get(bytes);
System.out.println("读取结果:" + new String(bytes));
System.out.println(byteBuffer.limit());
System.out.println(byteBuffer.position());
byteBuffer.clear();
}
@Test
public void test() {
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
String str = "6212261009\n" +
"3604218\n" +
"1. 基于Java EE技术的学校的问卷调查系统的设计与实现\n" +
"2. 基于B/S架构的工厂物料管理系统的设计与实现";
byteBuffer.put(str.getBytes());
int capacity = byteBuffer.capacity();
System.out.println(capacity);
System.out.println(byteBuffer.limit());
System.out.println(byteBuffer.position());
byteBuffer.flip();
// byteBuffer.clear();
System.out.println(capacity);
System.out.println(byteBuffer.limit());
System.out.println(byteBuffer.position());
byte[] array = byteBuffer.array();
String string = new String(array);
System.out.println(string);
}
}
参考网址:https://www.cnblogs.com/tengpan-cn/p/5809273.html
2. 通道
2.1 基本介绍
基本概念
:类似于流,但是可以异步读写数据(流只能同步读写),通道是双向的
,(流是单向的),通道的数据总是要先读到一个buffer 或者 从一个buffer写入,即通道与buffer进行数据交互。
2.2 详细介绍
常用API类型
:
FileChannel
:从文件读写数据DatagramChannel
:能通过UDP读写网络中的数据。SocketChannel
:能通过TCP读写网络中的数据。ServerSocketChannel
:可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel
- 注意:
FileChannel
:比较特殊,它可以与通道进行数据交互, 不能切换到非阻塞模式;- 套接字通道可以切换到非阻塞模式;
常见的获取通道的方式
:
分散读取
和聚集写入
:
分散读取
:是指从Channel
中读取的数据“分散”到多个Buffer
中。
聚集写入
:是指将多个 Buffer 中的数据“聚集”到 Channel。
2.3 FileChannel
详解
transferFrom()
和transferTo()
:
FileChannel
的常用方法:
使用案例
FileChannel
:文件读取和写入操作
public class FileChannelDemo01 {
private String filePath = "D:\\test.txt";
private String filePath2 = "D:\\aaa.txt";
// 通道之间的传输
@Test
public void test3() throws IOException { // 1846ms - 1819ms 1970ms - 2019ms - 1665ms - 1811ms
FileChannel read = null;
FileChannel write = null;
try {
read = FileChannel.open(Paths.get(filePath), StandardOpenOption.READ);
write = FileChannel.open(Paths.get(filePath2)
, StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE);
// read.transferTo(0, read.size(), write);
write.transferFrom(read, 0, read.size());
} catch (Exception e) {
e.printStackTrace();
} finally {
read.close();
write.close();
}
}
// 大文件之间的传输
@Test
public void test2() throws IOException {// 1372ms - 1271ms
FileChannel read = null;
FileChannel write = null;
try {
read = FileChannel.open(Paths.get(filePath), StandardOpenOption.READ);
write = FileChannel.open(Paths.get(filePath2)
, StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE);
MappedByteBuffer readMap = read.map(MapMode.READ_ONLY, 0, read.size());
MappedByteBuffer writeMap = write.map(MapMode.READ_WRITE, 0, read.size());
byte[] dst = new byte[readMap.limit()];
readMap.get(dst);
writeMap.put(dst);
} catch (Exception e) {
e.printStackTrace();
} finally {
read.close();
write.close();
}
// 测试:文件的读取和写入
@Test
public void test1() throws IOException { // 10240ms - 10812ms
File file = new File(filePath);
File file2 = new File(filePath2);
FileInputStream fileInputStream = new FileInputStream(file);
FileOutputStream fileOutputStream = new FileOutputStream(file2);
FileChannel iChannel = fileInputStream.getChannel();
FileChannel oChannel = fileOutputStream.getChannel();
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
while ((iChannel.read(buffer)) != -1) {
buffer.flip();
oChannel.write(buffer);
buffer.clear();
}
iChannel.close();
oChannel.close();
fileInputStream.close();
fileOutputStream.close();
}
}
2.4 阻塞式和非阻塞式SocketChannel
详解
阻塞与非阻塞
传统的 IO 流都是
阻塞式
的。也就是说,当一个线程调用 read() 或 write() 时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务。因此,在完成网络通信进行 IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量客户端时,性能急剧下降。
Java NIO 是
非阻塞模式
的。当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。因此,NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。
示例代码
SocketChannal
:
(阻塞型)
public class SocketChannelDemo01{
// 阻塞型 Socket 通信
// 客户端
@Test
public void clientTest02() throws IOException {
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9988));
FileChannel fileChannel = FileChannel.open(Paths.get(filePathClient), StandardOpenOption.READ);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while (fileChannel.read(byteBuffer) != -1) {
byteBuffer.flip();
socketChannel.write(byteBuffer);
byteBuffer.clear();
}
socketChannel.shutdownOutput();// 关闭输出流【否则,会进入阻塞状态】
int len;
while ((len = socketChannel.read(byteBuffer)) != -1) {
byteBuffer.flip();
System.out.println(new String(byteBuffer.array(), 0, len));
byteBuffer.clear();
}
fileChannel.close();
socketChannel.close();
}
@Test
public void serverSocketTest02() throws IOException {
ServerSocketChannel channel = ServerSocketChannel.open();
channel.bind(new InetSocketAddress(9988));
SocketChannel accept = channel.accept();
System.out.println("收到客户端请求!!!");
FileChannel fileChannel = FileChannel.open(Paths.get(filePathServer)
, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while (accept.read(byteBuffer) != -1) {
byteBuffer.flip();
fileChannel.write(byteBuffer);
byteBuffer.clear();
}
accept.shutdownInput(); // 关闭输入流【否则,会进入阻塞状态】
System.out.println("数据接收完成");
byteBuffer.put("你好,已经数据接收到!!!".getBytes());
byteBuffer.flip();
accept.write(byteBuffer);
accept.close();
fileChannel.close();
channel.close();
}
}
(非阻塞型)
// 非阻塞式 客户端
@Test
public void clientNonBlockingNioTest03() throws IOException {
SocketChannel channel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9988));
channel.configureBlocking(false);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put("再见了,朋友!!!".getBytes());
byteBuffer.flip();
channel.write(byteBuffer);
byteBuffer.clear();
channel.close();
}
// 非阻塞式 服务端
@Test
public void serverNonBlockNioTest03() throws Exception {
ServerSocketChannel channel = ServerSocketChannel.open();
channel.configureBlocking(false);
channel.bind(new InetSocketAddress(9988));
Selector selector = Selector.open();
channel.register(selector, SelectionKey.OP_ACCEPT);
while (selector.select() > 0) {
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
SocketChannel accept;
if (selectionKey.isAcceptable()) {
accept = channel.accept();
accept.configureBlocking(false);
accept.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
accept = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int len;
while ((len = accept.read(byteBuffer)) != -1) {
byteBuffer.flip();
System.out.println(new String(byteBuffer.array(), 0, len));
byteBuffer.clear();
}
}
iterator.remove();
}
}
}
2.5 总结
- 所有
channel
的的读写数据都是通过read()
和writer()
操作Buffer
,进行读取数据或写入数据。
3. Selector
选择器
3.1 基本介绍
基本概念
:相当于一个观察者,用来监听通道感兴趣的事件,一个选择器可以绑定多个通道;
选择器( Selector )
:是SelectableChannle
对象的多路复用器,Selector 可以同时监控多个 SelectableChannel 的 IO 状况,也就是说,利用 Selector 可使一个单独的线程管理多个 Channel。Selector 是非阻塞 IO 的核心。
3.2 选择器(Selector)的应用
- 1 创建
Selector
// 创建选择器
Selector selector = Selector.open();
- 2 向选择器注册通道
// 1. 创建 Socket 套接字
Socket socket = new Socket(InetAddress.getByName("127.0.0.1",9898));
// 2.1 获取 Socket 通道
SocketChannel channel = socket.getChannel();
// 2.2 将 SocketChannel 切换到 非阻塞模式
channel.configureBlocking(false);
// 3. 创建选择器
Selector selector = Selector.open();
// 4. 向 Selector 注册到 Channel
SelectionKey key = channel.register(selector,SelectionKey.OP_READ);
Selector
常用方法
3.3 SelectionKey的应用
常用方法及说明
: