深刻分析Java IO机制

1、IO介绍:

1.1 Java IO分类:

  1. IO按照处理的数据类型 可分为:(1)面向字节操做的I/O接口:inputStream,outputStream (2)面向字符操做的接口:Reader,Writer
  2. IO按照数据的传输方式 可分为:(1)面向磁盘操做的I/O接口:File (2)面向网络操做的I/O接口:Socket
  3. 因此I/O主要的操做能够总结为将什么类型的数据以何种传输方式传到什么地方去。

1.2 Unix中IO的五种模型:

以网络IO为例:html

当客户端发送的网络包通过路由器和交换器的转发后到达对应服务端的网络适配器(网卡),并存储在对应网络I/O的套接字文件中,而后操做系统会将该文件中的数据通常经过DMA复制到内存中供应用程序使用;java

Unix网络编程这本书中概述了完成上述操做的几种模型:编程

  1. 首先解释两组名词: 这两组名词其实只是对同一个场景的两种不一样的描述方式:

(1)阻塞与非阻塞: 阻塞与非阻塞主要是从 CPU 的消耗上来讲的,阻塞就是 CPU 停下来等待一个慢的操做完成后CPU 才接着完成其它的事。非阻塞就是在这个慢的操做在执行时 CPU 去干其它别的事,等这个慢的操做完成时,CPU 再接着完成后续的操做。bash

(2)同步与非同步: 同步与非同步主要是从程序方面来讲的,同步指程序发出一个功能调用后,没有结果前不会返回。非同步是指程序发出一个功能调用后,程序便会返回,而后再经过回调机制通知程序进行处理。服务器

  1. 同步阻塞IO(BIO):

注意,此时客户端与服务端已经经过三次握手创建了链接,便可以经过套接字文件进行数据的交换,因此在此模型下的服务端的用户进程阻塞在 recvfrom方法等待客户端发送的数据发送到内存并返回;

这个模型最大的问题就是操做系统中最典型的CPU速度与外设速度不匹配的问题,网络适配器的速度相对于CPU的速度是极慢的,而且此时CPU却一直在阻塞。网络

  1. 同步非阻塞IO:

当用户线程调用 recvfrom 方法后,若是此时套接字文件尚未准备好,则直接返回一个错误信息,而后CPU就会去作其余事情,而该线程会不断获取CPU时间片进行轮询,因此该模式下虽然是非阻塞,但其线程切换确实很频繁的,因此经过该方式增长的CPU使用时间与线程切换的成本仍是须要好好评估的;

而且当数据准备好后,而且线程获取到时间片再次调用recvfrom 时,线程仍是须要等待数据拷贝至内存的。异步

  1. 多路复用IO:(Java NIO原理) socket

    该模型经过一个方法select,该方法一直会阻塞到IO事件的到来(即套接字文件准备好)再返回,这个时候咱们再调用recvfrom方法就只须要等待数据拷贝至内存便可;而且select方法能够监听多个事件,因此联系到Java NIO中时,就是多个线程能够向同一个Selector注册多个事件,从而达到了多路复用的效果。

  2. 异步IO(AIO):post

该模型经过操做系统提供的异步IO方法 aio_read,应用程序调用后便直接返回,而且不须要像前几种模型同样须要等待数据拷贝至内存;

但其内在的实现仍是很复杂的,底层仍是使用BIO实现的,就不展开描述了,由于对编程人员好像并无太大的做用。性能

  1. 信号驱动IO:

其实笼统点讲,AIO和多路复用IO其实也是某种信号进行驱动的IO,即都不须要应用程序阻塞在 网络适配器(网卡)的数据准备好的这个过程当中,而都是通发出种信号进行通知应用程序,虽然信号的实现方式或是用 select 或是用更底层的方式,但本质上仍是很类似的;但信号驱动IO也是须要线程等待数据拷贝至用户空间的。

2、Java BIO:

2.1 简介:

注:《深刻理解计算机系统》中定义,Linux将全部外设抽象成文件,与外设的通讯被抽象成文件的读写;而网络也只是外设的一种;客户端与服务器端创建链接时互相交换了彼此的文件描述符,以后两端进行通讯即为向这两个文件描述符对应的套接字文件中写值


Java中的Socket是对进行通讯的两端的抽象,其封装了一系列TCP/IP层面的底层操做; 代码以下:

  1. 客户端:
//经过一个IP:PORT套接字新建一个Socket对象,肯定要链接的服务器的位置和端口
            Socket socket = new Socket("127.0.0.1", 8089);
            //经过Socket对象拿到OutputStream,能够将其理解经过其向服务器端对应的套接字文件写入数据
            OutputStream outputStream = socket.getOutputStream();
            //使用默认的字符集去解析outputStream的字节流
            PrintWriter printWriter = new PrintWriter(outputStream, true);
            /*向服务器发送一个HTTP1.1的请求*/
            printWriter.println("GET /index.html HTTP/1.1");
            printWriter.println("Host: localhost:8080");
            printWriter.println("Connection Close");
            printWriter.println();
复制代码
  1. 服务端:
//ServerSocket在该套接字上监听链接事件
            ServerSocket serverSocket = new ServerSocket(8089, 1, InetAddress.getByName("127.0.0.1"));
            //服务端阻塞在accept()方法上,直到客户端的connect()请求,并返回一个Socket对象
            socket = serverSocket.accept();
            //从返回的Socket对象中获取该Socket对应的套接字文件的内容并进行读取
            InputStream inputStream = socket.getInputStream();
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
            int i = 0;
            while (i != -1) {
            i = bufferedReader.read();
            System.out.println("拿到的数据为:"+(char)i);
            }
            socket.close();
复制代码

其实Java BIO 即为对系统提供的网络I/O方法的封装;

2.2 Java BIO 带来的问题:

咱们通常都是适用Acceptor模型来进行BIO服务端的建立,即经过一个ServerSocket()监听来自客户端的链接,而后经过三次握手创建链接后便会建立一个子线程并经过线程池进行相应的逻辑处理;

而上述逻辑带来了一系列问题:

  1. Acceptor是一个单线程,即全部链接的请求都是串行处理的,而ServerSocket是经过backlog这个参数来代表在服务端拒绝链接请求以前,能够排队的请求数量,因此这样的模型注定了BIO性能的局限性(排队的通讯线程可能要阻塞一段时间),处理量的局限性;
  2. 阻塞IO天生的问题,即须要一个线程对应一个链接,因此对资源的要求比较高;
  3. 一些特殊的应用场景,如多个线程须要共享资源的时候,而BIO模型下每一个线程之间是不共享资源的。

3、 Java NIO:

3.1 与BIO对比,改变了什么,又为何要这么改变?

图片及实例代码参考来自: juejin.im/post/5d1acd…

  1. Java NIO经过多路复用IO的模型实现了单个Selector线程管理了多个链接,解决了BIO最致命的一个问题;

  2. 不管是In/OutputStream仍是Java NIO中的通道channel 本质上都是对网络I/O文件的抽象,与前者不一样,channel是双通道的,既能够读又能够写。

因此按照I/O多路复用 的模型,当channel中的数据准备好了的时候会返回一个可读的事件,而且经过selector进行处理,安排相应的Socket进行相应数据的读取,这是一个数据可读的事件,而Selector可监听的事件有四种:

SelectionKey.OP_CONNECT // 链接事件
SelectionKey.OP_ACCEPT //接收事件
SelectionKey.OP_READ //数据可读事件
SelectionKey.OP_WRITE //可写事件
复制代码
  1. 为何要引入Buffer机制? 在BIO的时候咱们通常是经过相似于socket.getInputStream.write()方法来直接进行读写的,而NIO中向channel中写入数据必须从buffer中获取,而channel也只能向buffer写入数据,这样使得这样的操做更为接近操做系统执行I/O的方式;细一点讲,是由于在向OutputStream中write()数据即为向接收方Socket对象中的InputStream中的RecvQ队列中,而若是write()的数据大于队列中每一个数据对象限定的长度,就须要进行拆分,而这个过程,咱们是不能够控制的,并且涉及到用户空间与内核空间地址的转换;可是当咱们使用Buffer后,咱们能够控制Buffer的长度,是否扩容以及如何扩容咱们均可以掌握。 参考文章:www.ibm.com/developerwo…

3.2 咱们来看一段实例代码(服务端):

/**
 * @CreatedBy:CVNot
 * @Date:2020/2/21/15:30
 * @Description:
 */
public class NIOServer {
    public static void main(String[] args) {
        try {
            //建立一个多路复用选择器
            Selector selector = Selector.open();
            //建立一个ServerSocket通道,并监听8080端口
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open().bind(new InetSocketAddress(8080));
            //设置为非阻塞
            serverSocketChannel.configureBlocking(false);
            //监听接收数据的事件
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

            while (true){
                selector.select();
                //拿到Selector关心的已经到达事件的SelectionKey集合
                Set keys = selector.selectedKeys();
                Iterator iterator = keys.iterator();
                while (iterator.hasNext()){
                    SelectionKey selectionKey = (SelectionKey)iterator.next();
                    iterator.remove();
                    //由于咱们只注册了ACCEPT事件,因此这里只写了当链接处于这个状态时的处理程序
                    if(selectionKey.isAcceptable()){
                        //拿到产生这个事件的通道
                        ServerSocketChannel serverChannel = (ServerSocketChannel)selectionKey.channel();
                        SocketChannel clientChannel = serverChannel.accept();
                        clientChannel.configureBlocking(false);
                        //并为这个通道注册一个读事件
                        clientChannel.register(selectionKey.selector(), SelectionKey.OP_READ);
                    }

                    else if(selectionKey.isReadable()){
                        SocketChannel clientChannel = (SocketChannel)selectionKey.channel();
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        long bytesRead = clientChannel.read(byteBuffer);
                        while(bytesRead > 0){
                            byteBuffer.flip();
                            System.out.printf("来自客户端的数据" + new String(byteBuffer.array()));
                            byteBuffer.clear();
                            bytesRead = clientChannel.read(byteBuffer);
                        }

                        byteBuffer.clear();
                        byteBuffer.put("客户端你好".getBytes("UTF-8"));
                        byteBuffer.flip();
                        clientChannel.write(byteBuffer);
                    }
                }
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

复制代码

客户端:

/**
 * @CreatedBy:CVNot
 * @Date:2020/2/21/16:06
 * @Description:
 */
public class NIOClient {
    public static void main(String[] args) {
        try {
            Selector selector = Selector.open();
            SocketChannel clientChannel = SocketChannel.open();
            clientChannel.configureBlocking(false);
            clientChannel.connect(new InetSocketAddress(8080));
            clientChannel.register(selector, SelectionKey.OP_CONNECT);
            while (true) {
                //若是事件没到达就一直阻塞着
                selector.select();
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    iterator.remove();
                    if (key.isConnectable()) {
                        /**
                         * 链接服务器端成功
                         *
                         * 首先获取到clientChannel,而后经过Buffer写入数据,而后为clientChannel注册OP_READ事件
                         */
                        clientChannel = (SocketChannel) key.channel();
                        if (clientChannel.isConnectionPending()) {
                            clientChannel.finishConnect();
                        }
                        clientChannel.configureBlocking(false);
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        byteBuffer.clear();
                        byteBuffer.put("服务端你好,我是客户端".getBytes("UTF-8"));
                        byteBuffer.flip();
                        clientChannel.write(byteBuffer);
                        clientChannel.register(key.selector(), SelectionKey.OP_READ);
                    } else if (key.isReadable()) {
                        //通道能够读数据
                        clientChannel = (SocketChannel) key.channel();
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        long bytesRead = clientChannel.read(byteBuffer);
                        while (bytesRead > 0) {
                            byteBuffer.flip();
                            System.out.println("server data :" + new String(byteBuffer.array()));
                            byteBuffer.clear();
                            bytesRead = clientChannel.read(byteBuffer);
                        }
                    } else if (key.isWritable() && key.isValid()) {
                        //通道能够写数据
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}
复制代码

3.3 可用一张图大概总结流程:

相关文章
相关标签/搜索