传统的 IO 流都是阻塞式的。也就是说,当一个线程调用 read() 或 write()时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务。因此,在完成网络通信进行 IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量客户端时,性能急剧下降。
Java NIO 是非阻塞模式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。因此, NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。
本文着重介绍NIO在阻塞和非阻塞两种方式下如何工作,以及什么是选择器?它与通道之间是什么关系?
阻塞模式与非阻塞模式
传统的阻塞IO方式:客户端向服务器端发送读写请求,服务器端便开始监听客户端的数据是否到来。再次期间客户端准备自己的数据,而服务器端就一直这样不得抽身地干等着。即使服务器端是多线程,线程的数量也是有限的,而且有时一味单纯增加线程数,只会让阻塞的线程原来越多。
NIO的非阻塞方式:
将用于传输的通道全都注册到选择器上
选择器的作用是:监控这些通道的IO状况
(什么状况?读、写、连接、接收数据)
选择器和通道的关系
选择器和通道的关系:通道注册到选择器上,选择器监控通道。
当某一个通道上,某一个事件准备就绪时,那么选择器才会将这个通道分配到服务器端一个或多个线程上,再继续运行。比如说当客户端发送一些数据给服务器端,只有当客户端的所有数据都准备就绪时,选择器才会将这个注册的通道分配到服务器端的一个或者多个线程上。
那就意味这,如果客户端的线程没有将数据准备就绪时,服务器端的线程可以执行其他任务,而不必阻塞在那里。
打个比方来说,原先的传统的阻塞IO模式,相当于你没有手机等快递,算准了EMS每天中午1:00会到你们公司门口,所以你12:50在那里等着他们来,在这10分钟里你被这件事情阻塞着,什么事情都做不了,真是浪费时间;而NIO的这种通道注册选择器,选择器监控通道,等到数据准备就绪才会占用服务器线程的非阻塞IO方式,更像是带着手机等外卖,我在饿了么注册了一个用户(通道在选择器上注册了),然后定完外卖就忙自己的去了,等到外卖送来之后我接到电话下去取就可以了。
选择器( Selector)与SelectableChannle
选择器( Selector) 是 SelectableChannle 对象的多路复用器, Selector 可以同时监控多个 SelectableChannel 的 IO 状况,也就是说,利用 Selector可使一个单独的线程管理多个 Channel。 Selector 是非阻塞 IO 的核心。
SelectableChannle 的结构如下图(注意:FileChannel不是可作为选择器复用的通道!FileChannel不能注册到选择器Selector!FileChannel不能切换到非阻塞模式!FileChannel不是SelectableChannel的子类!)
选择器的使用方法
创建 Selector :通过调用 Selector.open() 方法创建一个 Selector
向选择器注册通道: SelectableChannel.register(Selector sel, int ops)
当调用 register(Selector sel, int ops) 将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数 ops 指定。
可以监听的事件类型( 可使用 SelectionKey 的四个常量表示):
读 : SelectionKey.OP_READ ( 1)
写 : SelectionKey.OP_WRITE ( 4)
连接 : SelectionKey.OP_CONNECT ( 8)
接收 : SelectionKey.OP_ACCEPT ( 16)
若注册时不止监听一个事件,则可以使用“位或”操作符连接。
这些常量我么诚挚为选择键。
选择键(SelectionKey)
SelectionKey: 表示 SelectableChannel 和 Selector 之间的注册关系。每次向选择器注册通道时就会选择一个事件(选择键)。 选择键包含两个表示为整数值的操作集。操作集的每一位都表示该键的通道所支持的一类可选择操作。
方 法 | 描 述 |
int interestOps() | 获取感兴趣事件集合 |
int readyOps() | 获取通道已经准备就绪的操作的集合 |
SelectableChannel channel() | 获取注册通道 |
Selector selector() | 返回选择器 |
boolean isReadable() | 检测 Channal 中读事件是否就绪 |
boolean isWritable() | 检测 Channal 中写事件是否就绪 |
boolean isConnectable() | 检测 Channel 中连接是否就绪 |
boolean isAcceptable() | 检测 Channel 中接收是否就绪 |
Set<SelectionKey> keys() | 所有的 SelectionKey 集合。代表注册在该Selector上的Channel |
selectedKeys() | 被选择的 SelectionKey 集合。返回此Selector的已选择键集 |
int select() | 监控所有注册的Channel,当它们中间有需要处理的 IO 操作时, 该方法返回,并将对应得的 SelectionKey 加入被选择的 SelectionKey 集合中,该方法返回这些 Channel 的数量。 |
int select(long timeout) | 可以设置超时时长的 select() 操作 |
int selectNow() | 执行一个立即返回的 select() 操作,该方法不会阻塞线程 |
Selector wakeup() | 使一个还未返回的 select() 方法立即返回 |
void close() | 关闭该选择器 |
SocketChannel
Java NIO中的SocketChannel是一个连接到TCP网络套接字的通道。
操作步骤:
打开 SocketChannel
读写数据
关闭 SocketChannel
Java NIO中的 ServerSocketChannel 是一个可以监听新进来的TCP连接的通道,就像标准IO中的ServerSocket一样。
这里有几个要点需要注意:
FileChannel不能使用非阻塞模式!!!
所以FileChannel不能切换到非阻塞模式。关于tFileChannel的其他要点,我忘记之前的文章中有没有提到,这里做个补充吧!前面通道间直接传输(即调用通道的transferFrom和transferTo方法),必须有一个是FileChannel。
想想也应该明白,读还好说,同一个本地文件如何同时写?阻塞是必然的。
网络NIO示例
阻塞IO模式:客户端向服务端发送文件
package com.happybks.nio.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Paths;
import java.nio.file