深入分析Java IO机制

一、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为例:

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

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

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

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

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

  1. 同步阻塞IO(BIO):

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

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

  1. 同步非阻塞IO:

当用户线程调用 recvfrom 方法后,如果此时套接字文件还没有准备好,则直接返回一个错误信息,然后CPU就会去做其他事情,而该线程会不断获取CPU时间片进行轮询,所以该模式下虽然是非阻塞,但其线程切换确实很频繁的,所以通过该方式增加的CPU使用时间与线程切换的成本还是需要好好评估的;

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

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

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

  2. 异步IO(AIO):

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

但其内在的实现还是很复杂的,底层还是使用BIO实现的,就不展开描述了,因为对编程人员好像并没有太大的作用。

  1. 信号驱动IO:

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

二、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模型下每个线程之间是不共享资源的。

三、 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 可用一张图大概总结流程:

猜你喜欢

转载自juejin.im/post/5e4a4d26f265da572815c105
今日推荐