Java NIO学习-详细内容(一)
一、三大类
1、Channels
-
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
2、Selector与SelectionKey
3、Buffer及其子类
-
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
说明:所有的Channel都需要和Buffer类结合使用,通过Buffer类实现缓冲区
直接通过Channels即可实现同步非阻塞io,SelectableChannel类configureBlocking(boolean block)方法配置是否阻塞,false不阻塞,默认true.(由上图可知,FileChannel是不能配置阻塞非阻塞的,因为直接通过系统IO读到文件,不会有阻塞和非阻塞的区分)
通过Selector与SelectionKey可以实现IO多路复用
nio包结构:
nio包接口:
Channels类说明:
主要负责把InputStream和OutputStream转为Channel,把Channel转为InputStream、OutputStream、Reader、Writer,经典IO与NIO互相转换
static ReadableByteChannel newChannel(InputStream in) static WritableByteChannel newChannel(OutputStream out) static InputStream newInputStream(ReadableByteChannel ch) static OutputStream newOutputStream(WritableByteChannel ch) static Reader newReader(ReadableByteChannel ch, CharsetDecoder dec, int minBufferCap) static Reader newReader(ReadableByteChannel ch, String csName) static Writer newWriter(WritableByteChannel ch, CharsetEncoder enc, int minBufferCap) static Writer newWriter(WritableByteChannel ch, String csName)
接口说明
Channel接口:void close();boolean isOpen()
ReadableByteChannel接口:int read(ByteBuffer dst)
WritableByteChannel接口:int write(ByteBuffer src)
ByteChannel接口:实现了以上所有接口的接口
ScatteringByteChannel接口:long read(ByteBuffer[] dsts);long read(ByteBuffer[] dsts, int offset, int length) 。读取一个网络协议的不同部分时有用,如http协议的header和body。
分散 读取操作可在单个调用中将一个字节序列读入一个或多个给定的缓冲区序列。分散读取通常在实现网络协议或文件格式时很有用,例如将数据分组放入段中(这些段由一个或多个长度固定的头,后跟长度可变的正文组成)。
GatheringByteChannel接口:long write(ByteBuffer[] srcs) ;long write(ByteBuffer[] srcs, int offset, int length)
集中 写入操作可在单个调用中写入来自一个或多个给定缓冲区序列的字节序列。集中写入通常在实现网络协议或文件格式时很有用,例如将数据分组放入段中(这些段由一个或多个长度固定的头,后跟长度可变的正文组成)。
InterruptibleChannel接口:void close()。可被异步关闭和中断的通道。 也可使用线程的interrupt方法来关闭。调用close与调用interrupt都将引发Exception,Exception不同,参看api文档。
二、Buffer常用方法及实现
Buffer有两个作用。
1、从ReadableByteChannel通道中读数据到Buffer中
2、从Buffer写数据到WritableByteChannel中
由于有读和写两个功能,于是Buffer中有几个比较重要的属性。
private int mark = -1; //用于调用mark()方法时标记当前的位置,只能使用一次。mark()中记录的是当前position。mark=position
private int position = 0; //当前要读取或者写入的位置
private int limit; //当前缓冲区限制,当处于写模式时,limit=capacity,处于读模式时,limit等于写模式写到的position
private int capacity; //Buffer的容量,通过调用allocate(int capacity) 静态方法来创建Buffer,capacity即为容量。
这四个量满足关系:0 <= 标记 <= 位置 <= 限制 <= 容量
子类中又多了final int offset;boolean isReadOnly;
isReadOnly:用于标记这个Buffer是否是只读的,该属性只能创建最低级子类HeapByteBufferR用到了,是在HeapByteBufferR的构造方法中设置为true的,而HeapByteBufferR对象的构造,只在父类HeapByteBuffer中的asReadOnlyBuffer()、duplicate()(重复)、slice()(切割)方法中构造了。
offset:则是当前底层实现数组的偏移量,同上,也只在HeapByteBufferR构造方法中修改过offset的值。
Buffer基本上都是通过底层一个数组来实现的,在数组中存储数据。下面是一些常用方法(一些方法支持chaining操作):
Object array() 返回底层实现数组
int arrayOffset() 返回数组偏移量
int capacity() 返回容量
Buffer clear() 用于清空缓冲区,开启写入模式。position = 0;limit = capacity;mark = -1;
Buffer flip() 反转此缓冲区,用于从写入模式切换到读取模式。limit = position;position = 0;mark = -1;
boolean hasRemaining() 告知在当前位置和限制之间是否有元素。return position < limit;
int limit() 返回Buffer的limit。return limit;
Buffer limit(int newLimit) 设置缓冲区新的限制,代码:
if ((newLimit > capacity) || (newLimit < 0)) throw new IllegalArgumentException(); limit = newLimit; if (position > limit) position = limit; if (mark > limit) mark = -1; return this;
Buffer mark() 设置标记,mark = position;
int position() 返回position。
Buffer position(int newPosition) 设置新的position
if ((newPosition > limit) || (newPosition < 0)) throw new IllegalArgumentException(); position = newPosition; if (mark > position) mark = -1; return this;
int remaining() 返回当前位置与限制之间的元素数。 return limit - position;
Buffer reset() 将此缓冲区的位置重置为以前标记的位置。position = m;
Buffer rewind() 重绕此缓冲区。将位置设置为 0 并丢弃标记。position = 0;mark = -1;可用于对数据进行重新操作(做与调用之前相同的操作则清除之前的操作)。也可用于变更操作(从读到写,或者从写到读,但是该操作不会修改limit,所以变换操作不适合使用此方法,当然要用还是可以的,不能保证取到数据是否正确就是了)
子类方法,为省事,只看ByteBuffer类(享此缓冲区意思是指缓冲区是相同的,一边更新另一边也更新)
static ByteBuffer allocate(int capacity) 生成指定大小的缓冲区
<T> abstract TBuffer asTBuffer() 转换为指定类型缓冲区
abstract ByteBuffer asReadOnlyBuffer() 创建共享此缓冲区内容的新的只读字节缓冲区。
abstract ByteBuffer compact() 压缩缓冲区,已读的数据丢弃,把未读的数据放到最前,设置position为未读数据量
System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
position(remaining());
limit(capacity());
discardMark();
abstract ByteBuffer duplicate() 创建共享此缓冲区内容的新的字节缓冲区。
get()、get(byte[] dst)、get(byte[] dst, int offset, int length)
ByteBuffer order(ByteOrder bo) 在读写多字节值以及为此字节缓冲区创建视图时使用该字节顺序。BIG_ENDIAN,LITTLE_ENDIAN
<T> getT()或者(int index) 获取指定类型的值
相对于get还有相同的put方法
abstract ByteBuffer slice() 创建新的字节缓冲区,其内容是此缓冲区内容的共享子序列。
static ByteBuffer wrap(byte[] array) 将 byte 数组包装到缓冲区中。即使用指定数组创建缓冲区。
static ByteBuffer wrap(byte[] array, int offset, int length) 同上
三、Scatter/Gather
Channel类的接口,实现该接口的类支持在read()和write()时传入一个Buffer数组,一次读取或者写入到多个Buffer。
带有offset与length的方法,两个参数意思为:
offset
- 第一个缓冲区(要获取该缓冲区中的字节)在缓冲区数组中的偏移量;必须为非负数并且不能大于 srcs.length(所有的src.length之和)
length
- 要访问的最大缓冲区数;必须为非负数并且不能大于 srcs.length - offset(所有的src.length之和)
Scattering Reads在移动下一个buffer前,必须填满当前的buffer,这也意味着它不适用于动态消息(译者注:消息大小不固定)。换句话说,如果存在消息头和消息体,消息头必须完成填充(例如 128byte),Scattering Reads才能正常工作。
buffers数组是write()方法的入参,write()方法会按照buffer在数组中的顺序,将数据写入到channel,注意只有position和limit之间的数据才会被写入。因此,如果一个buffer的容量为128byte,但是仅仅包含58byte的数据,那么这58byte的数据将被写入到channel中。因此与Scattering Reads相反,Gathering Writes能较好的处理动态消息。
四、FileChannel
文件通道,特点是不能设置为阻塞模式,因为是直接从底层读取的。同时比其他Channel多了几个方法
1、文件通道在其文件中有一个当前 position,可对其进行查询和修改。该文件本身包含一个可读写的长度可变的字节序列,并且可以查询该文件的当前大小。写入的字节超出文件的当前大小时,则增加文件的大小;截取该文件时,则减小文件的大小。文件可能还有某个相关联的元数据,如访问权限、内容类型和最后的修改时间;此类未定义访问元数据的方法。
2、以不影响通道当前位置的方式,对文件中绝对位置的字节进行读取或写入。
3、将文件中的某个区域直接映射到内存中;对于较大的文件,这通常比调用普通的 read 或 write 方法更为高效。
4、以一种可被很多操作系统优化为直接向文件系统缓存发送或从中读取的高速传输方法,将字节从文件传输到某个其他通道中,反之亦然。 (transferTo与transferFrom)
5、可以锁定某个文件区域,以阻止其他程序对其进行访问。
获取FileChannel的方法:
通过FileInputStream、FileOutputStream 或 RandomAccessFile 对象获得文件通道,方法是调用该对象的 getChannel 方法,这会返回一个连接到相同底层文件的文件通道。
此类在各种情况下指定要求“允许读取操作”、“允许写入操作”或“允许读取和写入操作”的某个实例。通过 FileInputStream 实例的 getChannel 方法所获得的通道将允许进行读取操作。通过 FileOutputStream 实例的 getChannel 方法所获得的通道将允许进行写入操作。最后,如果使用模式 "r" 创建 RandomAccessFile 实例,则通过该实例的 getChannel 方法所获得的通道将允许进行读取操作,如果使用模式 "rw" 创建实例,则获得的通道将允许进行读取和写入操作。
如果从文件输出流中获得了允许进行写入操作的文件通道,并且该输出流是通过调用 FileOutputStream(File,boolean) 构造方法且为第二个参数传入 true 来创建的,则该文件通道可能处于添加模式。在此模式中,每次调用相关的写入操作都会首先将位置移到文件的末尾,然后再写入请求的数据。在单个原子操作中是否移动位置和写入数据是与系统相关的,因此是未指定的。
方法列表:
abstract void force(boolean metaData) 强制将所有对此通道的文件更新写入包含该文件的存储设备中。
FileLock lock() 获取对此通道的文件的独占锁定。
abstract FileLock lock(long position, long size, boolean shared) 获取此通道的文件给定区域上的锁定。
abstract MappedByteBuffer map(FileChannel.MapMode mode, long position, long size) 将此通道的文件区域直接映射到内存中。
abstract long position() 返回此通道的文件位置。
abstract FileChannel position(long newPosition) 设置此通道的文件位置。
abstract int read(ByteBuffer dst) 将字节序列从此通道读入给定的缓冲区。
long read(ByteBuffer[] dsts) 将字节序列从此通道读入给定的缓冲区。
abstract long read(ByteBuffer[] dsts, int offset, int length) 将字节序列从此通道读入给定缓冲区的子序列中。
abstract int read(ByteBuffer dst, long position) 从给定的文件位置开始,从此通道读取字节序列,并写入给定的缓冲区。
abstract long size() 返回此通道的文件的当前大小。(就是文件大小)
abstract long transferFrom(ReadableByteChannel src, long position, long count) 将字节从给定的可读取字节通道传输到此通道的文件中。
abstract long transferTo(long position, long count, WritableByteChannel target) 将字节从此通道的文件传输到给定的可写入字节通道。
abstract FileChannel truncate(long size) 将此通道的文件截取为给定大小。
FileLock tryLock() 试图获取对此通道的文件的独占锁定。
abstract FileLock tryLock(long position, long size, boolean shared) 试图获取对此通道的文件给定区域的锁定。
abstract int write(ByteBuffer src) 将字节序列从给定的缓冲区写入此通道。
long write(ByteBuffer[] srcs) 将字节序列从给定的缓冲区写入此通道。
abstract long write(ByteBuffer[] srcs, int offset, int length) 将字节序列从给定缓冲区的子序列写入此通道。
abstract int write(ByteBuffer src, long position) 从给定的文件位置开始,将字节序列从给定缓冲区写入此通道。
两个通道间转移方法用法:
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw"); FileChannel fromChannel = fromFile.getChannel(); RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw"); FileChannel toChannel = toFile.getChannel(); long position = 0; long count = fromChannel.size(); toChannel.transferFrom(fromChannel, position, count); RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw"); FileChannel fromChannel = fromFile.getChannel(); RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw"); FileChannel toChannel = toFile.getChannel(); long position = 0; long count = fromChannel.size(); fromChannel.transferTo(position, count, toChannel);
方法的输入参数position表示从position处开始向目标文件写入数据,count表示最多传输的字节数。如果源通道的剩余空间小于 count 个字节,则所传输的字节数要小于请求的字节数。
此外要注意,在SoketChannel的实现中,SocketChannel只会传输此刻准备好的数据(可能不足count字节)。因此,SocketChannel可能不会将请求的所有数据(count个字节)全部传输到FileChannel中。
使用示例:
//打开FileChannel RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw"); FileChannel inChannel = aFile.getChannel(); //从FileChannel读取数据 ByteBuffer buf = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buf); //向FileChannel写数据 String newData = "New String to write to file..." + System.currentTimeMillis(); ByteBuffer buf = ByteBuffer.allocate(48); buf.clear(); buf.put(newData.getBytes()); buf.flip(); while(buf.hasRemaining()) { channel.write(buf); } //关闭FileChannel channel.close();
position方法:在FileChannel的某个特定位置进行数据的读/写操作。
如果将位置设置在文件结束符之后,然后试图从文件通道中读取数据,读方法将返回-1 —— 文件结束标志。
如果将位置设置在文件结束符之后,然后向通道中写数据,文件将撑大到当前位置并写入数据。这可能导致“文件空洞”,磁盘上物理文件中写入的数据间有空隙。
truncate方法:可以使用FileChannel.truncate()方法截取一个文件。截取文件时,文件将中指定长度后面的部分将被删除。从头开始截取,不能从中间开始。
force方法:将通道里尚未写入磁盘的数据强制写到磁盘上。出于性能方面的考虑,操作系统会将数据缓存在内存中,所以无法保证写入到FileChannel里的数据一定会即时写到磁盘上。要保证这一点,需要调用force()方法。
force()方法有一个boolean类型的参数,指明是否同时将文件元数据(权限信息等)写到磁盘上。