NIO的几个基础概念
Channel(通道),Buffer(缓冲区),Selector(选择器)
1.Channel
1>通道,为某sb提供了了渠道。而下面的例子中InputStream实际上就是读取文件的通道
1 public class Test { 2 3 public static void main(String[] args) throws IOException { 4 5 File file = new File("data.txt"); 6 7 InputStream inputStream = new FileInputStream(file); 8 9 byte[] bytes = new byte[1024]; 10 11 //read(byte[] b) : 从输入流中读取一定数量的字节,并将其存储在缓冲区数组 b 中。 12 13 //返回实际读取的整数形式字节数。 14 15 inputStream.read(bytes); 16 17 inputStream.close(); 18 19 } 20 21 }
如此,我们将NIO中的Channel同传统IO的Stream来类比。不同的是,IO中,Stream是单向的,比如InputStream只能进行读取操作,OutputStream只能进行写操作。而NIO中的Channel是双向的,既可以进行读操作,又可以进行写操作。
2>以下是常用的几种通道:
FileChannel | 读写本地文件的数据,不支持Selector控制,对应File类 |
SocketChannel | 通过TCP读写网络中的数据,对应Socket类 |
ServerSocketChannel | 监听新的TCP连接,并且会创建一个可读写的SocketChannel,对应ServerSocket类 |
DatagramChannel | 通过UDP读写网络中的数据,对应DatagramSocket类 |
3>Channel方法
int read(ByteBuffer dst) | 从Channel到中读取数据到ByteBuffer |
long read(ByteBuffer[] dsts) | 将Channel到中的数据“分散”到ByteBuffer[] |
int write(ByteBuffer src) | 将ByteBuffer到中的数据写入到Channel |
long write(ByteBuffer[] srcs) | 将ByteBuffer[]到中的数据“聚集”到Channel |
ong position() | 返回此通道的文件位置 |
FileChannel position(long p) | 设置此通道的文件位置 |
long size() | 返回此通道的文件的当前大小 |
FileChannel truncate(long s) | 将此通道的文件截取为给定大小 |
void force(boolean metaData) | 强制将所有对此通道的文件更新写入到存储设备中 |
4>主要获取方式
java针对支持通道的类提供了getChannel()方法
FileInputStream fis = new FileInputStream("D:\1.jpg");
FileChannel inChannel = fis.getChannel();
jdk1.7的nio2只对各个通道提供了一个静态方法open()
FileChannel inChannel = FileChannel.open(Paths.get("D:\1.jpg"),
StandardOpenOption.READ);
5>通道之间的数据传输
read&write
//将 Buffer 中数据写入 Channel
outChannel.write(buff)
//从 Channel 读取数据到 Buffer
inChannel.read(buff)
transferFrom
从源信道读取字节到这个通道的文件中。
1 //复制图片,利用直接缓存区 2 3 public void test() throws Exception{ 4 5 FileChannel inChannel = FileChannel.open(Paths.get("D:\1.jpg"), StandardOpenOption.READ); 6 7 FileChannel outChannel = FileChannel.open(Paths.get("D:\2.jpg"), StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE); 8 9 outChannel.transferFrom(inChannel,0, inChannel.size()); 10 11 inChannel.close(); 12 13 outChannel.close(); 14 15 }
transferTo
将字节从这个通道的文件传输到给定的可写字节通道。
1 //复制图片,利用直接缓存区 2 3 public void test2() throws Exception{ 4 5 FileChannel inChannel = FileChannel.open(Paths.get("D:\1.jpg"), StandardOpenOption.READ); 6 7 FileChannel outChannel = FileChannel.open(Paths.get("D:\3.jpg"), StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE); 8 9 inChannel.transferTo(0, inChannel.size(), outChannel); 10 11 inChannel.close(); 12 13 outChannel.close(); 14 15 } 16 17 }
2.Buffer
1>缓冲区,是NIO中非常重要的东西,NIO中所有数据的读和写都离不开Buffer。类似于,上述代码中,读取数据时放在byte数组中,而NIO中,读取的数据只能放在Buffer中,同样的写入数据也是写入到Buffer中。
2>下图是客户端向服务端发送数据,然后服务端接收数据的过程。
3>Buffer的属性:
capacity | 容量 | Buffer所能够存放的最大容量 |
position | 位置 | 下一个被读或写的位置 |
limit | 上界 | 可供读写的最大位置,用于限制position position < limit |
mark | 标记 | 标记位置,用于记录某次读写的位置,可以通过reset()方法回到这里 |
4>Buffer类型
除了boolean类型意外每个基元类型都会有缓冲区。主要讲解的是ByteBuffer.这里大概说一下,Buffer是一个抽象类,包括一个Buffer的最基本属性,比如,容量,位置,上界,标记.子类通过调用父类构造方法来实例化这几个参数,子类也都有各自的容量实现.比如ByteBuffer类用字节数组当缓冲区.旗下又有两个自己的实现类.
类型 | 缓冲区 |
byte | ByteBuffer |
char | CharBuffer |
double | DoubleBuffer |
float | FloatBuffer |
int | IntBuffer |
long | LongBuffer |
short | ShortBuffer |
5>Buffer的方法
方法 | 说明 |
position | 移动偏移量指针 |
limit | 移动限制大小指针 |
mark | 打标记,寄了当前偏移量的位置。可使用reset恢复到标记位置 |
reset | 恢复到标记位置 |
clear | 初始化指针,清理所有数据,转换为写模式(实际只是偏移指针,数据还在) |
flip | 转换为读取模式 byteBuffer.flip(); |
rewind | 重置偏移量指针到初始状态,可以重新写入或重新读取 |
remaining | 可读或可写容量 |
hasRemaining | 是否可读或可写 |
hasArray | 是否有数组缓存,若为堆缓冲区,则会有数据缓存,若为直接缓冲区,则没有 |
offset | 当前数组偏移量,当把当前数组切片时,无需复制内存,直接指向偏移量。 |
为了更清晰的说明缓冲区的功能,接下来以ByteBuffer举例(各类型的功能上基本大同小异)
方法 | 说明 |
allocate | 申请堆缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocate(8); |
allocateDirect | 申请直接缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocateDirect(8); |
wrap | 将字节数组包在缓冲区中,可以理解为将字节数组转换为字节堆缓冲区 |
slice | 缓冲区切片,当前偏移量到当前限制大小的内存生成一个缓冲区,无需复制内存,直接指向偏移量 |
duplicate | 共享一份缓冲区,缓冲区内容修改会互相影响,读取不影响 |
asReadOnlyBuffer | 拷贝一份只读的缓冲区 |
ix | 根据实际的offset偏移,对于外部来说是透明的,比如缓冲区切片之后,生成新的缓冲区实际是同一片内存的缓冲区存在offset偏移量,对切片后的缓冲区读写都会做便宜操作。 |
compact | 初始化指针,清理已读取数据,转换为写模式(实际只是偏移指针position,数据还在)此时将data初始化,会将未读取的2个字节复制到数组头部,同时转换为写模式。 |
getXXX | 读取数据 byte[] data1 = new byte[3]; byteBuffer.get(data1); |
putXXX | 写入数据 byte[] data = new byte[] {'H','E','L','L','O'}; byteBuffer.put(data); |
asXXXBuffer | 转换为指定类型的缓冲区,字节缓冲区可以转换为其他基元类型的缓冲区,其他基元类型缓冲区不能反过来转换 |
总结:NIO通过引入缓冲区的概念使得对字节操作比传统字节操作方便一些,但是读写模式需要来回转换会让人有点头晕。
3.Selector
1>选择器,多路复用器。NIO的核心之一。Selector的作用就是用来轮询每个Channel的状态是否处于可读、可写。一旦发现Channel有注册的事情发生,便获取事件然后进行处理。如此可以实现单线程管理多个channels,也就是可以管理多个网络连接。
2>Selector的创建过程
// 1.创建Selector
Selector selector = Selector.open();
// 2.将Channel注册到选择器中
// ....... new channel的过程 ....
//Notes:channel要注册到Selector上就必须是非阻塞的,所以FileChannel是不可以使用Selector的,因为FileChannel是阻塞的
channel.configureBlocking(false);
// 第二个参数指定了我们对 Channel 的什么类型的事件感兴趣
SelectionKey key = channel.register(selector , SelectionKey.OP_READ);
// 也可以使用或运算|来组合多个事件,例如
SelectionKey key = channel.register(selector , SelectionKey.OP_READ | SelectionKey.OP_WRITE);
// 不过值得注意的是,一个 Channel 仅仅可以被注册到一个 Selector 一次, 如果将 Channel 注册到 Selector 多次, 那么其实就是相当于更新 SelectionKey 的 interest set.
3>一个Channel在Selector注册其代表的是一个SelectionKey事件,SelectionKey类型包括:
OP_READ | 可读事件;值为1<<0 |
OP_WRITE | 可写事件;值为1<<2 |
OP_CONNECT | 客户端连接服务端的事件(tcp连接),一般为创建SocketChannel客户端channel;;值为1<<3 |
OP_ACCEPT | 服务端连接客户端的事件,一般为创建ServerSocketChannel客户端channel;;值为1<<4 |
OP_READ | OP_WRITE | OP_CONNECT | ACCEPT | |
服务器ServerSocketChannel | Y | |||
服务器SocketChannel | Y | Y | ||
客户端SocketChannel | Y | Y | Y |
4>Selector的10个方法
open() | 创建一个Selector对象 |
isOpen() | 是否是open状态,如果调用了close()方法则会返回false |
provider() | 获取当前Selector的Provider |
keys() | 获取当前channel注册在Selector上所有的key |
selectedKeys() | 获取当前channel就绪的事件列表 |
selectNow() | 获取当前是否有事件就绪,该方法立即返回结果,不会阻塞;如果返回值>0,则代表存在一个或多个 |
select(long timeout) | selectNow()的阻塞超时方法,超时时间内,有事件就绪时才会返回;否则超过时间也会返回 |
select() | selectNow()的阻塞方法,直到有事件就绪时才会返回 |
wakeup() | 调用该方法会时,阻塞在select()处的线程会立马返回;(ps:下面一句划重点)即使当前不存在线程阻塞在select()处,那么下一个执行select()方法的线程也会立即返回结果,相当于执行了一次selectNow()方法 |
close() | 用完Selector后调用其close()方法会关闭该Selector,且使注册到该Selector上的所有SelectionKey实例无效。channel本身并不会关 |
5>SelectionKey
选择键,谈到Selector就不得不提SelectionKey,两者是紧密关联,配合使用的;每个Channel向Selector注册时,都将会返回一个SelectionKey对象。选择键将Channel与Selector建立了关系,并维护了channel事件。
可以通过cancel方式取消键,取消的键不会立即从selector中移除,而是添加到cancelledKeys中,在下一次select操作时移除它。所以在调用某个key时,需要使用isValid进行校验。
6>SelectionKey的方法
channel() | 返回此选择键所关联的通道.即使此key已经被取消,仍然会返回. |
selector() | 返回此选择键所关联的选择器,即使此键已经被取消,仍然会返回. |
isValid() | 检测此key是否有效.当key被取消,或者通道被关闭,或者selector被关闭,都将导致此key无效.在AbstractSelector.removeKey(key)中,会导致selectionKey被置为无效. |
cancel() | 请求将此键取消注册.一旦返回成功,那么该键就是无效的,被添加到selector的cancelledKeys中.cancel操作将key的valid属性置为false,并执行selector.cancel(key)(即将key加入cancelledkey集合) |
interesOps() | 获得此键的interes集合. |
interestOps(int ops) | 将此键的interst设置为指定值.此操作会对ops和channel.validOps进行校验.如果此ops不会当前channel支持,将抛出异常. |
readyOps() | 获取此键上ready操作集合.即在当前通道上已经就绪的事件. |
isReadable() | 检测此键是否为"read"事件.等效于:k.,readyOps() & OP_READ != 0 |
isWritable() | 测试此键的通道是否已准备好进行写入 |
isAcceptable() | 测试此键的通道是否已准备好接受新的套接字连接 |
isConnectable() | 测试此键的通道是否已完成其套接字连接操作 |
attach(Object ob) | 将给定的对象作为附件添加到此key上.在key有效期间,附件可以在多个ops事件中传递. |
attachment() | 获取附件.一个channel的附件,可以再当前Channel(或者说是SelectionKey)生命周期中共享,但是attachment数据不会作为socket数据在网络中传输. |