NIO笔记
JAVA NIO,是区别于JAVA IO的NEW IO。和普通的IO存在一定的区别,先记下这么几个字吧:
走通道,用缓存,选择器,非阻塞
走通道
NIO有三大组成:Channel(通道)、Buffer(缓存)、Selector(选择器)
其中通道是指的实现java.nio.channels.Channel接口的对象,常见的对象有以下几种:
FileChannel(文件通道)
DatagramChannel(收发UDP包的通道)
SocketChannel(连接到TCP网络套接字的通道)
ServerSocketChannel(可以监听新进来的TCP连接的通道)
相比较于IO的时候,输入输出需要用InputStream,OutputStream两种,NIO的的通道只有一种,以FileChannel为例。
打开:
RandomAccessFile aFile = new RandomAccessFile("xxx.txt", "rw");
FileChannel inChannel = aFile.getChannel();
读取:
ByteBuffer buf = ByteBuffer.allocate(48); // nio的buffer
int bytesRead = inChannel.read(buf); // 初始化写入到buffer
while (bytesRead != -1) {
buf.flip(); // 切换读写模式
while(buf.hasRemaining()){
System.out.print((char) buf.get()); // 读取buff的内容
}
buf.clear(); // buffer清空,使之可以再次写入
bytesRead = inChannel.read(buf); // 写入到buffer
}
写入:
inChannel.write(buf);
可以看到,使用FileChannel不管是读取还是写入,都是使用了ByteBuffer作为中转,这个ByteBuffer,就是NIO中的缓存了。
用缓存
缓存类型:
缓存是实现了java.nio.Buffer抽象类接口的对象。
对于部分IO来说,其对象内部也存在缓存,但都是对象的成员变量,而NIO中的,缓存需要单独使用,走【数据】->【缓存】->【通道】->【对象】,或者【对象】->【通道】->【缓存】->【数据】的方式,进行数据的流通。
缓存有很多种,除了boolean以外,基本类型全部包括。个人感觉有的类似于ObjectInputStream中writeByte、writeInt等等的方法,把这些方法抽象为一个对象的感觉。
ByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
例子:
首先,可以看到,java.nio.Buffer的源码中的以下记得成员,这是buffer的关键:
private int position = 0;
private int limit;
private int capacity;
其中:
capacity:内存块,初始化时设置,限制缓存空间大小。
position:当前位置。写模式时初始为零,写一次移动一下。切换到读模式时,重置为0。position最大可为capacity – 1。
limit:写模式时,同capacity,一次写入大小。切换到读模式时, limit表示你最多能读到多少数据,此时,limit会被设置成写模式下的position值。
聚集和分散:
简单的说,buffer使用的时候,可以一个数组一起使用,这种使用方式类似水流,先填满一个,再填满另一个。
Scattering Reads(聚集)是指数据从一个channel读取到多个buffer中。
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.read(bufferArray);
read()方法按照buffer在数组中的顺序将从channel中读取的数据写入到buffer,当一个buffer被写满后,channel紧接着向另一个buffer中写。
Gathering Writes(分散)是指数据从多个buffer写入到同一个channel。
channel.write(bufferArray);
使用缓存:
初始化:使用allocate方法,设定capacity以初始化
ByteBuffer byteBuf = ByteBuffer.allocate(48);
写入缓存:直降使用缓存的put方法,或者使用通道的read方法进行写入
inChannel.read(buf); // 通道写入到buffer
buf.put(100); // put写入
模式切换:写入后,进行读取时,需要调用flip()方法,进行模式切换(实际是改变position位置)
buf.flip(); // 切换读写模式
重置:读取后想重新读取,使用rewind()方法。
buf.rewind(); // buffer重置,使之可以再次读取
清空:想要再次写入,或者清空写入内容时,使用clear()方法
buf.clear(); // buffer清空,使之可以再次写入
选择器
选择器即Selector,简单的说,就是用于管理多个Channel的对象,感觉有的类似于线程池,但Selector确实是单线程对象,通过注册->接收->处理的过程,对多个Channel进行管理。
其中将【选择器】同【通道】关联起来的线,即为选择键,SelectionKey。
总之先记着,选择器使用静态实例创建,使用注册通道返回选择键的方式进行关联。
Selector Selector=Selector.open(); // 创建实例
channel.configureBlocking(false); // 通道设置非阻塞,不然同Selector一起用会异常(FileChannel不能设非阻塞)
SelectionKey key= channel.register(selector,SelectionKey.OP_ACCEPT); // 通道注册
SelectionKey.OP_ACCEPT标识,是Selector要监视通道的那些动作。当通道的动作处在就绪状态时,Selector监视到,然后可进行操作。
关于SelectionKey,既然是Channel和Selector的管理,那么进行操作的时候,也是对SelectionKey进行操作了。可以使用isAcceptable(),isConnectable(),isReadable(),isWritable(),来判断Channel的是否可以进行对应的操作。isValid判断是否有数据(缓存)。attach来进行绑定和解绑等等。
因此,一般Selector的使用方式是弄个循环,使用selector.selectedKeys()的方法渠道所有就绪,能操作的SelectionKey,如下:
while(true){ // 循环内
Set selectedKeys = selector.selectedKeys(); // 取得可操作SelectionKey集合
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next(); // 取得SelectionKey,并判断不同的状态进行操作
if(key.isAcceptable()) {
...
} else if (key.isConnectable()) {
...
} else if (key.isReadable()) {
...
} else if (key.isWritable()) {
...
}
keyIterator.remove();
}
}
非阻塞
由于是同步非阻塞的,所以其原理基本上就是在设置好之后,利用循环不断遍历Channel,判断其状态,有可操作的地方就处理,没有则继续判断而已。
其中,由于使用了缓存通道分离的方式,最大的优势在于当有可进行的操作的时候,实际上通道已经准备就绪,可以立即执行,省去了读取的时间。如下,其他方法时,以下read的时候,会阻塞花费较多时间。而NIO的时候,由于已经换成,会马上读取完毕,可执行下面的操作。
SocketChannel socketChannel = (SocketChannel) key.channel();
int len = socketChannel.read(readBuffer); // **这里**