JAVA阻塞IO(BIO)簡介

MuXinu發表於2024-03-23

一、BIO程式設計

傳統的BIO程式設計

網路程式設計的基本模型是C/S模型,即兩個程序間的通訊。
服務端提供IP和監聽埠,客戶端透過連線操作想服務端監聽的地址發起連線請求,透過三次握手連線,如果連線成功建立,雙方就可以透過套接字進行通訊。
傳統的同步阻塞模型開發中,ServerSocket負責繫結IP地址,啟動監聽埠;Socket負責發起連線操作。連線成功後,雙方透過輸入和輸出流進行同步阻塞式通訊。
簡單的描述一下BIO的服務端通訊模型:
採用BIO通訊模型的服務端,通常由一個獨立的Acceptor執行緒負責監聽客戶端的連線,它接收到客戶端連線請求之後為每個客戶端建立一個新的執行緒進行鏈路處理沒處理完成後,透過輸出流返回應答給客戶端,執行緒銷燬。即典型的一請求一應答模型。

傳統BIO通訊模型圖

二、程式碼示例

模擬20個客戶端併發請求,伺服器端使用單執行緒:

伺服器端(SocketServer1)單個執行緒
package BIO;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class SocketServer1 {


    public static void main(String[] args) throws Exception {
        ServerSocket serverSocket = new ServerSocket(83);

        try {
            while (true) {
                Socket socket = serverSocket.accept();
                //下面我們收取資訊
                InputStream in = socket.getInputStream();
                OutputStream out = socket.getOutputStream();
                Integer sourcePort = socket.getPort();
                int maxLen = 2048;
                byte[] contextBytes = new byte[maxLen];
                //這裡也會被阻塞,直到有資料準備好
                int realLen = in.read(contextBytes, 0, maxLen);
                //讀取資訊
                String message = new String(contextBytes, 0, realLen);

                //下面列印資訊
                System.out.println("伺服器收到來自於埠: " + sourcePort + "的資訊: " + message);

                //下面開始傳送資訊
                out.write("回發響應資訊!".getBytes());

                //關閉
                out.close();
                in.close();
                socket.close();
            }
        } catch (Exception e) {
            System.out.println(e.getMessage() + e);
        } finally {
            serverSocket.close();
        }
    }
}

客戶端程式碼(SocketClientRequestThread模擬請求)

package BIO;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.concurrent.CountDownLatch;

public class SocketClientRequestThread implements Runnable {


    private CountDownLatch countDownLatch;

    /**
     * 這個線層的編號
     *
     * @param countDownLatch
     */
    private Integer clientIndex;

    /**
     * countDownLatch是java提供的同步計數器。
     * 當計數器數值減為0時,所有受其影響而等待的執行緒將會被啟用。這樣保證模擬併發請求的真實性
     *
     * @param countDownLatch
     */
    public SocketClientRequestThread(CountDownLatch countDownLatch, Integer clientIndex) {
        this.countDownLatch = countDownLatch;
        this.clientIndex = clientIndex;
    }

    @Override
    public void run() {
        Socket socket = null;
        OutputStream clientRequest = null;
        InputStream clientResponse = null;

        try {
            socket = new Socket("localhost", 83);
            clientRequest = socket.getOutputStream();
            clientResponse = socket.getInputStream();

            //等待,直到SocketClientDaemon完成所有執行緒的啟動,然後所有執行緒一起傳送請求
            this.countDownLatch.await();

            //傳送請求資訊
            clientRequest.write(("這是第" + this.clientIndex + " 個客戶端的請求。").getBytes());
            clientRequest.flush();

            //在這裡等待,直到伺服器返回資訊
            System.out.println("第" + this.clientIndex + "個客戶端的請求傳送完成,等待伺服器返回資訊");
            int maxLen = 1024;
            byte[] contextBytes = new byte[maxLen];
            int realLen;
            String message = "";
            //程式執行到這裡,會一直等待伺服器返回資訊(注意,前提是in和out都不能close,如果close了就收不到伺服器的反饋了)
            while ((realLen = clientResponse.read(contextBytes, 0, maxLen)) != -1) {
                message += new String(contextBytes, 0, realLen);
            }
            System.out.println("接收到來自伺服器的資訊:" + message);
        } catch (Exception e) {
            System.out.println(e.getMessage() + e);
        } finally {
            try {
                if (clientRequest != null) {
                    clientRequest.close();
                }
                if (clientResponse != null) {
                    clientResponse.close();
                }
            } catch (IOException e) {
                System.out.println(e.getMessage() + e);
            }
        }
    }
}

客戶端程式碼(SocketClientDaemon)

package BIO;


import java.util.concurrent.CountDownLatch;

public class SocketClientDaemon {
    public static void main(String[] args) throws Exception {
        Integer clientNumber = 20;
        CountDownLatch countDownLatch = new CountDownLatch(clientNumber);

        //分別開始啟動這20個客戶端
        for(int index = 0 ; index < clientNumber ; index++ , countDownLatch.countDown()) {
            SocketClientRequestThread client = new SocketClientRequestThread(countDownLatch, index);
            new Thread(client).start();
        }

        //這個wait不涉及到具體的實驗邏輯,只是為了保證守護執行緒在啟動所有執行緒後,進入等待狀態
        synchronized (SocketClientDaemon.class) {
            SocketClientDaemon.class.wait();
        }
    }
}

多執行緒來最佳化伺服器端

客戶端程式碼和上文一樣,最主要是更改伺服器端的程式碼:
伺服器端多執行緒(SocketServer2)
package BIO;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class SocketServer2 {


    public static void main(String[] args) throws Exception {
        ServerSocket serverSocket = new ServerSocket(83);

        try {
            while (true) {
                Socket socket = serverSocket.accept();
                //當然業務處理過程可以交給一個執行緒(這裡可以使用執行緒池),並且執行緒的建立是很耗資源的。
                //最終改變不了.accept()只能一個一個接受socket的情況,並且被阻塞的情況
                SocketServerThread socketServerThread = new SocketServerThread(socket);
                new Thread(socketServerThread).start();
            }
        } catch (Exception e) {
            System.out.println(e.getMessage() + e);
        } finally {
            if (serverSocket != null) {
                serverSocket.close();
            }
        }
    }
}

/**
 * 當然,接收到客戶端的socket後,業務的處理過程可以交給一個執行緒來做。
 * 但還是改變不了socket被一個一個的做accept()的情況。
 *
 * @author yinwenjie
 */
class SocketServerThread implements Runnable {

    private Socket socket;

    public SocketServerThread(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        InputStream in = null;
        OutputStream out = null;
        try {
            //下面我們收取資訊
            in = socket.getInputStream();
            out = socket.getOutputStream();
            Integer sourcePort = socket.getPort();
            int maxLen = 1024;
            byte[] contextBytes = new byte[maxLen];
            //使用執行緒,同樣無法解決read方法的阻塞問題,
            //也就是說read方法處同樣會被阻塞,直到作業系統有資料準備好
            int realLen = in.read(contextBytes, 0, maxLen);
            //讀取資訊
            String message = new String(contextBytes, 0, realLen);

            //下面列印資訊
            System.out.println("伺服器收到來自於埠: " + sourcePort + "的資訊: " + message);

            //下面開始傳送資訊
            out.write("回發響應資訊!".getBytes());
        } catch (Exception e) {
            System.out.println(e.getMessage() + e);
        } finally {
            //試圖關閉
            try {
                if (in != null) {
                    in.close();
                }
                if (out != null) {
                    out.close();
                }
                if (this.socket != null) {
                    this.socket.close();
                }
            } catch (IOException e) {
                System.out.println(e.getMessage() + e);
            }
        }
    }
}

三、結論

  模型最大的問題就是缺乏彈性伸縮能力,當客戶端併發訪問量增加後,服務端的執行緒個數和客戶端併發訪問數呈1:1的正比關係,Java中的執行緒也是比較寶貴的系統資源,執行緒數量快速膨脹後,系統的效能將急劇下降,隨著訪問量的繼續增大,系統最終當機或者假死。

參考: https://blog.csdn.net/yinwenjie/article/details/48274255

相關文章