NIO三大核心组件:
1. Buffer缓冲区
2. Channel通道
3. Selector选择器
- Buffer缓冲区
缓冲区本质上是一个可以写入数据的内存块(类似于数组),然后可以再次读取。此内存块包含在NIO Buffer对象中,改对象提供了一组方法,可以更轻松的使用内存块。
相比较直接对数组的操作,BufferAPI更容易操作和管理
使用Buffer进行数据写入与读取,需要进行如下四个步骤:
- 将数据写入缓冲器
- 调用buffer.flip(),转换为读取模式
- 缓冲区读取数据
- 调用buffer.clear()或buffer.compact()清除缓冲去。
- Buffer工作原理
Buffer三个重要属性
- capacity容量:作为一个内存块,buffer具有一定的大小,也成为“容量”。
- position位置:写入模式时代表写数据的位置。读取模式时代表读取数据的位置。
- limit:写入模式,限制等于buffer容量。读取模式下,limit等于写入的数量。
文章图片
public static void main(String[] args) {// 构建一个byte字节缓冲区,容量是4 //ByteBuffer byteBuffer = ByteBuffer.allocate(4); ByteBuffer byteBuffer = ByteBuffer.allocateDirect(4); // 默认写入模式,查看三个重要的指标 System.out.println(String.format("初始化:capacity容量:%s, position位置:%s, limit限制:%s", byteBuffer.capacity(), byteBuffer.position(), byteBuffer.limit())); // 写入2字节的数据 byteBuffer.put((byte) 1); byteBuffer.put((byte) 2); byteBuffer.put((byte) 3); // 再看数据 System.out.println(String.format("写入3字节后,capacity容量:%s, position位置:%s, limit限制:%s", byteBuffer.capacity(), byteBuffer.position(), byteBuffer.limit())); // 转换为读取模式(不调用flip方法,也是可以读取数据的,但是position记录读取的位置不对) System.out.println("#######开始读取"); byteBuffer.flip(); byte a = byteBuffer.get(); System.out.println(a); byte b = byteBuffer.get(); System.out.println(b); System.out.println(String.format("读取2字节数据后,capacity容量:%s, position位置:%s, limit限制:%s", byteBuffer.capacity(), byteBuffer.position(), byteBuffer.limit())); // 继续写入3字节,此时读模式下,limit=3,position=2.继续写入只能覆盖写入一条数据 // clear()方法清除整个缓冲区。compact()方法仅清除已阅读的数据。转为写入模式 byteBuffer.compact(); // buffer : 1 , 3 byteBuffer.put((byte) 3); byteBuffer.put((byte) 4); byteBuffer.put((byte) 5); System.out.println(String.format("最终的情况,capacity容量:%s, position位置:%s, limit限制:%s", byteBuffer.capacity(), byteBuffer.position(), byteBuffer.limit())); // rewind() 重置position为0 // mark() 标记position的位置 // reset() 重置position为上次mark()标记的位置}
- BateBuffer内存类型
ByteBuffer为性能关键型代码提供了直接内存(direct堆外)和非直接内存(heap堆)两种实现
堆外内存获取方式:ByteBuffer byteBuffer = ByteBuffer.allocateDirect(noBytes);
好处:
1. 进行网络IO或者文件IO时比heapBuffer少一次拷贝。(file/socket----OS memory----jvm heap)GC或移动对象内存,在写file或socket的过程中,JVM的实现中,会先把数据复制到堆外,在进行写入。
2. GC范围之外,降低GC压力,但实现了自动管理。DirectByteBuffer中有一个Cleaner对象(PhantomReference),Cleaner被GC前会执行clean方法,触发DirectByteBuffer中定义的Deallocator
建议:
- 性能确实客观的时候才去使用;分配给大型、长寿命;(网络传输、文件读写场景)
- 通过虚拟机参数MaxDirectMemorySize限制大小,防止耗尽整个机器的内存
- Channel 通道 (通道可以创建网络连接,也可以用来读取数据)
Channel的API涵盖了UDP/TCP网络和文件
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
在一个通道内进行读取和写入,stream通常是单向的(input或output)可以肥阻塞读取和写入通道,通道始终读取或写入缓冲区。
- SocketChannel
SocketChannel用于简历TCP网络连接,类似于java.net.Socket。有两种创建SocketChannel形式:
- 客户端主动发起和服务器的链接
- 服务端获取的新连接
// 客户端主动发起连接的方式 SocketChannel socketChannel = SocketChannel.open(); socketChannel.configureBlocking(false); // 设置为非阻塞模式 socketChannel.connect(new InetSocketAddress("www.baidu.com", 80)); socketChannel.write(byteBuffer); // 发送请求数据-->向通道中写数据 int bytesRead = socketChannel.read(byteBuffer); // 读取服务端返回-->读取缓冲区的数据 socketChannel.close();
write写:write()在尚未写入任何内容时就可能返回了。需要在循环中调用write()。
read读:read()方法可能直接返回而根本读取不到任何数据,根据返回的int值判断读取了多少字节。
- ServerSocketChannel
ServerSocketChannel 可以监听新建的TCP连接通道,类似于SocketChannel。
// 创建网络服务器 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.configureBlocking(false); // 设置为非阻塞模式 serverSocketChannel.socket().bind(new InetSocketAddress(8080)); // 绑定端口 while (true){ SocketChannel socketChannel = serverSocketChannel.accept(); // 获取新tcp连接通道 if (socketChannel != null){ // tcp请求 读取/相应 } }
serverSocketChannel.accept():如果该通道处于非阻塞模式,那么如果没有挂起的连接,该方法将立即返回null。必须检查返回的SocketChannel是否为null;
- Selector 选择器
Selector是一个Java NIO组件,可以检查一个或多个NIO通道,并确定哪些通道已经准备好进行读取或写入。实现单个线程可以管理对个网络连接。
一个线程使用Selector监听多个channel的不同事件:
四个事件分别对应SelectionKey四个常量。
- Connect 连接(SelectionKey.OP_CONNECT)
- Accept准备就绪(OP_ACCEPT)
- Read读取(OP_READ)
- Write(OP_WRITE)
非阻塞的网络通道下,开发者通过Selector注册对于通道感兴趣的事件类型,线程通过监听事件来触发相应的代码执行。(拓展:更底层是操作系统的多路复用机制)