java NIO
一、简介
1、I/O
I/O指的是计算机与外部世界,或者程序与计算机其他部分的接口,即输入/输出。
在JAVA中,通常都以流的方式完成I/O,通过一个Stream对象操作。这种操作方法是堵塞的,无法移动读取位置的(只能一直往下读,不能后退),并且效率较低。JAVA为了提高I/O效率,在1.4之后,推出了NIO。
2、NIO vs I/O
I/O | NIO |
面向流,一次读取一个或多个字节,在流中无法前后移动 | 面向缓存,读取的数据先统一放在缓存中,在缓存中能前后移动 |
堵塞,从流读取数据时,线程无法做其他事情 | 非堵塞,数据还未完整读取到缓存中时,线程可以先做其他事 |
一对一:一条线程负责一个数据操作任务 | 一对多,一条线程负责多个数据操作任务 |
二、主要概念
NIO要实现面向缓存的非堵塞数据读取,依赖"Channel(通道)"和"Buffer(缓冲区)";
NIO要实现一条线程管理多个数据操作,依赖"Selector(选择器)"。
1、Channel通道
定义
A channel represents an open connection to an entity such as a hardware device, a file, a network socket, or a program component that is capable of performing one or more distinct I/O operations, for example reading or writing.
即通过Channel,你可以和外部进行数据的读写。
虽然NIO的Channel和I/O的stream很像,不过还是有区别的:
- Channel总是面向缓存的,而流既可以一次读取一个或多个字节,也可以选择先把数据读到缓存中。
- Channel可读也可写,流只能读或者写。
- Channel可以异步读写
类型
根据Channel数据来源不同,Channel有不同的实现:
- FileChannel : A channel for reading, writing, mapping, and manipulating a file -> 用于文件的读写
- DatagramChannel : A selectable channel for datagram-oriented sockets -> 用于UDP的数据读写
- SocketChannel : A selectable channel for stream-oriented connecting sockets -> 用于TCP的数据读写
- ServerSocketChannel : A selectable channel for stream-oriented listening sockets -> 用于监听TCP连接请求,为每个请求再建立一个SocketChannel
I/O 和 NIO 数据操作对比
// I/O流操作 FileInputStream inputStream = new FileInputStream("io.txt"); byte[] data = new byte[1024]; while(inputStream.read(data) != -1){ // 从流中读取多个字节,进行操作 }
// NIO channel操作 // 从文件流中获取channel FileInputStream inputStream = new FileInputStream("nio.txt"); FileChannel fileChannel = inputStream.getChannel(); // 新建缓存,用于存放Channel读取的数据 ByteBuffer buffer = ByteBuffer.allocate(1024); // 通过Channel读取数据到缓存中 fileChannel.read(buffer);
Channel间数据传输
对于FileChannel之间,利用transferFrom和transferTo可以直接进行数据传输,提高性能。
将fromChannel中数据传输到toChannel 中,position指toChannel中开始的位置,count指接收的数据:
toChannel.transferFrom(fromChannel, position, count) 或
fromChannel.transferTo(position, count, toChannel)
2、buffer缓冲区
定义
A container for data of a specific primitive type.
A buffer is a linear, finite sequence of elements of a specific primitive type. Aside from its content, the essential properties of a buffer are its capacity, limit, and position.
buffer缓冲区是一块内存,用于存放原始类型数据。它的特点是:线性,有限,有序,只能存一种原始类型数据。
属性
- capacity : A buffer's capacity is the number of elements it contains. -> 缓冲区大小
- limit : A buffer's limit is the index of the first element that should not be read or written. -> 用于标记读写的边界。读模式,limit表示目前缓冲区内存放的数据量。写模式,limit表示缓冲区最大的存放数,等于capacity.
- position: A buffer's position is the index of the next element to be read or written. -> 标记目前读或写的位置
类型
根据buffer中存放的原始类型不同,有以下几种Buffer实现
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
- ByteBuffer
- CharBuffer
基本用法
- 调用allocate()分配大小
- channel写数据到buffer
- 调用flip() -> buffer默认是写模式,当要从Buffer中读数据时,需要手动调flip方法,转为读模式。(这时,limit置为position,position置为0)
- 从buffer中读数据
- 当要重新往buffer中写数据时,需要调用clear()或compact方法。
其他方法介绍
- clear : 将position置为0, limit置为capacity,清空buffer。对于还未读取的数据,直接丢失。
- compact:先将未读的数据复制到缓存的开头,position置到未读数据的后一位,limit置为capacity。
- rewind: 将position重置为0,这样我们可以重读Buffer数据
- mark和reset:用mark标记此时的posiiton,之后移动position。调用reset,会将position重置为mark标记的地方。
3、Selector选择器
定义
A multiplexor of SelectableChannel objects.
一个Selector管理多个可管理的Channel通道,它主要用于检查它负责的Channel通道的状态。
如下是一个线程通过使用selector管理多个Channel通道的示意图:
使用Selector管理多个Channel通道的过程如下
- 创建Selector
- 将Channel通道注册到Selector上(可以指定让Selector检查的状态:可读、可写、可连接、是否合法、是否已连接等),此时会返回一个Channel对应的唯一KEY对象
- 通过Selector查询处于就绪状态的KYE集合
- 遍历KEY集合,从每个KEY获取到Channel通道,进行相应的处理。
使用详解
创建Selector
Selector是由SelectorProvider创建出来的。具体有两种方式创建:
- Selector selector = Selector.open(); 这种方式将调用默认的SelectorProvider来创建Selector
- 自己实现SelectorProvider的openSelector()方法,用于创建Selector
注册Channel通道到Selector上
Selector要管理Channel,必须先将Channel注册到Selector上。只有SelectableChannel类才能用Selector管理。注册的方法有两个:
- SelectionKey register(Selector sel, int ops)
- SelectionKey register(Selector sel, int ops, Object att)
方法说明
- 当SelectableChannel的register被其他线程调用,或者SelectableChannel正在运行configureBlocking方法,那么调用都会block。
- 方法会同步该Channel在Selector上感兴趣的key集合,因此并行调用会导致block。
- 当Selector处于closed状态,或者Channel处于block状态时,都会抛出异常。因此在调用register之前,一般会先调用下channel.configureBlocking(false);
参数说明:
- ops是Selector关注的Channel事件,有SelectionKey.OP_READ、SelectionKey.OP_WRITE、SelectionKey.OP_CONNECT、SelectionKey.OP_ACCEPT四种。如果对多个事件感兴趣可利用位的或运算结合多个常量,比如:int ops = SelectionKey.OP_READ | SelectionKey.OP_WRITE
- sel是Channel注册到的Selector
- att: 可以在注册时,多传一个对象,一般用于记录上下文信息
返回值说明
SelectionKey用于记录Channel通道与Selector之间的关系。它有以下几个重要的方法
- int interestOps(). 返回Selector对于Channel的关注集合。我们可以通过与SelectionKey的四种事件进行&,来判断是否关注该事件。比如:
boolean isInterestedInAccept = interestSet & SelectionKey.OP_ACCEPT;
- int readyOps(). 返回处于就绪状态的事件。通过与四种事件进行&操作,即可知道哪种事件已就绪。当然也可以通过直接调用isAcceptable(),isConnectable(),isReadable(),isWritable()来直接获取就绪状态。
- channel() : 获取key上的Channel通道
- selector() : 获取key上的Selector
- attach(Object ob) : 附加一个对象到key上,也可在register时带上。这个对象可以记录一些附加信息,后面通过调用attachment()方法把对象重新取出。
从Selector上选择Channel
先调用select方法获取处于就绪状态的Channel数量,当数量大于0时,再调用selectedKeys方法获取处于就绪状态的SelectionKey集合。
select方法有以下三个:
- int select() : 返回处于就绪状态的Channel通道数量,当没有Channel通道就绪时,该方法block。
- int select(long timeout) : 返回就绪状态的Channel通道数量,没有就绪的Channel则block, 但超过timeout返回。
- int selectNow() : 不会block,立即返回。
Set<SelectionKey> selectedKeys() : 返回SelectionKey集合。
因select()而block的线程,另外一个线程可以调用该selector的wakeup()来唤醒。
selector使用完后,需要调用close()方法关闭。
完整示例
Selector selector = Selector.open(); channel.configureBlocking(false); SelectionKey key = channel.register(selector, SelectionKey.OP_READ); while(true) { int readyChannels = selector.select(); if(readyChannels == 0) continue; Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> keyIterator = selectedKeys.iterator(); while(keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if(key.isAcceptable()) { // a connection was accepted by a ServerSocketChannel. } else if (key.isConnectable()) { // a connection was established with a remote server. } else if (key.isReadable()) { // a channel is ready for reading } else if (key.isWritable()) { // a channel is ready for writing } keyIterator.remove(); } }
三、总结
NIO允许我们只用一条线程来管理多个通道(网络连接或文件),随之而来的代价是解析数据相对于阻塞流来说可能会变得更加的复杂。
如果你需要同时管理成千上万的链接,这些链接只发送少量数据,例如聊天服务器,用NIO来实现这个服务器是有优势的。或者,如果你需要维持大量的链接,例如P2P网络,用单线程来管理这些 链接也是有优势的。这种单线程多连接的设计可以用下图描述:
如果链接数不是很多,但是每个链接的占用较大带宽,每次都要发送大量数据,那么使用传统的IO设计服务器可能是最好的选择。下面是经典IO服务设计图: