上一章咱们介绍了操做系统层面的 IO 模型。java
而且介绍了 IO 多路复用的底层实现中,select,poll 和 epoll 的区别。程序员
咱们在这里在强调一下几个概念。windows
一个 IO 操做的具体步骤:api
对于操做系统来讲,进程是没有直接操做硬件的权限的,因此必须请求内核来帮忙完成。缓存
同步和异步的区别在于第二个步骤是否阻塞,若是从内核缓冲区复制到用户缓冲区的过程阻塞,那么就是同步 IO,不然就是异步 IO。因此上面提到的前四种 IO 模型都是同步 IO,最后一种是异步 IO。服务器
阻塞和非阻塞的区别在于第一步,发起 IO 请求是否会被阻塞,若是阻塞直到完成那么就是传统的阻塞 IO,不然就是非阻塞 IO。因此上面提到的第一种 IO 模型是阻塞 IO,其他的都是非阻塞 IO。网络
介绍完操做系统层面的 IO 模型,咱们来看看,Java 提供的 IO 相关的 API。并发
Java 中提供三种 IO 操做的 API,阻塞 IO(BIO,同步阻塞),非阻塞 IO(NIO,同步非阻塞)和异步 IO (AIO,异步非阻塞)。app
Java 中提供的 IO 有关的 API,在文件处理的时候,实际上是依赖操做系统层面的 IO 操做实现的。好比在 Linux 2.6 之后,Java 中的 NIO 和 AIO 都是经过 epoll(前面讲过的,IO 多路复用) 来实现的。而在 windows 上,AIO 是经过 IOCP 来实现的。dom
能够把 Java 中的 BIO,NIO 和 AIO 理解为是 Java 语言对操做系统的各类 IO 模型的封装。程序员在使用这些 API 的时候,不须要关心操做系统层面的知识,只须要使用 Java API 就能够了。
BIO 就是传统的 java.io 包,它是基于流模型实现的,交互方式是同步阻塞,也就是在读取或者写入输入输出流的时候,在读写动做完成以前,线程会一直阻塞在那里。它的效率比较低,容易成为性能瓶颈。
AIO 是 Java 1.7 引入的包,是 NIO 的升级版本,提供了异步非阻塞的 IO 操做方式,因此人们叫它 AIO,异步 IO 是基于事件回调机制实现的,也就是应用操做以后会直接返回,不会阻塞在那里,当后台处理完成,操做系统会通知相应的线程进行后续操做。底层也是依赖于 IO 多路复用模型,基于 epoll 实现,异步非阻塞模式。
传统的 Socket 实现
//服务端 ServerSocket serverSocket = ...... serverSocket.bind(8899); while(true){ Socket sokcet = serverSocket.accept(); //阻塞方法 new Thread(socket); run(){ socket.getInputStream(); .... .... } } //客户端 Socket socket = new Socket("localhost",8899); socket.connect(); 8899 是用于客户端向服务端发起链接的端口号,并非传递数据的端口号,服务端会根据每一个链接也就是 Socket 选择一个端口与客户端进行通讯。
在 Java 中,线程的实现是比较重量级的,因此线程的启动和销毁是很消耗服务器资源的,即便使用线程池来实现,使用上述传统的 Socket 方式,当链接数急剧上升也会带来性能瓶颈,缘由是线程的上下文切换开销会在高并发的时候体现的很明显,而且以上方式是同步阻塞,性能问题在高并发的时候会体现的尤其明显。
NIO 多路复用
Java new IO 底层是基于 IO 多路复用模型实现的。NIO 是利用了单线程轮训事件的机制,经过高效地地位就绪的 Channel,来决定作什么,仅仅 select 阶段是阻塞的,能够避免大量的客户端链接时,频繁切换线程带来的问题,应用的扩展能力有了很是大的提升。
// NIO 多路复用 ThreadPoolExecutor threadPool = new ThreadPoolExecutor(4, 4, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()); threadPool.execute(new Runnable() { @Override public void run() { try (Selector selector = Selector.open(); ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();) { serverSocketChannel.bind(new InetSocketAddress(InetAddress.getLocalHost(), port)); serverSocketChannel.configureBlocking(false); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while (true) { selector.select(); // 阻塞等待就绪的Channel Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); try (SocketChannel channel = ((ServerSocketChannel) key.channel()).accept()) { channel.write(Charset.defaultCharset().encode("你好,世界")); } iterator.remove(); } } } catch (IOException e) { e.printStackTrace(); } } }); // Socket 客户端(接收信息并打印) try (Socket cSocket = new Socket(InetAddress.getLocalHost(), port)) { BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(cSocket.getInputStream())); bufferedReader.lines().forEach(s -> System.out.println("NIO 客户端:" + s)); } catch (IOException e) { e.printStackTrace(); }
AIO 版的 Socket 实现
// AIO线程复用版 Thread sThread = new Thread(new Runnable() { @Override public void run() { AsynchronousChannelGroup group = null; try { group = AsynchronousChannelGroup.withThreadPool(Executors.newFixedThreadPool(4)); AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open(group).bind(new InetSocketAddress(InetAddress.getLocalHost(), port)); server.accept(null, new CompletionHandler<AsynchronousSocketChannel, AsynchronousServerSocketChannel>() { @Override public void completed(AsynchronousSocketChannel result, AsynchronousServerSocketChannel attachment) { server.accept(null, this); // 接收下一个请求 try { Future<Integer> f = result.write(Charset.defaultCharset().encode("你好,世界")); f.get(); System.out.println("服务端发送时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())); result.close(); } catch (InterruptedException | ExecutionException | IOException e) { e.printStackTrace(); } } @Override public void failed(Throwable exc, AsynchronousServerSocketChannel attachment) { } }); group.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS); } catch (IOException | InterruptedException e) { e.printStackTrace(); } } }); sThread.start(); // Socket 客户端 AsynchronousSocketChannel client = AsynchronousSocketChannel.open(); Future<Void> future = client.connect(new InetSocketAddress(InetAddress.getLocalHost(), port)); future.get(); ByteBuffer buffer = ByteBuffer.allocate(100); client.read(buffer, null, new CompletionHandler<Integer, Void>() { @Override public void completed(Integer result, Void attachment) { System.out.println("客户端打印:" + new String(buffer.array())); } @Override public void failed(Throwable exc, Void attachment) { exc.printStackTrace(); try { client.close(); } catch (IOException e) { e.printStackTrace(); } } }); Thread.sleep(10 * 1000);
AIO 就是在 NIO 的基础上提供了回调函数。
咱们读取磁盘文件读取到内存中,以流的形式发送或者传输,这种形式咱们使用的太多,太多了。咱们能够 new InputStream 指向一个文件,读取完毕后在写到目标中,这样整个流程就结束了。
一个从磁盘文件读取而且经过socket写出的过程,对应的系统调用以下:
File.read(file, buf, len); Socket.send(socket, buf, len);
传统的I/O方式会通过4次用户态和内核态的切换(上下文切换),两次CPU中内存中进行数据读写的过程。这种拷贝过程相对来讲比较消耗资源。
在整个过程当中,过程1和4是由DMA负责,并不会消耗CPU,只有过程2和3的拷贝须要CPU参与。
咱们思考一个问题,若是在应用程序中,不须要操做内容,过程2和3就是多余的,若是能够直接把内核态读取缓存冲区数据直接拷贝到套接字相关的缓存区,是否是能够达到优化的目的?
在Java中,正好FileChannel的transferTo() 方法能够实现这个过程,该方法将数据从文件通道传输到给定的可写字节通道, 上面的file.read()
和 socket.send()
调用动做能够替换为 transferTo()
调用。
public void transferTo(long position, long count, WritableByteChannel target);
在 UNIX 和各类 Linux 系统中,此调用被传递到 sendfile()
系统调用中,最终实现将数据从一个文件描述符传输到了另外一个文件描述符。
NIO 的零拷贝依赖于操做系统的支持,咱们来看看操做系统意义上的零拷贝的流程(没有内核空间和用户空间数据拷贝)。相比于传统 IO,减小了两次上下文切换和数据拷贝,从操做系统角度称为零拷贝。若是熟悉 JVM 的同窗应该知道,NIO 会使用一块 JVM 以外的内存区域,直接在该区域进行操做。
这种方式的I/O原理就是将用户缓冲区(user buffer)的内存地址和内核缓冲区(kernel buffer)的内存地址作一个映射,也就是说系统在用户态能够直接读取并操做内核空间的数据。
sendfile()系统调用也会引发用户态到内核态的切换,与内存映射方式不一样的是,用户空间此时是没法看到或修改数据内容,也就是说这是一次彻底意义上的数据传输过程。
从磁盘读取到内存是DMA的方式,从内核读缓冲区读取到网络发送缓冲区,依旧须要CPU参与拷贝,而从网络发送缓冲区到网卡中的缓冲区依旧是DMA方式。
从上面咱们能够看出,零拷贝的是指在操做过程当中,CPU 不须要为数据在内存之间拷贝消耗资源,传统的 IO 操做需用从用户态转为内核态,内核拿到数据后还须要由内核态转为用户态将数据拷贝到用户空间,而零拷贝不须要将文件拷贝到用户空间,而直接在内核空间中传输到网络的方式。
内核空间操做文件的过程对用户来讲是不透明的,用户只能请求和接受结果,若是用户想要参与这个过称怎么办?这时候就须要一个内存映射文件(将磁盘上的文件映射到内存之中,修改内存就能够修改磁盘上的文件),直接操做内核空间。
MappedByteBuffer,文件在内存中的映射,Java 程序不用和磁盘打交道,应用程序只须要对内存进行操做,这块内存是一个堆外内存。操做系统负责将咱们对内存映射文件的修改更新到磁盘。
File file = new File("test.zip"); RandomAccessFile raf = new RandomAccessFile(file, "rw"); FileChannel fileChannel = raf.getChannel(); SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("", 1234)); // 直接使用了transferTo()进行通道间的数据传输 fileChannel.transferTo(0, fileChannel.size(), socketChannel);
NIO 的零拷贝由 transferTo() 方法实现。transferTo() 方法将数据从 FileChannel 对象传送到可写的字节通道(如Socket Channel等)。在内部实现中,由 native 方法 transferTo0() 来实现,它依赖底层操做系统的支持。在UNIX 和 Linux 系统中,调用这个方法将会引发 sendfile() 系统调用。
咱们上面也说过,内核空间操做文件的过程对用户来讲是不透明的,用户只能请求和接受结果,若是用户想要参与这个过称怎么办?
File file = new File("test.zip"); RandomAccessFile raf = new RandomAccessFile(file, "rw"); FileChannel fileChannel = raf.getChannel(); MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
首先,它的做用位置处于传统IO(BIO)与零拷贝之间,为什么这么说?
MappedByteBuffer 使用的是 JVM 以外的一块直接内存。