抓TCP報文診斷 HTTP Content-Length 問題

歡迎訪問陳同學博客原文

抓TCP報文診斷 HTTP Content-Length 問題

本文分享一個 HTTP Content-Length 有誤時場的景,以 tcpdump 抓包來做真實演示,同時結合TCP狀態進行分析。

關於 Content-Length 的場景,比如提供文件下載的服務,需要設置好 Content-Length 以及斷點下載的一些參數。

小例子

下面是 Spring Boot 應用中一段代碼,設置 Content-Length 爲100字節,實際卻不返回任何數據。

@GetMapping("demo")
public void demo(HttpServletResponse response) {
    response.setContentLength(100);
}

用 curl 測試:

curl -X GET http://localhost:8080/demo

控制檯卡住1分鐘,然後輸出:

curl: (18) transfer closed with 100 bytes remaining to read

如果用HTTP客戶端(eg: HttpClient)調用,線程會一直處於 RUNABLE 狀態,下面是 jstack 拿到的線程狀態,socketRead0 是一個 native 方法,會使用socket的原生方法讀取數據。

java.lang.Thread.State: RUNNABLE
    at java.net.SocketInputStream.socketRead0(Native Method)
    at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
    at java.net.SocketInputStream.read(SocketInputStream.java:170)
    at java.net.SocketInputStream.read(SocketInputStream.java:141)
    at org.apache.http.impl.conn.LoggingInputStream.read(LoggingInputStream.java:84)

線程會一直阻塞在這裏,如果應用中有大量這樣的線程,可能會耗盡應用線程,導致應用無響應。

下面看看TCP報文傳輸情況。

TCP 連接狀態

爲便於理解,先簡述TCP的三次握手、四次揮手,熟悉的可直接跳過。

下面兩圖, INITIATOR 可看作client,RECEIVER 看做server。

三次握手

[外鏈圖片轉存失敗(img-b4azcsDM-1564881937137)(https://imgcdn.chenyongjun.vip/2019/08/03/1.png)]

  • client:你好,我是client。報文含SYN標誌位,SYN即同步、請求連接的意思,狀態變爲 SYN_SENT
  • server:收到,我是server。響應含 SYN+ACK 兩個標記的報文,ACK是對 client SYN 的確認,SYN表示請求連接,狀態變爲 SYN_RECEIVED
  • client:收到。對server的SYN做ACK,然後CS兩端狀態變爲ESTABLISHED

此時,連接便已創建,可以進行通信。

四次揮手:

[外鏈圖片轉存失敗(img-u2ncgwpW-1564881937138)(https://imgcdn.chenyongjun.vip/2019/08/03/2.png)]

  • client:再見,我不說話了(不能再發數據)。發送FIN標記的報文(FIN表示finish即結束),狀態變爲 FIN_WAIT_1 即等着 server 說再見
  • server:收到。向client發送ACK,server變爲 CLOSE_WAIT 即等待關閉連接(不急,等自己活幹完再關);client 收到ACK後,狀態變爲 FIN_WAIT_2,等着server結束。
  • server:再見,我活幹完了。發送FIN標記報文,狀態變爲LAST_ACK,此時server也不能再發送數據。
  • client:收到。狀態變爲TIME_WAIT,即過一段時間就自動關閉,然後對server的FIN做ACK。server收到後就CLOSED,client 過一會也自行CLOSED。

TCP 報文監控

上面介紹了TCP連接狀態,現在用 tcpdump 監控網卡8080端口(應用在8080端口)的數據。

sudo tcpdump  -n -i any port 8080

應用跑在本機,下面是tcpdump的動態輸出(爲了便於展示,僅摘取了關鍵字段)

65241 是client分配的臨時端口,8080是應用端口。Flags 表示標記位,S、P、F分別表示SYN、PSH、FIN,代表請求連接、推送數據、結束連接標記位。

建立TCP連接的三次握手報文, 對應 SYN、SYN+ACK、ACK 三個步驟

::1.65241 > ::1.8080: Flags [S], seq 2672664932, length 0
::1.8080 > ::1.65241: Flags [S.], seq 1257336122, ack 2672664933, length 0
::1.65241 > ::1.8080: Flags [.], ack 1, length 0

client發送請求數據的報文

第二行 client 以 HTTP/1.0 GET 請求 /demo,server 做了ACK表示收到

::1.8080 > ::1.65241: Flags [.], ack 1, win 6371, length 0
::1.65241 > ::1.8080: Flags [P.], seq 1:83, ack 1, length 82: HTTP: GET /demo HTTP/1.1
::1.8080 > ::1.65241: Flags [.], ack 83, win 6370, length 0

client 的報文如下:

GET /demo HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.54.0
Accept: */*

server推送數據的報文

server推送帶P標記位的報文,報文長度116,client馬上做了ACK

::1.8080 > ::1.65241: Flags [P.], seq 1:117, ack 83, length 116: HTTP: HTTP/1.1 200 
::1.65241 > ::1.8080: Flags [.], ack 117, win 6370, length 0

server的報文如下,其中 Content-Length 爲100。

HTTP/1.1 200 
X-Application-Context: application:8080
Content-Length: 100
Date: Sat, 03 Aug 2019 12:52:40 GMT

漫長等待階段

由於server告知client HTTP請求體中有100字節要推,實際上server又沒有推任何數據。此時,

  • 腦補 server:client 咋沒任何反應,數據都給你了,讀完後你倒是斷開連接呀。
  • 腦補 client:搞啥呢,有100個字節咋還不推過來,我再等等把。

兩方就乾耗着,一起站着茅坑(佔用了TCP連接、端口等資源),文章最上面Java線程的RUNNABLE狀態就對應在這裏。

結束

經過1分鐘,server 主動發起了FIN報文,終止了連接,下面對應着四次揮手:

::1.8080 > ::1.65241: Flags [F.], seq 117, ack 83, length 0
::1.65241 > ::1.8080: Flags [.], ack 118, length 0

::1.65241 > ::1.8080: Flags [F.], seq 83, ack 118, length 0
::1.8080 > ::1.65241: Flags [.], ack 84, length 0

當然,如果client設置 socketTimeout,假設爲2秒,那2秒之後,就變成client主動發起FIN報文來中斷連接了。

小結

實際工作中,有些問題需要去排查TCP連接的狀態甚至TCP報文的情況,本文以一個簡單的例子做了分享。

關於RCP的狀態流轉,可參考 RFC793


歡迎關注公衆號 [陳一樂],一起學習,一起成長

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