前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一文了解Java的IO模型

一文了解Java的IO模型

作者头像
Java极客技术
发布2024-01-31 14:44:39
950
发布2024-01-31 14:44:39
举报
文章被收录于专栏:Java极客技术Java极客技术

我们都知道在 Java 当中有许许多多的使用上的问题,比如 Java 的锁,Java 的安全性,以及 Java 的IO操作,Java 中各种设计模式的使用,今天我们就来说说关于这个 Java 的IO。

Java的IO是什么?

Java IO(输入输出)是Java程序与外部进行数据交互的接口,包括文件读写、标准设备输出等1。

Java IO是建立在流的基础上进行输入输出的,所有数据被串行化写入输出流,或者从输入流中读入1。

Java IO有字节流和字符流两种形式,其中字节流一次读写一个字节,而字符流一次读写一个字符。

Java 的 IO 模型

Java中的IO模型主要有三种:

1.BIO(Blocking IO):同步阻塞式IO,是比较常用的IO模型,特点是编写相对简单,分为输入流和输出流,进行网络通讯时,输入流的读操作会阻塞住线程,直到有输出流执行写操作。

2.NIO(Nonblocking IO):同步非阻塞式IO,IO操作不再阻塞线程,当数据准备好后,可以通过Selector选择通道进行数据的发送和接收。

3.AIO(Asynchronous IO):异步非阻塞式IO,是Java 7引入的新特性,基于NIO实现,提供了异步文件通道和异步通道的方式进行IO操作。

既然我们已经知道了IO模型是三种,那么我们分开来说一下这些 IO 模型。

BIO 同步阻塞IO

BIO(Blocking I/O):同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器)

BIO工作机制:服务端启动一个ServerSocket,客户端发出请求后,先咨询服务器是否有线程响应,如果没有则会等待,或者被拒绝。如果有响应,客户端线程会等待请求结束后,再继续执行。使用线程池,当一个客户端连接时就启动一个线程进行通信

我们简单来实现一下这个 BIO 客户端和服务端之间的代码:

服务端代码

代码语言:javascript
复制
import java.io.*;
import java.net.*;

public class Server {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(4444);
        } catch (IOException e) {
            System.err.println("Could not listen on port: 4444.");
            System.exit(1);
        }

        Socket clientSocket = null;
        try {
            clientSocket = serverSocket.accept();
        } catch (IOException e) {
            System.err.println("Accept failed.");
            System.exit(1);
        }

        PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
        BufferedReader in = new BufferedReader(
                new InputStreamReader(
                        clientSocket.getInputStream()));
        String inputLine;

        while ((inputLine = in.readLine()) != null) {
            out.println(inputLine);
            out.flush();
            if (inputLine.equals("Bye.")) {
                break;
            }
        }

        out.close();
        in.close();
        clientSocket.close();
        serverSocket.close();
    }
}

客户端代码

代码语言:javascript
复制
import java.io.*;
import java.net.*;

public class Client {
    public static void main(String[] args) throws IOException {
        Socket socket = null;
        PrintWriter out = null;
        BufferedReader in = null;
        try {
            socket = new Socket("localhost", 4444);
            out = new PrintWriter(socket.getOutputStream(), true);
            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        } catch (UnknownHostException e) {
            System.err.println("Don't know about host: localhost.");
            System.exit(1);
        } catch (IOException e) {
            System.err.println("Couldn't get I/O for the connection to: localhost.");
            System.exit(1);
        }
        BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in));
        String userInput;
        while ((userInput = stdIn.readLine()) != null) {
            out.println(userInput);
            System.out.println("echo: " + in.readLine());
            if (userInput.equals("Bye.")) {
                break;
            }
        } 
        out.close(); 
        in.close(); 
        stdIn.close(); 
        socket.close(); 
    } 
} 

但是BIO的缺点也很显著:

1.线程数量限制:每来一个请求就需要一个线程来处理,线程太多容易造成系统不可用。

2.无法处理大量并发请求:当发生大量并发请求时,超过最大数量的线程就只能等待,直到线程池中的有空闲的线程可以被复用。

3.对Socket的输入流读取时,会一直阻塞。

既然 BIO 的缺点这么明显,那么是不是其他的能把这些缺点给避免掉呢?

NIO 同步非阻塞的I/O模型

NIO(Non-blocking I/O,在Java领域,也称为New I/O),是一种同步非阻塞的I/O模型,也是I/O多路复用的基础,已经被越来越多地应用到大型应用服务器,成为解决高并发与大量连接、I/O处理问题的有效方式。

NIO主要有三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择区)。传统IO基于字节流和字符流进行操作,而NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。

我们说到了这个 NIO 的三大核心部分,那么我们应该怎么去理解这个通道,缓冲区,选择区呢?

我就来说说了不起对于这几个关键核心的理解吧:

Channel(通道):就像水管一样,负责传输数据,可以进行数据的读取和写入。

Buffer(缓冲区):是一个内存块,底层是一个数组。数据的读取和写入都是通过Buffer进行的。

Selector(选择器):用于监听多个通道的事件,比如连接请求、数据到达等。一个线程可以监听多个通道。

而 NIO 的代码实现,其实就是依赖于 Java 的 nio 包中的类实现的,我们来实现一下看看。

NIO 代码实例:

服务端

代码语言:javascript
复制
public class NioServer {
 
    public static void main(String[] args) throws IOException {
        //创建一个选择器selector
        Selector selector= Selector.open();
        //创建serverSocketChannel
        ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
        //绑定端口
        serverSocketChannel.socket().bind(new InetSocketAddress(8888));
        //必须得设置成非阻塞模式
        serverSocketChannel.configureBlocking(false);
        //将channel注册到selector并设置监听事件为ACCEPT
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("===========NIO服务端启动============");
        while(true){
            //超时等待
            if(selector.select(1000)==0){
                System.out.println("===========NIO服务端超时等待============");
                continue;
            }
            // 有客户端请求被轮询监听到,获取返回的SelectionKey集合
            Iterator<SelectionKey> iterator=selector.selectedKeys().iterator();
            //迭代器遍历SelectionKey集合
            while (iterator.hasNext()){
                SelectionKey key=iterator.next();
                // 判断是否为ACCEPT事件
                if (key.isAcceptable()){
                    // 处理接收请求事件
                    SocketChannel socketChannel=((ServerSocketChannel) key.channel()).accept();
                    //非阻塞模式
                    socketChannel.configureBlocking(false);
                    // 注册到Selector并设置监听事件为READ
                    socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                    System.out.println("成功连接客户端");
                }
                //判断是否为READ事件
                if (key.isReadable()){
                    SocketChannel socketChannel = (SocketChannel) key.channel();
 
                    try {
                        // 获取以前设置的附件对象,如果没有则新建一个
                        ByteBuffer buffer = (ByteBuffer) key.attachment();
                        if (buffer == null) {
                            buffer = ByteBuffer.allocate(1024);
                            key.attach(buffer);
                        }
                        // 清空缓冲区
                        buffer.clear();
                        // 将通道中的数据读到缓冲区
                        int len = socketChannel.read(buffer);
                        if (len > 0) {
                            buffer.flip();
                            String message = new String(buffer.array(), 0, len);
                            System.out.println("收到客户端消息:" + message);
                        } else if (len < 0) {
                            // 接收到-1,表示连接已关闭
                            key.cancel();
                            socketChannel.close();
                            continue;
                        }
                        // 注册写事件,下次向客户端发送消息
                        socketChannel.register(selector, SelectionKey.OP_WRITE, buffer);
                    } catch (IOException e) {
                        // 取消SelectionKey并关闭对应的SocketChannel
                        key.cancel();
                        socketChannel.close();
                    }
                }
                //判断是否为WRITE事件
                if (key.isWritable()){
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    //获取buffer
                    ByteBuffer buffer = (ByteBuffer) key.attachment();
                    String hello = "你好,坤坤!";
                    //清空buffer
                    buffer.clear();
                    //buffer中写入消息
                    buffer.put(hello.getBytes());
                    buffer.flip();
                    //向channel中写入消息
                    socketChannel.write(buffer);
                    buffer.clear();
                    System.out.println("向客户端发送消息:" + hello);
                    // 设置下次读写操作,向 Selector 进行注册
                    socketChannel.register(selector, SelectionKey.OP_READ, buffer);
                }
                // 移除本次处理的SelectionKey,防止重复处理
                iterator.remove();
            }
        }
 
    }
}

客户端

代码语言:javascript
复制
public class NioClient {
 
    public static void main(String[] args) throws IOException {
        // 创建SocketChannel并指定ip地址和端口号
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8888));
        System.out.println("==============NIO客户端启动================");
        // 非阻塞模式
        socketChannel.configureBlocking(false);
        String hello="你好,靓仔!";
        ByteBuffer buffer = ByteBuffer.wrap(hello.getBytes());
        // 向通道中写入数据
        socketChannel.write(buffer);
        System.out.println("发送消息:" + hello);
        buffer.clear();
        // 将channel注册到Selector并监听READ事件
        socketChannel.register(Selector.open(), SelectionKey.OP_READ, buffer);
        while (true) {
            // 读取服务端数据
            if (socketChannel.read(buffer) > 0) {
                buffer.flip();
                String msg = new String(buffer.array(), 0, buffer.limit());
                System.out.println("收到服务端消息:" + msg);
                break;
            }
        }
        // 关闭输入流
        socketChannel.shutdownInput();
        // 关闭SocketChannel连接
        socketChannel.close();
    }
}

Java NIO的缺点:

1.系统稳定度不够:服务端响应随着客户端的增加延时增加,每一个客户端需要建立一个线程,当到达一定的限制时,会使系统无法响应。

2.编程模型较复杂:NIO的编程模型相对比较复杂,需要处理多线程和异步I/O操作,对开发者的要求较高。

3.不支持阻塞模式:NIO在处理I/O请求时,默认情况下是不支持阻塞模式的,需要手动设置。

4.内存分配问题:使用NIO进行文件操作时,需要手动管理内存,如果处理不当可能会导致内存泄漏。

说完了 NIO 那最后就得来看看这个 AIO 了

AIO 异步非阻塞式IO

Java的AIO(Asynchronous I/O)是Java NIO的下一代版本,也称为NIO.2。AIO在Java 7中被引入,提供了一种基于事件驱动的非阻塞I/O模型,用于简化异步I/O操作的开发。

AIO的核心思想是使用异步I/O模型,而不是传统的同步或阻塞I/O模型。在AIO中,应用程序发出I/O请求后,不需要等待操作完成就可以继续执行其他任务。当操作完成后,应用程序会收到通知,然后可以处理结果。这种模型可以显著提高应用程序的吞吐量和响应能力。

它们的主要区别就在于这个异步通道,见名知意:使用异步通道去进行IO操作时,所有操作都为异步非阻塞的,当调用read()/write()/accept()/connect()方法时,本质上都会交由操作系统去完成,比如要接收一个客户端的数据时,操作系统会先将通道中可读的数据先传入read()回调方法指定的缓冲区中,然后再主动通知Java程序去处理。

我们直接来看看代码:

服务端代码

代码语言:javascript
复制
public class AioServer {
 
    public static void main(String[] args) throws Exception {
        // 创建异步通道组,处理IO事件
        AsynchronousChannelGroup group = AsynchronousChannelGroup.withFixedThreadPool(10, Executors.defaultThreadFactory());
        //创建异步服务器Socket通道,并绑定端口
        AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open(group).bind(new InetSocketAddress(8888));
        System.out.println("=============AIO服务端启动=========");
 
        // 异步等待接收客户端连接
        server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
            // 创建ByteBuffer
            final ByteBuffer buffer = ByteBuffer.allocate(1024);
 
            @Override
            public void completed(AsynchronousSocketChannel channel, Object attachment) {
                System.out.println("客户端连接成功");
                try {
                    buffer.clear();
                    // 异步读取客户端发送的消息
                    channel.read(buffer, null, new CompletionHandler<Integer, Object>() {
                        @Override
                        public void completed(Integer len, Object attachment) {
                            buffer.flip();
                            String message = new String(buffer.array(), 0, len);
                            System.out.println("收到客户端消息:" + message);
 
                            // 异步发送消息给客户端
                            channel.write(ByteBuffer.wrap(("你好,阿坤!").getBytes()), null, new CompletionHandler<Integer, Object>() {
                                @Override
                                public void completed(Integer result, Object attachment) {
                                    // 关闭输出流
                                    try {
                                        channel.shutdownOutput();
                                    } catch (IOException e) {
                                        e.printStackTrace();
                                    }
                                }
 
                                @Override
                                public void failed(Throwable exc, Object attachment) {
                                    exc.printStackTrace();
                                    try {
                                        channel.close();
                                    } catch (IOException e) {
                                        e.printStackTrace();
                                    }
                                }
                            });
                        }
 
                        @Override
                        public void failed(Throwable exc, Object attachment) {
                            exc.printStackTrace();
                            try {
                                channel.close();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    });
                } catch (Exception e) {
                    e.printStackTrace();
                }
                // 继续异步等待接收客户端连接
                server.accept(null, this);
            }
 
            @Override
            public void failed(Throwable exc, Object attachment) {
                exc.printStackTrace();
                // 继续异步等待接收客户端连接
                server.accept(null, this);
            }
        });
        // 等待所有连接都处理完毕
        group.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
    }
 
}

客户端代码

代码语言:javascript
复制
public class AioClient {
 
    public static void main(String[] args) throws Exception {
        // 创建异步Socket通道
        AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
        // 异步连接服务器
        client.connect(new InetSocketAddress("127.0.0.1", 8888), null, new CompletionHandler<Void, Object>() {
            // 创建ByteBuffer
            final ByteBuffer buffer = ByteBuffer.wrap(("你好,靓仔!").getBytes());
 
            @Override
            public void completed(Void result, Object attachment) {
                // 异步发送消息给服务器
                client.write(buffer, null, new CompletionHandler<Integer, Object>() {
                    // 创建ByteBuffer
                    final ByteBuffer readBuffer = ByteBuffer.allocate(1024);
 
                    @Override
                    public void completed(Integer result, Object attachment) {
                        readBuffer.clear();
                        // 异步读取服务器发送的消息
                        client.read(readBuffer, null, new CompletionHandler<Integer, Object>() {
                            @Override
                            public void completed(Integer result, Object attachment) {
                                readBuffer.flip();
                                String msg = new String(readBuffer.array(), 0, result);
                                System.out.println("收到服务端消息:" + msg);
                            }
 
                            @Override
                            public void failed(Throwable exc, Object attachment) {
                                exc.printStackTrace();
                                try {
                                    client.close();
                                } catch (IOException e) {
                                    e.printStackTrace();
                                }
                            }
                        });
                    }
 
                    @Override
                    public void failed(Throwable exc, Object attachment) {
                        exc.printStackTrace();
                        try {
                            client.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                });
            }
 
            @Override
            public void failed(Throwable exc, Object attachment) {
                exc.printStackTrace();
                try {
                    client.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
        // 等待连接处理完毕
        Thread.sleep(1000);
        // 关闭输入流和Socket通道
        client.shutdownInput();
        client.close();
    }
}

那么 AIO 有什么缺点么?

实现复杂:AIO的实现相对复杂,需要处理异步I/O操作和多线程编程,对开发者的要求较高。

需要额外的技能:使用AIO需要具备Java多线程编程的知识,否则很难写出高质量的代码。

存在著名的Selector空轮询bug:这个bug可能导致CPU占用率过高,影响系统性能。

可靠性差:网络状态复杂多样,可能会出现各种各样的问题,如网络断开重连、缓存失效、半包读写等,导致AIO的可靠性较差。

了不起还是劝大家,如果你不是很精通,可以慢慢学习,不能强制的去在各自的系统中使用,量力而行,毕竟总比捣鼓出bug自己解决不了强很多。

关于 IO 模型,你了解了么?

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-01-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java极客技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Java的IO是什么?
  • Java 的 IO 模型
    • BIO 同步阻塞IO
      • NIO 同步非阻塞的I/O模型
      • AIO 异步非阻塞式IO
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档