Java 網絡編程實戰筆記:BIO、NIO、AIO

Java 網絡編程學習筆記

前置概念

Java IO 模型

IO 模型 對應的 Java 版本
BIO(同步阻塞 IO) 1.4 之前
NIO(同步非阻塞 IO) 1.4
AIO(異步非阻塞 IO) 1.7

Linux 內核 IO 模型

  • 阻塞 IO

    最傳統的一種 IO 模型,在讀寫數據過程中會發生阻塞。

    當用戶線程發出 IO 請求後,內核會去查看數據是否就緒,如果沒有就緒就會等待數據就緒,而用戶線程就會處於阻塞狀態,用戶線程交出CPU。當數據就緒之後,內核會將數據拷貝到用戶線程,並返回 IO 執行結果給用戶線程,用戶線程解除阻塞狀態並開始處理數據。

    對應於 Java 中的 BIO。

  • 非阻塞 IO

    當用戶線程發起 IO 請求後並不需要等待,即使內核數據還沒有準備好也會馬上得到內核返回的一個結果,用戶線程可以之後再次詢問內核。一旦內核中的數據準備好了,並且又再次收到了用戶線程的請求,那麼內核就將數據拷貝到用戶線程並通知用戶線程。

    在非阻塞 IO 中,用戶線程需要不斷詢問內核數據是否準備就緒,在數據未就緒時可以處理其他任務。

  • 多路複用 IO

    在多路複用 IO 模型中會有一個 Selector 線程不斷輪詢多個 Socket 的狀態,只有當 Socket 真正有讀寫事件時才通知用戶線程進行實際的 IO 讀寫操作。阻塞 IO 和 非阻塞 IO 模型需要爲每個 Socket 建立一個單獨的線程處理數據,而多路複用 IO 只需要一個線程管理多個 Socket,並且只在真正有讀寫事件時纔會使用操作系統的 IO 資源,大大節約了系統資源。

    在非阻塞 IO 中不斷詢問 Socket 狀態是通過用戶線程進行的,而在多路複用 IO 中輪詢每個 Socket 狀態是內核在進行的,效率要比用戶線程高。

    對於多路複用 IO 模型來說在事件響應體很大時,Selector 線程會成爲性能瓶頸,導致後續事件無法及時處理,影響下一輪事件輪詢,因此實際應用中方法體內不做複雜邏輯處理,只做數據的接收和轉發,而將具體業務操作轉發給業務線程處理。

    對應於 Java 中的 NIO,在 NIO 中通過 selector.select() 去查詢每個 Channel 是否有事件到達,如果沒有事件到達用戶線程會一直阻塞,因此 NIO 也會導致用戶線程的阻塞。

  • 信號驅動 IO

    當用戶線程發起一個 IO 請求操作,會給對應的 Socket 註冊一個信號函數,然後用戶線程會繼續執行,當內核數據就緒時會發送一個信號給用戶線程,用戶線程接收到信號之後,便在信號函數中調用 IO 讀寫操作來進行實際的 IO 請求操作。

    一般用於 UDP 中。

  • 異步 IO

    當用戶線程發起異步 read 操作後,立刻就可以開始去做其它事。另一方面,從內核的角度,當它收到一個異步 read 之後會立刻返回一個狀態,說明請求是否成功發起,用戶線程不會任何阻塞。然後內核會等待數據準備完成並將數據拷貝到用戶線程,完成後內核會給用戶線程發送一個信號通知 read 操作已完成。

    用戶線程完全不需要關心實際的整個 IO 操作是如何進行的,只需要先發起一個請求,當接收內核返回的成功信號時表示 IO 操作已經完成,就可以直接去使用數據了。

    在異步IO模型中,IO操作的兩個階段都不會阻塞用戶線程,這兩個階段都是由內核自動完成,然後發送一個信號告知用戶線程操作已完成,用戶線程不需要再次調用 IO 函數進行具體的讀寫。在信號驅動模型中,當用戶線程接收到信號表示數據已經就緒,然後需要用戶線程調用IO函數進行實際的讀寫操作。

    對應於 Java 中的 AIO。


URL 解析與構造

當在瀏覽器輸入: http://www.google.com 時,

可能發出的實際請求是: http://www.google.com:80/search?q=test&safe=strict

其中 http 表示應用協議,www.google.com 表示域名,80 表示端口(可以明確指定要進行數據交換的進程),search 表示路徑(請求提供的服務),q=test&safe=strict 表示請求參數。

其中域名通過 DNS 域名解析系統將域名解析爲主機的 IP 地址,解析時是從右向左解析的,www.google.com 實際上是www.google.com.root.root 是根域名,一般會省略。

域名的層級如下:

域名層級 實例
根域名 .root
頂級域名 .com.edu.org
次級域名 .google.baidu.qq
主機名 www

DNS 本質是一個分佈式數據庫,域名對應 IP 地址的映射關係存儲在不同層級的域名服務器上,查詢方式分爲遞歸查詢和迭代查詢。

**遞歸查詢:**瀏覽器將域名發給 DNS 客戶端,DNS 客戶端將查詢請求發送給根域名服務器,根域名服務器如果知道對應 IP 地址將返回結果,否則發送給其已知的頂級域名服務器查詢,頂級域名如果知道對應的 IP 地址就返回結果,否則就發給其已知的二級域名服務器查詢,二級域名服務器也進行類似操作,最終將結果一路返回給瀏覽器。

**迭代查詢:**DNS 客戶端將請求發送給根域名服務器,如果根域名服務器不知道對應 IP 地址不會去請求頂級域名服務器,而是把已知的頂級域名服務器返回給 DNS 客戶端(不會幫 DNS 客戶端去查詢),DNS 客戶端得到頂級域名服務器地址後就再去請求頂級域名服務器發送查詢請求,以此類推。

不管使用哪種方式,一旦查詢成功,都會將結果緩存在查詢經過的域名服務器、DNS 客戶端或瀏覽器上。根域名服務器很少,一般其地址都內置在 DNS 客戶端中。


網絡分層

網絡分層 數據格式 協議 作用
應用層 報文 HTTP、FTP、SMTP 通過應用進程之間的交互來完成特定網絡應用。
運輸層 報文段/用戶數據報 TCP、UDP 向兩臺主機進程之間的通信提供的數據傳輸服務。
網絡層 分組/包 IP、ICMP、IGMP、ARP 爲分組交換網上的不同主機提供通信服務。
數據鏈路層 PPP、CSMA/CD 將網絡層 IP 數據報組裝成幀,在兩個相鄰結點之間的鏈路上傳輸。
物理層 0、1電信號 FDDI 屏蔽掉傳輸媒體和通信手段的差異。

java.io

網絡編程的本質是進程間的通信,通信的基礎是 IO 模型

IO 流分類:

java.io 包中用到了裝飾器模式:

例如創建 BufferedInputStream 對象時必須傳入一個 InputStream 對象(如 FileInputStream 對象)作爲參數,可以利用緩衝區在內存讀寫數據提高效率。


Socket 概述

Socket 也是一種數據源,是網絡通信的端點,可以把 Socket 理解爲一個唯一的 IP 地址和端口號,例如 127.0.0.1:8080

發送數據:

應用進程創建 Socket 並綁定到網卡的驅動程序,通過 Socket 發送數據,網卡驅動程序從 Socket 讀取數據併發送。

接收數據:

應用進程創建 Socket 並綁定到網卡的驅動程序,網卡驅動程序從網絡中接收到數據並傳輸給 Socket,Socket 將數據再傳輸給應用進程。


同步/異步/阻塞/非阻塞

同步和異步是通信機制,阻塞和非阻塞是調用狀態。

  • 同步 IO 是用戶線程發起 I/O 請求後需要等待或者輪詢內核 I/O 操作完成後才能繼續執行。

  • 異步 IO 是用戶線程發起 I/O 請求後仍可以繼續執行,當內核 I/O 操作完成後會通知用戶線程,或者調用用戶線程註冊的回調函數。

  • 阻塞 IO 是指 I/O 操作需要徹底完成後才能返回用戶空間 。

  • 非阻塞 IO 是指 I/O 操作被調用後立即返回一個狀態值,無需等 I/O 操作徹底完成。


線程池

線程池可以通過複用線程,減小線程創建和銷燬的開銷,提高程序效率。Executors 類提供了幾種靜態方法直接創建線程池:

  • newSingleThreadExecutor

    線程池中只有一個線程,不斷複用

  • newFixedThreadPool

    線程池中的線程數量是固定的

  • newCachedThreadPool

    提交的任務如果有空閒線程則處理,否則創建一個新線程來處理任務

  • newScheduledThreadPool

    提交的任務可以在指定的時間執行


BIO 同步阻塞

BIO 即同步阻塞式 IO,每一個客戶端請求都對應一個線程來處理。

服務器端通常由一個獨立的 Acceptor 線程負責監聽客戶端的連接。一般通過在 while(true) 循環中調用 accept() 方法接收客戶端連接的方式監聽連接請求,請求一旦接收到一個連接請求,就可以建立通信套接字在這個通信套接字上進行讀寫操作,此時不能再接收其他客戶端連接請求,只能等待同當前連接的客戶端的操作執行完成, 可以通過多線程來支持多個客戶端的連接。

模擬 TCP 通信

Socket類 是客戶端的 Socket 連接,使用步驟:

  • 創建對象時要提供服務器的主機和端口參數。
  • 數據通信結束後通過 close 方法關閉連接。

ServerSocket 類是服務器端的 Socket 連接,使用步驟:

  • 創建對象時要提供端口參數進行綁定,服務器會監聽該端口,任何發送到該端口的信息都會被服務器接收並處理。

  • 通過 accept 方法獲取客戶端連接,通過該連接與客戶端進行數據通信,是一種阻塞式調用。


服務器的代碼:

public class Server {

    public static void main(String[] args) {
        // 退出條件
        final String QUIT = "quit";
        // 服務器端口
        final int PORT = 8080;
        // 服務器的 socket 對象
        ServerSocket serverSocket = null;

        try {
            // 綁定監聽端口
            serverSocket = new ServerSocket(PORT);
            System.out.println("啓動服務器,監聽端口:" + PORT);

            while (true){
                // 等待客戶端連接
                Socket socket = serverSocket.accept();
                System.out.println("客戶端[" + socket.getPort() + "]已連接");
                // 獲取和客戶端通信的字符輸入流和字符輸出流
                BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));

                // 讀取客戶端發送的消息
                String message;
                while ((message = br.readLine()) != null){
                    System.out.println("客戶端[" + socket.getPort() + "]發送消息:" + message);

                    // 回覆客戶端
                    bw.write("服務器已經收到消息:" + message + "\n");
                    bw.flush();

                    // 查看客戶端是否關閉
                    if (QUIT.equals(message)){
                        System.out.println("客戶端[" + socket.getPort() + "]已斷開連接");
                        break;
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 釋放資源
            if(serverSocket != null){
                try {
                    serverSocket.close();
                    System.out.println("關閉 serverSocket");
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

客戶端的代碼:

public class Client {

    public static void main(String[] args) {
        // 退出條件
        final String QUIT = "quit";
        // 服務器主機和端口
        final String HOST = "127.0.0.1";
        final int PORT = 8080;
        // 客戶端的 socket 對象
        Socket socket = null;
        BufferedWriter bw = null;

        try {
            // 創建 socket
            socket = new Socket(HOST, PORT);

            // 獲取和服務器通信的字符輸入流和字符輸出流
            BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));

            // 等待用戶輸入信息
            while (true) {
                System.out.println("請輸入要發送的消息:");
                BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in));
                String input = consoleReader.readLine();

                // 發送消息給服務器
                bw.write(input + "\n");
                bw.flush();

                // 讀取服務器返回的消息
                String message= br.readLine();
                if (message != null) {
                    System.out.println("服務器發送消息:" + message);
                }

                // 查看用戶是否退出
                if(QUIT.equals(input)){
                    break;
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            // 釋放資源
            try {
                if(bw != null) {
                    bw.close();
                    System.out.println("關閉 socket");
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

基於 BIO 的多人聊天室

  • 基於 BIO 模型
  • 支持多人同時在線
  • 每個用戶的發言都會被轉發給其他在線用戶

一共由四個類組成:

  • 服務器類:ChatServer

    public class ChatServer {
    
        /** 服務器端口 */
        private final int PORT = 8080;
    
        /** 結束條件 */
        private final String QUIT = "quit";
    
         /** 服務器端的 socket 連接 */
        private ServerSocket serverSocket;
    
         /** 存儲在線用戶 */
        private final Map<Integer, Writer> clients;
    
        public ChatServer(){
            clients = new HashMap<>();
        }
    
    
        /**
         * 添加在線用戶
         * @param socket 上線的客戶端 socket 連接對象
         * @throws IOException 可能產生的異常
         */
        public synchronized void addClient(Socket socket) throws IOException {
            if(socket != null) {
                int port = socket.getPort();
                BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
                clients.put(port, writer);
                System.out.println("客戶端[" + port + "]已連接到服務器");
            }
        }
    
        /**
         * 移除離線用戶
         * @param socket 下線的客戶端 socket 連接對象
         * @throws IOException 可能產生的異常
         */
        public synchronized void removeClient(Socket socket) throws IOException {
            if(socket != null){
                int port = socket.getPort();
                // 如果當前用戶仍在在線用戶集合中則移除
                if(clients.containsKey(port)){
                    clients.get(port).close();
                }
                clients.remove(port);
                System.out.println("客戶端[" + port + "]已斷開連接");
            }
        }
    
        /**
         * 轉發消息給其他用戶
         * @param socket 要發送消息的客戶端的 socket 連接對象
         * @param message 要發送的消息
         * @throws IOException 可能產生的異常
         */
        public synchronized void forwardMessage(Socket socket, String message) throws IOException {
            for (Integer port : clients.keySet()){
                // 遍歷在線用戶,通過比較端口號判斷當前用戶是否是發送消息的用戶,如果不是則轉發消息
                if(!port.equals(socket.getPort())){
                    Writer writer = clients.get(port);
                    writer.write(message);
                    writer.flush();
                }
            }
        }
    
        /**
         * 查看用戶是否準備退出
         * @param message 用戶發送的消息
         * @return true 表示準備退出,反之
         */
        public boolean checkQuit (String message) {
            return QUIT.equals(message);
        }
    
        /**
         * 釋放資源
         */
        public synchronized void close() {
            if(serverSocket != null){
                try {
                    serverSocket.close();
                    System.out.println("關閉 serverSocket");
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    
        /**
         * 啓動服務器端開始監聽
         */
        public void start() {
            try {
                // 綁定監聽端口
                serverSocket = new ServerSocket(PORT);
                System.out.println("服務器已經啓動,監聽端口:" + PORT);
                while (true) {
                    // 等待獲取客戶端連接
                    Socket socket = serverSocket.accept();
                    // 創建線程處理客戶端連接
                    new Thread(new ChatHandler(this, socket)).start();
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                //釋放資源
                close();
            }
        }
    
        /**
         * 用 main 線程模擬 Acceptor 線程
         * @param args main方法參數
         */
        public static void main(String[] args) {
            ChatServer chatServer = new ChatServer();
            // 啓動服務器
            chatServer.start();
        }
    }
    
  • 服務器處理客戶端連接類:ChatHandler

    public class ChatHandler implements Runnable{
    
        /** 服務端 */
        private ChatServer chatServer;
    
        /** 客戶端連接 */
        private Socket socket;
    
        public ChatHandler(ChatServer chatServer, Socket socket) {
            this.chatServer = chatServer;
            this.socket = socket;
        }
    
        /**
         * 處理客戶端連接
         */
        @Override
        public void run() {
            try {
                // 添加新上線用戶
                chatServer.addClient(socket);
                // 讀取用戶發送的信息
                BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                String message;
                while ((message = reader.readLine()) != null) {
                    String forwardMessage = "客戶端[" + socket.getPort() + "]發送了一條消息:" + message + "\n";
                    System.out.println(forwardMessage);
                    // 轉發消息給其他在線用戶
                    chatServer.forwardMessage(socket, forwardMessage);
                    // 查看用戶是否準備退出
                    if(chatServer.checkQuit(message)){
                        break;
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                // 移除離線用戶
                try {
                    chatServer.removeClient(socket);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
  • 客戶端類:ChatClient

    public class ChatClient {
    
        /** 服務器主機 */
        private final String HOST = "127.0.0.1";
    
        /** 服務器端口 */
        private final int PORT = 8080;
    
        /** 結束條件 */
        private final String QUIT = "quit";
    
        /** 和服務器通信的 socket */
        private Socket socket;
    
        /** 和服務器通信的輸入流 */
        private BufferedReader reader;
    
        /** 和服務器通信的輸出流 */
        private BufferedWriter writer;
    
        /**
         * 發送信息給服務器
         * @param message 要發送的信息
         * @throws IOException 可能拋出的異常
         */
        public void send(String message) throws IOException {
            // 確保輸出流是開放狀態
            if(!socket.isOutputShutdown()){
                writer.write(message + "\n");
                writer.flush();
            }
        }
    
        /**
         * 從服務器端接收消息
         * @return 返回接受的消息
         * @throws IOException 可能拋出的異常
         */
        public String receive() throws IOException {
            String message = null;
            // 確保輸入流是開放狀態
            if(!socket.isInputShutdown()){
                message = reader.readLine();
            }
            return message;
        }
    
        /**
         * 查看用戶是否準備退出
         * @param message 用戶發送的消息
         * @return true 表示準備退出,反之
         */
        public boolean checkQuit (String message) {
            return QUIT.equals(message);
        }
    
        /**
         * 釋放資源
         */
        public synchronized void close() {
            if(writer != null){
                try {
                    writer.close();
                    System.out.println("關閉 socket");
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    
        /**
         * 啓動客戶端
         */
        public void start() {
            try {
                // 創建 socket
                socket = new Socket(HOST, PORT);
                // 創建 IO 流
                reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
                // 創建新線程處理用戶的輸入
                new Thread(new UserInputHandler(this)).start();
                // 讀取服務器轉發的消息
                String message;
                while ((message = receive()) != null){
                    System.out.println(message);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                //釋放資源
                close();
            }
        }
    
        public static void main(String[] args) {
            ChatClient chatClient = new ChatClient();
            // 啓動客戶端
            chatClient.start();
        }
    }
    
  • 客戶端處理用戶輸入類:UserInputHandler

    public class UserInputHandler implements Runnable{
    
        /** 客戶端 */
        private ChatClient chatClient;
    
        public UserInputHandler(ChatClient chatClient) {
            this.chatClient = chatClient;
        }
    
        /**
         * 處理用戶輸入信息
         */
        @Override
        public void run() {
            try {
                // 等待用戶輸入信息
                BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in));
                while (true) {
                    String message = consoleReader.readLine();
                    // 向服務器發送消息
                    chatClient.send(message);
                    // 檢查用戶是否準備退出
                    if(chatClient.checkQuit(message)){
                        break;
                    }
                }
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }
    

僞異步 IO 改進多人聊天室

在基於 BIO 的多人聊天室中,一個客戶端請求對應着一個線程,當用戶數量很大時線程的開銷也會很大。我們可以通過線程池來實現僞異步 IO 來進行優化,限制線程的總數量,實現線程的複用。

在 ChatServer 中添加屬性並修改構造器:

/** 線程池 */
private ExecutorService executorService;

public ChatServer(){
    // 限制服務器處理請求的線程數爲 10
    executorService = Executors.newFixedThreadPool(10);
    clients = new HashMap<>();
}

同時修改 ChatServer 的 start 方法中創建線程部分的代碼,改爲使用線程池:

while (true) {
    // 等待獲取客戶端連接
    Socket socket = serverSocket.accept();
    // 將處理請求任務提交給線程池執行
    executorService.execute(new ChatHandler(this, socket));
}

NIO 同步非阻塞

BIO 中的阻塞:

  • ServerSocket 的 accept()
  • InputStream 和 OutputStream 的讀和寫
  • 無法在同一個線程中處理多個 Stream IO

可以使用非阻塞的 IO 即 NIO,在 NIO 中:

  • 使用 Channel 代替 Stream,Channel 是雙向的,既可以讀數據也可以寫數據,既可以阻塞讀寫也可以非阻塞讀寫
  • 使用 Selector 監控多條 Channel
  • 可以在一個線程裏處理多個 Channel IO

NIO 的核心組件:

  • Selector

    選擇器或多路複用器,主要作用是輪詢檢查多個 Channel 的狀態,判斷 Channel 註冊的事件是否發生,即判斷 Channel 是否處於可讀或可寫狀態。

    在使用之前需要將 Channel 註冊到 Selector 上,註冊之後會得到一個 SelectionKey。

    SelectionKey 的相關方法:

    • interestOps() : 查看 Channel 對象註冊的狀態集合,例如 Accept(接收到一個客戶端請求)、Read、Wirte 狀態。
    • readyOps() :查看 Channel 對象可操作的狀態集合
    • channel() :獲取 Channel 對象
    • selector():獲取 Selector 對象
    • attachment():附加一個對象或更多信息

    使用 Selector 選擇 Channel:

    • select():獲取處於就緒狀態的 Channel 對象數量
  • Channel

    雙向通道,替換了 IO 中的 Stream,不能直接訪問數據,要通過 Buffer 來讀寫數據,也可以和其他 Channel 交互。

    分類:

    • FileChannel(處理文件)

    • DatagramChannel(處理 UDP 數據)

    • SocketChannel(處理 TCP 數據,用作客戶端)

    • ServerSocketChannel(處理 TCP 數據,用作服務器端)。

  • Buffer

    緩衝區,本質是一塊可讀寫數據的內存,這塊內存被包裝成 NIO 的 Buffer 對象,用來簡化數據的讀寫。Buffer 的三個重要屬性:position 表示下一次讀寫數據的位置,limit 表示本次讀寫的極限位置,capacity 表示最大容量。

    • flip() 將寫轉爲讀,底層實現原理是把 position 置 0,並把 limit 設爲當前的 position 值。
    • 通過clear() 將讀轉爲寫模式(用於讀完全部數據的情況,把 position 置 0,limit 設爲 capacity)。
    • 通過compact() 將讀轉爲寫模式(用於沒有讀完全部數據,存在未讀數據的情況,讓 position 指向未讀數據的下一個)。
    • 通道的方向和 Buffer 的方向是相反的,讀取數據相當於向 Buffer 寫入,寫出數據相當於從 Buffer 讀取。

    使用步驟:

    • 向 Buffer 寫入數據
    • 調用 flip 方法將 Buffer 從寫模式切換爲讀模式
    • 從 Buffer 中讀取數據
    • 調用 clear 或 compact 方法來清空 Buffer

本地文件拷貝

使用 4 種方法進行本地文件的拷貝,分別是不使用/使用緩衝區的 BIO 方式,使用 Buffer/直接使用 Channel 的 NIO 方式。

其中不使用緩衝區的 BIO 效率最低,其餘效率差不多,直接使用 Channel 的 NIO 方式效率較高。

/** 拷貝文件的接口 */
public interface FileCopy {

    /**
     * 拷貝文件
     * @param source 拷貝文件源
     * @param target 拷貝文件目的地
     */
    void copyFile(File source, File target);
}

class FileCopyDemo {
    
    private static void close(Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {

        // BIO 不使用緩衝區
        FileCopy noBufferStreamCopy = (source, target) -> {
            InputStream is = null;
            OutputStream os = null;
            try {
                is = new FileInputStream(source);
                os = new FileOutputStream(target);
                int read;
                while ((read = is.read()) != -1) {
                    os.write(read);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                close(is);
                close(os);
            }
        };

        // BIO 使用緩衝區
        FileCopy bufferStreamCopy = (source, target) -> {
            InputStream is = null;
            OutputStream os = null;
            try {
                is = new BufferedInputStream(new FileInputStream(source));
                os = new BufferedOutputStream(new FileOutputStream(target));
                int read;
                byte[] buffer = new byte[1024];
                while ((read = is.read(buffer)) != -1) {
                    os.write(buffer, 0 ,read);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                close(is);
                close(os);
            }
        };

        // NIO 使用 Buffer 交互
        FileCopy nioBufferCopy = (source, target) -> {
            FileChannel in = null;
            FileChannel out = null;
            try {
                in = new FileInputStream(source).getChannel();
                out = new FileOutputStream(target).getChannel();
                // 分配一個大小爲 1024KB 的緩衝區
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                while ((in.read(buffer)) != -1) {
                    // 將 Buffer 的寫模式轉爲讀模式(讀取數據 = 向 Buffer 寫)
                    buffer.flip();
                    // 只要還有數據就全部寫出
                    while (buffer.hasRemaining()) {
                        out.write(buffer);
                    }
                    // 將 Buffer 的讀模式轉爲寫模式(寫出數據 = 從 Buffer 讀)
                    buffer.clear();
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                close(in);
                close(out);
            }
        };

        //NIO 直接使用 Channel 交互
        FileCopy nioTransferCopy = (source, target) -> {
            FileChannel in = null;
            FileChannel out = null;
            try {
                in = new FileInputStream(source).getChannel();
                out = new FileOutputStream(target).getChannel();
                long transferred = 0;
                long size = in.size();
                while (transferred != size) {
                    transferred += in.transferTo(0, size, out);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                close(in);
                close(out);
            }
        };
    }
}

使用 NIO 改寫多人聊天室

服務器端代碼:

public class ChatServer {

    /** 服務器端口 */
    private static final int DEFAULT_PORT = 8080;

    /** 結束條件 */
    private static final String QUIT = "quit";

    /** 緩衝區大小 */
    private static final int BUFFER_SIZE = 1024;

    /** 服務端 Channel */
    private ServerSocketChannel server;

    /** Selector 選擇器 */
    private Selector selector;

    /** 讀緩衝 */
    private ByteBuffer readBuffer = ByteBuffer.allocate(BUFFER_SIZE);

    /** 寫緩衝 */
    private ByteBuffer writeBuffer = ByteBuffer.allocate(BUFFER_SIZE);

    /** 使用的編碼 */
    private Charset charset = StandardCharsets.UTF_8;

    /** 可以自定義服務器端口 */
    private int port;

    public ChatServer() {
        this(DEFAULT_PORT);
    }
    public ChatServer(int port){
        this.port = port;
    }

    /**
     * 查看用戶是否準備退出
     * @param message 用戶發送的消息
     * @return true 表示準備退出,反之
     */
    private boolean checkQuit (String message) {
        return QUIT.equals(message);
    }

    /** 釋放資源 */
    private void close(Closeable closeable) {
        if(closeable != null){
            try {
                closeable.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private String getClientName(SocketChannel client) {
        return "客戶端[" + client.socket().getPort() + "]";
    }

    private void forwardMessage(SocketChannel client, String message) throws IOException {
        // 遍歷所有註冊的 channel
        Set<SelectionKey> selectionKeys = selector.keys();
        for(SelectionKey selectionKey : selectionKeys) {
            Channel channel = selectionKey.channel();
            // 是服務端的 channel 則跳過
            if (channel instanceof  ServerSocketChannel) {
                continue;
            }
            // channel 有效且不是發送消息的客戶端 channel
            if (selectionKey.isValid() && !client.equals(channel)) {
                // 向 Buffer 寫入數據,準備發送
                writeBuffer.clear();
                writeBuffer.put(charset.encode(getClientName(client) + ":" + message));
                writeBuffer.flip();
                // 從 Buffer 讀取數據併發送
                while (writeBuffer.hasRemaining()) {
                    ((SocketChannel)channel).write(writeBuffer);
                }
            }
        }
    }

    // 讀取消息
    private String receive(SocketChannel client) throws IOException {
        // 清空殘留信息,將讀模式轉爲寫模式
        readBuffer.clear();
        // 接收消息,相等於向 Buffer 寫數據
        while (client.read(readBuffer) > 0);
        // 寫完後將寫模式轉爲讀模式
        readBuffer.flip();
        return String.valueOf(charset.decode(readBuffer));
    }

    /** 處理事件 */
    private void handles(SelectionKey selectionKey) throws IOException{
        // 如果觸發了 ACCEPT 事件 —— 和客戶端建立了連接
        if (selectionKey.isAcceptable()) {
            ServerSocketChannel server = (ServerSocketChannel) selectionKey.channel();
            // 獲取客戶端的 channel
            SocketChannel client = server.accept();
            // 設爲非阻塞調用模式
            client.configureBlocking(false);
            // 註冊客戶端的 READ 事件到 selector
            client.register(selector, SelectionKey.OP_READ);
            System.out.println(getClientName(client)  + "已連接服務器");
        }else if (selectionKey.isReadable()){
            // 如果觸發了 READ 事件 —— 客戶端發送了數據
            SocketChannel client = (SocketChannel) selectionKey.channel();
            // 讀取數據
            String message = receive(client);
            if (message.isEmpty()) {
                // 消息爲空,客戶端異常,取消監聽
                selectionKey.cancel();
                // 刷新
                selector.wakeup();
            } else {
                // 轉發消息
                forwardMessage(client, message);
                // 查看用戶是否準備退出
                if(checkQuit(message)) {
                    selectionKey.cancel();
                    selector.wakeup();
                    System.out.println(getClientName(client) + "已斷開連接");
                }
            }
        }
    }

    /** 啓動服務器端開始監聽 */
    private void start() {
        try {
            server = ServerSocketChannel.open();
            // 設置非阻塞調用
            server.configureBlocking(false);
            // 綁定監聽端口
            ServerSocket serverSocket = server.socket();
            serverSocket.bind(new InetSocketAddress(port));
            // 創建 selector
            selector = Selector.open();
            // 註冊服務器 Channel 的 ACCEPT(接收客戶端請求) 事件到 selector
            server.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("啓動服務器,監聽端口:" + port + "...");
            // 不斷監聽請求
            while (true) {
                // select 是阻塞式調用,如果成功調用說明已有 Channel 就緒
                selector.select();
                // select 監聽到的所有事件
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                for (SelectionKey selectionKey : selectionKeys) {
                    // 處理被觸發的事件
                    handles(selectionKey);
                }
                // 處理後清空
                selectionKeys.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            close(selector);
        }
    }

    /**
     * @param args main方法參數
     */
    public static void main(String[] args) {
        ChatServer chatServer = new ChatServer();
        chatServer.start();
    }
}

客戶端代碼:

public class ChatClient {

    /** 服務器默認主機 */
    private static final String DEFAULT_HOST = "127.0.0.1";

    private String host;

    /** 服務器默認端口 */
    private static final int DEFAULT_PORT = 8080;

    private int port;

    /** 結束條件 */
    private static final String QUIT = "quit";

    /** 和服務器通信的 socket */
    private SocketChannel client;

    /** 緩衝區大小 */
    private static final int BUFFER_SIZE = 1024;

    /** 讀取緩衝 */
    private ByteBuffer readBuffer = ByteBuffer.allocate(BUFFER_SIZE);

    /** 寫出緩衝 */
    private ByteBuffer writeBuffer = ByteBuffer.allocate(BUFFER_SIZE);

    /** Selector 選擇器 */
    private Selector selector;

    /** 使用的編碼 */
    private Charset charset = StandardCharsets.UTF_8;

    public ChatClient() {
        this(DEFAULT_HOST, DEFAULT_PORT);
    }

    public ChatClient(String host, int port) {
        this.host = host;
        this.port = port;
    }


    /**
     * 向服務器發送消息
     * @param message 要發送的消息
     */
    public void send(String message) throws IOException {
        if (message.isEmpty()) {
            return;
        }
        // 讀模式轉寫模式
        writeBuffer.clear();
        writeBuffer.put(charset.encode(message));
        // 寫模式轉讀模式
        writeBuffer.flip();
        while (writeBuffer.hasRemaining()) {
            client.write(writeBuffer);
        }
        // 判斷用戶是否準備退出
        if (checkQuit(message)) {
            close(selector);
        }
    }

    /**
     * 查看用戶是否準備退出
     * @param message 用戶發送的消息
     * @return true 表示準備退出,反之
     */
    public boolean checkQuit (String message) {
        return QUIT.equals(message);
    }

    /** 釋放資源 */
    private void close(Closeable closeable) {
        if(closeable != null){
            try {
                closeable.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /** 啓動客戶端 */
    public void start() {
        try {
            // 創建 channel
            client = SocketChannel.open();
            // 設置非阻塞調用
            client.configureBlocking(false);
            // 創建 selector
            selector = Selector.open();
            // 註冊客戶端 channel 的 CONNECT 事件到 selector
            client.register(selector, SelectionKey.OP_CONNECT);
            // 客戶端向服務器發起連接請求
            client.connect(new InetSocketAddress(host, port));
            while (true) {
                // select 是阻塞式調用,如果成功調用說明已有 Channel 就緒
                selector.select();
                // select 監聽到的所有事件
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                for (SelectionKey selectionKey : selectionKeys) {
                    // 處理被觸發的事件
                    handles(selectionKey);
                }
                // 處理後清空
                selectionKeys.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClosedSelectorException e) {
            // 用戶正常退出
        } finally {
            close(selector);
        }
    }

    private void handles(SelectionKey selectionKey) throws IOException {
        // 如果觸發了 CONNECT 事件 —— 連接就緒事件
        if (selectionKey.isConnectable()) {
            SocketChannel client = (SocketChannel) selectionKey.channel();
            // 如果連接已經建立了
            if (client.isConnectionPending()) {
                client.finishConnect();
                // 處理用戶輸入
                new Thread(new UserInputHandler(this)).start();
            }
            // 註冊客戶端的 READ 事件到 selector
            client.register(selector, SelectionKey.OP_READ);
        }else if (selectionKey.isReadable()) {
            // 如果觸發了 READ 事件 —— 服務器轉發了數據
            SocketChannel client = (SocketChannel) selectionKey.channel();
            // 讀取數據
            String message = receive(client);
            if (message.isEmpty()) {
                // 消息爲空,服務器異常,取消監聽
                close(selector);
            } else {
                System.out.println(message);
            }
        }
    }

    private String receive(SocketChannel client) throws IOException {
        readBuffer.clear();
        while (client.read(readBuffer) > 0);
        readBuffer.flip();
        return String.valueOf(charset.decode(readBuffer));
    }

    public static void main(String[] args) {
        ChatClient chatClient = new ChatClient();
        // 啓動客戶端
        chatClient.start();
    }
}

處理用戶輸入的類 UserInputHandler 代碼不變。


AIO 異步非阻塞

服務器端通道AsynchronousServerSocketChannel,通過 accept 獲取客戶端連接

客戶端通道 AsynchronousSocketChannel,通過 connect 連接服務器

實現方式:

  • 通過 Future 的 get 阻塞式調用
  • 通過實現 CompletionHandler 接口,重寫回調方法 completed 和 failed

迴音聊天室

實現功能:可以將用戶發送給服務器的消息再返回給用戶,類似“迴音”效果。

服務器代碼:

public class Server {

    final String LOCALHOST = "127.0.0.1";
    final int DEFAULT_PORT = 8080;

    /** 異步的服務器端 channel */
    AsynchronousServerSocketChannel serverChannel;

    public static void main(String[] args) {
        new Server().start();
    }

    public void start() {
        try {
            // 綁定監聽的端口
            serverChannel = AsynchronousServerSocketChannel.open();
            serverChannel.bind(new InetSocketAddress(LOCALHOST, DEFAULT_PORT));
            System.out.println("啓動服務器,監聽端口:" + DEFAULT_PORT);
            while (true) {
                // 異步調用,接收客戶端請求
                serverChannel.accept(null, new AcceptHandler());
                // 控制調用頻率
                System.in.read();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            close(serverChannel);
        }
    }

    /**
     * 釋放資源
     * @param closeable 要釋放的資源
     */
    private void close(Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
                System.out.println("關閉" + closeable);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 處理異步 ACCEPT 事件的方法,第一個泛型參數是成功回調的 result 類型,第二個泛型參數是 attachment 類型
     */
    private class AcceptHandler implements CompletionHandler<AsynchronousSocketChannel, Object> {

        /**
         * 成功的回調方法
         * @param result 和服務器建立連接的客戶端 channel
         * @param attachment 額外數據
         */
        @Override
        public void completed(AsynchronousSocketChannel result, Object attachment) {
            // 讓服務器繼續監聽其他客戶端
            if (serverChannel.isOpen()) {
                serverChannel.accept(null, this);
            }
            AsynchronousSocketChannel clientChannel = result;
            if (clientChannel != null && clientChannel.isOpen()) {
                // 處理異步 read
                ClientHandler clientHandler = new ClientHandler(clientChannel);
                // 讀取客戶端數據
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                // 附加信息,一個 map 集合
                Map<String, Object> info = new HashMap<>();
                info.put("type", "read");
                info.put("buffer", buffer);
                // 異步調用 read,相當於向 buffer 寫數據,buffer 是寫模式
                clientChannel.read(buffer, info, clientHandler);
            }
        }

        /**
         * 失敗的回調方法
         * @param exc 異常
         * @param attachment 額外數據
         */
        @Override
        public void failed(Throwable exc, Object attachment) {
            // 處理錯誤
        }
    }

    /**
     * 處理讀寫事件,第一個參數表示 Buffer 寫入的字節
     */
    private class ClientHandler implements CompletionHandler<Integer, Object> {

        private AsynchronousSocketChannel clientChannel;

        ClientHandler(AsynchronousSocketChannel clientChannel) {
            this.clientChannel = clientChannel;
        }

        @Override
        public void completed(Integer result, Object attachment) {
            Map<String, Object> info = (Map<String, Object>) attachment;
            // 判斷操作類型是 read 還是 write
            String type = (String) info.get("type");
            // 讀完客戶端數據後寫出
            if ("read".equals(type)) {
                ByteBuffer buffer = (ByteBuffer) info.get("buffer");
                // 將 buffer 從寫模式轉爲讀模式
                buffer.flip();
                info.put("type", "write");
                // 寫出數據,相當於從 buffer 讀
                clientChannel.write(buffer, info, this);
                // 將 buffer 從讀模式轉爲寫模式
                buffer.clear();
            } else if ("write".equals(type)) {
                // 寫出後繼續讀取客戶端數據
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                info.put("type", "read");
                info.put("buffer", buffer);
                // 異步調用 read
                clientChannel.read(buffer, info, this);
            }
        }

        @Override
        public void failed(Throwable exc, Object attachment) {
            //處理錯誤
        }
    }
}

客戶端代碼:

public class Client {

    final String LOCALHOST = "127.0.0.1";
    final int DEFAULT_PORT = 8080;

    /** 異步的客戶端 channel */
    AsynchronousSocketChannel clientChannel;

    public static void main(String[] args) {
        new Client().start();
    }

    public void start() {
        try {
            // 創建 channel
            clientChannel = AsynchronousSocketChannel.open();
            Future<Void> future = clientChannel.connect(new InetSocketAddress(LOCALHOST, DEFAULT_PORT));
            // get 是阻塞式調用,在客戶端和服務器建立連接前會阻塞
            future.get();
            // 等待用戶輸入
            BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in));
            while (true) {
                String input = consoleReader.readLine();
                // 將用戶消息發送給服務器
                byte[] inputBytes = input.getBytes();
                ByteBuffer buffer = ByteBuffer.wrap(inputBytes);
                Future<Integer> writeResult = clientChannel.write(buffer);
                // 阻塞直到消息成功發送
                writeResult.get();
                // 從服務器讀取響應消息
                // 寫出數據相當於從 Buffer 讀,所以現在將讀轉爲寫模式
                buffer.clear();
                Future<Integer> readResult = clientChannel.read(buffer);
                // 阻塞直到成功讀取消息
                readResult.get();
                String echo = new String(buffer.array());
                // 讀取數據相當於向 Buffer 寫,所以現在將寫轉爲讀模式
                buffer.flip();
                System.out.println(echo);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            close(clientChannel);
        }
    }

    /**
     * 釋放資源
     * @param closeable 要釋放的資源
     */
    private void close(Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
                System.out.println("關閉" + closeable);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

使用 AIO 改寫多人聊天室

服務器端:

public class ChatServer {

    /** 服務器主機 */
    private static final String LOCALHOST = "localhost";

    /** 服務器端口 */
    private static final int DEFAULT_PORT = 8080;

    /** 結束條件 */
    private static final String QUIT = "quit";

    /** 緩衝區大小 */
    private static final int BUFFER_SIZE = 1024;

    /** 線程池大小 */
    private static final int THREAD_POOL_SIZE = 8;

    /** 自定義 ChannelGroup */
    private AsynchronousChannelGroup channelGroup;

    /** 服務端 Channel */
    private AsynchronousServerSocketChannel serverChannel;

    /** 使用的編碼 */
    private Charset charset = StandardCharsets.UTF_8;

    /** 可以自定義服務器端口 */
    private int port;

    /** 存儲用戶在線列表 */
    private List<ClientHandler> connectedClients;

    public ChatServer() {
        this(DEFAULT_PORT);
    }

    public ChatServer(int port){
        this.port = port;
        this.connectedClients = new ArrayList<>();
    }

    /**
     * 程序入口
     * @param args 參數
     */
    public static void main(String[] args) {
        new ChatServer().start();
    }

    /**
     * 啓動服務器
     */
    private void start() {
        try {
            // 創建自定義 channel group
            ExecutorService executorService = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
            channelGroup = AsynchronousChannelGroup.withThreadPool(executorService);
            // 創建 channel 並綁定 group 和端口
            serverChannel = AsynchronousServerSocketChannel.open(channelGroup);
            serverChannel.bind(new InetSocketAddress(LOCALHOST, port));
            System.out.println("啓動服務器,監聽端口:" + port);
            while (true) {
                // 接收客戶端請求並處理
                serverChannel.accept(null, new AcceptHandler());
                System.in.read();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            close(serverChannel);
        }
    }

    /**
     * 查看用戶是否準備退出
     * @param message 用戶發送的消息
     * @return true 表示準備退出,反之
     */
    private boolean checkQuit (String message) {
        return QUIT.equals(message);
    }

    /**
     * 獲取客戶端名
     * @param clientChannel 客戶端 channel
     * @return 返回客戶端名
     */
    private String getClientName(AsynchronousSocketChannel clientChannel) {
        int clientPort = -1;
        try {
            InetSocketAddress inetSocketAddress = (InetSocketAddress) clientChannel.getRemoteAddress();
            clientPort = inetSocketAddress.getPort();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "客戶端[" + clientPort + "]";
    }

    /**
     * 服務器轉發消息給其他客戶端
     * @param clientChannel 發送消息的客戶端
     * @param message 發送的具體消息
     */
    private synchronized void forwardMessage(AsynchronousSocketChannel clientChannel, String message) {
        for (ClientHandler handler : connectedClients){
            // 該信息不用再轉發到發送信息的客戶端
            if (!handler.clientChannel.equals(clientChannel)){
                try {
                    // 將要轉發的信息寫入到 buffer 中
                    ByteBuffer buffer = charset.encode(getClientName(handler.clientChannel) + ":" + message);
                    // 寫事件不添加附加信息
                    handler.clientChannel.write(buffer,null, handler);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 服務器接收客戶端消息
     * @param buffer buffer 緩衝區
     * @return 返回接收的消息
     */
    private synchronized String receive(ByteBuffer buffer) {
        CharBuffer charBuffer = charset.decode(buffer);
        return String.valueOf(charBuffer);
    }

    /**
     * 添加一個新的客戶端進到在線列表
     * @param handler 客戶端對應的 handler
     */
    private synchronized void addClient(ClientHandler handler) {
        connectedClients.add(handler);
        System.out.println(getClientName(handler.clientChannel) + "已經連接到服務器");
    }

    /**
     * 將下線用戶從在線列表中刪除
     * @param clientHandler 客戶端對應的 handler
     */
    private synchronized void removeClient(ClientHandler clientHandler) {
        connectedClients.remove(clientHandler);
        System.out.println(getClientName(clientHandler.clientChannel) + "已斷開連接");
        // 關閉該客戶對應流
        close(clientHandler.clientChannel);
    }

    /**
     * 釋放資源
     */
    private void close(Closeable closeable) {
        if(closeable != null){
            try {
                closeable.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 處理服務器的 ACCEPT 事件,第一個泛型參數是異步調用方法應該返回的類型,ACCEPT 應該返回一個客戶端的 channel
     */
    private class AcceptHandler implements CompletionHandler<AsynchronousSocketChannel, Object> {


        @Override
        public void completed(AsynchronousSocketChannel clientChannel, Object attachment) {
            // 讓服務器繼續監聽其他客戶端
            if (serverChannel.isOpen()) {
                serverChannel.accept(null, this);
            }
            // 客戶端處於有效狀態
            if (clientChannel != null && clientChannel.isOpen()) {
                // 處理異步 read
                ClientHandler clientHandler = new ClientHandler(clientChannel);
                // 讀取客戶端數據
                ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
                // 將新用戶添加到在線用戶列表
                addClient(clientHandler);
                // 異步調用 read,向 buffer 寫數據,因此此時 buffer 是寫模式。
                // 第二個參數表示將 buffer 作爲附加信息,第三個參數表示處理異步讀的 handler
                clientChannel.read(buffer, buffer, clientHandler);
            }
        }

        @Override
        public void failed(Throwable exc, Object attachment) {
            System.out.println("連接失敗:" + exc);
        }

    }

    /**
     * 處理客戶端的讀寫事件
     */
    private class ClientHandler implements CompletionHandler<Integer, Object> {


        private AsynchronousSocketChannel clientChannel;

        public ClientHandler(AsynchronousSocketChannel clientChannel) {
            this.clientChannel = clientChannel;
        }

        @Override
        public void completed(Integer result, Object attachment) {
            ByteBuffer buffer = (ByteBuffer) attachment;
            // buffer 附加信息不爲空,說明處理的是異步讀
            if (buffer != null) {
                if (result <= 0) {
                    // 客戶端異常
                    removeClient(this);
                } else {
                    // 將 buffer 從寫模式轉爲讀模式,準備寫出數據
                    buffer.flip();
                    String message = receive(buffer);
                    System.out.println(getClientName(clientChannel) + ":" + message);
                    forwardMessage(clientChannel, message);
                    // 將 buffer 轉回寫模式
                    buffer.clear();
                    // 判斷用戶是否準備下線
                    if (checkQuit(message)){
                        // 將用戶從在線客戶列表中去除
                        removeClient(this);
                    }else {
                        // 如果不是則繼續等待讀取用戶輸入的信息
                        clientChannel.read(buffer, buffer,this);
                    }
                }
            }
        }

        @Override
        public void failed(Throwable exc, Object attachment) {
            System.out.println("讀寫失敗:"+exc);
        }
    }
}

客戶端:

public class ChatClient {

    private static final String LOCALHOST = "localhost";
    private static final int DEFAULT_PORT = 8080;
    private static final String QUIT = "quit";
    private static final int BUFFER_SIZE = 1024;

    private AsynchronousSocketChannel clientChannel;
    private Charset charset = StandardCharsets.UTF_8;
    private String host;
    private int port;

    public ChatClient(){
        this(LOCALHOST , DEFAULT_PORT);
    }

    public ChatClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    /**
     * 程序入口
     * @param args 參數
     */
    public static void main(String[] args) {
        ChatClient client = new ChatClient();
        client.start();
    }

    /**
     * 判斷用戶是否準備下線
     * @param msg 用戶輸入的信息
     * @return 用戶是否準備下線
     */
    public boolean checkQuit(String msg){
        boolean flag = QUIT.equalsIgnoreCase(msg);
        if(flag){
            close(clientChannel);
        }
        return flag;
    }

    /**
     * 客戶端發送數據給服務器
     * @param message 要發送的數據
     */
    public void send(String message) {
        if (message.isEmpty()) {
            return;
        }
        // 將要發送的消息編碼並寫入緩衝區
        ByteBuffer writeBuffer = charset.encode(message);
        // 發送消息
        Future<Integer> writeResult = clientChannel.write(writeBuffer);
        try {
            // 阻塞直到發送結束
            writeResult.get();
        } catch (InterruptedException | ExecutionException e) {
            System.out.println("發送消息失敗");
            e.printStackTrace();
        }
    }

    /**
     * 釋放資源
     * @param closeable 要釋放的資源
     */
    private void close(Closeable closeable){
        if(closeable != null){
            try {
                closeable.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }


    /**
     * 啓動客戶端
     */
    private void start(){
        try {
            // 打開管道
            clientChannel = AsynchronousSocketChannel.open();
            // 異步調用connect連接服務器
            Future<Void> future = clientChannel.connect(new InetSocketAddress(host, port));
            // 阻塞直到客戶端和服務器建立連接
            future.get();
            new Thread(new UserInputHandler(this)).start();
            // 讀取服務器轉發的消息
            ByteBuffer readBuffer = ByteBuffer.allocate(BUFFER_SIZE);
            while (true) {
                Future<Integer> readResult = clientChannel.read(readBuffer);
                // 阻塞直到讀取完畢
                int result = readResult.get();
                // 假如無法讀到數據
                if (result <= 0) {
                    System.out.println("服務器斷開連接");
                    close(clientChannel);
                    System.exit(1);
                } else {
                    // 成功讀取到了數據,打印在控制檯
                    // 先把緩衝區轉爲讀模式
                    readBuffer.flip();
                    String message = String.valueOf(charset.decode(readBuffer));
                    System.out.println(message);
                    // 讀完將緩衝區轉爲寫模式
                    readBuffer.clear();
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            close(clientChannel);
        }
    }
}

處理用戶輸入的類 UserInputHandler 代碼不變。


簡易 Web 服務器的實現

在客戶端向服務器請求資源時,請求的資源可以分爲:

  • 靜態資源:不因請求的次數或順序變化,例如 HTML、CSS、GIF、PNG 等。
  • 動態資源:隨着請求發起方/發起時間/請求內容等因素變化,例如商品庫存量等。客戶端發起請求,服務器將請求交給容器,容器再交給 Servlet 處理,處理完後再返回給容器、服務器及客戶端。

Tomcat 的結構:

  • Server:Tomocat 服務器最頂層的組件,負責運行 Tomcat 服務器、加載服務器資源和環境變量。

  • Service:集合了 Connector 和 Engine 的抽象組件,一個 Service 可以包含多個 Service,多個 Connector 和 一個 Engine。

  • Connector:提供基於不同特定協議的實現,接收解析請求並返回響應。具體包括負責提供給客戶端可以和服務器創建連接的端點、接收連接請求和建立連接後的資源請求、將服務器的響應返回給客戶等。Connector 本身不會處理請求,會將請求交給 Processor 派遣至 Engine 進行處理。

  • 容器是 Tomcat 用來處理請求的組件,內部的組件按照層級排列,其中 Engine 是容器的頂級組件。Engine 通過解析請求內容將請求交發送給對應的 Host,Host 代表一個虛擬主機,一個 Engine 可以支持對多個虛擬主機的請求。Host 將請求交給 Context 組件,Context 代表一個 Web Application,是 Tomcat 最複雜的組件之一,負責應用資源管理、應用類加載、Servlet 管理、安全管理等。Context 下一層是 Wrapper 組件,Wrapper 是容器最底層的組件,包裹 Servlet 實例,負責管理 Servlet 的生命週期。


處理靜態資源請求

HTTPStatus 狀態碼枚舉類

public enum HTTPStatus {

    OK(200, "OK"),
    NOT_FOUND(404, "File Not Found");

    private int statusCode;
    private String reason;

    HTTPStatus(int statusCode, String reason) {
        this.statusCode = statusCode;
        this.reason = reason;
    }

    public int getStatusCode() {
        return statusCode;
    }

    public String getReason() {
        return  reason;
    }
}

ConnectorUtils 工具類

public class ConnectorUtils {

    /** 存放靜態資源的根路徑 根據自己的情況設置 */
    public static final String WEB_ROOT = "C:\\Users\\Administrator\\IdeaProjects\\java_study\\test" + File.separator + "webroot";

    public static final String PROTOCOL = "HTTP/1.1";

    public static final String CARRIAGE = "\r";

    public static final String NEWLINE = "\n";

    public static final String SPACE = " ";

    public static String renderStatus(HTTPStatus status) {
        // 協議 + 狀態 + 狀態碼
        return PROTOCOL + SPACE + status.getStatusCode() + SPACE + status.getReason() + CARRIAGE + NEWLINE + CARRIAGE + NEWLINE;
    }
}

Request 處理請求

public class Request {

    /**
     * 默認緩衝區大小
     */
    private static final int BUFFER_SIZE = 1024;

    /**
     * 客戶端和服務器的 socket 的輸入流,通過該流獲取請求內容
     */
    private InputStream inputStream;

    /**
     * 請求的資源標識符
     */
    private String URI;

    public Request(InputStream inputStream) {
        this.inputStream = inputStream;
    }

    /**
     * 獲取請求 URI
     * @return 返回 URI 字符串
     */
    public String getURI() {
        return URI;
    }

    /**
     * 解析客戶端請求
     */
    public void parse() {
        int len = 0;
        byte[] buffer = new byte[BUFFER_SIZE];
        try {
            len = inputStream.read(buffer);
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 將讀取到的字節數組轉爲字符串
        String requestURL = new String(buffer, 0, len);
        URI = parseURI(requestURL);
    }

    /**
     * 解析靜態請求的 URI
     * @param requestStr 請求文本
     * @return 返回 URI
     * 例如 GET /index.html HTTP1.1,先找到第一個空格的位置,再找到第二個空格的位置,截取兩個位置中間作爲結果返回。
     */
    private String parseURI(String requestStr) {
        int index1, index2;
        index1 = requestStr.indexOf(' ');
        if(index1 != -1) {
            index2 = requestStr.indexOf(' ', index1 + 1);
            if (index2 != -1) {
                return requestStr.substring(index1 + 1, index2);
            }
        }
        // 無法解析出 URI
        return "";
    }

}

Response 處理響應

public class Response {

    /**
     * 默認緩衝區大小
     */
    private static final int BUFFER_SIZE = 1024;

    /**
     * 客戶端請求
     */
    private Request request;

    /**
     * 客戶端和服務器的 socket 的輸出流,通過該流響應客戶端
     */
    private OutputStream outputStream;

    public Response(OutputStream outputStream) {
        this.outputStream =outputStream;
    }

    /**
     * 設置客戶端請求
     * @param request 客戶端請求
     */
    public void setRequest(Request request) {
        this.request = request;
    }

    /**
     * 響應靜態資源的請求
     */
    public void sendStaticResource() throws IOException {
        File file = new File(ConnectorUtils.WEB_ROOT, request.getURI());
        try {
            // 成功處理
            write(file, HTTPStatus.OK);
        } catch (IOException e) {
            // 失敗處理
            write(new File(ConnectorUtils.WEB_ROOT, "404.html"), HTTPStatus.NOT_FOUND);
        }
    }

    private void write(File resource, HTTPStatus status) throws IOException {
        try (FileInputStream inputStream = new FileInputStream(resource)){
            // 響應狀態信息
            outputStream.write(ConnectorUtils.renderStatus(status).getBytes());
            // 響應資源
            byte[] buffer = new byte[BUFFER_SIZE];
            int length;
            while ((length = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, length);
            }
        }
    }

}

StaticProcessor 處理靜態資源

/**
 * 處理靜態資源的 processor
 */
public class StaticProcessor {

    public void process(Request request, Response response) {
        try {
            response.sendStaticResource();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Connector 處理服務器連接、請求、響應

public class Connector implements Runnable{

    private static final int DEFAULT_PORT = 8080;

    private ServerSocket serverSocket;

    private int port;

    public Connector() {
        this(DEFAULT_PORT);
    }

    public Connector(int port) {
        this.port = port;
    }

    public void start() {
        Thread thread = new Thread(this);
        thread.start();
    }

    @Override
    public void run() {
        try {
            // 綁定端口
            serverSocket = new ServerSocket(port);
            System.out.println("服務器已啓動,監聽端口:" + port);
            while (true) {
                // 等待客戶端連接
                Socket socket = serverSocket.accept();
                // 客戶端輸入流
                InputStream inputStream = socket.getInputStream();
                // 客戶端輸出流
                OutputStream outputStream = socket.getOutputStream();
                // 創建 Request 實例並解析 URI
                Request request = new Request(inputStream);
                request.parse();
                // 創建 Response 實例並設置 request
                Response response = new Response(outputStream);
                response.setRequest(request);
                // 處理靜態資源
                StaticProcessor staticProcessor = new StaticProcessor();
                staticProcessor.process(request, response);
                // 處理完後斷開連接
                close(socket);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            close(serverSocket);
        }
    }

    /**
     * 釋放資源
     * @param closeable 要關閉的資源
     */
    private void close(Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

BootStrap 服務器啓動類

public final class BootStrap {

    public static void main(String[] args) {
        // 啓動服務器
        new Connector().start();
    }
}

ClientTest 客戶端測試類

也可以直接在瀏覽器輸入 localhost:8080/index.html 進行測試。

public class ClientTest {

    public static void main(String[] args) throws IOException {
        // 建立客戶端 socket,連接服務器
        Socket socket = new Socket("localhost", 8080);
        // 獲取客戶端輸出流,發送請求
        OutputStream outputStream = socket.getOutputStream();
        outputStream.write("GET /index.html HTTP/1.1".getBytes());
        socket.shutdownOutput();
        // 獲取客戶端輸入流,接收響應
        InputStream inputStream = socket.getInputStream();
        byte[] buffer = new byte[2048];
        int length;
        while ((length = inputStream.read(buffer)) != -1) {
            String receive = new String(buffer, 0 ,length);
            System.out.print(receive);
        }
        socket.shutdownInput();
        // 關閉連接
        socket.close();
    }
}

處理動態資源請求

修改 Request 和 Response

讓 Request 和 Response 分別實現 ServletRequest 和 ServletResponse 接口,並使用默認的重寫方法,只需要修改 Response 中的 getWriter 方法:

@Override
public PrintWriter getWriter() throws IOException {
    return new PrintWriter(outputStream);
}

ServletProcessor 處理動態資源

/**
 * 處理動態資源的 processor
 */
public class ServletProcessor {

    public void process(Request request, Response response) {
        URLClassLoader servletLoader = null;
        try {
            servletLoader = getServletLoader();
            Servlet servlet = getServlet(servletLoader, request);
            servlet.service(request, response);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    URLClassLoader getServletLoader() throws MalformedURLException {
        File webroot = new File(ConnectorUtils.WEB_ROOT);
        URL webrootURL = webroot.toURI().toURL();
        return URLClassLoader.newInstance(new URL[]{webrootURL});
    }

    Servlet getServlet(URLClassLoader loader, Request request) throws Exception {
        // 請求 servlet 時,例如請求 XxxServlet 實際請求 servlet/XxxServlet
        String URI = request.getURI();
        String servletName = URI.substring(URI.lastIndexOf("/") + 1);
        // 通過反射創建 servlet 的實例
        Class servletClass = loader.loadClass(servletName);
        Servlet servlet = (Servlet) servletClass.getConstructor().newInstance();
        return servlet;
    }
}

TimeServlet 動態資源

添加在 webroot下:

/**
 * 動態資源 servlet
 */
public class TimeServlet implements Servlet {
    @Override
    public void init(ServletConfig servletConfig) throws ServletException {

    }

    @Override
    public ServletConfig getServletConfig() {
        return null;
    }

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        // 得到響應的輸出流
        PrintWriter writer = servletResponse.getWriter();
        // 寫出響應頭
        writer.println(ConnectorUtils.renderStatus(HTTPStatus.OK));
        // 寫出當前的時間
        writer.println("當前時間:");
        writer.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
        // 不添加可能空數據
        writer.flush();
    }

    @Override
    public String getServletInfo() {
        return null;
    }

    @Override
    public void destroy() {

    }
}

修改 Connector 的 run 方法

// 根據 URI 的開頭判斷資源類型
if (request.getURI().startsWith("/servlet")) {
    // 處理動態資源
    ServletProcessor servletProcessor = new ServletProcessor();
    servletProcessor.process(request, response);
} else {
    // 處理靜態資源
    StaticProcessor staticProcessor = new StaticProcessor();
    staticProcessor.process(request, response);
}

ClientTest 客戶端測試類

也可以直接在瀏覽器輸入 localhost:8080/servlet/TimeServlet 進行測試。

public class ClientTest {

    public static void main(String[] args) throws IOException {
        ...
        outputStream.write("GET /servlet/TimeServlet HTTP/1.1".getBytes());
		...
    }
}

使用 NIO 改寫 Connector

public class Connector implements Runnable{

    private static final int DEFAULT_PORT = 8080;

    private ServerSocketChannel server;

    private Selector selector;

    private int port;

    public Connector() {
        this(DEFAULT_PORT);
    }

    public Connector(int port) {
        this.port = port;
    }

    public void start() {
        Thread thread = new Thread(this);
        thread.start();
    }

    @Override
    public void run() {
        try {
            // 打開 serverSocket的 Channel
            server = ServerSocketChannel.open();
            // 設置非阻塞調用
            server.configureBlocking(false);
            // 綁定監聽端口
            ServerSocket serverSocket = server.socket();
            serverSocket.bind(new InetSocketAddress(port));
            // 創建 selector
            selector = Selector.open();
            // 註冊服務器 Channel 的 ACCEPT(接收客戶端請求) 事件到 selector
            server.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("啓動服務器,監聽端口:" + port + "...");
            // 不斷監聽請求
            while (true) {
                // select 是阻塞式調用,如果成功調用說明已有 Channel 就緒
                selector.select();
                // select 監聽到的所有事件
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                for (SelectionKey selectionKey : selectionKeys) {
                    // 處理被觸發的事件
                    handles(selectionKey);
                }
                // 處理後清空
                selectionKeys.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            close(selector);
        }
    }

    /** 處理事件 */
    private void handles(SelectionKey selectionKey) throws IOException{
        // 如果觸發了 ACCEPT 事件 —— 和客戶端建立了連接
        if (selectionKey.isAcceptable()) {
            // 獲取客戶端的 channel
            SocketChannel client = ((ServerSocketChannel)(selectionKey.channel())).accept();
            // 設爲非阻塞調用模式
            client.configureBlocking(false);
            // 註冊客戶端的 READ 事件到 selector
            client.register(selector, SelectionKey.OP_READ);
        } else {
            // 觸發的是 READ 事件
            // 獲取客戶端的 channel
            SocketChannel client = (SocketChannel) selectionKey.channel();
            // 避免設爲 BIO 模式報錯,取消註冊
            selectionKey.cancel();
            // 由於要使用 BIO,重設爲阻塞模式
            client.configureBlocking(true);
            Socket socket = client.socket();
            // 客戶端輸入流
            InputStream inputStream = socket.getInputStream();
            // 客戶端輸出流
            OutputStream outputStream = socket.getOutputStream();
            // 創建 Request 實例並解析 URI
            Request request = new Request(inputStream);
            request.parse();
            // 創建 Response 實例並設置 request
            Response response = new Response(outputStream);
            response.setRequest(request);
            // 根據 URI 的開頭判斷資源類型
            if (request.getURI().startsWith("/servlet")) {
                ServletProcessor servletProcessor = new ServletProcessor();
                servletProcessor.process(request, response);
            } else {
                StaticProcessor staticProcessor = new StaticProcessor();
                staticProcessor.process(request, response);
            }
            // 處理完後斷開連接
            close(socket);
        }

    }

    /**
     * 釋放資源
     * @param closeable 要關閉的資源
     */
    private void close(Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

總結

BIO

  • ServerSocket 服務器端通信套接字
    • bind(): 綁定服務器信息。
    • accept(): 獲取客戶端連接請求,阻塞調用。
    • 通過 InputStream / OutputStream 進行讀寫操作,阻塞調用。
    • close():釋放資源。
  • Socket 客戶端通信套接字
    • connect():發送連接請求,和服務器建立連接。
    • 通過 InputStream / OutputStream 進行讀寫操作,阻塞調用。
    • close():釋放資源。
  • 特點
    • 同步阻塞 IO,每個連接分配一個線程。
    • 適用場景:連接數目少、服務器資源多、開發難度低。

NIO

  • Channel 通道
    • 可雙向操作,既可讀也可寫。
    • 兩種模式,既支持阻塞也支持非阻塞。
    • ServerSocketChannel 服務器端通道,通過靜態方法 open() 獲取實例。
    • SocketChannel 客戶端通道,通過靜態方法 open() 獲取實例。
  • Buffer 緩衝區
    • flip() 寫模式轉爲讀模式,可以準備從 buffer 獲取數據。
    • clear() / compact() 讀模式轉爲寫模式,可以準備向 buffer 寫入數據。
  • Selector 多路複用器
    • 輪詢監控多條 Channel。
    • select():查詢每個 Channel 是否有事件到達。
  • 特點
    • 同步非阻塞 IO,只需要一個線程管理多個連接。
    • 適應場景:連接數目多、連接時間短、開發難度高。

AIO

  • AsynchronousServerSocketChannel 異步服務器端通道,通過靜態方法 open() 獲取實例。
  • AsynchronousSocketChannel 異步客戶端通道,通過靜態方法 open() 獲取實例。
  • AsynchronousChannelGroup 異步通道分組管理器,它可以實現資源共享。創建時需要傳入一個ExecutorService,也就是綁定一個線程池,該線程池負責兩個任務:處理 IO 事件和觸發 CompletionHandler 回調接口。
  • 兩種異步調用機制:Future 和 CompletionHandler。
  • 適用場景:連接數目多、連接時間長、開發難度高。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章