NIO 总结
IO 基础概念
IO,指的是 input/ouput ,指的是计算机与其他设备的数据交互。而 IO 的模式又分为多种。
阻塞与非阻塞:
阻塞:在处理一个请求时,服务器端会分配一个线程,在处理请求过程中并不是全程都会用到 CPU 的,阻塞式就是即使没有用到 CPU,请求也会占用线程 CPU 直到请求全部执行完成。
非阻塞:在请求未用到 CPU 时,当前线程会去执行其他操作,并且会轮询判断该请求是否需要用到 CPU 了,需要用到时再回来处理请求。
同步与异步:
同步:指多个线程按既定的流程执行,一段代码一段时间只会有一个线程执行。
异步:在发送请求后,会立刻返回,然后当前线程可以处理其他操作,而返回结果会在执行完成后通过回调函数的形式返回。
关于阻塞、非阻塞与同步、异步的联系:
阻塞与非阻塞侧重于 CPU 是否在执行当前操作过程中执行了其他操作,而同步与异步侧重于发送请求后是否立刻返回结果返回。阻塞与非阻塞本质就是属于同步的范畴,因为其都是在等待一次返回的结果。为了更好的解释,改编一下知乎上的一篇回答:
阻塞就是打电话给图书馆,在询问书名后,店员寻找,此时你是一直处于等待状态,直到确定有无该书后通知你。
非阻塞就是在等待时你先干其他事,并且每隔一段时间再询问一下查看是否完成(主动寻求结果)。
同步就是电话未挂断,直到通知你结果,此期间你是否干其他事都行,但是不能挂电话(也就是不能获取到结果(同步代码的执行权))。
异步就是店主先通知你说有其他事,先挂电话,等他找到后再通知你。
阻塞与非阻塞都是在等待第一次电话返回的结果,属于用户线程主动获取返回结果,所以其都是属于同步的,而异步是二次电话。
同步阻塞式 IO 就是接通电话后你就一直等待店主那边通知,期间你不做其他事。
同步非阻塞式 IO 就是接通电话你先干其他事,并且每隔一段时间询问店主是否完成。
异步 IO 就是店主先挂电话,随后你忙其他事,店主打来通知电话再来接。由于异步是通过回调返回结果的,所以异步不存在阻塞非阻塞的概念,都是非阻塞的。
IO 类型
BIO:同步阻塞式 IO ,效率低下,因为是同步阻塞式的,所以一个线程只能处理一个线程请求,当一个连接内没有IO操作时还是会占用线程,这样会严重影响效率。且数据是以流形式进行传输,在高并发场景下效率极低。方向:输入流,输出流(单向)。适用场景:连接数少且连接时间长的架构。
NIO:同步非阻塞式IO,一个线程可以处理多个连接请求。具体过程是当连接请求传来时,服务端会为其创建通道(Channel,双向),然后由选择器(Selector )进行判断选择需要IO操作的请求分配线程去执行,且数据是以缓冲(Buffer ,多组数据,双向)形式进行传输的。所以NIO是双向的。适用场景:连接数多且连接时间较短。
AIO:异步非阻塞式 IO。由于在 Linux 中其底层也是基于 epoll 实现的,所以其效率并没有比 NIO 高多少,再加上 Linux 对 aio 没有优化好,在一些场景中效率甚至比如 NIO ,所以目前使用的并不多。
NIO 三大组件
缓冲区Buffer
本质上就是一个可以读写数据的内存块,可以理解成是一个容器对象,底层包含了一个byte数组用于保存字节数据。也正是因为缓冲区的存在使得NIO有了非阻塞这一特性,因为其他连接在进行IO操作时,可以将当前的IO操作数据暂存在缓冲区,下次再被selector侦测到进行数据传输(从 buffer 微观上看是异步的,但从宏观上总体过程还是同步的)。
jdk 实现:
Buffer 本身是一个抽象类,一共有七种实现,分别是 IntBuffer、FloatBuffer、CharBuffer、DoubleBuffer、ShortBuffer、LongBuffer、ByteBuffer(常用),各自对应着各种的类型数组。
buffer及其实现类中有四个重要属性:1、capacity:能装载的最大容量 2、limit:缓冲区的当前最大终点位置,在 buffer 里填写的数据不能超过 limit 规定的值,可变。 3、position:下一个要读(写)元素的索引位置 4、mark:当前位置的标记
常用方法
红色代表常用
clear():重置 buffer 底层的标记。一般在循环读取 buffer 的数据时调用,如果没有重置,那么在缓冲区容量刚好和读取数据容量一致时position与limit就会相等,那么下一次循环read就会等于0,从而死循环,一次性读完是-1
ByteBuffer
wrap:直接根据数据的字节数组大小来创建一个ByteBuffer
通道 Channel
channel 是 buffer 进行传输的通道,与 BIO 的流不同,它是双向的,既可以读,也可以写。在 JDK 中,channel 常见的实现类有 FileChannel(文件读写),DatagramChannel(UDP读写),ServerSocketChannel和SocketChannel(TCP读写)
常用方法,以 FileChannel 举例
注意:在 linux 环境中,执行一此 transferTo 方法就可以完成传输,因为传输数据大小是没有限制的;而在 windows 环境中 transferTo 方法一次性只能发送 8M,所以需要分段多次传输文件。
选择器 Selector
selector是用来处理多个客户端的连接,它会检测多个注册的通道上是否有事件发生(多个Channel以事件的方式可以注册到同一个selector上(新连接进来也算是事件)),如果有事件发生,便获取事件然后针对每个事件进行相应的操作,这样就可以达到只用一个单线程去管理多个通道,实现多路复用。
常用方法:
多路复用的原理:
1、服务器端启动时,会创建一个ServerSocketChannel对象(这个对象需要注册到selector上),每个客户端首先会创建一个SocketChannel对象,然后通过进行连接。
2、一个selector就管理多个连接,管理方式就是将这些连接生成的SocketChannel注册到selector上。
3、注册后会为这个连接返回一个SelectionKey,会和该Selector关联(集合方式)
4、selector进行监听(通过select方法),返回有事件发生的通道的个数。
5、进一步获取到有事件发生的连接的SelectionKey(通过Selector的selectedKeys方法返回SelectionKey类型的set集合)
6、通过SelectionKey反向获取SocketChannel(通过SelectionKey的channel方法)以及ByteBuffer
7、通过Channel对象进行IO操作
SelectionKey的四种事件类型
1、SelectionKey.OP_ACCEPT —— 接收连接进行事件,表示服务器监听到了客户连接,服务器可以接收这个连接了(第一次连接时的事件)
2、SelectionKey.OP_CONNECT —— 连接就绪事件,表示客户与服务器的连接已经建立成功
3、SelectionKey.OP_READ —— 读就绪事件,表示通道中已经有了可读的数据,可以执行读操作了(通道目前有数据,可以进行读操作了)
4、SelectionKey.OP_WRITE —— 写就绪事件,表示已经可以向通道写数据了(通道目前可以用于写操作)
SelectionKey 相关方法:
selector.keys():获取当前selector所监听的所有连接对应的SelectionKey集合,
selector.selectedKeys():获取当前selector监听中含有事件的连接对应的SelectionKey集合
三者联系
一个线程对应一个 Selector,一个 Selector 管理多个 Channel,一个 Channel 对应一个 Buffer,其中 Channel 与 Buffer 都是双向的,可以高效地进行 IO 操作。
零拷贝
首先要明确,零拷贝并不是没有数据拷贝的发生,而是在拷贝过程中没有用到 CPU ,因为 CPU 是程序执行的重要资源,没有占用 CPU,就增强了程序的执行效率。零拷贝是在 Linux 环境下对 NIO IO 过程的一种优化。在 NIO 的 channel 实现类的 transferTo 方法就实现了零拷贝。
传统 IO
在传统 IO 过程中,会经历三次状态切换和四次拷贝
1、线程在用户空间发起 read() 方法,线程从用户态转成内核态
2、DMA将磁盘数据拷贝到内核缓存后,CPU又将数据从内核缓存拷贝到用户缓存,这时线程从内核态又切换为用户态
3、这时候知道了数据要往哪里写,CPU将数据从用户缓存拷贝至socket缓存,线程又从用户态切换为内核态
4、最后DMA将数据从内核缓存拷贝至协议栈,read()调用结束返回,线程又从内核态切换为用户态。
DMA :直接内存拷贝(不经过 CPU)
MMAP 优化
通过内存映射,使用mmap()函数将用户空间映射到内核缓冲区,用户空间共享内核空间的数据,所以在拷贝时就减少了上面第二步的cpu拷贝,直接从内核缓存拷贝到socket缓存,但是状态切换还是三次。
sendFile优化
Linux2.1引入了sendFile函数,直接摒弃了与用户空间的交互,相比于mmap减少一次状态的切换
零拷贝
linux2.4对sendFile函数做了一些修改,将内核缓存直接拷贝到协议栈,从而取消了仅有的一次CPU拷贝(还是有一次基本信息的socket缓存CPU拷贝的,但是消耗很低。)
mmap与sendFile区别:
1、mmap适合小数据量读写,sendFile适合大文件传输
2、mmap需要4次上下文切换(这里算上了切换为初始状态),3次数据拷贝;sendFile需要3次上下文切换,最少2次数据拷贝(零拷贝优化)
3、sendFile可以利用DMA方式,减少CPU拷贝,mmap则不能(必须从内核拷贝到Socket缓冲区)
案例
1、Put、Get 使用
ByteBuffer 支持类型化的 put 和 get,put 和 get 的顺序、类型必须一致,否则会抛出异常。
public class BufferPutGet { public static void main(String[] args) { //创建一个Buffer 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.getInt()); //当数据顺序类型匹配不上时就会抛出异常 System.out.println(buffer.getShort()); } }
2、只读 Buffer
可以将普通的 buffer 转成只读 buffer。转成只读 buffer 后再 put 数据就会抛 ReadOnlyBufferException 异常。
public class BufferReadOnly { 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 readOnlyBuffer = buffer.asReadOnlyBuffer(); System.out.println(readOnlyBuffer.getClass()); //读取 while (readOnlyBuffer.hasRemaining()) { System.out.println(readOnlyBuffer.get()); } readOnlyBuffer.put((byte)100); //ReadOnlyBufferException } }
3、文件拷贝
public class NioFileChannelCopy { //普通拷贝 @Test public void test01() throws IOException { FileInputStream fileInputStream = new FileInputStream("d://text01.txt"); FileChannel inputStreamChannel = fileInputStream.getChannel(); FileOutputStream fileOutputStream = new FileOutputStream("d://text02.txt"); FileChannel outputStreamChannel = fileOutputStream.getChannel(); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); while(true){ byteBuffer.clear(); //重置buffer底层的标记,如果没有重置,那么在缓冲区容量刚好和读取数据容量一致时,position与limit就会相等,那么下一次循环read就会等于0,从而死循环 int read = inputStreamChannel.read(byteBuffer); if(read==-1){ break ; } byteBuffer.flip(); outputStreamChannel.write(byteBuffer); } fileInputStream.close(); fileOutputStream.close(); } //通过FileChannel的方法直接拷贝 @Test public void test02() throws IOException { FileInputStream fileInputStream = new FileInputStream("d://text01.txt"); FileChannel inputStreamChannel = fileInputStream.getChannel(); FileOutputStream fileOutputStream = new FileOutputStream("d://text03.txt"); FileChannel outputStreamChannel = fileOutputStream.getChannel(); //进行拷贝 outputStreamChannel.transferFrom(inputStreamChannel,0,inputStreamChannel.size()); outputStreamChannel.close(); inputStreamChannel.close(); fileOutputStream.close(); fileInputStream.close(); } }
4、CS零拷贝测试
// 传统方式传输 public class OldIOClient { public static void main(String[] args) throws Exception { Socket socket = new Socket("localhost", 7001); String fileName = "protoc-3.6.1-win32.zip"; InputStream inputStream = 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 = inputStream.read(buffer)) >= 0) { total += readCount; dataOutputStream.write(buffer); } System.out.println("发送总字节数: " + total + ", 耗时: " + (System.currentTimeMillis() - startTime)); dataOutputStream.close(); socket.close(); inputStream.close(); } } 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[] byteArray = new byte[4096]; while (true) { int readCount = dataInputStream.read(byteArray, 0, byteArray.length); if (-1 == readCount) { break; } } } catch (Exception ex) { ex.printStackTrace(); } } } }
零拷贝:
// 零拷贝 public class NewIOClient { public static void main(String[] args) throws IOException { SocketChannel socketChannel = SocketChannel.open(); socketChannel.connect(new InetSocketAddress("localhost",7001)); String fileName="protoc-3.6.1-win32.zip"; //得到一个文件channel FileChannel fileChannel = new FileInputStream(fileName).getChannel(); long startTime = System.currentTimeMillis(); //在linux下一个transferTo方法就可以完成传输 //但是在windows下一次调用transferTo 只能发送8M,就需要分段传输文件,而且要注意传输的位置 //transferTo方法底层实现了零拷贝 long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel); System.out.println("发送的总字节数="+transferCount+"耗时:"+(System.currentTimeMillis()-startTime)); //关闭通道 fileChannel.close(); } } public class NewIOServer { public static void main(String[] args) throws IOException { ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); ServerSocket serverSocket = serverSocketChannel.socket(); serverSocket.bind(new InetSocketAddress(7001)); //创建buffer ByteBuffer byteBuffer = ByteBuffer.allocate(1024); while(true){ SocketChannel socketChannel = serverSocketChannel.accept(); int readCount=0; while(readCount!=-1){ readCount=socketChannel.read(byteBuffer); byteBuffer.rewind(); } } } }
5、综合聊天室
客户端
public class GroupChatClient { private SocketChannel socketChannel; private static final int PORT=7777; private Selector selector; private final String HOST="localhost"; private String name; public GroupChatClient() { try { selector = Selector.open(); socketChannel=SocketChannel.open(new InetSocketAddress(HOST,PORT)); socketChannel.configureBlocking(false); socketChannel.register(selector,SelectionKey.OP_READ); name=socketChannel.getLocalAddress().toString(); System.out.println(name+"is OK ..."); } catch (IOException e) { e.printStackTrace(); } } //向服务器发送消息 public void senMsg(String msg){ msg=name+"说:"+msg; try { socketChannel.write(ByteBuffer.wrap(msg.getBytes())); } catch (IOException e) { e.printStackTrace(); } } //读取消息 public void readMsg(){ try { int selectCount = selector.select(); //阻塞等待连接 if(selectCount>0){ Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator(); while(keyIterator.hasNext()){ SelectionKey selectionKey = keyIterator.next(); if(selectionKey.isReadable()){ SocketChannel channel = (SocketChannel) selectionKey.channel(); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); channel.read(byteBuffer); String msg=new String(byteBuffer.array()); System.out.println(msg.trim()); } keyIterator.remove(); } }else{ // System.out.println("没有新连接"); } } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args){ GroupChatClient groupChatClient = new GroupChatClient(); Scanner scanner = new Scanner(System.in); new Thread() { @Override public void run() { while (true){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } groupChatClient.readMsg(); } } }.start(); while(true){ while(scanner.hasNext()){ String line = scanner.nextLine(); groupChatClient.senMsg(line); } } } }
服务器端
public class GroupChatServer { private Selector selector; private ServerSocketChannel listenChannel; private static final int PORT=7777; public GroupChatServer(){ try { selector=Selector.open(); listenChannel=ServerSocketChannel.open(); listenChannel.socket().bind(new InetSocketAddress(PORT)); listenChannel.configureBlocking(false); listenChannel.register(selector,SelectionKey.OP_ACCEPT); } catch (IOException e) { e.printStackTrace(); } } public void listen() throws IOException { while(true){ if(selector.select(1000)==0){ // System.out.println("没有事件"); continue; } Iterator<SelectionKey> keyIterator= selector.selectedKeys().iterator(); //获取有事件的selectionKeys集合对应的迭代器 while (keyIterator.hasNext()){ SelectionKey selectionKey = keyIterator.next(); if(selectionKey.isAcceptable()){ //如果当前事件是新连接事件 SocketChannel socketChannel = listenChannel.accept(); socketChannel.configureBlocking(false); socketChannel.register(selector,SelectionKey.OP_READ); System.out.println(socketChannel.getRemoteAddress()+" 上线 "); } if(selectionKey.isReadable()){ //事件是read事件,即通道是可读的状态 getMsg(selectionKey); } keyIterator.remove(); } } } public void getMsg(SelectionKey selectionKey){ SocketChannel socketChannel = (SocketChannel) selectionKey.channel(); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); try { int count = socketChannel.read(byteBuffer); if(count>0){ //如果读取的数据量不为0就输出并转发给其他客户端 String msg=new String(byteBuffer.array()); System.out.println("from客户端:"+msg); sendToOthers(msg,socketChannel); } } catch (IOException e) { try { System.out.println(socketChannel.getRemoteAddress()+"离线了"); //取消注册 selectionKey.cancel(); //关闭通道 socketChannel.close(); } catch (IOException e1) { e1.printStackTrace(); } e.printStackTrace(); } } public void sendToOthers(String msg,SocketChannel socketChannel){ System.out.println("消息转发给客户端线程:"+Thread.currentThread().getName()); Iterator<SelectionKey> allKeyIterator = selector.keys().iterator(); while (allKeyIterator.hasNext()){ SelectionKey key = allKeyIterator.next(); Channel channel = key.channel(); if(channel instanceof SocketChannel && channel!=socketChannel){ SocketChannel channel1=(SocketChannel)channel; ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes()); try { channel1.write(byteBuffer); } catch (IOException e) { e.printStackTrace(); } } } } public static void main(String[] args){ GroupChatServer groupChatServer = new GroupChatServer(); try { groupChatServer.listen(); } catch (IOException e) { e.printStackTrace(); } } }