引用:https://blog.csdn.net/oMaoYanEr/article/details/79976359
什么是NIO:
NIO是非阻塞式的高伸缩性网络,除了Boolean类型外的所有原始类型提供缓存支持的数据容器。
为什么用NIO,传统IO缺陷?
传统IO是阻塞、面相流的I/O系统,如果一个请求对应一个线程模式,一旦存在高并发的大量请求会发生:
- 线程不够用,就算使用复用线程也无济于事;
- 阻塞I/O模式下,会有大量的线程被阻塞,一直在等待数据,这个时候的线程被挂起,只能干等,CPU利用率很低,换句话说,系统的吞吐量差
- 如果网络I/O堵塞或者有网络抖动或者网络故障等,线程的阻塞时间可能很长。整个系统也变的不可靠
理解:
NIO的全称是NoneBlocking IO,非阻塞IO,区别于BIO,BIO的全称是Blocking IO,阻塞IO。那这个阻塞是什么意思呢?例如传统的多线程服务器是BlockingIO模式的,从头到尾所有的线程都是阻塞。
传统IO 在读写过程中是不允许停止的需要一直占用线程,对于一个一直有内容可读文件不用切换线程效率是不错的。但是在网络通讯并发场景下需要不停的创建新线程而且线程大部分时间是阻塞。CPU利用率较低,吞吐量差。所以改用NIO非阻塞式的IO。非阻塞式的IO,讲内容写入一个Buffer中通过操作Buffer的标识,来改变读写状态,将它变为分块模式,通过通道Channel进行数据传递,对内存中内容进行映射,读完就了结,不需要一直占用线程。对应每个Channel需要标识知道每个Channel对应谁写谁读问题,于是就有Selector,Selector通过事件驱动多个Channel对象,Selector可以实现一个线程管理多个数据的Channel。
Java NIO:
- Channels和Buffers:在标准IO接口中我们最常用的是字节流(byte strams)和字符流(character streams)。在NIO接口中我们需要使用Channel和Buffer进行IO操作,Channel模拟了流的概念,但是又有不同。数据总是从一个Channel读到一个buffer中,或者从一个buffer中写到channel中。
- Non-blocking IO: Java NIO接口的核心就是提供了非阻塞IO的能力(Non-blocking IO)。例如:一个线程可以请求channel读取数据到buffer中,在channel读取数据的过程中,线程可以处理其他的事情,一旦数据已经读取到buffer中,线程可以继续处理buffer中的数据;对于将buffer中的数据写到channel中道理是一样的。
- Selectors:Java NIO包含了Selectors的设计,Selector通过事件驱动多个Channel的对象,Selector可以实现让一个线程管理使用多个数据的Channel。
Channel:
- Channel可以支持同时读写,流Stream只支持单向读写
- Channels可以异步读写,流Stream是同步的。
- Channels只会从Buffer进行读取。
Channel 主要实现类:
- FileChannel: 可以读写文件中的数据
- DatagramChannel: 可以通过UDP协议读写数据
- SocketChannel:可以通过TCP协议读写数据
ServerSocketChannel:允许像web服务器监听TCP链接请求,对每个链接请求创建一个SocketChannel
public class FileChannelExam {
public static void main(String[] args){
try {
String path = FileChannelExam.class.getResource("/data/nio-data.txt").getPath();
// 创建一个文件通道
RandomAccessFile file = new RandomAccessFile(path, "rw");
FileChannel channel = file.getChannel();
// 创建一个字节buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 读取数据到buffer
int len = channel.read(buffer);
while (len != -1){
System.out.println("Read " + len);
// 将写模式转变为读模式,
// 将写模式下的buffer内容最后位置设为读模式下的limit位置,作为读越界位,同时将读位置设为0
// 表示转换后重头开始读,同时消除写模式的mark标记
buffer.flip();
// 判断当前读取位置是否到达越界位(position < limit)
while (buffer.hasRemaining()){
// 读取当前position的字节(position++)
System.out.println(buffer.get());
}
// 清空当前buffer内容
buffer.clear();
len = channel.read(buffer);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
需要注意buffer.flip()方法,首先我们从Channel读取数据写入到Buffer,然后调用flip将切换到读模式,才能从buffer中读取数据。
Channel 到Channel 数据传输
在Java NIO中我们可以直接将数据从一个Channel传输到另一个Channel中,比如FileChannel中有transferTo()和transferFrom()方法。
Scatter和Gather
Java NIO内置支持分散(Scatter)和聚集(Gather),Scatter和Gather是用于读取和写入Channel的概念。
Scatter:从一个Channel中分散读取数据到一个或多个Buffer的操作,因此Channel将数据分散到多个Buffer中;
Gather:将一个或多个Buffer中的数据写入一个Channel的操作,一次Channel可以从多个Buffer中收集数据。
Scatter和Gather在解决传输数据拥有多个部分需要进行分离的场景下有很大的用处;
比如,一个消息数据中包含消息头(header)和消息体(body)两部分,我们就可以将消息头和消息体分别读入不同的Buffer保存,使得消息的分离处理更加方便。
- Scatter操作
当Channel的read()方法传入参数为buffer数据的时候,read()方法会按照顺序将数据写入到传入的多个buffer中,当一个buffer写满后便会写入下一个buffer直到写满所有的buffer;因为分离读取的时候,Channel写入buffer的数据是按顺序的,Scatter操作并不适合动态长度的数据传输,也就意味着传输数据的每一部分都是固定长度时,Scatter才能发挥它的作用
- Gather操作
当channel的write()方法可以接受buffer数据作为参数,write()方法会按照顺序将多个buffer中的数据依次写入channel。需要注意的是,write()操作只会写入buffer中已写入的数据,即position到limit之间的数据;例如一个buffer的容量为128字节,但buffer中只写入了28字节的数据,只有这28个字节会写入channel中,因此Gather操作和Scatter相反非常适合动态长度数据写入。