002-核心技术-IO模型-NIO【Selector、Channel、Buffer】、零拷贝
一、概述
java NIO 即java non-blocking IO ,是指JDK提供的新API,从jdk4开始。
类地址:java.nio包下
三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器)
NIO是面向缓冲区、或者面向块编程的,数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。
非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是他仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞。所以直至数据变得可以读取之前,该线程可以继续做其他的事情。
非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程就可以去做其他任务
Http2.0 使用多路复用技术,做到了一个连接并发处理多个请求。
1.1、对比
BIO以流的方式处理数据,NIO以块的方式处理数据,块IO的效率比流IO高
BIO是阻塞的,NIO是非阻塞的
BIO基于字节流和字符流操作,而NIO基于channel(通道)和Buffer(缓冲区) 进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如连接请求、数据到达等),因此使用单个线程就可以监听多个客户端通道。
二、三大核心组件Selector、Channel、Buffer
1)每个channel多会对应一个Buffer
2)Selector对应一个线程、一个线程对应多个channel(连接)
3)该图反映了有3个channel注册到了Selector
4)程序切换到哪个channel是由事件决定的,Event比较重要
5)Selector会根据不同的事件,在各个通道上切换
6)Buffer就是一个内存块,底层是有一个数组
7)数据的读取、写入是通过Buffer,通BIO不同,BIO中要么是输入流,或者是输出流,不能双向,但是nio的Buffer是可以读也可以写,需要flip方法切换
8)channel是双向的,可以返回底层操作系统的情况,比如linux,底层操作系统通道是双向的。
3.1、Buffer(缓冲区)
3.1.1、概述
本质上市一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。
Channel提供从文件、网络读取数据的通道,但是读取或写入的数据都必须经由Buffer
3.1.2、Buffer类及其子类
Buffer是一个顶层父类,是一个抽象类
Buffer类定义了所有的缓冲区都具有的四个属性类提供关于其所包含的数据元素的信息
属性 | 描述 |
Capacity | 容量,即可以容纳的最大数据量,创建时设定,不能被改变 |
Limit | 表示缓冲区当前终点,不能对缓冲区超过极限的位置进行读写操作,且极限是可以修改的 |
Position | 位置,下一个要被读或写的元素的索引,每次读写缓冲区数据时都会改变值,为下次读写做准备 |
Mark | 标记 |
方法一览表
//jdk 1.4 public final int capacity() public final int position() public final Buffer position(int newPosition) public final int limit() public final Buffer limit(int newLimit) public final Buffer mark() public final Buffer reset() public final Buffer clear() public final Buffer flip() //反转次缓冲区 public final boolean hasRemaining() public abstract boolean isReadOnly() //jdk 1.6 public abstract boolean hasArray() public abstract Object array()
ByteBuffer,最常用的类型
public static ByteBuffer allocateDirect(int capacity)//创建直接缓冲区 public static ByteBuffer allocate(int capacity)//设置容量 public abstract byte get();//从当前位置position上get,get之后,position位置会加1 public abstract byte get(int index); //从绝对位置上get public abstract ByteBuffer put(byte b); ////从当前位置position上添加,put之后,position位置会加1 public abstract ByteBuffer put(int index, byte b);//从绝对位置上put
3.2、Channel(通道)
1)类似流,但是有区别
1、通道可以同时进行读写,而流只能读或者只能写
2、通道可以实现异步读写数据
3、通道可以实现从缓冲读取数据,也可以写数据到缓存。
2)BIO的Stream是单向的,例如FileInputStream对象只能进行读取数据的操作,而NIO中的通道Channel是双向的
3)Channel在NIO中是一个接口:public interface Channel extends Closeable
4)常用给的Channel类:FileChannel(文件读写)、DatagramChannel(UDP数据读写)、ServerSocketChannel、SocketChannel,后两个用于TCP数据读写在NetworkChannel下。
查看下ServerSocketChannel、SocketChannel实现位置
3.2.1、FileChannel
在AbstractInterruptibleChannel类下,具体实现FileChannelImpl
主要是用来对本地文件进行IO操作,常见方法
1)public int read(ByteBuffer var1),从通道读取数据并放到缓冲区中
2)public int write(ByteBuffer var1),把u缓冲区中的数据写到通道
3)public long transferFrom(ReadableByteChannel src, long position, long count),从目标通道中复制数据到当前通道
4)public long transferTo(long position, long count, WritableByteChannel target),把数据从当前通道复制给目标通道。
示例一、本地文件写入
图示理解
代码开发
public static void main(String[] args) throws Exception { String str = "hello lihongxu"; //创建一个输出流 -》channel FileOutputStream fileOutputStream = new FileOutputStream("test.txt"); //通过fileOutputStream 获取对应的filechannel ,FileChannel 实现是FileChannelImpl FileChannel channel = fileOutputStream.getChannel(); //创建一个缓冲区ByteBuffer ByteBuffer byteBuffer = ByteBuffer.allocate(1024); //将Str放入到buffer byteBuffer.put(str.getBytes()); //反转buffer byteBuffer.flip(); //将byteBuffer数据写入到fileChannel中 channel.write(byteBuffer); fileOutputStream.close(); }
示例二、本地文件读取
代码:
public static void main(String[] args) throws Exception { //创建输入流 File file = new File("test.txt"); FileInputStream fileInputStream = new FileInputStream(file); //fileInputStream 获取对应的filechannel ,FileChannel 实现是FileChannelImpl FileChannel channel = fileInputStream.getChannel(); //创建一个缓冲区ByteBuffer ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length()); //将fileChannel数据读取到byteBuffer中 channel.read(byteBuffer); System.out.println(new String(byteBuffer.array())); fileInputStream.close(); }
示例三、通过一个ByteBuffer进行文件读取写入
代码
public static void main(String[] args) throws Exception { //创建输入流 // FileInputStream fileInputStream = new FileInputStream(file); FileInputStream fileInputStream = new FileInputStream("testIn.txt"); FileChannel channelIn = fileInputStream.getChannel(); FileOutputStream fileOutputStream = new FileOutputStream("testOut.txt"); FileChannel channelOut = fileOutputStream.getChannel(); //创建一个缓冲区ByteBuffer ByteBuffer byteBuffer = ByteBuffer.allocate(5); while (true) { //需要每次重置 /** * position = 0; * limit = capacity; * mark = -1; */ byteBuffer.clear(); //将fileChannel数据读取到byteBuffer中 int read = channelIn.read(byteBuffer); System.out.println("read=" + read); if (read == -1) { break; } byteBuffer.flip(); channelOut.write(byteBuffer); } fileInputStream.close(); fileOutputStream.close(); }
示例四、Channel拷贝文件
通过使用 transferFrom 文件拷贝
public static void main(String[] args) throws Exception { // 创建相关的流 FileInputStream fileInputStream = new FileInputStream("testIn.txt"); FileOutputStream fileOutputStream = new FileOutputStream("testIn.txt.copy"); //获取各个流对应的fileChanel FileChannel sourceCh = fileInputStream.getChannel(); FileChannel destCh = fileOutputStream.getChannel(); // 使用transferFrom destCh.transferFrom(sourceCh, 0, sourceCh.size()); sourceCh.close(); destCh.close(); fileInputStream.close(); fileOutputStream.close(); }
示例五、ByteBuffer与Channel更多示例
ByteBuffer支持类型化的put和get,put是放入的是什么数据类型,get就应该使用相应的数据类型来取出,否则有可能有BufferUnderflowException异常。
public static void main(String[] args) throws Exception { // 创建相关的流 ByteBuffer buffer = ByteBuffer.allocate(64); buffer.putInt(100); buffer.putLong(9); buffer.putChar('李'); buffer.putShort((short) 4); // 取出 buffer.flip(); System.out.println(); System.out.println(buffer.getInt()); System.out.println(buffer.getLong()); System.out.println(buffer.getChar()); System.out.println(buffer.getShort()); }
只读Buffer示例
public static void main(String[] args) { ByteBuffer buffer = ByteBuffer.allocate(64); for (int i = 0; i < 64; i++) { buffer.put((byte) i); } buffer.flip(); //得到一个只读buffer ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer(); System.out.println(readOnlyBuffer.getClass()); //读取 while (readOnlyBuffer.hasRemaining()) { System.out.println(readOnlyBuffer.get()); } }
MappedByteBuffer,可以让文件直接在内存(堆外的内存)中进行修改,而如何同步到文件有NIO来完成
public static void main(String[] args) throws Exception { RandomAccessFile randomAccessFile = new RandomAccessFile("testIn.txt", "rw"); // 获取对应通道 FileChannel channel = randomAccessFile.getChannel(); //第三个参数:映射到内存的大小,即将文件的多少个字节映射到内存 //可直接修改的范围就是0-5 // MappedByteBuffer 可让文件直接在内存(堆外内存)中修改,操作系统不需要拷贝一次 MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5); mappedByteBuffer.put(0, (byte) 'H'); mappedByteBuffer.put(3, (byte) '9'); randomAccessFile.close(); System.out.println("修改成功"); }
上面都是通过一个Buffer完成,NIO还支持通过多个Bufer(Buffer数组)完成读写操作,即Scattering和Gathering【分散和聚合】
public static void main(String[] args) throws Exception { //Scattering:将数据写入Buffer时,可以采用Buffer数组,依次写 //Gathering:从buffer读取数据时,可以采用buffer数组,依次读 // 使用ServerSocketChannel 和SocketChannel 网络 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); InetSocketAddress inetSocketAddress = new InetSocketAddress(7000); // 绑定端口到socket,并启动 serverSocketChannel.socket().bind(inetSocketAddress); ByteBuffer[] byteBuffers = new ByteBuffer[2]; byteBuffers[0] = ByteBuffer.allocate(5); byteBuffers[1] = ByteBuffer.allocate(3); // 等客户端链接(telnet) SocketChannel socketChannel = serverSocketChannel.accept(); int messageLength = 8; while (true) { int byteRead = 0; while (byteRead < messageLength) { long read = socketChannel.read(byteBuffers); byteRead += read;//累计读取到的字节数 System.out.println("byteRead=" + byteRead); // 使用流打印,看看当前的这个Bufferde postion和limit Arrays.asList(byteBuffers).stream() .map(p -> "position=" + p.position() + ",limit=" + p.limit()) .forEach(p -> System.out.println(p)); } Arrays.asList(byteBuffers).forEach(p -> p.flip()); //将数据读出显示到客户端 long byteWrite = 0; while (byteWrite < messageLength) { long write = socketChannel.write(byteBuffers); byteWrite += write; } // close Arrays.asList(byteBuffers).forEach(p -> p.clear()); System.out.println("byteRead=" + byteRead + ",byteWrite=" + byteWrite + ",messageLength=" + messageLength); } }
使用telnet测试
命令行输入:telnet 127.0.0.1 7000
输入内容测试即可
3.3、Selector(选择器)
1)Java的NIO,用非阻塞的IO方式,可以用一个线程,处理多个的客户端连接,就会使用到Selector
2)Selector能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector上),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
3)只有在连接/通道真正有读写 事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。
4)避免了多线程之间的上下文切换导致的开销
netty相关
1)netty的IO线程NioEventLoop聚合了Selector(选择器,也叫多路复用器),可以同时并发处理成百上千个客户端连接。
2)当线程从某客户端Socket通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务
3)线程通常将非阻塞IO的空闲时间用于在其他通道执行IO操作,所以单独的线程可以管理多个输入和输出通道。
4)由于读写操作都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由于频繁操作IO阻塞导致的线程挂起。
5)一个IO线程可以并发处理N个客户端连接和读写操作,这从根本上解决传统同步阻塞IO一连接一线程模型,架构的性能,弹性伸缩能力和可靠性都得到了极大提升。
3.3.1、相关方法
NIO中的ServerSocketChannel功能类似于ServerSocket,SocketChannel功能类似于Socket
selector是一个抽象类,常用方法如下:
public static Selector open()//得到一个选择器对象
public abstract int select(long timeout)//监控所有注册的通道,当其中有IO操作可以进行时,将对应的SelectKey加入到内部集合中并返回,参数用来设置超时时间
public abstract Set<SelectionKey> selectedKeys();从内部集合中得到所有的SelectionKey
其中select() 阻塞、select(1000)阻塞1000毫秒、wakeup()唤醒selector、selectNow()不阻塞,立即返回
3.3.2、工作原理图
1、当客户端连接时,会通过ServerSocketChannel得到SocketChannel
2、Selector进行监听Select方法,返回有事件发生的通道的个数
3、将SocketChannel注册到Selector上register(selector sel,int ops),一个selector上可以注册多个SocketChannel
4、注册后返回一个SelectionKey,会和该Selector关联(Set集合)
5、进一步得到各个SelectionKey(有事件发生的)
6、再通过SelectionKey反向获取SocketChannel,方法时channel
7、可以通过得到的channel,完成业务处理
3.3.3、nio服务端-客户端实例
服务端
public class NIOServer { public static void main(String[] args) throws Exception { // 创建ServerSocketChannel-》ServerSocket ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 得到一个Selector Selector selector = Selector.open(); // 绑定一个6666端口,在服务端监听 serverSocketChannel.socket().bind(new InetSocketAddress(6666)); // 设置为非阻塞 serverSocketChannel.configureBlocking(false); // 把serversocketchannel注册到Selector上。关系事件为OP serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while (true) { if (selector.select(1000) == 0) { // 没有事件发生, System.out.println("服务器等待了1s.无连接"); continue; } // 有时间发生的集合 Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> keyIterator = selectionKeys.iterator(); while (keyIterator.hasNext()) { SelectionKey selectionKey = keyIterator.next(); if (selectionKey.isAcceptable()) { SocketChannel socketChannel = serverSocketChannel.accept(); System.out.println("客户端连接成,生产了一个socketChannel=" + socketChannel.hashCode()); socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024)); } if (selectionKey.isReadable()) { SocketChannel channel = (SocketChannel) selectionKey.channel(); ByteBuffer buffer = (ByteBuffer) selectionKey.attachment(); channel.read(buffer); System.out.println("from客户端" + new String(buffer.array())); } keyIterator.remove(); } } } }
客户端
public class NIOClient { public static void main(String[] args) throws Exception { SocketChannel socketChannel = SocketChannel.open(); socketChannel.configureBlocking(false); InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666); if(!socketChannel.connect(inetSocketAddress)){ while (!socketChannel.finishConnect()){ System.out.println("连接需要时间,客户端不会阻塞,可以做其他工作"); } } ByteBuffer buffer = ByteBuffer.wrap("hello lhx".getBytes()); socketChannel.write(buffer); System.in.read(); } }
3.3.4、SelectionKey
表示Selector和网络通道的注册关系,共四种:
public static final int OP_READ = 1 << 0; public static final int OP_WRITE = 1 << 2; public static final int OP_CONNECT = 1 << 3; public static final int OP_ACCEPT = 1 << 4;
相关方法
public abstract Selector selector();//得到与之关联的Selector对象 public abstract SelectableChannel channel();//得到与之关联的通道 public final Object attachment()//得到与之关联的共享数据 public abstract SelectionKey interestOps(int ops);//设置或改变监听事件
3.3.5、ServerSocketChannel
在服务器端监听新的客户端Socket连接
public static ServerSocketChannel open();//得到一个ServerSocketChannel通道 public final ServerSocketChannel bind(SocketAddress local)//设置服务器端口号 public final SelectableChannel configureBlocking(boolean block)//设置阻塞或非阻塞模式,取值FALSE表示采用非阻塞模式 public abstract SocketChannel accept()//接受一个连接,返回代表这个连接通道的对象 public final SelectionKey register(Selector sel, int ops,Object att)//注册一个选择器并设置监听事件
3.3.6、SocketChannel
网络IO通道,具体负责进行读写操作。NIO把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区。
public static SocketChannel open();//得到一个SocketChannel通道 public final SelectableChannel configureBlocking(boolean block)//设置阻塞或非阻塞模式,取值FALSE表示采用非阻塞模式 public boolean connect(SocketAddress local)//连接服务器 public boolean finishConnect()//若果上面的方法连接失败,接下来就通过该方法完成连接操作 public int write(ByteBuffer sec)//往通道里写数据 public int read(ByteBuffer dst)//从通道里读数据 public final SelectionKey register(Selector sel, int ops,Object att)//注册一个选择器并设置监听事件
四、NIO与零拷贝
1)零拷贝时网络编程的关键,性能优化
2)在Java程序中,常用的零拷贝有mmap(内存映射)和sendFile。
注意:零拷贝是从操作系统角度,是没有CPU拷贝。因为内核缓冲区之间,没有数据时重复的(只有kernel buffer有一份数据)
零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,比如更少的上下文切换,更少的CPU缓存伪共享以及无CPU校验和计算。
DMA:direct memory access(直接内存拷贝[不适用CPU])
4.0、mmap(内存映射)和sendFile区别
1)mmap适合小数据量读写,sendFile适合大文件传输。
2)mmap需要4次上下文切换,3次数据拷贝;sendFile需要3次上下文切换,最少2次数据拷贝。
3)sendFile可以利用DMA方式,减少CPU拷贝,mmap则不能(必须从内核拷贝到Socket缓冲区)
4.1、传统IO,数据拷贝,用户态与内核态切换
4.2、mmap,数据拷贝,用户态与内核态切换
mmap通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间数据。这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。
4.3、sendFile,数据拷贝,用户态与内核态切换
1)linux2.1版本提供了sendFile函数,其基本原理如下:数据根本不经过用户态,直接通内核缓冲区进入到socket buffer,同时,由于和用户态完全无关,就减少了一次上下文切换。
2)Linux在2.4版本中,做了一些修改,避免了从内核缓冲区拷贝到Socket buffer的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。
其实有一cpu 拷贝kernel buffer-》socket buffer,但是拷贝的信息很少,比如length、offset,消耗底,可以忽略。
4.4、实际案例
1)传统方式
Server
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package com.github.bjlhx15.netty.demo.nio.zerocopy; import java.io.DataInputStream; import java.net.ServerSocket; import java.net.Socket; public class OldIOServer { public static void main(String[] args) throws Exception { ServerSocket serverSocket = new ServerSocket(7001); while (true){ Socket socket = serverSocket.accept(); DataInputStream dataInputStream = new DataInputStream(socket.getInputStream()); try { byte[] bytes = new byte[4096]; while (true){ int read = dataInputStream.read(bytes, 0, bytes.length); if(-1==read){ break; } } }catch (Exception e){ e.printStackTrace(); } } } }
Client
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package com.github.bjlhx15.netty.demo.nio.zerocopy; import java.io.DataOutputStream; import java.io.FileInputStream; import java.net.Socket; public class OldIOClient { public static void main(String[] args) throws Exception { Socket socket = new Socket("localhost", 7001); String fileName = "commons-math3-3.6.1.jar"; FileInputStream fileInputStream = new FileInputStream(fileName); DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream()); byte[] buffer = new byte[4096]; long readCount; long total = 0; long startTime = System.currentTimeMillis(); while ((readCount = fileInputStream.read(buffer)) >= 0) { total += readCount; dataOutputStream.write(buffer); } System.out.println("发送总字节数:" + total + ",耗时:" + (System.currentTimeMillis() - startTime)); dataOutputStream.close(); socket.close(); fileInputStream.close(); } }
2)NIO方式
Server
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package com.github.bjlhx15.netty.demo.nio.zerocopy; import java.io.IOException; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.nio.ByteBuffer; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; public class NewIOServer { public static void main(String[] args) throws Exception { InetSocketAddress address = new InetSocketAddress(7001); ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); ServerSocket serverSocket = serverSocketChannel.socket(); serverSocket.bind(address); ByteBuffer byteBuffer = ByteBuffer.allocate(4096); while (true) { SocketChannel socketChannel = serverSocketChannel.accept(); int readCount = 0; while (-1 != readCount) { try { readCount = socketChannel.read(byteBuffer); } catch (IOException e) { // e.printStackTrace(); break; } byteBuffer.rewind(); } } } }
Client
public static void main(String[] args) throws Exception { SocketChannel socketChannel = SocketChannel.open(); socketChannel.connect(new InetSocketAddress("localhost", 7001)); String fileName = "commons-math3-3.6.1.jar"; FileChannel fileChannel = new FileInputStream(fileName).getChannel(); long startTime = System.currentTimeMillis(); // 在linux下一个transferTo方法就可以完成传输 //在windows下一次调用transferTo只能发送8m,就需要分段传输文件,而且要记录传输位置 //transferTo底层使用到零拷贝 long total = fileChannel.transferTo(0, fileChannel.size(), socketChannel); System.out.println("发送总字节数:" + total + ",耗时:" + (System.currentTimeMillis() - startTime)); fileChannel.close(); }
五、Java AIO基本介绍
1)JDK7引入了Asynchronous IO,即AIO。在进行IO编程中,常用到两种模式:Reactor和Proactor。Java的NIO时Reactor,当有事件触发时,服务器得到通知,进行相应的处理。
2)AIO即NIO2.0,叫做异步不阻塞的IO,AIO引入异步通道的概念,采用了Proactor模式,简化了程序编写,有效的请求才启动线程,他的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接事件较长的应用
3)目前AIO还没有广泛应用,Netty也是基于NIO,不是AIO。
六、BIO、BIO、AIO对比