关于后端:Java-SE基础巩固六Java-IO

38次阅读

共计 7979 个字符,预计需要花费 20 分钟才能阅读完成。

Java SE 根底坚固(六):Java IO

到当初为止,Java IO 可分为三类:BIO、NIO、AIO。最早呈现的是 BIO,而后是 NIO,最近的是 AIO,BIO 即 Blocking IO,NIO 有的文章说是 New NIO,也有的文章说是 No Blocking IO,我查了一些材料,官网说的应该是 No Blocking IO,提供了 Selector,Channle,SelectionKey 形象,AIO 即 Asynchronous IO(异步 IO),提供了 Fauture 等异步操作。

1 BIO

[](https://imgchr.com/i/i81QZn)

[

](https://imgchr.com/i/i81QZn)

上图是 BIO 的架构体系图。能够看到 BIO 次要分为两类 IO,即字符流 IO 和字节流 IO,字符流即把输入输出数据当做字符来对待,Writer 和 Reader 是其继承体系的最高层,字节流即把输入输出当做字节来对待,InputStream 和 OutputStream 是其继承体系的最高层。上面以文件操作为例,其余的实现类也十分相似。

《2020 最新 Java 根底精讲视频教程和学习路线!》

顺便说一下,整个 BIO 体系大量应用了装璜者模式,例如 BufferedInputStream 包装了 InputStream,使其领有了缓冲的能力。

1.1 字节流

public class Main {public static void main(String[] args) throws IOException {
        // 写入文件
        FileOutputStream out = new FileOutputStream("E:Java_projecteffective-javasrctopyeononiobiotest.txt");
        out.write("hello,world".getBytes("UTF-8"));
        out.flush();
        out.close();

        // 读取文件
        FileInputStream in = new FileInputStream("E:Java_projecteffective-javasrctopyeononiobiotest.txt");
        byte[] buffer = new byte[in.available()];
        in.read(buffer);
        System.out.println(new String(buffer, "UTF-8"));
        in.close();}
}

复制代码 

向 FileOutputStream 构造函数中传入文件名来创立 FileOutputStream 对象,即关上了一个字节流,之后应用 write 办法向字节流中写入数据,实现之后调用 flush 刷新缓冲区,最初记得要敞开字节流。读取文件也是相似的,先关上一个字节流,而后从字节流中读取数据并存入内存中(buffer 数组),而后再敞开字节流。

因为 InputStream 和 OutputStream 都继承了 AutoCloseable 接口,所以如果应用的是 try-resource 的语法来进行字节流的 IO 操作,可不须要手动显式调用 close 办法了,这也是十分举荐的做法,在示例中我没有这样做只是为了不便。

1.2 字符流

字节流次要应用的是 InputStream 和 OutputStream,而字符流次要应用的就是与之对应的 Reader 和 Writer。上面来看一个示例,该示例的性能和上述示例的一样,只不过实现伎俩不同:

public class Main {public static void main(String[] args) throws IOException {Writer writer = new FileWriter("E:Java_projecteffective-javasrctopyeononiobiotest.txt");
        writer.write("hello,worldn");
        writer.write("hello,yeononn");
        writer.flush();
        writer.close();

        BufferedReader reader = new BufferedReader(new FileReader("E:Java_projecteffective-javasrctopyeononiobiotest.txt"));

        String line = "";
        int lineCount = 0;
        while ((line = reader.readLine()) != null) {System.out.println(line);
            lineCount++;
        }
        reader.close();
        System.out.println(lineCount);
    }
}
复制代码 

Writer 非常简单,无奈就是关上字符流,而后向字符流中写入字符,而后敞开。要害是 Reader,示例代码中应用了 BufferedReader 来包装 FileReader,使得本来没有缓冲性能的 FileReader 有了缓冲性能,这就是下面提到过的装璜者模式,BufferedReader 还提供了方便使用的 API,例如 readLine(),这个办法每次调用会读取文件中的一行。

以上就是 BIO 的简略应用,源码的话因为波及太多的底层,所以如果对底层不是很理解的话会很难了解源码。

2 NIO

BIO 是同步阻塞的 IO,而 NIO 是同步非阻塞的 IO。NIO 中有几个比拟重要的组件:Selector,SelectionKey,Channel,ByteBuffer,其中 Selector 就是所谓的选择器,SelectionKey 能够简略了解为选择键,这个键将 Selector 和 Channle 进行一个绑定(或者所 Channle 注册到 Selector 上),当有数据达到 Channel 的时候,Selector 会从阻塞状态中恢复过来,并对该 Channle 进行操作,并且,咱们不能间接对 Channle 进行读写操作,只能对 ByteBuffer 操作。如下图所示:

[](https://imgchr.com/i/i8YB80)

[

](https://imgchr.com/i/i8YB80)

上面是一个 Socket 网络编程的例子:

// 服务端
public class SocketServer {

    private Selector selector;
    private final static int port = 9000;
    private final static int BUF = 10240;

    private void init() throws IOException {
        // 获取一个 Selector
        selector = Selector.open();
        // 获取一个服务端 socket Channel
        ServerSocketChannel channel = ServerSocketChannel.open();
        // 设置为非阻塞模式
        channel.configureBlocking(false);
        // 绑定端口
        channel.socket().bind(new InetSocketAddress(port));
        // 把 channle 注册到 Selector 上,并示意对 ACCEPT 事件感兴趣
        SelectionKey selectionKey = channel.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {
            // 该办法会阻塞,直到和其绑定的任何一个 channel 有数据过去
            selector.select();
            // 获取该 Selector 绑定的 SelectionKey
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {SelectionKey key = iterator.next();
                // 记得删除,否则就有限循环了
                iterator.remove();
                // 如果该事件是一个 ACCEPT,那么就执行 doAccept 办法,其余的也一样
                if (key.isAcceptable()) {doAccept(key);
                } else if (key.isReadable()) {doRead(key);
                } else if (key.isWritable()) {doWrite(key);
                } else if (key.isConnectable()) {System.out.println("连贯胜利!");
                }
            }
        }
    }

    // 写办法,留神不能间接对 channle 进行读写操作,只能对 ByteBuffer 进行操作
    private void doWrite(SelectionKey key) throws IOException {ByteBuffer buffer = ByteBuffer.allocate(BUF);
        buffer.flip();
        SocketChannel socketChannel = (SocketChannel) key.channel();
        while (buffer.hasRemaining()) {socketChannel.write(buffer);
        }
        buffer.compact();}

    // 读取音讯
    private void doRead(SelectionKey key) throws IOException {SocketChannel socketChannel = (SocketChannel) key.channel();
        ByteBuffer buffer = ByteBuffer.allocate(BUF);
        long reads = socketChannel.read(buffer);
        while (reads > 0) {buffer.flip();
            byte[] data = buffer.array();
            System.out.println("读取到音讯:" + new String(data, "UTF-8"));
            buffer.clear();
            reads = socketChannel.read(buffer);
        }
        if (reads == -1) {socketChannel.close();
        }
    }

    // 当有连贯过去的时候,获取连贯过去的 channle,而后注册到 Selector 上,并设置成对读音讯感兴趣,当客户端有音讯过去的时候,Selector 就能够让其执行 doRead 办法,而后读取音讯并打印。private void doAccept(SelectionKey key) throws IOException {ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
        System.out.println("服务端监听中...");
        SocketChannel socketChannel = serverChannel.accept();
        socketChannel.configureBlocking(false);
        socketChannel.register(key.selector(), SelectionKey.OP_READ);
    }
    
    public static void main(String[] args) throws IOException {SocketServer server = new SocketServer();
        server.init();}
}

// 客户端,写得比较简单
public class SocketClient {

    private final static int port = 9000;
    private final static int BUF = 10240;


    private void init() throws IOException {
        // 获取 channel
        SocketChannel channel = SocketChannel.open();
        // 连贯到近程服务器
        channel.connect(new InetSocketAddress(port));
        // 设置非阻塞模式
        channel.configureBlocking(false);
        // 往 ByteBuffer 里写音讯
        ByteBuffer buffer = ByteBuffer.allocate(BUF);
        buffer.put("Hello,Server".getBytes("UTF-8"));
        buffer.flip();
        // 将 ByteBuffer 内容写入 Channle,即发送音讯
        channel.write(buffer);
        channel.close();}


    public static void main(String[] args) throws IOException {SocketClient client = new SocketClient();
            client.init();}
}

复制代码 

尝试启动一个服务端,多个客户端,后果大抵如下所示:

 服务端监听中...
读取到音讯:Hello,Server                       
服务端监听中...
读取到音讯:Hello,Server  
复制代码 

正文写得挺分明了,我这里只是简略应用了 NIO,但实际上 NIO 远远不止这些货色,光一个 ByteBuffer 就能说一天,如果有机会,我会在前面 Netty 相干的文章中具体说一下这几个组件。在此就不再多说了。

吐槽一些,纯 NIO 写的服务端和客户端切实是太麻烦了,一不小心就会写错,还是应用 Netty 相似的框架好一些啊。

3 AIO

在 JDK7 中新增了一些 IO 相干的 API,这些 API 称作 AIO。因为其提供了一些异步操作 IO 的性能,但实质是其实还是 NIO,所以能够简略的了解为是 NIO 的裁减。AIO 中最重要的就是 Future 了,Future 示意未来的意思,即这个操作可能会继续很长时间,但我不会等,而是到未来操作实现的时候,再过去告诉我,这就是异步的意思。上面是两个应用 AIO 的例子:

 public static void main(String[] args) throws ExecutionException, InterruptedException, IOException {Path path = Paths.get("E:Java_projecteffective-javasrctopyeononioaiotest.txt");
        AsynchronousFileChannel channel = AsynchronousFileChannel.open(path);
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        Future<Integer> future = channel.read(buffer,0);
        Integer readNum = future.get(); // 阻塞,如果不调用该办法,main 办法会继续执行
        buffer.flip();
        System.out.println(new String(buffer.array(), "UTF-8"));
        System.out.println(readNum);
    }
复制代码 

第一个例子应用 AsynchronousFileChannel 来异步的读取文件内容,在代码中,我应用了 future.get() 办法,该办法会阻塞以后线程,在例子中即主线程,当工作线程,即读取文件的线程执行结束后才会从阻塞状态中恢复过来,并将后果返回。之后就能够从 ByteBuffer 中读取数据了。这是应用未来时的例子,上面来看看应用回调的例子:

public class Main {public static void main(String[] args) throws ExecutionException, InterruptedException, IOException {Path path = Paths.get("E:Java_projecteffective-javasrctopyeononioaiotest.txt");
        AsynchronousFileChannel channel = AsynchronousFileChannel.open(path);
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
            @Override
            public void completed(Integer result, ByteBuffer attachment) {System.out.println("实现读取");
                try {System.out.println(new String(attachment.array(), "UTF-8"));
                } catch (UnsupportedEncodingException e) {e.printStackTrace();
                }
            }

            @Override
            public void failed(Throwable exc, ByteBuffer attachment) {System.out.println("读取失败");
            }
        });
        System.out.println("继续执行主线程");
        // 调用实现之后不须要期待工作实现,会间接继续执行主线程
        while (true) {Thread.sleep(1000);
        }
    }
}

复制代码 

输入的后果大抵如下所示,但不肯定,这取决于线程调度:

 继续执行主线程
实现读取

hello,world
hello,yeonon
复制代码 

当工作实现,即读取文件结束的时候,会调用 completed 办法,失败会调用 failed 办法,这就是回调。具体接触过回调的敌人应该不难理解。

4 BIO、NIO、AIO 的区别

  1. BIO 是同步阻塞的 IO,NIO 是同步非阻塞 IO,AIO 异步非阻塞 IO,这是最根本的区别。阻塞模式会导致其余线程被 IO 线程阻塞,必须期待 IO 线程执行结束能力继续执行逻辑,非阻塞和异步并不等同,非阻塞模式下,个别会采纳事件轮询的形式来执行 IO,即 IO 多路复用,尽管依然是同步的,但执行效率比传统的 BIO 要高很多,AIO 则是异步 IO,如果把 IO 工作当做一个工作的话,在以后线程中提交一个工作之后,不会有阻塞,会继续执行以后线程的后续逻辑,在工作实现之后,以后线程会收到告诉,而后再决定如何解决,这种形式的 IO,CPU 效率是最高的,CPU 简直没有产生过进展,而时始终至于忙状态,所以效率十分高,但编程难度会比拟大。
  2. BIO 面向的是流,无论是字符流还是字节流,艰深的讲,BIO 在读写数据的时候会依照一个接一个的形式读写,而 NIO 和 AIO(因为 AIO 实际上是 NIO 的裁减,所以从这个方面来看,能够把他们放在一块)读写数据的时候是依照一块一块的读取的,读取到的数据会缓存在内存中,而后在内存中对数据进行解决,这种形式的益处是缩小了硬盘或者网络的读写次数,从而升高了因为硬盘或网络速度慢带来的效率影响。
  3. BIO 的 API 尽管比拟底层,但如果相熟之后编写起来会比拟容易,NIO 或者 AIO 的 API 抽象层次高,一般来说应该更容易应用才是,但实际上却很难“正确”的编写,而且 DEBUG 的难度也较大,这也是为什么 Netty 等 NIO 框架受欢迎的起因之一。

以上就是我了解的 BIO、NIO 和 AIO 区别。

5 小结

本文简略粗略的讲了一下 BIO、NIO、AIO 的应用,并未波及源码,也没有波及太多的原理,如果读者心愿理解更多对于三者的内容,倡议参看一些书籍,例如老外写的《Java NIO》,该书全面零碎的解说了 NIO 的各种组件和细节,十分举荐。

正文完
 0