成為高手前必懂的TCP乾貨

美碼師發表於2019-05-19

我們在平時的開發過程中,或多或少都會涉獵到網路傳輸這塊。
這篇文章,主要是整理一下 TCP 的一些知識要點,作為一名開發者來說,儘管有那麼多的基礎設施(框架、元件)幫我們遮蔽了這些細節。當我仍然認為了解它的一些基本原理必有些裨益,尤其是當你在分散式環境上遇到一些棘手問題時,一些原理性的知識可能會讓你快速找到答案。

一、起源

TCP 是傳輸層的協議,全稱是叫做 Transmission Control Protocol,這個協議在 IETF RFC 793 進行了定義。
在網際網路產生之前,我們的電腦都是相互獨立的,每臺機器都有著自己的作業系統並保持著自己的執行。
於是,為了將這些電腦連線起來,並能夠基於一種"通道"的形式進行資料、資源的傳輸及互動,IETF 制定了 TCP 協議。

那麼,IETF又是什麼? 這是一個令人尊敬的技術組織,叫 Internet Engineering Task Force,即網際網路工程任務組。
這是一個成立於1985年的開放性組織,現在我們所提到的 HTTP、TCP、IP 這些重要的網路協議,都是出自於該組織。
可以這麼說,IETF 是網際網路的始作俑者,沒有它就沒有現在繁榮的網際網路了。

值得一提的是,IETF並非權貴組織,它是一個"來自民間" 的自組織、自管理的團隊,非常崇尚於自由平等的精神。

整個網際網路的底層機制是由一套標準網路協議組成的,為了更方便於理解,人們便定義了所謂的“網路分層模型"。
在學習計算機網路課程的時候,都會提到兩種網路模型,如下:

成為高手前必懂的TCP乾貨

  • OSI 模型,全稱為 Open System Interconnection,即開放系統互聯模型,這個是由 ISO(International Organization for Standardization) 國際標準化組織提出的。
    它主要是用來解決當時各個網路技術供應商在協議上無法統一的問題,通過將整個網路體系結構抽象為 7層,從最底層的物理層、資料鏈路層一直到最上面的應用層都做了定義。

在以前,由於術語眾多,有許多人經常被OSI、ISO所迷惑..

  • TCP/IP,即 TCP/IP Protocol Suite(協議套件)是一個以TCP協議和IP協議為核心的通訊模型,該模型採用協議堆疊的方式來實現許多通訊協議,並將通訊體系抽象為4層。
    TCP/IP 模型最早發源於美國國防部(縮寫為DoD)的ARPA網專案,此後就交由IETF組織來維護。

從上面的圖中可以看出,TCP/IP 基本上是OSI 模型的簡化版,當然也更加容易理解。
在網路層以下,物理層、資料鏈路層所涉及的一些技術手段及概念都相對晦澀難懂,就比如光纜、中繼器、交換機等需要一些專業背景才能掌握通透。
對於大多數的軟體應用來說,將網路層以下的部分統稱為“網路介面層" 無疑是更加簡單的。

因此,OSI 模型儘管非常完善且全面,但已經被 TCP/IP 模型所淘汰,在網際網路應用盛行的今天很少被提及。

成為高手前必懂的TCP乾貨
圖-TCP/IP 網路模型

二、TCP 協議

TCP 是整個 TCP/IP 協議族中最重要的傳輸層協議,它定義了一種面向連線的、可靠的、基於流的傳輸方式。
HTTP 是基於 TCP 的,所以說 TCP 是整個網際網路的協議其一併不為過。
同時,我們在使用 HTTP 協議實現應用系統間的互動時,也經常免不了會與 TCP 打上交道。因此有必要了解一些基本機制。

TCP 的特點?

  • 首先,TCP 是基於連線的,也就是在進行資料傳輸之前,客戶端與服務端(或者說是通訊的雙方)需要先建立一個可信的連線。
    在資料傳輸結束後,再通過一種協定的方式斷開連線,由通訊的雙方釋放資源。這裡涉及到的,就是常說的"三次握手"、"四次揮手"

  • 其次,TCP 是可靠的,它定義了一種資料包的"超時重傳機制",簡單說,就是每一個資料包在傳送出去後的都會等待一個響應。
    如果指定時間內沒有收到響應,由傳送方進行一定次數的重傳來保證資料的可靠傳輸。

  • 最後,TCP 是基於流的,這是指在傳輸資料時應用層不需要關注資料包的邊界,TCP在資料傳輸時會自動根據網路環境將資料進行緩衝、分組、合併。
    這點跟基於報文的協議(UDP)是截然不同的。當然,基於流的傳輸也保證了資料收發的有序性,因此每個資料包都附帶上一個屬於當前連線的序列號。

怎麼理解全雙工?

全雙工是通訊上的術語,一般在軟體開發領域提到的並不多。
這是指資料同時在兩個方向上傳輸,TCP 是基於全雙工的可信傳輸協議。
當然 UDP 也可以實現全雙工的傳輸,但 TCP 只能實現點對點的傳輸,無法支援廣播或者多播(分組)。

黑板:半雙工的區別在於,同一時間只能有一個方向的傳輸

TCP 的資料包如何組織?

透視一個協議的最原始的方法就是看它的資料包,一個TCP 的報文格式如下:

成為高手前必懂的TCP乾貨

這裡面的欄位就包括了:

源埠
表明傳送端所使用的埠號,用於目標主機回應。

目的埠
表明要連線的目標主機的埠號。

序號
表明傳送的資料包的順序,一般為上次傳送包中的順序號+1。
若該資料包是整個TCP連線中的第一個包(SYN包),則該值是隨機生成的。

確認號
表明本端TCP已經接收到的資料,其值表示期待對端傳送的下一個位元組的序號。
實際上告訴對方,在這個序號減1以前的位元組已正確接收。
若該資料包是整個TCP連線中的第一個包(SYN包),則確認號一般為0。

資料偏移
表示以32位(4位元組)為單位的TCP分組頭的總長度(首部長度),用於確定使用者資料區的起始位置。
在沒有可變內容的情況下,TCP頭部的大小為20位元組,對應該值為5。

標誌位
緊急標誌位(URG):開啟時表明此資料包處於緊急狀態應該優先處理
確認標誌位(ACK):開啟時表明確認號有效,否則忽略確認號
推送標誌位(PSH):開啟時表明應該儘快交付給應用程式,而不必等到快取區填滿才推送,比如 telnet 的場景
復位標誌位(RST):開啟時表明TCP連線出現連線出現錯誤,資料包非法拒絕連線
同步標誌位(SYN):開啟時表明連線建立的標誌
終止標誌位(FIN):開啟時表明釋放一個連線

視窗大小
表明期望接受到的資料包位元組數,用於擁塞控制。

校驗和
實現對TCP報文頭以及資料區進行校驗。

緊急指標
在緊急狀態下(URG開啟),指出視窗中緊急資料的位置(末端)。

選項(可變)
用於支援一些特殊的變數,比如最大分組長度(MSS)。

填充
用於保證可變選項為32 bit的整數倍。

黑板:一般情況下TCP 頭部為20位元組,加上20位元組的 IP頭部,一個資料包至少包含40位元組的頭部

三、TCP 工作流程

鏈是指鏈路,這個是物理層的概念,比如光纜光纖,或是無線的電磁波。
但這裡所說的鏈路其實是網路連線的意思,即IP 上層的概念。
那麼,一個TCP 正常的通訊流程,會包含建鏈(建立連線)、傳輸資料、拆鏈(關閉連線),如下圖所示:

成為高手前必懂的TCP乾貨

(圖來自網路)

據上圖所示,在進行 TCP 進行資料傳輸時,都不可避免的會經過這兩個階段:

  • 三次握手建立連線
  • 執行資料傳輸、雙方讀寫
  • 四次揮手釋放連線

下面,重點說明下建鏈與拆鏈的過程

四、 三次握手

成為高手前必懂的TCP乾貨

在建立TCP連線時,需要經過三次互動,也成為三次握手(HandShake)。

1、客戶端發起連線請求,傳送 SYN包(SYN=i)到伺服器,並進入到SYN-SEND狀態,等待伺服器確認
2、伺服器收到SYN包後,必須確認客戶的 SYN(ack=i+1),同時自己也傳送一個SYN包(SYN=k),即SYN+ACK包,此時伺服器進入SYN-RECV狀態
3、客戶端收到伺服器的SYN+ACK包,向伺服器傳送確認報ACK(ack=k+1),此後客戶端和伺服器進入ESTABLISHED狀態,雙方可以開始傳送資料。

在談論三次握手的時候,有幾個問題是需要關注的:

問題1. 為什麼是三次握手

這個問題在技術面試時屢試不爽,原話是能不能兩次,或者是四次握手呢?
答案就是,TCP 是可靠的傳輸,在建立連線時就應該經過兩端的確認過程,如上面的流程,
只有在三次握手的情況下,客戶端和服務端都經過了一次真正(SYN+ACK)的確認過程。這樣的連線便認為是可信的。
此外,如果僅僅只是兩次握手,一旦網路不穩定造成 SYN 包重傳則會直接導致重複建立連線,浪費資源。

問題2. 什麼是syn flood攻擊

syn flood 是一種經典的 ddos攻擊手段,這裡面用到了TCP 三次握手存在的漏洞。
在上面的圖中,可以看到當服務端接收到 SYN 後進入 SYN-RECV 狀態,此時的連線稱為半連線,同時會被服務端寫入一個 半連線佇列。
想象一下,如果攻擊者在短時間內不斷的向服務端傳送大量的 SYN 包而不響應,那麼伺服器的 半連線佇列很快會被寫滿,從而導致無法工作。
實現 syn flood 的手段,可以通過偽造源 IP 的方式,這樣伺服器的響應就永遠到達不了客戶端(握手無法完成);
當然,通過設定客戶端防火牆規則也可以達到同樣的目的。

對 syn flood 實現攔截是比較困難的,可以通過啟用 syn_cookies 的方式實現緩解,但這通常不是最佳方案。
最好的辦法是通過專業的防火牆來解決,基本上所有的雲端計算大T 都具備這個能力。
關於 syn flood 可以看看這篇文章

問題3. 半連線佇列和全連線佇列如何調優

這裡提到了一個"半連線佇列"(syns queue),與其對應的還有一個 "全連線佇列"(accept queue)
前者用於暫存未建立完全的連線,後者是連線在成功建立後進入的一個佇列。
半連線佇列預設大小可以通過核心引數調整:

echo 4096 > /proc/sys/net/ipv4/tcp_max_syn_backlog

黑板:tcp_max_syn_backlog 在 syn_cookies 開啟時是無效的,這兩個選項存在衝突

對於全連線佇列,如果伺服器未能及時通過 accept 呼叫將其中的連線取走,會導致佇列溢位(連線失效)
全連線佇列的大小的核心調優方式:

echo 4096 > /proc/sys/net/core/somaxconn

那麼,是不是隻有核心調優這種方法能影響這兩個引數呢?答案是否定的。
實際上,在應用層呼叫 socket listen 時也支援設定一個 backlog引數,這幾個之間的關係如下:

半連線佇列長度 = min(backlog,核心 net.core.somaxconn,核心 tcp_max_syn_backlog)
全連線佇列長度 = min(backlog,核心 net.core.somaxconn)

黑板:一般的應用伺服器如 netty、tomcat 都支援設定 backlog 引數,但是在真正進行調優時還需要配合考慮核心引數的配置。

五、 四次揮手

成為高手前必懂的TCP乾貨

在釋放連線時,由於TCP是全雙工的,因此最後要由兩端分別進行關閉,這個流程如下:

1、客戶端傳送一個FIN,用來關閉客戶端到伺服器的資料傳送,客戶端進入FIN_WAIT_1狀態。
2、伺服器收到FIN後,傳送一個ACK給客戶端,確認序號為收到序號+1(與SYN相同,一個FIN佔用一個序號),伺服器進入CLOSE_WAIT狀態,而客戶端進入FIN_WAIT2狀態。
3、伺服器傳送一個FIN,用來關閉伺服器到客戶端的資料傳送,伺服器進入LAST_ACK狀態。
4、客戶端收到FIN後,客戶端進入TIME_WAIT狀態,接著傳送一個ACK給伺服器,確認序號為收到序號+1,伺服器進入CLOSED狀態,完成釋放。

關閉連線有主動關閉和被動關閉一說,這裡為了簡化理解,我們以客戶端作為主動關閉方,伺服器為被動關閉方。

四次揮手需要關注的問題:

問題1. 為什麼是四次揮手

傳送FIN的一方就是主動關閉(客戶端),而另一方則為被動關閉(伺服器)。
當一方傳送了FIN,則表示在這一方不再會有資料的傳送。
其中當被動關閉方受到對方的FIN時,此時往往可能還有資料需要傳送過去,因此無法立即傳送FIN(也就是無法將FIN與ACK合併傳送),
而是在等待自己的資料傳送完畢後再單獨傳送FIN,因此整個過程需要四次互動。

問題2. 什麼是半關閉

客戶端在收到第一個FIN的ACK響應後,會進入FIN_WAIT2 狀態時,此時伺服器處於 CLOSE_WAIT狀態,這種狀態就稱之為半關閉
從半關閉到全關閉,需要等待第二次FIN的確認才算結束。此時,客戶端要等到伺服器的FIN才能進入TIME_WAIT,
如果對方遲遲不傳送FIN呢,則會等待一段時間後超時,這個可以通過核心引數tcp_fin_timeout控制,預設是60s。

問題3. 為什麼伺服器會有大量 closewait

半關閉的狀態下的伺服器連線會處於 closewait 狀態,直到伺服器傳送了FIN。
那麼在應用層則是呼叫socket.close()會執行FIN的傳送,如果伺服器出現大量CLOSE_WAIT狀態的連線,那麼有可能的原因:

  • 伺服器壓力過大,根本來不及呼叫close
  • 存在連線洩露問題(Bug),伺服器未及時關閉連線

問題4. timewait 會帶來什麼問題

當客戶端收到了對方的FIN時,會進入TIME_WAIT狀態,此時會保持一段時間再進入CLOSE狀態。
這麼做的原因主要還是為了可靠的關閉連線。在將TCP 進行可靠性設計之時就考慮了許多網路的不穩定性的因素,比如:
傳送給對方的ACK 可能會無法及時收到,此時對方可能重傳FIN過來,如果提前進入CLOSE則會返回RST而不是ACK,就會影響關閉流程。
因此 TIME_WAIT 狀態預設會持續一段時間,直到確認不會再有重傳的資料包之後再安全的關閉。

黑板:這裡timewait的持續時間預設是 2*MSL(總共1分鐘),這個MSL叫Max Segment Lifetime,也就是關於一個資料包在網路中傳輸的最大生命週期的預設。
MSL預設是30s,當然這個值在現在已經可以大幅度縮減。可見在當時在設計之初,網路狀況有多麼的糟糕。

那麼timewait會帶來什麼問題?
如果頻繁的主動關閉連線,可能會產生大量 timewait,由於timewait 的連線佔用了一個控制程式碼及少量記憶體(4K),那麼就有可能會影響其他連線的建立,比如:
出現 too many open files 異常..

該如何解決:

  • 重用連線,避免頻繁關閉,比如使用連線池
  • 引數調優,比如開啟tcp_tw_reuse選項支援timewait連線的重複使用。

黑板:HTTP 協議裡頭發現了timewait的問題,於是在 HTTP 1.1 中定義了 KeepAlive 用來支援連線的重用。

問題5. RST 是什麼,為什麼會出現

RST 是一個特殊的標記,用來表示當前應該立即終止連線。以下這些情況都會產生RST:

  • 向一個未被監聽的埠傳送資料
  • 對方已經呼叫 close 關閉連線
  • 存在一些資料未處理(接收緩衝區),請求關閉連線時,會傳送RST強制關閉
  • 某些請求發生了超時

RST 機制有時候也會被利用,做一些埠的掃描,如下:

成為高手前必懂的TCP乾貨

-> 埠開啟,可接受SYN

成為高手前必懂的TCP乾貨

-> 埠關閉,響應RST

小結

原文只是想總結下 TCP 引數調優的幾個細節,沒想到TCP 牽扯出來的東西實在太多,光是一個簡單的握手、揮手流程就存在這麼多的細節和坑。
可以說為了保證資料傳輸的可靠性,早期的設計者確實考慮了太多的東西。當然,這也為上層的應用實現鋪平了道路。
鑑於篇幅原因,只做了TCP 建鏈、拆鏈方面的介紹。關於資料的傳輸的一些細節,將在下篇文章梳理及分享。

相關文章