Java3種IO模型,一次搞懂!

ITPUB社群發表於2023-05-10

來源:三分惡

大家好,我是老三,上一節我們討論了Linux的五種IO模型,接下來,我們從Java語言層面,來看看對IO的實現。

在Java中,一共有三種IO模型,分別是阻塞IO(BIO)非阻塞IO(NIO)非同步IO(AIO)

Java3種IO模型,一次搞懂!

Java BIO

Java BIO就是Java的傳統IO模型,對應了作業系統IO模型裡的阻塞IO。

Java BIO相關的實現都位於java.io包下,其通訊原理是客戶端、服務端之間透過Socket套接字建立管道連線,然後從管道中獲取對應的輸入/輸出流,最後利用輸入/輸出流物件實現傳送/接收資訊。

我們來看個Demo:

  • BioServer:
/**
 * @Author 三分惡
 * @Date 2023/4/30
 * @Description BIO服務端
 */

public class BioServer {

    public static void main(String[] args) throws IOException {
        //定義一個ServerSocket服務端物件,併為其繫結埠號
        ServerSocket server = new ServerSocket(8888);
        System.out.println("===========BIO服務端啟動================");
        //對BIO來講,每個Socket都需要一個Thread
        while (true) {
            //監聽客戶端Socket連線
            Socket socket = server.accept();
            new BioServerThread(socket).start();
        }

    }

    /**
     * BIO Server執行緒
     */

    static class BioServerThread extends Thread{
        //socket連線
        private Socket socket;
        public BioServerThread(Socket socket){
            this.socket=socket;
        }

        @Override
        public void run() {
            try {
                //從socket中獲取輸入流
                InputStream inputStream=socket.getInputStream();
                //轉換為
                BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(inputStream));
                String msg;
                //從Buffer中讀取資訊,如果讀取到資訊則輸出
                while((msg=bufferedReader.readLine())!=null){
                    System.out.println("收到客戶端訊息:"+msg);
                }

                //從socket中獲取輸出流
                OutputStream outputStream=socket.getOutputStream();
                PrintStream printStream=new PrintStream(outputStream);
                //透過輸出流物件向客戶端傳遞資訊
                printStream.println("你好,吊毛!");
                //清空輸出流
                printStream.flush();
                //關閉socket
                socket.shutdownOutput();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
  • BioClient
/**
 * @Author 三分惡
 * @Date 2023/4/30
 * @Description BIO客戶端
 */

public class BioClient {

    public static void main(String[] args) throws IOException {
        List<String> names= Arrays.asList("帥哥","靚仔","坤坤");
        //透過迴圈建立多個多個client
        for (String name:names){
            //建立socket並根據IP地址與埠連線服務端
            Socket socket=new Socket("127.0.0.1",8888);
            System.out.println("===========BIO客戶端啟動================");
            //從socket中獲取位元組輸出流
            OutputStream outputStream=socket.getOutputStream();
            //透過輸出流向服務端傳遞資訊
            String hello="你好,"+name+"!";
            outputStream.write(hello.getBytes());
            //清空流,關閉socket輸出
            outputStream.flush();
            socket.shutdownOutput();

            //從socket中獲取位元組輸入流
            InputStream inputStream=socket.getInputStream();
            BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(inputStream));
            //讀取服務端訊息
            String msg;
            while((msg=bufferedReader.readLine())!=null){
                System.out.println("收到服務端訊息:"+msg);
            }
            inputStream.close();
            outputStream.close();
            socket.close();
        }
    }
}
  • 先啟動BioServer,再啟動BioClient,執行結果
===========BIO服務端啟動================
收到客戶端訊息:你好,帥哥!
收到客戶端訊息:你好,靚仔!
收到客戶端訊息:你好,坤坤!
===========BIO客戶端啟動================
收到服務端訊息:你好,吊毛!
===========BIO客戶端啟動================
收到服務端訊息:你好,吊毛!
===========BIO客戶端啟動================
收到服務端訊息:你好,吊毛!

在上述Java-BIO的通訊過程中,如果客戶端一直沒有傳送訊息過來,服務端則會一直等待下去,從而服務端陷入阻塞狀態。同理,由於客戶端也一直在等待服務端的訊息,如果服務端一直未響應訊息回來,客戶端也會陷入阻塞狀態。

BioServer定義了一個類BioServerThread,繼承了Thread類,run方法裡主要是透過socket和流來讀取客戶端的訊息,以及傳送訊息給客戶端,每處理一個客戶端的Socket連線,就得新建一個執行緒。

同時,IO讀寫操作也是阻塞的,如果客戶端一直沒有傳送訊息過來,執行緒就會進入阻塞狀態,一直等待下去。

BioClient裡,迴圈建立Socket,向服務端收發訊息,客戶端的讀寫也是阻塞的。

在這個Demo裡就體現了BIO的兩個特點:

  • 一個客戶端連線對應一個處理執行緒
  • 讀寫操作都是阻塞的
Java3種IO模型,一次搞懂!

毫無疑問,不管是建立太多執行緒,還是阻塞讀寫,都會浪費伺服器的資源。

Java NIO

那麼我們就進入Java的下一種IO模型——Java NIO,它對應作業系統IO模型中的多路複用IO,底層採用了epoll實現。

Java-NIO則是JDK1.4中新引入的API,它在BIO功能的基礎上實現了非阻塞式的特性,其所有實現都位於java.nio包下。NIO是一種基於通道、面向緩衝區的IO操作,相較BIO而言,它能夠更為高效的對資料進行讀寫操作,同時與原先的BIO使用方式也大有不同。

我們還是先來看個Demo:

  • NioServer
/**
 * @Author 三分惡
 * @Date 2023/4/30
 * @Description NIO服務端
 */

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();
            }
        }

    }
}
  • NioClient
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();
    }
}
  • 先執行NioServer,再執行NioClient,執行結果:
===========NIO服務端啟動============
===========NIO服務端超時等待============
===========NIO服務端超時等待============
成功連線客戶端
收到客戶端訊息:你好,靚仔!
向客戶端傳送訊息:你好,坤坤!
==============NIO客戶端啟動================
傳送訊息:你好,靚仔!
收到服務端訊息:你好,坤坤!

我們在這個案例裡實現了一個比較簡單的Java NIO 客戶端服務端通訊,裡面有兩個小的點需要注意,註冊到選擇器上的通道都必須要為非阻塞模型,同時透過緩衝區傳輸資料時,必須要呼叫flip()方法切換為讀取模式。

Java3種IO模型,一次搞懂!

Java-NIO中有三個核心概念:**Buffer(緩衝區)、Channel(通道)、Selector(選擇器)**。

Java3種IO模型,一次搞懂!
  • 每個客戶端連連線本質上對應著一個Channel通道,每個通道都有自己的Buffer緩衝區來進行讀寫,這些ChannelSelector選擇器管理排程

  • Selector負責輪詢所有已註冊的Channel,監聽到有事件發生,才提交給服務端執行緒處理,服務端執行緒不需要做任何阻塞等待,直接在Buffer裡處理Channel事件的資料即可,處理完馬上結束,或返回執行緒池供其他客戶端事件繼續使用。

  • 透過Selector,服務端的一個Thread就可以處理多個客戶端的請求

  • Buffer(緩衝區)就是飯店用來存放食材的儲藏室,當服務員點餐時,需要從儲藏室中取出食材進行製作。

  • Channel(通道)是用於傳輸資料的車道,就像飯店裡的上菜視窗,可以快速把點好的菜品送到客人的桌上。

  • Selector(選擇器)就是大堂經理,負責協調服務員、廚師和客人的配合和溝通,以保證整個就餐過程的效率和順暢。

Java AIO

Java-AIO也被成為NIO2,它是在NIO的基礎上,引入了新的非同步通道的概念,並提供了非同步檔案通道和非同步套接字的實現。

Java3種IO模型,一次搞懂!

它們的主要區別就在於這個非同步通道,見名知意:使用非同步通道去進行IO操作時,所有操作都為非同步非阻塞的,當呼叫read()/write()/accept()/connect()方法時,本質上都會交由作業系統去完成,比如要接收一個客戶端的資料時,作業系統會先將通道中可讀的資料先傳入read()回撥方法指定的緩衝區中,然後再主動通知Java程式去處理。

我們還是先來看個Demo:

  • AioServer
/**
 * @Author 三分惡
 * @Date 2023/5/1
 * @Description AIO服務端
 */

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(nullnew 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, nullnew 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()), nullnew 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(nullthis);
            }

            @Override
            public void failed(Throwable exc, Object attachment) {
                exc.printStackTrace();
                // 繼續非同步等待接收客戶端連線
                server.accept(nullthis);
            }
        });
        // 等待所有連線都處理完畢
        group.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
    }

}
  • AioClient
/**
 * @Author 三分惡
 * @Date 2023/5/1
 * @Description AIO客戶端
 */

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), nullnew CompletionHandler<Void, Object>() {
            // 建立ByteBuffer
            final ByteBuffer buffer = ByteBuffer.wrap(("你好,靚仔!").getBytes());

            @Override
            public void completed(Void result, Object attachment) {
                // 非同步傳送訊息給伺服器
                client.write(buffer, nullnew CompletionHandler<Integer, Object>() {
                    // 建立ByteBuffer
                    final ByteBuffer readBuffer = ByteBuffer.allocate(1024);

                    @Override
                    public void completed(Integer result, Object attachment) {
                        readBuffer.clear();
                        // 非同步讀取伺服器傳送的訊息
                        client.read(readBuffer, nullnew 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服務端啟動=========
客戶端連線成功
收到客戶端訊息:你好,靚仔!
收到服務端訊息:你好,阿坤!

可以看到,所有的操作都是非同步進行,透過completed接收非同步回撥,透過failed接收錯誤回撥。

而且我們發現,相較於之前的NIO而言,AIO其中少了Selector選擇器這個核心元件,選擇器在NIO中充當了協調者的角色。

但在Java-AIO中,類似的角色直接由作業系統擔當,而且不是採用輪詢的方式監聽IO事件,而是採用一種類似於“訂閱-通知”的模式。

Java3種IO模型,一次搞懂!

AIO中,所有建立的通道都會直接在OS上註冊監聽,當出現IO請求時,會先由作業系統接收、準備、複製好資料,然後再通知監聽對應通道的程式處理資料。

Java-AIO這種非同步非阻塞式IO也是由作業系統進行支援的,在Windows系統中提供了一種非同步IO技術:IOCP(I/O Completion Port,所以Windows下的Java-AIO則是依賴於這種機制實現。不過在Linux系統中由於沒有這種非同步IO技術,所以Java-AIOLinux環境中使用的還是epoll這種多路複用技術進行模擬實現的。

因為Linux的非同步IO技術實際上不太成熟,所以Java-AIO的實際應用並不是太多,比如大名鼎鼎的網路通訊框架Netty就沒有采用Java-AIO,而是使用Java-NIO,在程式碼層面,自行實現非同步。

小結

那麼這期我們就快速過了一下Java的三種IO機制,它們的特點,我們直接看下圖:

Java3種IO模型,一次搞懂!

我們也發現,雖然Java-NIOJava-AIO,在效能上比Java-BIO要強很多,但是可以看到,寫法上一個比一個難搞,不過好在基本也沒人直接用Java-NIOJava-AIO,如果要進行網路通訊,一般都會採用Netty,它對原生的Java-NIO進行了封裝最佳化,接下來,我們會繼續走近Netty,敬請期待。


參考:

[1].《Netty權威指南》

[2].

[3].

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024420/viewspace-2951113/,如需轉載,請註明出處,否則將追究法律責任。

相關文章