tft每日頭條

 > 圖文

 > tcp協議難點

tcp協議難點

圖文 更新时间:2024-11-24 09:48:34

TCP是一個有狀态通訊協議,所謂的有狀态是指通信過程中通信的雙方各自維護連接的狀态。

一、TCP keepalive

先簡單回顧一下TCP連接建立和斷開的整個過程。(這裡主要考慮主流程,關于丢包、擁塞、窗口、失敗重試等情況後面詳細讨論。)

首先是客戶端發送syn(Synchronize Sequence Numbers:同步序列編号)包給服務端,告訴服務端我要連接你,syn包裡面主要攜帶了客戶端的seq序列号;服務端回發一個syn ack,其中syn包和客戶端原理類似,隻不過攜帶的是服務端的seq序列号,ack包則是确認客戶端允許連接;最後客戶端再次發送一個ack确認接收到服務端的syn包。這樣客戶端和服務端就可以建立連接了。整個流程稱為“三次握手”。

tcp協議難點(TCP漫談之keepalive和timewait)1

建立連接後,客戶端或者服務端便可以通過已建立的socket連接發送數據,對端接收數據後,便可以通過ack确認已經收到數據。

數據交換完畢後,通常是客戶端便可以發送FIN包,告訴另一端我要斷開了;另一端先通過ack确認收到FIN包,然後發送FIN包告訴客戶端我也關閉了;最後客戶端回應ack确認連接終止。整個流程成為“四次揮手”。

TCP的性能經常為大家所诟病,除了TCP IP額外的header以外,它建立連接需要三次握手,關閉連接需要四次揮手。如果隻是發送很少的數據,那麼傳輸的有效數據是非常少的。

是不是建立一次連接後續可以繼續複用呢?的确可以這樣做,但這又帶來另一個問題,如果連接一直不釋放,端口被占滿了咋辦。為此引入了今天讨論的第一個話題TCP keepalive。所謂的TCP keepalive是指TCP連接建立後會通過keepalive的方式一直保持,不會在數據傳輸完成後立刻中斷,而是通過keepalive機制檢測連接狀态。

Linux控制keepalive有三個參數:保活時間net.ipv4.tcp_keepalive_time、保活時間間隔net.ipv4.tcp_keepalive_intvl、保活探測次數net.ipv4.tcp_keepalive_probes,默認值分别是 7200 秒(2 小時)、75 秒和 9 次探測。如果使用 TCP 自身的 keepalive 機制,在 Linux 系統中,最少需要經過 2 小時 9*75 秒後斷開。譬如我們SSH登錄一台服務器後可以看到這個TCP的keepalive時間是2個小時,并且會在2個小時後發送探測包,确認對端是否處于連接狀态。

tcp協議難點(TCP漫談之keepalive和timewait)2

之所以會讨論TCP的keepalive,是因為發現服器上有洩露的TCP連接:

# ll /proc/11516/fd/10 lrwx------ 1 root root 64 Jan 3 19:04 /proc/11516/fd/10 -> socket:[1241854730] # date Sun Jan 5 17:39:51 CST 2020

已經建立連接兩天,但是對方已經斷開了(非正常斷開)。由于使用了比較老的go(1.9之前版本有問題)導緻連接沒有釋放。

解決這類問題,可以借助TCP的keepalive機制。新版go語言支持在建立連接的時候設置keepalive時間。首先查看網絡包中建立TCP連接的DialContext方法中。

if tc, ok := c.(*TCPConn); ok && d.KeepAlive >= 0 { setKeepAlive(tc.fd, true) ka := d.KeepAlive if d.KeepAlive == 0 { ka = defaultTCPKeepAlive } setKeepAlivePeriod(tc.fd, ka) testHookSetKeepAlive(ka) }

其中defaultTCPKeepAlive是15s。如果是HTTP連接,使用默認client,那麼它會将keepalive時間設置成30s。

var DefaultTransport RoundTripper = &Transport{ Proxy: ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, DualStack: true, }).DialContext, ForceAttemptHTTP2: true, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, }

下面通過一個簡單的demo測試一下,代碼如下:

func main() { wg := &sync.WaitGroup{} c := http.DefaultClient for i := 0; i < 2; i { wg.Add(1) go func() { defer wg.Done() for { r, err := c.Get("http://**.***.***.**:****") if err != nil { fmt.Println(err) return } _, err = ioutil.ReadAll(r.Body) r.Body.Close() if err != nil { fmt.Println(err) return } time.Sleep(30 * time.Millisecond) } }() } wg.Wait() }

執行程序後,可以查看連接。初始設置keepalive為30s。

tcp協議難點(TCP漫談之keepalive和timewait)3

然後不斷遞減,至0後,又會重新獲取30s。

tcp協議難點(TCP漫談之keepalive和timewait)4

整個過程可以通過tcpdump抓包獲取。

# tcpdump -i bond0 port 35832 -nvv -A

其實很多應用并非是通過TCP的keepalive機制探活的,因為默認的兩個多小時檢查時間對于很多實時系統是完全沒法滿足的,通常的做法是通過應用層的定時監測,如PING-PONG機制(就像打乒乓球,一來一回),應用層每隔一段時間發送心跳包,如websocket的ping-pong。

二、TCP time_wait

第二個希望和大家分享的話題是TCP的Time_wait狀态。

tcp協議難點(TCP漫談之keepalive和timewait)5

為啥需要time_wait狀态呢?為啥不直接進入closed狀态呢?直接進入closed狀态能更快地釋放資源給新的連接使用了,而不是還需要等待2MSL(Linux默認)時間。

有兩個原因:

一是為了防止“迷路的數據包”。如下圖所示,如果在第一個連接裡第三個數據包由于底層網絡故障延遲送達。等待新的連接建立後,這個遲到的數據包才到達,那麼将會導緻接收數據紊亂。

tcp協議難點(TCP漫談之keepalive和timewait)6

第二個原因則更加簡單,如果因為最後一個ack丢失,那麼對方将一直處于last ack狀态,如果此時重新發起新的連接,對方将返回RST包拒絕請求,将會導緻無法建立新連接。

tcp協議難點(TCP漫談之keepalive和timewait)7

為此設計了time_wait狀态。在高并發情況下,如果能将time_wait的TCP複用, time_wait複用是指可以将處于time_wait狀态的連接重複利用起來,從time_wait轉化為established,繼續複用。Linux内核通過net.ipv4.tcp_tw_reuse參數控制是否開啟time_wait狀态複用。

讀者可能很好奇,之前不是說time_wait設計之初是為了解決上面兩個問題的嗎?如果直接複用不是反而會導緻上面兩個問題出現嗎?這裡先介紹Linux默認開啟的一個TCP時間戳策略net.ipv4.tcp_timestamps = 1。

tcp協議難點(TCP漫談之keepalive和timewait)8

時間戳開啟後,針對第一個迷路數據包的問題,由于晚到數據包的時間戳過早會被直接丢棄,不會導緻新連接數據包紊亂;針對第二個問題,開啟reuse後,當對方處于last-ack狀态時,發送syn包會返回FIN,ACK包,然後客戶端發送RST讓服務端關閉請求,從而客戶端可以再次發送syn建立新的連接。

最後還需要提醒讀者的是,Linux 4.1内核版本之前除了tcp_tw_reuse以外,還有一個參數tcp_tw_recycle,這個參數就是強制回收time_wait狀态的連接,它會導緻NAT環境丢包,所以不建議開啟。

作者:陳曉宇

陳曉宇著作《雲計算那些事兒:從IaaS到PaaS進階》

,

更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!

查看全部

相关圖文资讯推荐

热门圖文资讯推荐

网友关注

Copyright 2023-2024 - www.tftnews.com All Rights Reserved