NIO (非阻塞式IO)
1、来看看IO、NIO区别?
当用户线程发起一个IO请求操作(本文以读请求操作为例),内核会去查看要读取的数据是否就绪,对于阻塞IO来说,如果数据没有就绪,则会一直在那等待,直到数据就绪;对于非阻塞IO来说,如果数据没有就绪,则会返回一个标志信息告知用户线程当前要读的数据没有就绪。当数据就绪之后,便将数据拷贝到用户线程,这样才完成了一个完整的IO读请求操作,也就是说一个完整的IO读请求操作包括两个阶段:
1)查看数据是否就绪;
2)进行数据拷贝(内核将数据拷贝到用户线程)。
那么阻塞(blocking IO)和非阻塞(non-blocking IO)的区别就在于第一个阶段,如果数据没有就绪,在查看数据是否就绪的过程中是一直等待,还是直接返回一个标志信息。
IO(面向流的,单向的,输入流、输出流是两种不同的流):
-----------------
【磁盘、网络中的文件】》== 这里有字节的流动 ==》 【程序】
-----------------
NIO(面向缓冲区的,双向的,火车往前往后开):
通道
-----------------
【磁盘、网络中的文件】《== 【缓冲区】 ==》 【程序】
-----------------
通道(看做铁路)负责传输,缓冲区(看做火车)负责存储数据
2、缓冲区负责数据的存取。缓冲区就是数组,用于存储不同数据类型的数组
针对不同基本类型数据的缓冲区:ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer
除了boolean类型没有
2.1 缓冲区的核心方法
put():存入数据到缓冲区中
get():获取缓冲区中的数据
2.2 缓冲区的四个核心属性
capacity:容量,表示缓冲区中的最大容量,一旦声明无法改变
limit:界限,表示缓冲区中可以操作的数据大小(limit后的数据不能读写)
position:位置,表示缓冲区中正在操作数据的位置
mark:可以记录position的位置 ,通过buffer.mark()记录下当前position,之后position移动过了就可以通过buffer.reset()使position回到上一个位置
0 <= mark <= position <= limit <= capacity
2.3 Demo(读数据模式,写数据模式)
//1.分配一个指定大小的缓冲区,capacity=5,position=0,limit=5
ByteBuffer buffer = ByteBuffer.allocate(5);
System.out.println("---allocate()---");
System.out.println(buffer.position());//position=0
System.out.println(buffer.limit());//limit=5
System.out.println(buffer.capacity());//capacity=5
//2、利用put存入数据到缓冲区,相当于写数据模式
System.out.println("---put()---");
String str = "abc"; buffer.put(str.getBytes());
System.out.println(buffer.position());//3
System.out.println(buffer.limit());//5
System.out.println(buffer.capacity());//5
//3、利用filp()方法切换读取数据模式
System.out.println("---filp()---");
buffer.flip();
System.out.println(buffer.position());//0
System.out.println(buffer.limit());//3
System.out.println(buffer.capacity());//5
//4、利用get()读取缓冲区的数据
System.out.println("---get()---");
byte[] dst = new byte[buffer.limit()];
buffer.get(dst);//缓冲区中的数据读到字节数组中去
System.out.println(new String(dst, 0, dst.length));
System.out.println(buffer.position());//3
System.out.println(buffer.limit());//3
System.out.println(buffer.capacity());//5
//5、rewind():可重复读数据
System.out.println("---rewind()---");
buffer.rewind();
System.out.println(buffer.position());//0
System.out.println(buffer.limit());//3
System.out.println(buffer.capacity());//5
//6、clear():清空缓冲区,但是缓冲区中的数据还在,但是处于“被遗忘”状态
System.out.println("---clear()---");
buffer.clear();
System.out.println(buffer.position());//0
System.out.println(buffer.limit());//5
System.out.println(buffer.capacity());//5
2.4 (NIO如何提升性能?)直接缓冲区与非直接缓冲
* 非直接缓冲区:通过 allocate() 方法分配缓冲区,将缓冲区建立在 JVM 的内存中
* 直接缓冲区:通过 allocateDirect() 方法分配直接缓冲区,将缓冲区建立在物理内存中。可以提高效率
在JVM堆外分配内存,好处就是减少了Java堆和native堆中来回copy数据的消耗。坏处就是缓冲区建立在内存中,不可控,也受到物理内存空间的限制
3、通道
3.1 是什么?
用于源节点与目标节点的连接。在 Java NIO 中负责缓冲区中数据的传输。Channel 本身不存储数据,因此需要配合缓冲区进行传输。
3.2 通道的主要实现类 以及 获取通道的api
主要的实现类:
java.nio.channels.Channel 接口:
|--FileChannel 文件传输通道
|--SocketChannel TCP传输
|--ServerSocketChannel TCP传输
|--DatagramChannel UDP传输
获取通道的方式:
1)Java 针对支持通道的类提供了 getChannel() 方法
本地 IO:
FileInputStream/FileOutputStream
RandomAccessFile
网络IO:
Socket
ServerSocket
DatagramSocket
2)在 JDK 1.7 中的 NIO.2 针对各个通道提供了静态方法 open()
3)在 JDK 1.7 中的 NIO.2 的 Files 工具类的 newByteChannel()
3.3 几个Demo
3.3.1 Demo1:利用通道进行本地文件复制
①获取输入输出流,并getChannel得到源通道与目标通道
while(源通道中数据不为空)
②不断地将源通道中的数据存入缓冲区
③切换到读模式,并在目标通道中读取缓冲区中的数据
④缓冲区.clear(),下一次又可以到源通道拿数据
⑤关闭所有的通道,输入输出流
3.3.2 Demo2:利用直接缓冲区完成本地文件的复制
3.3.2.1 将缓冲区建立在内存中,由内存映射文件MappedByteBuffer来负责处理数据
3.3.2.2 利用通道的transferTo或者transferFrom方法,在通道之间进行数据传输(简单)
3.4 分散读取和聚集写入
分散读取(Scattering Reads):将从通道中读取的数据分散到多个缓冲区中
FileChannel channel1 = raf1.getChannel();
ByteBuffer buf1 = ByteBuffer.allocate(100);
ByteBuffer buf2 = ByteBuffer.allocate(1024);
ByteBuffer[] bufs = {buf1, buf2};
channel1.read(bufs);
聚集写入(Gathering Writes):将多个缓冲区中的数据聚集到通道中
RandomAccessFile raf2 = new RandomAccessFile("2.txt", "rw");
FileChannel channel2 = raf2.getChannel();
channel2.write(bufs);
4、字符集
字符集:Charset
编码:Unicode字符串 -> 指定字符集的字节数组
解码:指定字符集的字节数组 -> Unicode字符串
//获取指定的字符集
Charset cs1 = Charset.forName("GBK");
//获取编码器
CharsetEncoder ce = cs1.newEncoder();
//获取解码器
CharsetDecoder cd = cs1.newDecoder();
5、Selector是NIO中实现I/O多路复用的关键类,NIO在网络编程中的作用
(一)Selector是非阻塞式IO的核心
选择器( Selector) 是 SelectableChannel 对象的多路复用器。另一个角度说,FileChannel是阻塞式的,而非阻塞式IO要求服务端、客户端通道都是处于非阻塞式模式下的。
看一下SelectableChannel的结构
SelectableChannel
|--AbstractSelectableChannel
|--SocketChannel
|--ServerSocketChannel
|--DatagramChannel
这就说明了只有网络IO中才可以用到Selector选择器。一个Selector可以监控多个SelectableChannel对象的IO状况。利用Selector可使一个单独的线程或者几个(比如几个线程分别处理读、写、接收事件)来管理多个Channel!!!!
(二)
调用通道的register向某个选择器注册,可以监听四种“事件”类型:连接、接收、读、写。
》分别表示,某个channel成功连接到另一个服务器称为”连接就绪“。
》一个ServerSocketChannel准备好接收新进入的连接称为”接收就绪“。
》一个有数据可读的通道可以说是”读就绪“。
》等待写数据的通道可以说是”写就绪“。
register方法会返回一个SelectionKey:表示SelectableChannel和Selector之间的注册关系,并将这个SelectionKey加入到Selector的keys集合中。可以通过SelectionKey.cancel()移出该集合。
调用Selector的select()返回一个int型的值,表示被Selector捕获到的就绪事件
使用NIO完成非阻塞式的网络通信(缓冲区+通道+选择器)(TCP方式)
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Date;
import java.util.Iterator;
import java.util.Scanner;
import org.junit.Test;
/**
* 模拟用NIO实现客户端向服务端发送数据,非阻塞式的(用到了通道+缓冲区+选择器Selector)
* @author huzangyi
*
*/
public class TestNonBlockingIO {
@Test
public void client() throws IOException{
//1.开启客户端通道
SocketChannel clientChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9000));
//2.设置为非阻塞式的
clientChannel.configureBlocking(false);
//3.分配一个缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//4.发送一条时间数据给服务端
// buffer.put(new Date().toString().getBytes());
// buffer.flip();
// clientChannel.write(buffer);
// buffer.clear();
Scanner scan = new Scanner(System.in);
while(scan.hasNext()){
String str = scan.next();
buffer.put(str.getBytes());
buffer.flip();
clientChannel.write(buffer);
buffer.clear();
}
//5.关闭通道
clientChannel.close();
}
@Test
public void server() throws IOException{
//1.开启服务端通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//2.切换到非阻塞式的
serverSocketChannel.configureBlocking(false);
//3.绑定指定端口
serverSocketChannel.bind(new InetSocketAddress(9000));
//4.获取Selector并向Selector注册,指定监听“接收”事件
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//5.轮询式的获取选择器上已经“准备就绪”的事件
while(selector.select() > 0){
//5.1.获取当前选择器中所有的已经注册的“选择键”
Iterator<SelectionKey> it = selector.selectedKeys().iterator();//所有被选择器捕获的key
while (it.hasNext()) {
SelectionKey selectionKey = it.next();
//5.2.根据不同的事件作出不同的处理
if (selectionKey.isAcceptable()) {
System.out.println("--有个通道连接就绪");
//若“接收就绪”,获取客户端连接通道
SocketChannel socketChannel = serverSocketChannel.accept();
//切换非阻塞
socketChannel.configureBlocking(false);
//将该通道注册到选择器上
socketChannel.register(selector, SelectionKey.OP_READ);//把“读就绪”选择键加入到了所有key列表l中
}else if(selectionKey.isReadable()){
System.out.println("--有个通道有数据可读");
//获取“读就绪”状态的通道
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
//分配buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取数据
int len = 0;
while((len = socketChannel.read(buffer))>0){
buffer.flip();
System.out.println(new String(buffer.array(),0,len));
buffer.clear();
}
//若cancel(),从列表l中移除这个Key
//selectionKey.cancel();
}
//5.3 取消这个选择键SelectionKey
it.remove();
}
}
}
}
转载知乎上一个很形象的比喻
作者:levin
链接:https://www.zhihu.com/question/32163005/answer/255238636
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
IO 多路复用是5种I/O模型中的第3种,对各种模型讲个故事,描述下区别:
故事情节为:老李去买火车票,三天后买到一张退票。参演人员(老李,黄牛,售票员,快递员),往返车站耗费1小时。
1.阻塞I/O模型
老李去火车站买票,排队三天买到一张退票。
耗费:在车站吃喝拉撒睡 3天,其他事一件没干。
2.非阻塞I/O模型
老李去火车站买票,隔12小时去火车站问有没有退票,三天后买到一张票。
耗费:往返车站6次,路上6小时,其他时间做了好多事。
3.I/O复用模型
老李去火车站买票,委托黄牛,然后每隔6小时电话黄牛询问,黄牛三天内买到票,然后老李去火车站交钱领票。
耗费:往返车站2次,路上2小时,黄牛手续费100元,打电话17次
2.epoll
老李去火车站买票,委托黄牛,黄牛买到后即通知老李去领,然后老李去火车站交钱领票。
耗费:往返车站2次,路上2小时,黄牛手续费100元,无需打电话
4.信号驱动I/O模型
老李去火车站买票,给售票员留下电话,有票后,售票员电话通知老李,然后老李去火车站交钱领票。
耗费:往返车站2次,路上2小时,免黄牛费100元,无需打电话
5.异步I/O模型
老李去火车站买票,给售票员留下电话,有票后,售票员电话通知老李并快递送票上门。
耗费:往返车站1次,路上1小时,免黄牛费100元,无需打电话
1同2的区别是:自己轮询
2同3的区别是:委托黄牛
3同4的区别是:电话代替黄牛
4同5的区别是:电话通知是自取还是送票上门