3、Java網絡編程之深入理解BIO原理和實現

章節概覽

Netty源碼分析章節概覽


1、概述

關於網絡方面的知識,這裏不再贅述。可以看七層網絡模型,TCP/IP協議,三次握手,四次揮手等網絡編程方面的知識。本章節主要結合Java BIO 講解BIO編程的原理和過程。

1.1、七層網絡協議

在這裏插入圖片描述

1.2、 五層網絡協議

在這裏插入圖片描述


2、socket發送和接受數據過程

在這裏插入圖片描述

  • 發送過程:
  1. 應用程序調用系統調用,將數據發送給socket
  2. socket檢查數據類型,調用相應的send函數
  3. send函數檢查socket狀態、協議類型,傳給傳輸層
  4. tcp/udp(傳輸層協議)爲這些數據創建數據結構,加入協議頭部,比如端口號、檢驗和。傳給下層(網絡層)ip(網絡層協議)添加ip頭,比如ip地址、檢驗和如果數據包大小超過了mtu(最大數據包大小),則分片;ip將這些數據包傳給鏈路層鏈路層寫到網卡隊列
  5. 網卡調用響應中斷驅動程序,發送到網絡
  • 接收過程:
  1. 數據包從網絡到達網卡,網卡接收幀,放入網卡buffer,在向系統發送中斷請求
  2. cpu調用相應中斷函數,這些中斷處理程序在網卡驅動中
  3. 中斷處理函數從網卡讀入內存,交給鏈路層
  4. 鏈路層將包放入自己的隊列,置軟中斷標誌位
  5. 進程調度器看到了標誌位,調度相應進程
  6. 該進程將包從隊列取出,與相應協議匹配,一般爲ip協議,再將包傳遞給該協議接收函數
    ip層對包進行錯誤檢測,無錯,路由。路由結果,packet被轉發或者繼續向上層傳遞
    如果發往本機,進入鏈路層
  7. 鏈路層再進行錯誤偵測,查找相應端口關聯socket,包被放入相應socket接收隊列
  8. socket喚醒擁有該socket的進程,進程從系統調用read中返回,將數據拷貝到自己的buffer,返回用戶態。

Socket是在傳輸層tcp/udp 之上抽象出來的,爲了滿足於傳出層和應用層之間的通信。每個請求對應一個socket連接,socket連接都會放入到一個接受隊列裏面,等待服務端進行消費。默認的socket連接的隊列的長度:backlog是50。


3、Java BIO 原理和實現

在這裏插入圖片描述
早期的jdk中,採用BIO通信模式。通常有一個acceptor(消費者) 去負責監聽客戶端的連接,它接收到客戶端的連接請求之後爲每個客戶端創建一個線程進行鏈路處理,處理完成之後,線程銷燬。從其通信結構圖中,我們可以清晰的看到,一個客戶端連接,對應贏處理線程。他們之間的對應關係是 1:1。

由於客戶端連接和服務端的處理之間的對應關係是1:1,如果遇到任務比較大,處理比較慢。或者併發量比較大的情況下,系統會創建大量的線程。從而導致服務器線程暴增,性能急劇下降,甚至宕機。

3.1 客戶端代碼
public class TimeClient {
    public static void main(String[] args) {
        int port = 8080;
        Socket socket = null;
        BufferedReader in = null;
        PrintWriter out = null;
        try {
            socket = new Socket("127.0.0.1", port);
            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            out = new PrintWriter(socket.getOutputStream(), true);
            out.println("Query Time Order");
            System.out.println("Send order 2 server succeed.");
            String resp = in.readLine();
            System.out.println("Now is : " + resp);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

            try {
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

            out.close();
            out = null;
        }
    }
}
3.2、服務端代碼
public class TimeServer {
    public static void main(String[] args) {
        int port = 8080;
        ServerSocket serverSocket = null;

        try {
            serverSocket = new ServerSocket(port);
            Socket socket = null;
            while (true){
                // 從socket的隊列中獲取socket的連接
                // 相當於一個消費者
                socket = serverSocket.accept();
                // 獲得到socket連接之後,分配線程任務進行處理
                new Thread(new TimeServerHandler(socket)).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(serverSocket != null){
                System.out.println("The time server close");
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                serverSocket = null;
            }
        }
    }
}
public class TimeServerHandler implements Runnable {

    private Socket socket;

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

    @Override
    public void run() {
        BufferedReader in = null;
        PrintWriter out = null;

        try {
            in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
            out = new PrintWriter(this.socket.getOutputStream(), true);
            String currentTime = null;
            String body = null;
            while (true) {
                // 讀取數據的部分是阻塞的
                body = in.readLine();
                if (body == null) {
                    break;
                }
                System.out.println("The time server receive order: " + body);

                currentTime = "Query Time Order".equalsIgnoreCase(body) ?
                        new Date(System.currentTimeMillis()).toString() : "Bad Order";
                // 輸出數據
                out.println(currentTime);

            }
        } catch (IOException e) {
            e.printStackTrace();
            if(in != null){
                try {
                    in.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }

            if(out != null){
                out.close();
                out = null;
            }
        }
    }
}

我們實現了了一個簡單的BIO程序,獲取服務端的時間案例。從這個簡單demo裏面,我們分析下,bio
主要阻塞在哪裏?!

  1. 當前的線程阻塞在accept 方法上面。該方法一直阻塞,除非獲取到socket連接返回。
  2. 讀取io流產生阻塞,從上一章節中:大白話分析BIO,NIO,AIO中,我們分析了阻塞IO流。那麼在這個案例裏面,主要的阻塞IO流阻塞的點有以下的幾個方面:
    2.1、 內核程序從網卡中把數據讀取到內核空間中,是一直阻塞的。
    2.2、用戶程序把內核空間的數據複製到用戶空間的過程是阻塞的。
    這兩個過程中,對應的程序部分就是read方法的阻塞。我們看下jdk源碼中關於read方法的描述信息:
/**
     * Reads some number of bytes from the input stream and stores them into
     * the buffer array <code>b</code>. The number of bytes actually read is
     * returned as an integer.  This method blocks until input data is
     * available, end of file is detected, or an exception is thrown.
     *
     * <p> If the length of <code>b</code> is zero, then no bytes are read and
     * <code>0</code> is returned; otherwise, there is an attempt to read at
     * least one byte. If no byte is available because the stream is at the
     * end of the file, the value <code>-1</code> is returned; otherwise, at
     * least one byte is read and stored into <code>b</code>.
     *
     * <p> The first byte read is stored into element <code>b[0]</code>, the
     * next one into <code>b[1]</code>, and so on. The number of bytes read is,
     * at most, equal to the length of <code>b</code>. Let <i>k</i> be the
     * number of bytes actually read; these bytes will be stored in elements
     * <code>b[0]</code> through <code>b[</code><i>k</i><code>-1]</code>,
     * leaving elements <code>b[</code><i>k</i><code>]</code> through
     * <code>b[b.length-1]</code> unaffected.
     *
     * <p> The <code>read(b)</code> method for class <code>InputStream</code>
     * has the same effect as: <pre><code> read(b, 0, b.length) </code></pre>
     *
     * @param      b   the buffer into which the data is read.
     * @return     the total number of bytes read into the buffer, or
     *             <code>-1</code> if there is no more data because the end of
     *             the stream has been reached.
     * @exception  IOException  If the first byte cannot be read for any reason
     * other than the end of the file, if the input stream has been closed, or
     * if some other I/O error occurs.
     * @exception  NullPointerException  if <code>b</code> is <code>null</code>.
     * @see        java.io.InputStream#read(byte[], int, int)
     */
    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }

當對Socket的輸入流進行讀取操作的時候,它會一直阻塞下去,直到發生以下三種事情:

  1. 有數據可讀,直接返回,讀取至少一個字節。
  2. 可用的數據已經讀取完成,這裏的完成可用是返回-1,socket連接關閉。
  3. 發生空指針或者 I/O異常。

假設存在這麼一個場景,由於網絡延遲,導致數據發送緩慢。而由於使用的是阻塞IO,那麼read方法一直處於阻塞狀態,要等到數據傳送完成才結束(返回-1)。那麼這種場景下,在高併發。直接導致線程暴增、服務器宕機。


4、 小結

從上面的原理分析,最後的案例。理解了BIO阻塞的本質,如果有不懂的,歡迎博客留言。本人才疏學淺,如有錯誤的地方,歡迎指正。立刻修改。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章