关于系统涉及的I/O模型

本文已被收录至 GitHub: https://github.com/JavaLiuTongXue/JavaCoding ,想要获取更多的干货文章,可以关注公众号:不会说话的刘同学

我们平时说的I/O操作有两种,一种是本地磁盘I/O,也就是我们经常使用的 InputStream 和 OutputStream (这两个类是用于本地磁盘I/O操作),另外一种是网络I/O操作,也就是我平时通过网络进行读取或发送数据,比如通过 http 请求一个接口,接口里所涉及到的请求数据和响应数据

Java 中对于网络I/O操作主要是通过 Socket 进行的,而 Sokcet 又会调用底层操作系统的相关接口,我们想要弄清楚 Java 的网络I/O操作就需要先搞清楚底层操作系统提供的I/O模型

我们的程序一般都是运行在linux系统上,linux系统对于网络I/O操作提供了五种I/O模型–同步阻塞 I/O、同步非阻塞 I/O、I/O 多路复用、信号驱动 I/O 和异步I/O

信号驱动I/O主要是运用于硬件层面,就不作详细的讲解,Java也很少会使用到这种I/O模型,本篇文章会详细的讲解除信号驱动I/O之外的其中的四种I/O模型并给出对应的代码示例

程序对于网络数据的读取

在弄清楚I/O模型之前,我们先来看看程序对于网络数据的读取会经过哪些阶段

我们现在有A、B两个系统,A系统去调用B系统的一个程序接口,这个程序接口除了一些请求数据之外,还会响应一些数据,那么从A系统发起请求调用到响应数据会经过以下几个步骤:

1、A系统调用底层操作系统的网络相关接口,底层操作系统会通过 TCP 三次握手的方式尝试与B系统建立连接

2、连接建立完成之后,也就意味着TCP通道建立完成了,A系统会这个通道向B系统发送请求的数据

3、A系统发送数据时并不是直接就发送出去了,这些数据会以二进制的形式经过系统用户空间、系统内核空间、系统的网卡,网卡再将数据发送给B系统

4、B系统接收数据的时候,数据会经过B系统的网卡、系统内核空间、系统用户空间,程序再从系统的用户空间里读取数据

这里需要注意下,操作系统是分为用户空间和内核空间的,我们的应用程序就是运行在内核空间里,因此A系统在发送数据的时候,数据会从用户空间出发,最后通过网卡发送出去

系统具体的数据发送和接收过程如下所示:

关于系统涉及的I/O模型_第1张图片

数据传输到网卡一共也分为两步:

1、应用程序调用操作系统的接口将发送的数据拷贝到内核缓冲区(这里要注意应用内程序是无法直接操作内核缓冲区的,只能通过接口来操作)

2、内核缓冲区再将数据拷贝到网卡

数据从网卡传输到应用程序步骤相反

同步阻塞I/O

同步阻塞I/O也就是我们所说的BIO,在数据拷贝的过程中就涉及到数据的读和写,同步阻塞I/O就是用户程序在读取数据的时候用户线程会阻塞,让出 CPU,内核等待网卡数据到来,把数据从网卡拷贝到内核空间,接着把数据拷贝到用户空间,再把用户线程叫醒

对应的时序图如下:

关于系统涉及的I/O模型_第2张图片

相关 Java API

Java 提供了两个网络操作类 ServerSocket 和 Sokcet ,分别对应服务端和客户端,ServerSocket 提供了 accept() 方法,accept() 会阻塞线程等待客户端的连接,连接完成之后,会唤醒线程从连接通道里读取数据

Java 服务端代码如下:


public static void main(String[] args) throws IOException {
    ServerSocket serverSocket = new ServerSocket();
    SocketAddress socketAddress = new InetSocketAddress(8080);
    serverSocket.bind(socketAddress);
    byte[] bytes = new byte[1024];
    while (true){
       // 获取到客户端连接
        Socket accept = serverSocket.accept();
        InputStream inputStream = accept.getInputStream();
        // 从客户端连接里读取数据
        int read = inputStream.read(bytes);
        if(read != -1){
            System.out.println(new String(bytes));
        }

    }
}
    

这里要注意一下,当没有客户端来连接的时候,accept() 方法会阻塞住,有客户端连接,但是客户端如果没有发送数据过来,inputStream.read() 也会阻塞住

而这里的 read() 方法也就是上面时序图里所说的调用 read 方法读取数据

Java 客户端代码如下:

public static void main(String[] args) throws IOException, InterruptedException {
    Socket socket = new Socket("127.0.0.1",8080);
    OutputStream outputStream = socket.getOutputStream();
    outputStream.write("Hello World".getBytes());
    Thread.sleep(60000*10);      //睡眠10分钟

}

这里Java客户端的也比较的简单,我这里就不做过多的讲述了

同步非阻塞I/O

这种I/O模型也是我们所说的NIO,同步非阻塞I/O与同步阻塞I/O最大的区别就在于当用户线程发起 read 调用的时候,用户线程在某一时间段内不会被阻塞住

为啥是在某一时间内用户线程不会被阻塞住呢

这是因为如果数据没有到达内核空间时,每次 read 调用都会返回失败,不会被阻塞住,直到数据到了内核空间后,再一次 read 调用时,此时用户线程就会被阻塞住,等待数据从内核空间拷贝到用户空间,然后再唤醒用户线程

因此这里的非阻塞只是针对数据还未到达内核空间时,数据到达内核空间后用户线程还是会处于一个阻塞状态,这里需要注意下

为了方便理解,可以查看下图:

关于系统涉及的I/O模型_第3张图片

相关 Java API

Java 中提供了 ServerSocketChannel 和 SocketChannel,分别对标BIO的 ServerSocket 和 Socket ,ServerSocketChannel 提供的 accept 方法与 SocketChannel 提供的 read 方法可以通过 configureBlocking 设置为不阻塞

Java服务端代码 :


static List<SocketChannel> socketChannelList = new ArrayList<>();

public static void main(String[] args) throws IOException {
    // 创建 ServerSocketChannel 对象
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    // 绑定端口
    serverSocketChannel.socket().bind(new InetSocketAddress(8080));
    // 将客户连接设置为不阻塞
    serverSocketChannel.configureBlocking(false);
    ByteBuffer allocate = ByteBuffer.allocate(128);
    while (true){
        SocketChannel socketChannel = serverSocketChannel.accept();
        if(socketChannel != null){
            // 将读写数据设置为不阻塞
            socketChannel.configureBlocking(false);
            socketChannelList.add(socketChannel);
        }
        
        Iterator<SocketChannel> iterator = socketChannelList.iterator();

        while (iterator.hasNext()){
            SocketChannel channel = iterator.next();
            // 读取数据
            int read = channel.read(allocate);
            if(read > 0){
                System.out.println(new String(allocate.array()));
                allocate.clear();
            }
        }
    }
}

Java客户端代码 :

public static void main(String[] args) throws IOException, InterruptedException {
    SocketChannel socketChannel = SocketChannel.open();
    socketChannel.connect(new InetSocketAddress("localhost",8080));
    ByteBuffer byteBuffer = ByteBuffer.allocate(128);
    byteBuffer.put("123".getBytes());
    byteBuffer.flip();
    while (byteBuffer.hasRemaining()) {
        socketChannel.write(byteBuffer);
    }
    Thread.sleep(100000);
}

I/O多路复用

I/O多路复用其实就是对同步非阻塞I/O的一个增强,也是NIO的一种,这种I/O模型将用户线程的数据读取操作分了两步,首先线程发起 select 调用,目的就是查询内核空间里的数据是否准备好,如果准备好了,用户线程再次发起 read 调用,在等待数据从内核空间拷贝到用户空间这段时间里,用户线程仍然还是阻塞的

那为啥要叫I/O多路复用呢,这是因为一次 select 调用可以向内核查询多个数据通道的状态,所以叫I/O多路复用

调用顺序如下:

关于系统涉及的I/O模型_第4张图片

相关 Java API

在Java API使用上在服务端比同步非阻塞I/O多了个 Selector 注册,其他的基本上都差不多

Java 服务端代码:

public static void main(String[] args) throws IOException {
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    serverSocketChannel.socket().bind(new InetSocketAddress(8080));
    serverSocketChannel.configureBlocking(false);
    ByteBuffer allocate = ByteBuffer.allocate(128);
    Selector selector = Selector.open();
    // 注册 Selector 
    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    while (true){
        // 阻塞等待需要处理的事件发生
        selector.select();
        Set<SelectionKey> selectionKeys = selector.selectedKeys();
        Iterator<SelectionKey> iterator1 = selectionKeys.iterator();
        while (iterator1.hasNext()){
            SelectionKey selectionKey = iterator1.next();
            if(selectionKey.isAcceptable()){
                ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel) selectionKey.channel();
                SocketChannel accept = serverSocketChannel1.accept();
                accept.configureBlocking(false);
                // 这里只注册了读事件,如果还需要往客户端写的话需要注册写事件
                accept.register(selector,SelectionKey.OP_READ);
                System.out.println("客户端连接成功....");
            }else if (selectionKey.isReadable()){
                SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                int read = socketChannel.read(allocate);
                if(read > 0){
                    System.out.println("收到消息:"+ new String(allocate.array()));
                }else if(read == -1){
                    System.out.println("客户端断开连接");
                    socketChannel.close();
                }
            }
            iterator1.remove();
        }
    }
}

异步I/O

异步I/O也就是我们所说的AIO,上面的几种I/O模型都需要同步的去完成客户端的连接和数据的读取

而异步I/O则会在用户发起 read 调用的同时注册一个回调函数,read 立即返回,等内核将数据准备好,再调用指定的回调函数完成处理,在这个过程中,用户线程一直没有阻塞

关于系统涉及的I/O模型_第5张图片

相关 Java API

Java 提供了 AsynchronousServerSocketChannel 和 AsynchronousSocketChannel,

AsynchronousServerSocketChannel 提供了两个 accept 方法,其中一个需要去注册一个回调函数

Java 服务端代码:

public static void main(String[] args) throws IOException, InterruptedException {
    AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open();
    serverSocketChannel.bind(new InetSocketAddress(8080));
    // 创建一个回调函数
    CompletionHandler<AsynchronousSocketChannel, Object> handler = new CompletionHandler<AsynchronousSocketChannel,
            Object>() {
        @Override
        public void completed(final AsynchronousSocketChannel result, final Object attachment) {
            // 继续监听下一个连接请求
            serverSocketChannel.accept(attachment, this);
            try {
                System.out.println("接受了一个连接:" + result.getRemoteAddress()
                        .toString());
                // 给客户端发送数据并等待发送完成
                result.write(ByteBuffer.wrap("From Server:Hello i am server".getBytes()))
                        .get();
                ByteBuffer readBuffer = ByteBuffer.allocate(128);
                // 阻塞等待客户端接收数据
                result.read(readBuffer)
                        .get();
                System.out.println(new String(readBuffer.array()));

            } catch (IOException | InterruptedException  | ExecutionException e) {
                e.printStackTrace();
            }
        }
        @Override
        public void failed(final Throwable exc, final Object attachment) {
            System.out.println("出错了:" + exc.getMessage());
        }
    };
    // 注册回调函数
    serverSocketChannel.accept(null, handler);
    // 由于serverSocketChannel.accept(null, handler);是一个异步方法,调用会直接返回,
    // 为了让子线程能够有时间处理监听客户端的连接会话,
    // 这里通过让主线程休眠一段时间(当然实际开发一般不会这么做)以确保应用程序不会立即退出。
    TimeUnit.MINUTES.sleep(Integer.MAX_VALUE);

}

Java 客户端代码:

public static void main(String[] args) throws IOException, InterruptedException {
     // 打开一个SocketChannel通道并获取AsynchronousSocketChannel实例
        AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
        // 连接到服务器并处理连接结果
         client.connect(new InetSocketAddress("127.0.0.1", 8080), null, new CompletionHandler<Void, Void>() {
            @Override
            public void completed(final Void result, final Void attachment) {
                System.out.println("成功连接到服务器!");
                try {
                    // 给服务器发送信息并等待发送完成
                    client.write(ByteBuffer.wrap("From client:Hello i am client".getBytes())).get();
                    ByteBuffer readBuffer = ByteBuffer.allocate(128);
                    // 阻塞等待接收服务端数据
                    client.read(readBuffer).get();
                    System.out.println(new String(readBuffer.array()));
                } catch (InterruptedException  | ExecutionException e) {
                    e.printStackTrace();
                }
            }

            @Override
            public void failed(final Throwable exc, final Void attachment) {
                exc.printStackTrace();
            }
        });
        TimeUnit.MINUTES.sleep(Integer.MAX_VALUE);
}

结束语

码字不易,还希望多多点赞、收藏支持下

你可能感兴趣的:(java,后端,网络,服务器)