浅谈NIO
概述
1.Java NIO 全称 Java non-blocking IO,是指 JDK 提供的新 API。从 JDK1.4 开始,Java 提供了一系列改进的输入/输出的新特性,被统称为 NIO(即 NewIO),是同步非阻塞的。
2.NIO 相关类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写。【基本案例】
NIO 有三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器) 。
3.NIO 是面向缓冲区,或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络。
4.Java NIO 的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。【后面有案例说明】
5.通俗理解:NIO 是可以做到用一个线程来处理多个操作的。假设有 10000 个请求过来,根据实际情况,可以分配 50 或者 100 个线程来处理。不像之前的阻塞 IO 那样,非得分配 10000 个。
6.HTTP 2.0 使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比 7.HTTP 1.1 大了好几个数量级。
7.案例说明 NIO 的 Buffer
public class NIOTest1 {
public static void main(String[] args) {
/*创建一个Buffer,可以存储5个字节的大小*/
IntBuffer intBuffer = IntBuffer.allocate(5);
/*向Buffer存放数据*/
for (int i = 0; i < intBuffer.capacity(); i++) {
intBuffer.put(i);
}
/*从Buffer读取数据*/
/*这是一个对内部索引重新赋值的操作*/
intBuffer.flip();
/*intBuffer.hasRemaining()当前位置有没有超过对应的界限*/
while(intBuffer.hasRemaining()){
/*获取对应索引位置的元素*/
System.out.println(intBuffer.get());
}
}
}
NIO与BIO的比较
BIO读取过程:
**
读取过程(共三次复制拷贝过程):
1 jvm堆执行fileInputStream.read()请求操作系统,然后操作系统请求磁盘。
2 从磁盘中读取到数据,然后写到操作系统缓冲区中。
3 将数据从操作系统缓冲区放到jvm进程缓冲区(按字节流读取,即一个一个字节byte读取数据)。
4 jvm将jvm进程中缓冲区东西拷贝到jvm堆内存中(应用部署位置)。
NIO读取过程:
与BIO的区别有如下两点:
1 相对于bio的一个一个byte传,nio是以channel形式读取buffer缓冲区,然后以块数据传输
2 nio减少了复制过程(这里共两种方法)
第一种:jvm进程的虚拟地址空间直接从磁盘中读取。
直接把磁盘映射到JVM进程的虚拟地址空间,放置对应到页表上。
上图可见BIO中2,3,4复制过程都没有了,变成如下:
– 1 jvm堆执行fileInputStream.read()请求操作系统,然后操作系统请求磁盘。
– 2 直接从磁盘映射到jvm进程的虚拟地址空间。
第二种:直接内存(堆外内存 DirectBuffer),删掉了BIO第4步的复制。
3 采取多路复用技术监听请求
关键点如下:
1 只有一个线程用于监听
2 对应通道会注册感兴趣的事件(如读写事件)
3 假如通道注册了读事件,当通道发现有数据了,操作系统就会通知线程这个通道有数据了
4 这样的话,注册大量channel,都可以只要一个线程监听,只要事件触发了,操作系统就会通知该线程去处理。
5 这样当前线程会一直存在,避免上下文切换,该线程数据就能一直缓存在L1,L2,L3缓存当中(L1 L2 L3缓存讲解连接:https://baijiahao.baidu.com/s?id=1598811284058671259&wfr=spider&for=pc),页表也不需要重新刷新(描述逻辑页和物理页帧映射关系)。
1.NIO以块的方式处理数据,BIO以流的方式处理数据
2.BIO是阻塞的,NIO是非阻塞的
3.BIO基于字节流与字符流进行操作,而NIO基于Channel(通道)与缓冲区进行操作。数据总是以通道读取到缓冲区中,或者从缓冲区中写入到通道。Selector(选择器)用于监听多个通道的事件,(比如连接请求,数据到达),所以使用单个线程就可以监听多个客户端通道。
NIO的三大核心
概述
1.Channel(通道)
2.Buffer(缓冲区)
3.Selector(选择器)
1.一个Channel对应一个Buffer
2.Selector对应一个线程,一个线程可以对应多个通道,也即监听多个客户端的意思
3.该图反应了有三个Channel注册到Selector的
4.程序切换到哪个Channel是由事件决定的,Event就是一个重要的概念
5.Selector会根据不同的事件,在各个通道上切换
6.Buffer就是一个内存块,底层是已给数组
7.数据读取与写入是通过Buffer,这个和BIO是不同的,BIO中要么是输入流,或者是输出流,不可以双向,但BIO的Buffer是可以读也可以写的,需要flip方法切换Channel是双向的,可以返回底层操作系统的情况,比如Linux,底层的操作系统通道就是双向的。
缓冲区(Buffer)
缓冲区(Buffer):缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,缓冲区对象内置一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel提供从文件,网络读取数据的渠道,但是读取或写入数据必须经过Buffer。
Buffer类及其子类
- 在
NIO
中,Buffer
是一个顶层父类,它是一个抽象类,类的层级关系图:
Buffer
类定义了所有的缓冲区都具有的四个属性来提供关于其所包含的数据元素的信息:
属性 | 描述 |
---|---|
Capacity | 容量,即可以容纳的最大数据量;在缓冲区创建时被设定并且不能改变 |
Limit | 表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作。且极限是可以修改的 |
Position | 位置,下一个要被读或写的元素的索引,每次读写缓冲区数据时都会改变改值,为下次读写作准备 |
Mark | 标记 |
Buffer
类相关方法一览
ByteBuffer
从前面可以看出对于 Java
中的基本数据类型(boolean
除外),都有一个 Buffer
类型与之相对应,最常用的自然是 ByteBuffer
类(二进制数据),该类的主要方法如下:
public abstract class Buffer {
//JDK1.4时,引入的api
public final int capacity( )//返回此缓冲区的容量
public final int position( )//返回此缓冲区的位置
public final Buffer position (int newPositio)//设置此缓冲区的位置
public final int limit( )//返回此缓冲区的限制
public final Buffer limit (int newLimit)//设置此缓冲区的限制
public final Buffer mark( )//在此缓冲区的位置设置标记
public final Buffer reset( )//将此缓冲区的位置重置为以前标记的位置
public final Buffer clear( )//清除此缓冲区, 即将各个标记恢复到初始状态,但是数据并没有真正擦除, 后面操作会覆盖
public final Buffer flip( )//反转此缓冲区
public final Buffer rewind( )//重绕此缓冲区
public final int remaining( )//返回当前位置与限制之间的元素数
public final boolean hasRemaining( )//告知在当前位置和限制之间是否有元素
public abstract boolean isReadOnly( );//告知此缓冲区是否为只读缓冲区
//JDK1.6时引入的api
public abstract boolean hasArray();//告知此缓冲区是否具有可访问的底层实现数组
public abstract Object array();//返回此缓冲区的底层实现数组
public abstract int arrayOffset();//返回此缓冲区的底层实现数组中第一个缓冲区元素的偏移量
public abstract boolean isDirect();//告知此缓冲区是否为直接缓冲区
}
通道(Channel)
基本介绍
1.NIO的通道类似于流,但是有些区别ruxia
- 1.1通道可以同时进行读写,而流只能读或者只能写
- 1.2通道可以异步读取数据
- 1.3通道可以从缓冲区读数据,也可以写数据到缓冲区
2.BIO 中的 Stream 是单向的,例如 FileInputStream 对象只能进行读取数据的操作,而 NIO 中的通道(Channel)是双向的,可以读操作,也可以写操作。
3.Channel 在 NIO 中是一个接口 public interface Channel extends Closeable{}
4.常用的 Channel 类有:FileChannel、DatagramChannel、ServerSocketChannel 和 SocketChannel。【ServerSocketChanne 类似 ServerSocket、SocketChannel 类似 Socket】
5.FileChannel 用于文件的数据读写,DatagramChannel 用于 UDP 的数据读写,ServerSocketChannel 和 SocketChannel 用于 TCP 的数据读写。
图示
FileChannel类
FileChannel 主要用来对本地文件进行 IO 操作,常见的方法有
- public int read(ByteBuffer dst),从通道读取数据并放到缓冲区中
- public int write(ByteBuffer src),把缓冲区的数据写到通道中
- public long transferFrom(ReadableByteChannel src, long position, long count),从目标通道中复制数据到当前通道
- public long transferTo(long position, long count, WritableByteChannel target),把数据从当前通道复制给目标通道
应用实例1 - 本地文件写数据
实例要求:
- 使用前面学习后的
ByteBuffer
(缓冲)和FileChannel
(通道),将 “hello,世界” 写入到file01.txt
中 - 文件不存在就创建
- 代码演示
public class NIOFileOutPutTest1 {
public static void main(String[] args) throws IOException {
String value="hell,世界";
/*创建一个输出流*/
FileOutputStream fileOutputStream = new FileOutputStream("D:\\test1.txt");
//通过 fileOutputStream 获取对应的 FileChannel
//这个 fileChannel 真实类型是 FileChannelImpl
FileChannel channel = fileOutputStream.getChannel();
/*创建一个缓冲区*/
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
/*将数据放入缓冲区*/
byteBuffer.put(value.getBytes());
/*将Buffer装换读写切换*/
byteBuffer.flip();
/*将byteBuffer 数据写入到 fileChannel*/
channel.write(byteBuffer);
fileOutputStream.close();
}
}
应用实例2 - 本地文件读数据
实例要求:
- 使用前面学习后的
ByteBuffer
(缓冲)和FileChannel
(通道),将file01.txt
中的数据读入到程序,并显示在控制台屏幕 - 假定文件已经存在
- 代码演示
public class Test2 {
public static void main(String[] args) throws Exception {
//创建文件的输入流
File file = new File("D:\\test1.txt");
FileInputStream fileInputStream = new FileInputStream(file);
//通过 fileInputStream 获取对应的 FileChannel -> 实际类型 FileChannelImpl
FileChannel fileChannel = fileInputStream.getChannel();
//创建缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate((int)file.length());
//将通道的数据读入到 Buffer
fileChannel.read(byteBuffer);
//将 byteBuffer 的字节数据转成 String
System.out.println(new String(byteBuffer.array()));
fileInputStream.close();
}
}
应用实例3 - 使用一个 Buffer 完成文件读取、写入
实例要求:
- 使用
FileChannel
(通道)和方法read、write
,完成文件的拷贝 - 拷贝一个文本文件
1.txt
,放在项目下即可 - 代码演示
public class Test {
public static void main(String[] args) throws IOException {
/*获取输入流*/
FileInputStream fileInputStream = new FileInputStream("D:\\test1.txt");
FileChannel readchannel = fileInputStream.getChannel();
/*获取输出流*/
FileOutputStream fileOutputStream = new FileOutputStream("D:\\test2.txt");
FileChannel writechannel1 = fileOutputStream.getChannel();
/*创建缓冲区*/
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
while(true){
//这里有一个重要的操作,一定不要忘了
/*
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
*/
byteBuffer.clear();
int read = readchannel.read(byteBuffer);
/*等于-1说明读取完成*/
if (read==-1){
break;
}
//将 buffer 中的数据写入到 test2.text
/*
* public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
* */
byteBuffer.flip();
writechannel1.write(byteBuffer);
}
/*关闭对应的流*/
fileInputStream.close();
fileOutputStream.close();
}
}
应用实例4 - 拷贝文件 transferFrom 方法
- 实例要求:
- 使用
FileChannel
(通道)和方法transferFrom
,完成文件的拷贝 - 拷贝一张图片
- 代码演示
public class test3 {
public static void main(String[] args) throws IOException {
FileInputStream fileInputStream = new FileInputStream("D:\\test1.txt");
FileChannel SourceChannel = fileInputStream.getChannel();
FileOutputStream fileOutputStream = new FileOutputStream("D:\\test3.text");
FileChannel destChannel = fileOutputStream.getChannel();
destChannel.transferFrom(SourceChannel,0,SourceChannel.size());
/*关闭通道与流*/
destChannel.close();
SourceChannel.close();
fileInputStream.close();
fileOutputStream.close();
}
}
关于 Buffer 和 Channel 的注意事项和细节
ByteBuffer
支持类型化的put
和get
,put
放入的是什么数据类型,get
就应该使用相应的数据类型来取出,否则可能有BufferUnderflowException
异常。【举例说明】
public class Test4 {
public static void main(String[] args) {
//创建一个 Buffer
ByteBuffer buffer = ByteBuffer.allocate(64);
//类型化方式放入数据
buffer.putInt(100);
buffer.putLong(9);
buffer.putChar('尚');
buffer.putShort((short) 4);
//取出
buffer.flip();
System.out.println();
System.out.println(buffer.getInt());
System.out.println(buffer.getLong());
System.out.println(buffer.getChar());
System.out.println(buffer.getShort());
}
}
- 可以将一个普通
Buffer
转成只读Buffer
【举例说明】
public class Test5 {
public static void main(String[] args) {
//创建一个 buffer
ByteBuffer buffer = ByteBuffer.allocate(64);
for (int i = 0; i < 64; i++) {
buffer.put((byte) i);
}
//读取
buffer.flip();
//得到一个只读的 Buffer
ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
System.out.println(readOnlyBuffer.getClass());
//读取
while (readOnlyBuffer.hasRemaining()) {
System.out.println(readOnlyBuffer.get());
}
readOnlyBuffer.put((byte) 100); //ReadOnlyBufferException
}
}
NIO
还提供了MappedByteBuffer
,可以让文件直接在内存(堆外的内存)中进行修改,而如何同步到文件由NIO
来完成。【举例说明】
public class Test6 {
public static void main(String[] args) throws IOException {
RandomAccessFile randomAccessFile = new RandomAccessFile("1.txt", "rw");
//获取对应的通道
FileChannel channel = randomAccessFile.getChannel();
/**
* 参数 1:FileChannel.MapMode.READ_WRITE 使用的读写模式
* 参数 2:0:可以直接修改的起始位置
* 参数 3:5: 是映射到内存的大小(不是索引位置),即将 1.txt 的多少个字节映射到内存
* 可以直接修改的范围就是 0-5
* 实际类型 DirectByteBuffer
*/
MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
mappedByteBuffer.put(0, (byte) 'H');
mappedByteBuffer.put(3, (byte) '9');
mappedByteBuffer.put(5, (byte) 'Y');//IndexOutOfBoundsException
randomAccessFile.close();
System.out.println("修改成功~~");
}
}
- 前面我们讲的读写操作,都是通过一个
Buffer
完成的,NIO
还支持通过多个Buffer
(即Buffer
数组)完成读写操作,即Scattering
和Gathering
【举例说明】
public class Test7 {
public static void main(String[] args) throws IOException {
//使用 ServerSocketChannel 和 SocketChannel 网络
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress(7000);
//绑定端口到 socket,并启动
serverSocketChannel.socket().bind(inetSocketAddress);
//创建 buffer 数组
ByteBuffer[] byteBuffers = new ByteBuffer[2];
byteBuffers[0] = ByteBuffer.allocate(5);
byteBuffers[1] = ByteBuffer.allocate(3);
//等客户端连接 (telnet)
SocketChannel socketChannel = serverSocketChannel.accept();
int messageLength = 8; //假定从客户端接收 8 个字节
//循环的读取
while (true) {
int byteRead = 0;
while (byteRead < messageLength) {
long l = socketChannel.read(byteBuffers);
byteRead += l; //累计读取的字节数
System.out.println("byteRead = " + byteRead);
//使用流打印,看看当前的这个 buffer 的 position 和 limit
Arrays.asList(byteBuffers).stream().map(buffer -> "position = " + buffer.position() + ", limit = " + buffer.limit()).forEach(System.out::println);
}
//将所有的 buffer 进行 flip
Arrays.asList(byteBuffers).forEach(buffer -> buffer.flip());
//将数据读出显示到客户端
long byteWirte = 0;
while (byteWirte < messageLength) {
long l = socketChannel.write(byteBuffers);//
byteWirte += l;
}
//将所有的buffer进行clear
Arrays.asList(byteBuffers).forEach(buffer -> {
buffer.clear();
});
System.out.println("byteRead = " + byteRead + ", byteWrite = " + byteWirte + ", messagelength = " + messageLength);
}
}
}
Selector选择器
概述
1.Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到Selector(选择器)
2.Selector 能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
3.只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程
4.避免了多线程上下文切换的开销
特点概述
特点再说明:
1.Netty 的 IO 线程 NioEventLoop 聚合了 Selector(选择器,也叫多路复用器),可以同时并发处理成百上千个客户端连接。
2.当线程从某客户端 Socket 通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。
- 线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。
4.由于读写操作都是非阻塞的,这就可以充分提升 IO 线程的运行效率,避免由于频繁 I/O 阻塞导致的线程挂起。
- 一个 I/O 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 I/O 一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。
Selector类相关方法
public abstract class Selector implements Closeable {
public static Selector open();//得到一个选择器对象
public int select(long timeout);//监控所有注册的通道,当其中有 IO 操作可以进行时,将 对应的 SelectionKey 加入到内部集合中并返回,参数用来设置超时时间
public Set<SelectionKey> selectedKeys();//从内部集合中得到所有 SelectionKey
}