Netty學習問題總結
本篇記錄了Netty學習過程中想到的問題和自己的一些思考,對於應用層的協議也有了更好的理解,所以在此做一個記錄。
一、HTTP協議分包
TCP是作為面來流的協議,所以需要應用層協議自己去分包。常見的分包格式如下:
- 定長: 比如100位元組每個報文,不足的前面補0,這時候每次取訊息就取到100位元組算整包)
- 分隔符: 換行符其實是一種特別的分隔符,每次讀取到分隔符就知道一個包讀取完畢)
- 指明長度: 比如前兩個位元組為長度欄位,先讀取兩個位元組,知道了需要多少位元組,讀取到對應的位元組就是一個整包了
然而HTTP協議格式並不是上面的簡單的一種,它結合了2和3兩種來進行分包,因為請求和響應報文格式一樣,所以這裡針對請求報文進行說明。我們知道HTTP分為請求行,請求頭,請求體。
下面是報文說明摘自 HTTP RFC文件 中4.1 Message Types:
HTTP報文格式:
generic-message = start-line *(message-header CRLF) CRLF [ message-body ]
報文讀取過程
- 讀取請求行:每個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欄位,這是一個變長欄位,為了節省位元組數,含義如下:
- 如果資料長度小於等於125的話,那麼該7位用來表示實際資料長度。
- 如果資料長度為126到65535(2的16次方)之間,該7位值固定為126,也就是 1111110,往後擴充套件2個位元組(16為,第三個區塊表示),用於儲存資料的實際長度。
- 如果資料長度大於65535, 該7位的值固定為127,也就是 1111111 ,往後擴充套件8個位元組(64位),用於儲存資料實際長度。
摘錄自: https://www.cnblogs.com/tugenhua0707/p/8542890.html,具體關於WebSocket可以查閱資料,有非常多的講解 。
WebSocket RFC 中websocket報文格式
0123 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/):
四、TIME WAIT狀態佔用了什麼資源
我們知道TCP四次揮手時主動發起方會經歷一個TIME WAIT狀態,也正是因為這個原因我們儘量讓客戶端主動關閉連線。對於這個狀態有人說是佔用了檔案描述符,有人說是埠,那麼究竟是佔用了什麼資源?
根據我自己的實驗,Windows系統下,TIME WAIT狀態佔用了埠,該埠不能作為客戶端埠使用,但仍然可以作為服務端埠使用。實驗埠:服務端8080,客戶端6060 。情況如下:
- 客戶端主動關閉,客戶端重啟時報BindException,服務端用6060埠仍可正常啟動
- 服務端主動關閉,服務端重啟正常,客戶端重啟也正常,但是如果停掉服務端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
五、關於
待補充