一、基本概念
1. 概念
1)java.nio全称java non-blocking IO,是指jdk1.4 及以上版本里提供的新api(New IO) ,为所有的原始类型(boolean类型除外)提供缓存支持的数据容器,使用它可以提供非阻塞式的高伸缩性网络
2) NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector。传统IO基于字节流和字符流进行操作,而NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道
2. NIO与传统IO的区别
1)NIO和传统IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的
2)传统的IO又称BIO,即阻塞式IO,NIO就是非阻塞IO(non-blocking IO)
3)线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)
3. 同步与异步
1)同步就是:如果有多个任务或者事件要发生,这些任务或者事件必须逐个地进行,一个事件或者任务的执行会导致整个流程的暂时等待,这些事件没有办法并发地执行
2)异步就是:如果有多个任务或者事件发生,这些事件可以并发地执行,一个事件或者任务的执行不会导致整个流程的暂时等待
4. 阻塞与非阻塞
1)阻塞就是:当某个事件或者任务在执行过程中,它发出一个请求操作,但是由于该请求操作需要的条件不满足,那么就会一直在那等待,直至条件满足
2)非阻塞就是:当某个事件或者任务在执行过程中,它发出一个请求操作,如果该请求操作需要的条件不满足,会立即返回一个标志信息告知条件不满足,不会一直在那等待
也就是说阻塞和非阻塞的区别关键在于当发出请求一个操作时,如果条件不满足,是会一直等待还是返回一个标志信息
5. 阻塞IO与非阻塞IO
1)一个完整的IO读请求操作包括两个阶段:
(1)查看数据是否就绪
(2)进行数据拷贝(内核将数据拷贝到用户线程)
2)对比
(1)当用户线程发起一个IO请求操作(本文以读请求操作为例),内核会去查看要读取的数据是否就绪,对于阻塞IO来说,如果数据没有就绪,则会一直在那等待,直到数据就绪
(2)对于非阻塞IO来说,如果数据没有就绪,则会返回一个标志信息告知用户线程当前要读的数据没有就绪
(3)阻塞(blocking IO)和非阻塞(non-blocking IO)的区别就在于第一个阶段,如果数据没有就绪,在查看数据是否就绪的过程中是一直等待,还是直接返回一个标志信息
3)说明
(1)Java中传统的IO都是阻塞IO,比如通过socket来读数据,调用read()方法之后,如果数据没有就绪,当前线程就会一直阻塞在read方法调用那里,直到有数据才返回
(2)如果是非阻塞IO的话,当数据没有就绪,read()方法应该返回一个标志信息,告知当前线程数据没有就绪,而不是一直在那里等待
6. 同步IO与异步IO
1)对比
(1)同步IO即 如果一个线程请求进行IO操作,在IO操作完成之前,该线程会被阻塞
(2)而异步IO为 如果一个线程请求进行IO操作,IO操作不会导致请求线程被阻塞
2)说明
(1)对于同步IO:当用户发出IO请求操作之后,如果数据没有就绪,需要通过用户线程或者内核不断地去轮询数据是否就绪,当数据就绪时,再将数据从内核拷贝到用户线程;
(2)而异步IO:只有IO请求操作的发出是由用户线程来进行的,IO操作的两个阶段都是由内核自动完成,然后发送通知告知用户线程IO操作已经完成。也就是说在异步IO中,不会对用户线程产生任何阻塞
(3)这是同步IO和异步IO关键区别所在,同步IO和异步IO的关键区别反映在数据拷贝阶段是由用户线程完成还是内核完成。所以说异步IO必须要有操作系统的底层支持
二、五种IO模型
1. 阻塞IO模型
(1)最传统的一种IO模型,即在读写数据过程中会发生阻塞现象
(2)当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除block状态
(3)典型的阻塞IO模型的例子为:data = socket.read(); 如果数据没有就绪,就会一直阻塞在read方法。
2. 非阻塞IO模型
(1)当用户线程发起一个read操作后,并不需要等待,而是马上就得到了一个结果
(2)如果结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回
(3)事实上,在非阻塞IO模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU
(4)典型模型:
while(true){
data = socket.read();
if(data!= error){
处理数据
break;
}
}
3. 多路复用IO模型
(1)多路复用IO模型是目前使用得比较多的模型。Java NIO实际上就是多路复用IO
(2)在多路复用IO模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作
(3)因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket读写事件进行时,才会使用IO资源,所以它大大减少了资源占用
(4)在Java NIO中,是通过selector.select()去查询每个通道是否有到达事件,如果没有事件,则一直阻塞在那里,因此这种方式会导致用户线程的阻塞
(5)多路复用IO为何比非阻塞IO模型的效率高是因为在非阻塞IO中,不断地询问socket状态时通过用户线程去进行的,而在多路复用IO中,轮询每个socket状态是内核在进行的,这个效率要比用户线程要高的多
(6)多路复用IO模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件逐一进行响应。因此对于多路复用IO模型来说,一旦事件响应体很大,那么就会导致后续的事件迟迟得不到处理,并且会影响新的事件轮询
4. 信号驱动IO模型
(1)在信号驱动IO模型中,当用户线程发起一个IO请求操作,会给对应的socket注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作
5. 异步IO模型
(1)异步IO模型才是最理想的IO模型,在异步IO模型中,当用户线程发起read操作之后,立刻就可以开始去做其它的事
(2)从内核的角度,当它受到一个asynchronous read之后,它会立刻返回,说明read请求已经成功发起了,因此不会对用户线程产生任何block
(3)然后,内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它read操作完成了
(4)也就说用户线程完全不需要实际的整个IO操作是如何进行的,只需要先发起一个请求,当接收内核返回的成功信号时表示IO操作已经完成,可以直接去使用数据了
(6)也就说在异步IO模型中,IO操作的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完成,然后发送一个信号告知用户线程操作已完成。用户线程中不需要再次调用IO函数进行具体的读写
(7)注意,异步IO是需要操作系统的底层支持,在Java 7中,提供了Asynchronous IO
前面四种IO模型实际上都属于同步IO,只有最后一种是真正的异步IO,因为无论是多路复用IO还是信号驱动模型,IO操作的第2个阶段都会引起用户线程阻塞,也就是内核进行数据拷贝的过程都会让用户线程阻塞
三、NIO组件

1. Selector
1)概念
(1)仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道。对于操作系统来说,线程之间上下文切换的开销很大,而且每个线程都要占用系统的一些资源(如内存)。因此,使用的线程越少越好
(2)当像Selector注册Channel时,Channel.register()方法会返回一个SelectionKey 对象。这个对象代表了注册到该Selector的通道(其实就是一个桥梁)
2)常用方法
(1)创建Selector
Selector selector = Selector.open()
(2)向Selector注册通道
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, Selectionkey.OP_READ);
与Selector一起使用时,Channel必须处于非阻塞模式下。这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式。而套接字通道都可以
3)监听的事件类型
(1)Connect SelectionKey.OP_CONNECT 某个channel成功连接到另一个服务器称为“连接就绪”
(2)Accept SelectionKey.OP_ACCEPT 一个server socket channel准备好接收新进入的连接称为“接收就绪”
(3)Read SelectionKey.OP_READ 有数据可读的通道可以说是“读就绪”
(4)Write SelectionKey.OP_WRITE 等待写数据的通道可以说是“写就绪”
4) 检测channel事件就绪
(1)selectionKey.isAcceptable();
(2)selectionKey.isConnectable();
(3)selectionKey.isReadable();
(4)selectionKey.isWritable();
2. Channel
1)概念
(1)既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的
(2)通道可以异步地读写
(3)通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入
2)通道类型
(1)FileChannel FileChannel 从文件中读写数据
(2)DatagramChannel DatagramChannel 能通过UDP读写网络中的数据
(3)SocketChannel 能通过TCP读写网络中的数据
(4)ServerSocketChannel 可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel
3. Buffer

1)概念
(1)Java NIO中的Buffer用于和NIO通道进行交互。如你所知,数据是从通道读入缓冲区,从缓冲区写入到通道中的
(2)缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存
(3)当向buffer写入数据时,buffer会记录下写了多少数据。一旦要读取数据,需要通过flip()你好方法将Buffer从写模式切换到读模式。在读模式下,可以读取之前写入到buffer的所有数据
(4)一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用clear()或compact()方法。clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面
2)使用Buffer读写数据一般遵循以下四个步骤:
(1)写入数据到Buffer
(2)调用flip()方法
(3)从Buffer中读取数据
(4)调用clear()方法或者compact()方法
2)常用方法
(1)Buffer的分配(字节)
ByteBuffer buf = ByteBuffer.allocate(1024);
(2)向Buffer中写数据
int bytesRead = inChannel.read(buf); 从Channel写到Buffer
buf.put(127); 通过Buffer的put()方法写到Buffer里
(3)flip()方法(flip方法将Buffer从写模式切换到读模式。调用flip()方法会将position设回0,并将limit设置成之前position的值)
buf.flip();
(4)从Buffer中读取数据
int bytesWritten = inChannel.write(buf); 从Buffer读取数据到Channel
byte aByte = buf.get(); 使用get()方法从Buffer中读取数据
3)Buffer的capacity,position和limit
(1)capacity
a. 作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”.你只能往里写capacity个byte、long,char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据
(2)position
a. 当你写数据到Buffer中时,position表示当前的位置。初始的position值为0.当一个byte、long等数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1.
b. 当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0. 当从Buffer的position处读取数据时,position向前移动到下一个可读的位置
(3)limit
a. 在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity
b. 当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)
四、代码示例
1. TCP服务端
public class MyNioServer {
private Selector selector;
private final static int PORT = 8686;
private final static int BUF_SIZE = 10240;
private void initServer() throws IOException, InterruptedException {
// 创建通道管理器对象selector
this.selector = Selector.open();
// 创建一个通道channel
ServerSocketChannel channel = ServerSocketChannel.open();
// 将通道设置为非阻塞
channel.configureBlocking(false);
// 将通道绑定在8686端口
channel.socket().bind(new InetSocketAddress(PORT));
// 将通道管理器和通道绑定,并为该通道注册OP_ACCEPT事件
// 注册事件后,当该事件到达时,selector.select()会返回(一个key),如果该事件没到达selector.select()会一直阻塞
SelectionKey selectionKey = channel.register(selector, SelectionKey.OP_ACCEPT);
// 轮询
while(true){
// 这是一个阻塞方法,一直等待知道有数据到达,返回值是key的数量(可以有多个)
selector.select();
// 如果channel有数据了,将生成的key放入keys集合中
Set<SelectionKey> keys = selector.selectedKeys();
Iterator iterator = keys.iterator();
// 使用迭代器遍历集合
while(iterator.hasNext()){
// 得到集合中的一个实例
SelectionKey key = (SelectionKey)iterator.next();
// 拿到当前key实例之后记得在迭代器中奖这个元素删除,非常重要,否则会出错
iterator.remove();
// 判断当前key所代表的channel是否在Acceptable状态,如果是就进行接收
if(key.isAcceptable()){
System.out.println("****** 接收了");
doAccept(key);
}else if(key.isReadable()){
System.out.println("****** 读了");
doRead(key);
}else if(key.isWritable() && key.isValid()){
System.out.println("****** 写了");
doWrite(key);
}else if(key.isConnectable()){
System.out.println("连接成功");
}
}
}
}
public void doAccept(SelectionKey key) throws IOException {
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
System.out.println("ServerSocketChannel正在循环监听");
// ServerSocketChannel可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel
SocketChannel clientChannel = channel.accept();
clientChannel.configureBlocking(false);
clientChannel.register(key.selector(), SelectionKey.OP_READ);
}
public void doRead(SelectionKey key) throws IOException, InterruptedException {
SocketChannel clientChannel = (SocketChannel)key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(BUF_SIZE);
long bytesRead = clientChannel.read(byteBuffer);
while (bytesRead > 0){
byteBuffer.flip();
byte[] data = byteBuffer.array();
String info = new String(data).trim();
System.out.println("从客户端发送过来的消息是:"+ info);
byteBuffer.clear();
bytesRead = clientChannel.read(byteBuffer);
}
String toClient = "客户端你好呀,我是服务端, hahahahahaha";
byteBuffer.put(toClient.getBytes("UTF-8"));
// 这里如果删掉了,表明position没有在读模式下归0,还是写模式下的position,从而导致channel从buffer读取的时候是空的
byteBuffer.flip();
clientChannel.write(byteBuffer);
byteBuffer.clear();
}
public void doWrite(SelectionKey key) throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocate(BUF_SIZE);
String info = "客户单你好呀,我是服务端, hahahahahaha";
byteBuffer.put(info.getBytes("UTF-8"));
byteBuffer.flip();
SocketChannel clientChannel = (SocketChannel)key.channel();
while(byteBuffer.hasRemaining()){
clientChannel.write(byteBuffer);
}
byteBuffer.compact();
}
public static void main(String[] args) throws IOException, InterruptedException {
MyNioServer myNioServer = new MyNioServer();
myNioServer.initServer();
}
}
2. TCP客户端
public class MyNioClient {
// 创建一个选择器
private Selector selector;
private final static int PORT = 8686;
private final static int BUF_SIZE = 10240;
private static ByteBuffer byteBuffer = ByteBuffer.allocate(BUF_SIZE);
private void initClient() throws IOException, InterruptedException {
this.selector = Selector.open();
SocketChannel clientChannel = SocketChannel.open();
clientChannel.configureBlocking(false);
clientChannel.connect(new InetSocketAddress(PORT));
clientChannel.register(selector, SelectionKey.OP_CONNECT);
while(true){
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while(iterator.hasNext()){
SelectionKey key = iterator.next();
iterator.remove();
if(key.isConnectable()){
System.out.println("客户端连接了");
doConnect(key);
}else if(key.isReadable()){
System.out.println("客户端开始读了");
doRead(key);
}
}
}
}
public void doConnect(SelectionKey key) throws IOException, InterruptedException {
SocketChannel clientChannel = (SocketChannel) key.channel();
if(clientChannel.isConnectionPending()){
clientChannel.finishConnect();
}
clientChannel.configureBlocking(false);
String info = "服务端你好,我是客户端,啦啦啦";
byteBuffer.put(info.getBytes("UTF-8"));
byteBuffer.flip();
clientChannel.write(byteBuffer);
byteBuffer.clear();
clientChannel.register(selector, SelectionKey.OP_READ);
}
public void doRead(SelectionKey key) throws IOException, InterruptedException {
SocketChannel clientChannel = (SocketChannel) key.channel();
clientChannel.read(byteBuffer);
byte[] data = byteBuffer.array();
String msg = new String(data).trim();
System.out.println("服务端发送消息" + msg);
}
public static void main(String[] args) throws IOException, InterruptedException {
MyNioClient myNioClient = new MyNioClient();
myNioClient.initClient();
}
}
参考网址
注:文章是经过参考其他的文章然后自己整理出来的,有可能是小部分参考,也有可能是大部分参考,但绝对不是直接转载,觉得侵权了我会删,我只是把这个用于自己的笔记,顺便整理下知识的同时,能帮到一部分人。
ps : 有错误的还望各位大佬指正,小弟不胜感激
本文深入讲解Java NIO的基本概念、核心组件及工作原理,对比传统IO模型,介绍五种IO模型,并通过示例代码演示NIO的应用。
1584

被折叠的 条评论
为什么被折叠?



