前言
Java NIO 是从jdk1.4版本开始引入的一个新的IO API,可以代替标准的JavaIO API.你可以称它为NEW IO亦或non-blocking IO,NIO 支持面向缓冲区,基于通道的IO操作,NIO以更加高效的方式进行文件的读写操作。
NIO与IO的区别
NIO | IO |
---|---|
面向缓冲区(Buffer) | 面向流(Stream) |
非阻塞IO(non-blocking io) | 阻塞IO(blocking io) |
选择器(Selectors) | 无 |
NIO的三个核心
- Buffer
- Channel
- Selectors
Buffer
缓冲区,用于存储数据,Buffer就像一个数组,可以保存多个相同类型的数据。根据数据类型不同(boolean 除外),有以下Buffer常用子类:
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
它们都采用相似的方法进行管理数据,都是通过调用静态方法获得Buffer对象:
static XxxxBuffer allocate(int capacity)
Buffer中重要的概念:
- capacity(容量):表示Buffer最大数据容量,缓存区容量不能为负,并且创建后不能更改。
- limit(限制):第一个不应该读取或写入的数据的索引,即位于limit后的数据不可读写。缓冲区的限制不能为负,并且不能大于其容量。
- position(位置):下一个要读取或写入的数据的索引。缓冲区的位置不能为负,并且不能大于其限制。
- mark(标记)和reset(重置):标记是一个索引,通过Buffer中的mark()方法指定Buffer中一个特定的position,之后可以通过调用reset()方法恢复到这个position。
Buffer的常用方法:
方法 | 描述 |
---|---|
Buffer clear() | 清空缓冲区并返回对缓冲区的引用 |
Buffer flip() | 将缓冲区的界限设置为当前位置,并将当前位置充值为 0 |
int capacity() | 返回 Buffer 的 capacity 大小 |
boolean hasRemaining() | 判断缓冲区中是否还有元素 |
int limit() | 返回 Buffer 的界限(limit) 的位置 |
Buffer limit(int n) | 将设置缓冲区界限为 n, 并返回一个具有新 limit 的缓冲区对象 |
Buffer mark() | 对缓冲区设置标记 |
int position() | 返回缓冲区的当前位置 position |
Buffer position(int n) | 将设置缓冲区的当前位置为 n , 并返回修改后的 Buffer 对象 |
int remaining() | 返回 position 和 limit 之间的元素个数 |
Buffer reset() | 将位置 position 转到以前设置的 mark 所在的位置 |
Buffer rewind() | 将位置设为 0, 取消设置的 mark |
缓冲区的数据操作:
Buffer的所有子类都提供了get()和put()方法进行数据操作
获取 Buffer 中的数据:
- get() :读取单个字节
- get(byte[] dst):批量读取多个字节到 dst 中
- get(int index):读取指定索引位置的字节(不会移动 position)
放入数据到 Buffer 中:
- put(byte b):将给定单个字节写入缓冲区的当前位置
- put(byte[] src):将 src 中的字节写入缓冲区的当前位置
- put(int index, byte b):将指定字节写入缓冲区的索引位置(不会移动position)
Channel
通道,用于运输数据(运输buffer),注意普通IO的流是单向的,Channel是双向的。
Java 为 Channel 接口提供的最主要实现类如下:
- FileChannel:用于读取、写入、映射和操作文件的通道。
- DatagramChannel:通过 UDP 读写网络中的数据通道。
- SocketChannel:通过 TCP 读写网络中的数据。
- ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。
获取通道:
- 过通道类的静态方法 open() 打开并返回指定通道
- 使用 Files 类的静态方法 newByteChannel() 获取字节通道
- 对支持通道的对象调用getChannel() 方法。
支持通道的的类有:FileInputStream, FileOutputStream,RandomAccessFile,DatagramSocket, Socket,ServerSocket
Selectors
选择器( Selector) 是 SelectableChannle 对象的多路复用器, Selector 可以同时监控多个 SelectableChannel 的 IO 状况,也就是说,利用 Selector可使一个单独的线程管理多个 Channel。 Selector 是非阻塞 IO 的核心。
SelectableChannel 的结构如下图:
选择器( Selector)的应用:
创建 Selector :通过调用 Selector.open() 方法创建一个 Selector
Select selector = Selector.open();
向选择器注册通道:SelectableChannel.register(Selector sel,int ops)
Socket socket = new Socket(InetAddress.getByName("127.0.0.1"),10086);
//获取SocketChannel
SocketChannel channel = socket.getChannel();
//创建选择器
Selector selector = Selector.open();
//将SocketChannel切换到非阻塞模式
channel.configureBlocking(false);
//向Selector 注册 Channel
SelectionKey key = channel.register(selector,SelectionKey.OP_READ);
注意当调用 register(Selector sel, int ops) 将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数 ops 指定。
可以监听的事件类型( 可使用 SelectionKey 的四个常量表示):
- 读 : SelectionKey.OP_READ ( 1)
- 写 : SelectionKey.OP_WRITE ( 4)
- 连接 : SelectionKey.OP_CONNECT ( 8)
- 接收 : SelectionKey.OP_ACCEPT ( 16)
若注册时不止监听一个事件,则可以使用“位或”操作符连接。
如:
int even = SelectionKey.OP_READ|SelectionKey.OP_WRITE;
SelectionKey:
表示 SelectableChannel 和 Selector 之间的注册关系。
每次向选择器注册通道时就会选择一个事件(选择键)。
选择键包含两个表示为整数值的操作集。
操作集的每一位都表示该键的通道所支持的一类可选择操作。
SelectionKey常用方法:
方 法 | 描 述 |
---|---|
int interestOps() | 获取感兴趣事件集合 |
int readyOps() | 获取通道已经准备就绪的操作的集合 |
SelectableChannel channel() | 获取注册通道 |
Selector selector() | 返回选择器 |
boolean isReadable() | 检测 Channal 中读事件是否就绪 |
boolean isWritable() | 检测 Channal 中写事件是否就绪 |
boolean isConnectable() | 检测 Channel 中连接是否就绪 |
boolean isAcceptable() | 检测 Channel 中接收是否就绪 |
Selector 的常用方法:
方法 | 描述 |
---|---|
Set 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() | 关闭该选择器 |
实战案例(简单的NIO Scoket通信)
客户端:
/**
* @author [email protected]
* @date 18-6-3 上午9:54
*/
public class NIOClient {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
socketChannel.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(1024);
String username = UUID.randomUUID().toString().replace("-","");
//发数据
Scanner cin = new Scanner(System.in);
while (cin.hasNext()) {
String content = cin.nextLine();
buffer.put(("["+LocalDateTime.now().toString() + "]" +username+"说: "+ content).getBytes());
buffer.flip();
socketChannel.write(buffer);
// socketChannel.shutdownOutput();
buffer.clear()
}
socketChannel.close();
}
}
服务器端:
/**
* @author [email protected]
* @date 18-6-3 上午9:54
*/
public class NIOServer {
public static void main(String[] args) throws IOException {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress(9898));
Selector selector = Selector.open();
//注册选择器
ssc.register(selector, SelectionKey.OP_ACCEPT);
//手动轮询
//当前线程阻塞
while (selector.select() > 0) {
//阻塞操作,获取选择器上已经"准备就绪"的事件,当至少有一个通道被选择时才返回
//获取多路选择器上的已选择的键集,这个键集是就绪状态的通道的集合,
// 还有一个方法叫keys()返回的是注册到这个多路复选器上的键,不管是就绪状态的还是非就绪状态的。
Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
while (keyIterator.hasNext()) {
SelectionKey sk = keyIterator.next();
if (sk.isAcceptable()) {//如果有连接
SocketChannel socketChannel = ssc.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (sk.isReadable()) {
new Thread(new NIOServerRead(sk)).start();
}
keyIterator.remove();
}
}
//关闭通道
//ssc.close();
}
}
class NIOServerRead implements Runnable {
//
SelectionKey selectionKey;
SocketChannel socketChannel;
public NIOServerRead(SelectionKey selectionKey) {
this.selectionKey = selectionKey;
socketChannel = (SocketChannel) selectionKey.channel();
}
@Override
public void run() {
ByteBuffer buffer = ByteBuffer.allocate(1024);
try {
int len = 0;
while ((len = socketChannel.read(buffer)) > 0) {
buffer.flip();
System.out.println(new String(buffer.array(), 0, len));
buffer.clear();
}
} catch (IOException e) {
selectionKey.cancel();
try {
socketChannel.close();
} catch (IOException e1) {
e1.printStackTrace();
}
e.printStackTrace();
}
}
}
运行结果: