Netty學習問題總結

[TOC]

本篇記錄了Netty學習過程中想到的問題和自己的一些思考,對於應用層的協議也有了更好的理解,所以在此做一個記錄。

一、HTTP協議分包

TCP是作爲面來流的協議,所以需要應用層協議自己去分包。常見的分包格式如下:

  1. 定長: 比如100字節每個報文,不足的前面補0,這時候每次取消息就取到100字節算整包)
  2. 分隔符: 換行符其實是一種特別的分隔符,每次讀取到分隔符就知道一個包讀取完畢)
  3. 指明長度: 比如前兩個字節爲長度字段,先讀取兩個字節,知道了需要多少字節,讀取到對應的字節就是一個整包了

然而HTTP協議格式並不是上面的簡單的一種,它結合了2和3兩種來進行分包,因爲請求和響應報文格式一樣,所以這裏針對請求報文進行說明。我們知道HTTP分爲請求行,請求頭,請求體。 下面是報文說明摘自HTTP RFC文檔中4.1 Message Types:

HTTP報文格式

    generic-message = start-line
                      *(message-header CRLF)
                      CRLF
                      [ message-body ]

HTTP報文格式

報文讀取過程

  • 讀取請求行:每個HTTP請求的第一行作爲請求行,所以知道讀取到CRLF就說明結束了
  • 讀取請求頭:請求頭有多行,行數不是固定的,他的結尾是根據連續的兩個CRLF來判斷的,在message-body之前會有一個CRLF
  • 讀取請求體:對於POST等帶有請求體的方法來說,請求體的長度是不固定的,這時候請求頭中會有個Content-Length字段說明了請求體的長度,所以只要讀取完Content-Length個字節,整個HTTP請求報文也就得到了

關於報文分割一點備註:早期的HTTP 1.0時代,因爲它每次請求都會經歷tcp的三次握手連接過程,所以它是通過連接的關閉來判斷報文已經讀取完畢,但是這裏還有一個問題,如果這個連接關閉時因爲服務端的錯誤引起的那客戶端就無法區分了。到了HTTP 1.1,因爲很多請求會重用一個連接,所以需要用到Content-Length這個字段來做分包。另外還有一種不需要Content-Length的方法就是請求頭中Transfer-Encoding爲chunked,這是一種分塊傳輸,在壓縮傳輸,動態內容生成等響應在一開始長度未知的場景下很有用。他的報文分割也很簡單,詳情參見:分塊傳輸編碼

二、WebSocket協議分包

理解了HTTP協議的分包,WebSocket的協議也容易理解,道理都是想通的。一開始在谷歌的時候一直搜索不到相關的報文,最後搜索WebSocket數據幀才搜到了結果(搜索是門技巧啊)。我最關注的是opcode字段,因爲在用WebSocket的時候就用到了這個字段來判斷是什麼幀類型。第二是Payload len字段,這是一個變長字段,爲了節省字節數,含義如下:

  1. 如果數據長度小於等於125的話,那麼該7位用來表示實際數據長度。
  2. 如果數據長度爲126到65535(2的16次方)之間,該7位值固定爲126,也就是 1111110,往後擴展2個字節(16爲,第三個區塊表示),用於存儲數據的實際長度。
  3. 如果數據長度大於65535, 該7位的值固定爲127,也就是 1111111 ,往後擴展8個字節(64位),用於存儲數據實際長度。

摘錄自:https://www.cnblogs.com/tugenhua0707/p/8542890.html,具體關於WebSocket可以查閱資料,有非常多的講解。

WebSocket RFC中websocket報文格式

      0                   1                   2                   3
      0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
     +-+-+-+-+-------+-+-------------+-------------------------------+
     |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
     |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
     |N|V|V|V|       |S|             |   (if payload len==126/127)   |
     | |1|2|3|       |K|             |                               |
     +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
     |     Extended payload length continued, if payload len == 127  |
     + - - - - - - - - - - - - - - - +-------------------------------+
     |                               |Masking-key, if MASK set to 1  |
     +-------------------------------+-------------------------------+
     | Masking-key (continued)       |          Payload Data         |
     +-------------------------------- - - - - - - - - - - - - - - - +
     :                     Payload Data continued ...                :
     + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
     |                     Payload Data continued ...                |
     +---------------------------------------------------------------+

三、HTTP和WebSocket協議共用一個端口的問題

之前對這個現象十分不理解,很多Web服務器例如Tomcat都支持HTTP和WebSocket共用一個端口,他是怎麼做到的?

其實理解了報文的解析就很容易理解了,HTTP和WebSocket協議的下層都是TCP連接,他們是應用層連接,所以在處理TCP的字節流時,可以先獲取幾個字節,如果前幾個字節解析出來是GET,POST等HTTP協議的用到的,那麼就根據HTTP報文的分包規則獲取一個HTTP報文然後流轉到後端處理。如果是WebSocket協議就根據WebSocket報文來解析然後做響應的處理。

補充:很多的協議棧都是以魔數打頭,這樣就更容易實現同端口多協議的支持,如Dubbo協議棧前兩個字節是魔數,只需要判斷報文的前兩個字節就知道是不是Dubbo協議了,Dubbo協議棧頭報文如下圖(摘抄自http://ifeve.com/dubbo-protocol/):

Dubbo協議棧

四、TIME WAIT狀態佔用了什麼資源

我們知道TCP四次揮手時主動發起方會經歷一個TIME WAIT狀態,也正是因爲這個原因我們儘量讓客戶端主動關閉連接。對於這個狀態有人說是佔用了文件描述符,有人說是端口,那麼究竟是佔用了什麼資源?

根據我自己的實驗,Windows系統下,TIME WAIT狀態佔用了端口,該端口不能作爲客戶端端口使用,但仍然可以作爲服務端端口使用。實驗端口:服務端8080,客戶端6060 。情況如下:

  1. 客戶端主動關閉,客戶端重啓時報BindException,服務端用6060端口仍可正常啓動
  2. 服務端主動關閉,服務端重啓正常,客戶端重啓也正常,但是如果停掉服務端8080,客戶端用6060報BindException

實驗代碼如下,可根據需要自己修改試驗:

服務端主動關閉

public class ServerSocketCloseTest {
    public static void main(String[] args) throws IOException, InterruptedException {
        Runnable runnable = () -> {
            try {
                ServerSocket serverSocket = null;
                serverSocket = new ServerSocket(8080, 10);
                while (true) {
                    Socket accept = serverSocket.accept();
                    accept.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        };
        new Thread(runnable).start();
        Socket socket = new Socket(InetAddress.getLocalHost(), 8080, InetAddress.getLocalHost(), 6060);
        Thread.sleep(1000);
        socket.close();
        System.in.read();
    }
}

客戶端主動關閉場景

public class ClientSocketCloseTest {

    public static void main(String[] args) throws IOException, InterruptedException {
        Runnable runnable = () -> {
            try {
                ServerSocket serverSocket = new ServerSocket(8080, 10);
                while (true) {
                    Socket accept = serverSocket.accept();
                    Thread.sleep(1000);
                    accept.close();
                }
            } catch (IOException | InterruptedException e) {
                e.printStackTrace();
            }
        };
        new Thread(runnable).start();

        Socket socket = new Socket(InetAddress.getLocalHost(), 8080, InetAddress.getLocalHost(), 6060);
        socket.close();
        Thread.sleep(1000);
        System.in.read();
    }

}

結論:處於TIME WAIT狀態下的端口不能作爲客戶端端口使用。對於服務端端口沒有影響,服務端是仍然是可以正常綁定,接收到客戶端連接後本地端口和監聽端口是同一個所以不存在端口占用。另外通過查閱資料,TIME WAIT是釋放了文件描述符,但是TCP連接的五元組並未釋放,還佔用一定的內存。參考地址如下:https://stackoverflow.com/questions/1803566/what-is-the-cost-of-many-time-wait-on-the-server-side/1806033#1806033

五、關於

待補充

原文出處:https://www.cnblogs.com/chenfangzhi/p/10353920.html

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