1.NIO三大组件
1.1Channel & Buffer介绍
channel
类似于stream
,区别在于:
- channel它是读写数据的双向通道,可以从channel将数据输出到buffer中,也可以将buffer中的数据输出到channel中
- 而stream是单向的 (BIO是面向stream的),所以分为了
输入流(InputStream)
和输出流(OutputStream)
关于Java的IO流参考
常见的Channel
- FileChannel
(操作文件)
- DatagramChannel
(UDP协议连接)
- SocketChannel
(TCP协议连接)
- ServerSocketChannel
专门处理TCP协议Accept事件
常见的buffer
buffer用作缓冲读写数据
,Buffer本质上是一个内存块,可以向里面写入数据,或者从里面读取数据,在Java中它被包装成了Buffer对象,并提供了一系列的方法用于操作这个内存块
常见的buffer有
ByteBuffer
MappedByteBuffer
MappedByteBuffer是一种特殊的ByteBuffer,它使用内存映射的方式加载物理文件,并不会耗费同等大小的物理内存,是一种直接操作堆外内存的方式,读写性能比较高。DirectByteBuffer
HeapByteBuffer
ShortBuffer/IntBuffer/LongBuffer/FloatBuffer/DoubleBuffer/ChaBuffer/
1.2Channel与Buffer使用
1.2.1简单实例
使用channel/buffer读取data.txt的内容(123456789@xxx)
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/*
* 说明:这里为例演示代码清晰,所以没有使用try-catch去处理资源
* */
public class TestByteBuffer {
private static final String PATH = "data.txt";
public static void main(String[] args) throws IOException {
//获取channel 调用getChannel()方法
FileChannel channel = new FileInputStream(PATH).getChannel();
//准备一个缓冲区buffer 大小为10个字节
ByteBuffer buffer = ByteBuffer.allocate(10);
while (true) {
/*
* read()方法,将channel的数据输出到buffer中,
* 返回值就是此次输出buffer中数据的字节数
* 返回-1,表示channel中的数据全部输出到了buffer中
* */
int len = channel.read(buffer);
if (len == -1) break;
/*
* 切换缓冲区至输出模式,本质就是position指针移动到第一个
* 未读数据的位置。
* */
buffer.flip();
/*
* hasRemaining表示buffer中是否还有未读数据
* */
while (buffer.hasRemaining()) {
byte b = buffer.get(); //读取数据
System.out.println((char) b);
}
/*
* 清空buffer中的数据,本质就是移动position指针(后续详细讲)到开头
* 下次从channel中输入数据的时候从头开始输入
* */
buffer.clear();
}
}
}
运行结果
读到的字节数 = 10
1
2
3
4
5
6
7
8
9
@
读到的字节数 = 3
x
x
x
读到的字节数 = -1 //结束
1.2.2使用流程
- 1-获取channel() 可以通过网络或者stream
- 2-通过ByteBuffer.allocate(n)分配一个Buffer(缓冲)
- 3-将channel中的数据输出到buffer中
channer.read(buffer)
- 4-开启buffer的输出模式,
buffer.flip()
- 5-从buffer中获取数据,
buffer.get()
- 6-清空buffer
(切换为写(输入)模式)
继续从channel中读数据,然后继续循环1-5
1.2.3buffer内部结构详解
ByteBuffer有下面三个重要属性
-
capacity(buffer总容量)
Buffer作为一个存储块,是有固定大小的,这个固定大小我们称作“容量”。
当Buffer写满之后,需要先清空或者读取数据,才能继续写入新的数据。
-
position(当前位置)
-写模式下,position从0开始
,每写入一个单位的数据,position前进一位,position最大 可到达(capacity-1)的位置。
-当Buffer从写模式切换为读模式时,position将重置为0。读取数据时,同样地,position每读取一个单位,前进一位,此时,position最大可到达limit的位置实际最大可读取的位置是(limit-1)
-
limit(限制长度)
写模式下,limit最大值等于capacity。
读模式下,limit最大值等于切换为读模式时position
总结
这里可能有点绕,position类似于数组的下标,是从0开始的,limit表示最大可以读取或者写入的长度,capacity表示最大的容量,limit和capacity不是下标,类似于数组的长度,所以跟position比较需要-1。在写模式下,position指向的是下一个待写入的位置;在读模式下,position指向的是下一个待读取的位置
。
内部变化
Buffer刚创建时
向Buffer中输入了四个字节的数据
调用flip()进入Buffer的输出模式
输出了四个字节的数据
调用clear()进入Buffer输入模式
1.2.4buffer常用方法
获取buffer
可以使用allocate()方法为ByteBuffer分配空间,其他buffer类也有该方法
ByteBuffer buffer = ByteBuffer.allocate(16) //创建了一个大小为16字节的buffer
向buffer中写入数据
-
第一种方法
通过channel将数据输入到buffer中
int readBytes = channel.read(buffer);
-
第二种方法
调用buffer的put()方法,自己为自己输入数据
buffer.put(2)
put()有很多不同的类型,比如在特定位置写入,写入不同类型的数据等等
从buffer中输出数据
- 输出到channel中,调用channel的write()方法
channel.write(buffer)
- 调用buffer自己的get()方法
byte b = buffer.get();
注意:get()方法会让position读指针向后移动,如果想重复读取数据
1.可以调用rewind()
方法将position重新置为0
2.或者调用get(int i)
方法获取索引为i的数据,不会移动position指针
rewind()
rewind()方法会重置position为0
,但limit保持不变
,因此可以用来重新读取数据。通常是在重新读取数据之前调用。
clear()
clear()方法用于清空整个Buffer,并将Buffer从读模式切换回写模式
,且position归位到0位置。
compact()
compact()方法用于清空已读取的数据,并将未读取的数据移至Buffer的头部,position的位置移动到从头开始计算的未读取的数据的下一个位置,它也会将Buffer从读模式切换回写模式。
mark() 和 reset()
mark()方法用于标记给定位置,然后可以在之后通过reset()方法重新回到mark的位置,示例如下:
buffer.mark();
//多次调用buffer.get(),例如在解析过程中。
buffer.reset(); //将位置重新设置为标记。
1.2.5分散读取与聚合
分散输出:将channel中的数据分散输出至多个buffer
//伪代码 假设现在有三个buffer
bufferA
bufferB
bufferC
//将channel中的数据分散读取到三个buffer中 (根据buffer的容量依次输入)
channel.read(new ByteBuffer[]{bufferA, bufferB, bufferC});
聚合输入: 将多个buffer的内容出入到一个channel中
//伪代码 假设现在有三个buffer
bufferA
bufferB
bufferC
//分别开启三个buffer的输出模式 即调用flip()
//将三个buffer中的内容输出到channel中
channel.write(new ByteBuffer[]{bufferA, bufferB, bufferC})
1.3Selector介绍
selector但从字面意思不好理解,需要结合服务器的设计演化来理解其用途。
多线程版设计
服务器建立连接,每获取一个连接(Socket)
后开启一个线程去处理任务.
缺点:连接数多的话开启过多的线程会使系统崩掉。
基于这一点,就可以使用线程池优化
线程池版
缺点:
- 阻塞模式下,线程仅能处理一个socket连接
- 仅适用于短连接场景
selector版设计
- selector的作用就是配合一个线程来管理多个channel(在这里可以认为就是一个网络连接),获取这些channel上发生的时间,这些channel工作在
非阻塞模式
下,不会让线程吊死在一个channel上,适合连接数特别多,但流量低的场景
- 调用selector的select()会阻塞知道channel发生了读/写就绪时间,这些时间发生,select()方法就会返回这些时间交给thread去处理
(就是IO多路复用)
- 通过调用selector的select()方法会阻塞到某个或者某些channel发生了读写就绪事件,这些事件发生,select()方法就会返回这些时间交给thread去处理
更多参考