NIO结构
BIO和NIO的区别
NIO三大核心部分
- Channel
- Buffer
- Selector
1. NIO结构(java.nio包)
2. BIO和NIO区别
- BIO面向流,基于字节流和字符流
- NIO面向块(Buffer),基于Channel(通道)和Buffer(缓冲区)
- BIO是阻塞的,NIO是非阻塞的
3. 三大核心
3.1 Buffer
(1)继承关系
- 主要有8种
- 基本类型7种(都为abstract类):除了booleanBuffer没有,剩下的基本类型都有对应的Buffer类
- 具体实现类为 HeapXXXBuffer
- MappedByteBuffer(extends ByteBuffer)是专门用于内存映射的一种ByteBuffer
- 基本类型7种(都为abstract类):除了booleanBuffer没有,剩下的基本类型都有对应的Buffer类
(2)内部结构
属性 | |
private int mark = -1; | 用于记录当前position的前一个位置或者默认是-1 |
private int position = 0; | 下一个要操作的数据元素的位置 |
private int limit; | 缓冲区数组中不可操作的下一个元素的位置:limit<=capacity
|
private int capacity; |
|
final int[] hb; | 以intBuffer为例,实际的存储数组 |
- 总结
- capacity >= limit >= position >= mark >= 0
- 例子
- 初始调用ByteBuffer.allocate(n)时:position=0,limit = capacity = n
- 如果这时写入5字节:position=5, limit = capacity = n
- 调用ByteBuffer.flip()方法,将缓冲区的5字节写入Channel通道:position = 0, limit = 5, capacity = n
(3)方法,以IntBuffer为例
Buffer抽象类方法 | 获取相关属性 capacity position limit | public final int capacity() | 返回缓冲区的capacity |
public final int position() public final Buffer position(int newPosition) | 返回/设置 缓冲区的position | ||
public final int limit() public final Buffer limit(int newLimit) | 返回/设置 缓冲区的limit | ||
重置 clear | public final Buffer clear() | 清除此缓冲区(仅仅将position, limit等重置,不清除数据) 写模式下:调用 clear()方法,切换成读模式,可以从头读取 buffer 的数据 读模式下:将buffer切换为写模式,重置参数,让其从头开始写 | |
写转换成读 flip | public final Buffer flip() | 写模式转读模式,反转此缓冲区(limit = position, position = 0) | |
倒带 rewind | public final Buffer rewind() | position=0, remark=-1,可以让Buffer倒回去重新读 与flip()不同之处在于rewind()不修改limit | |
是否到底了 hasRemaining | public final boolean hasRemaining() | return position < limit | |
抽象方法 isReadOnly | public abstract boolean isReadOnly() | 是否是只读缓冲区,不同底层实现类返回结果不同。 | |
返回hb[]数组 hasArray,array 委托IntBuffer实现 | public abstract boolean hasArray() public abstract Object array() | 是否有可访问的底层实现数组 返回缓冲区的底层实现数组 | |
IntBuffer抽象类方法 | 分配内存(初始化)allocate 委托HeapIntBuffer实现 | public static XXXBuffer allocate(int capacity){ return new HeapXXXBuffer(capacity, limit)} | 抽象类Buffer没有这个方法,实现类基本都有这个静态方法,实际调用调用子类HeapXXXBuffer eg. IntBuffer.allocate(20);DoubleBuffer.allocate(20)... |
写模式 put 委托HeapIntBuffer实现 | public IntBuffer put(int x) | 写入数据,position++; allocate(),Buffer.clear(),Buffer.compact()之后,都可以将Buffer转换成写模式 | |
读模式 get 委托HeapIntBuffer实现 | public int get() | 读position位置的数据,position++ 读之后不能直接写数据,一旦读取了所有的 Buffer 数据,那么我们必须清理 Buffer 让其重新可写,可以调用 Buffer.clear() 或 Buffer.compact()。 |
(4)只读Buffer——>HeapByteBuffer
(5)基本使用步骤
- 步骤
- allocate分配Buffer
- buffer.put()
- 调用 Buffer.flip()方法,将 NIO Buffer 转换为读模式
- buffer.get()
- 调用 Buffer.clear() 或 Buffer.compact()方法,将 Buffer 转换为写模式
- 案例
-
public static void main(String[] args){ //创建一个buffer ByteBuffer buffer = ByteBuffer.allocate(64); //放入数据 for(int i = 0; i < 64; i++){ buffer.put((byte)i); } //读取 buffer.flip(); //得到一个只读的buffer ByteBuffer byteBuffer = buffer.asReadOnlyBuffer(); }
3.2 Channel
和老的Java IO相比,有三点区别
- 传统Stream相比,前者是单向的。Channel是双向的
- 传统是读写阻塞的,Channle是异步的
- Channel总是基于缓冲区Buffer来读写
(1)继承关系
- public interface Channel extends Closeable
- 较为常用的类:
- FileChannel(不支持非阻塞模式) ——> FileChannelImpl:文件数据读写
- DatagramChannelImpl:UDP数据读写
- ServerSocketChannel,SocketChannel ——> ServerSocketChannelImpl,SocketChannelImpl:TCP数据读写的Server和Client
(2)方法
方法:FileChannel为例 | |
public int read(ByteBuffer dst) public int write(ByteBuffer src) | Channel ——> Buffer Buffer ——> Channel |
public long transferFrom(ReadableByteChannel src, long position, long count) public long transferTo(long position, long count, WritableByteChannel target) | src Channel ——> 当前Channel 当前Channel ——> target Channel |
(3)案例
- 1. string写入文件
-
public static void main(String[] args) throw Exception{ String str = "hello, 写入文件案例"; //写入一个输出流 FileOutputStream fos = new FileOutputStream("d:\\file.txt"); //通过FileOutputStream获取对应FileChannel //实际上获得的真实类型是FileChannelImpl FileChannel fc = fos.getChannel(); //创建一个缓冲区ByeBuffer ByteBuffer bf = ByteBuffer.allocate(1024); //将str放入Buffer bf.put(str.getBytes()); //对byteBuffer进行flip bf.flip(); //将buffer————>channel fc.write(bf); //关闭 fos.close(); }
-
- 2. 文件写出到string
-
public static void main(String[] args) throw Exception{ //写入一个输入流 File file = new File("d:\\file.txt"); FileInputStream fis = new iFileInputStreaim(file); //通过FileInputStream获取对应FileChannel //实际上获得的真实类型是FileChannelImpl FileChannel fc = fis.getChannel(); //创建一个缓冲区ByeBuffer ByteBuffer bf = ByteBuffer.allocate((int)file.length()); //将buffer————>channel fc.read(bf); //获取str str = new String(byteBuffer.array()); //关闭 fos.close(); }
-
- 3. 利用transform直接拷贝
-
public static void main(String[] args) throw Exception{ //创建相关流 FileInputStream fis = new FileInputStream("d:\\a1.jpg"); FileOutputStream fos = new FileOutputStream("d:\\a2.jpg"); //获取对应fileChannel FileChannel sourceCh = fis.getChannel(); FileChannel destCh = fos.getChannel(); //使用transferFrom完成拷贝 destCh.transferFrom(sourceCh, 0, sourceCh.size()); //关闭 sourceCh.close(); destCh .close(); fis.close(); fos.close(); }
-
3.3 Selector
(1)传统的服务器设计:利用多线程,每个线程处理一个socket
- 缺点:
- 内存占用高
- 线程上下文切换成本高
- 只适合连接数少的场景
(2)线程池版优化
- 缺点:
- 阻塞模式下,线程仅能处理一个socket连接(eg. socket1执行时,socket被迫阻塞)
- 仅适合短连接的场景
(3)selector版设计
selector的作用就是配合一个线程来管理多个channel,获取这些channel上发送的事件。适合连接数特别多,但流量低的场景
调用selector的select()会阻塞直到channel发生了读写就绪事件。发生事件,selector就会返回这些事件给thread来处理