TCP 連接的前世今生

前言

大家好!我是盼盼!

之前寫了幾篇關於算法和 linux 命令的文章,今天來學習下,網絡協議相關的知識。不管你是客戶端,還是服務端開發,網絡協議這塊都是要學習和了解的。

工作和麪試中,網絡協議都會用到。雖然學習網絡對編碼沒有實質的幫助,但對你處理一下網絡連接問題,幫助大大的。下面來一起學習下,發車!

緣起

在世界上各地,各種各樣的電腦,運行着各自不同的操作系統爲大家服務。這些電腦,在表達同一種信息的時候,所使用的方法是千差萬別。

計算機使用者意識到,計算機只是單兵作戰,並不會發揮太大的作用。只有把它們聯合起來,電腦纔會發揮出它最大的潛力。

於是人們就想方設法的,用電線把電腦連接到了一起。但是簡單的連到一起是遠遠不夠的,就好像語言不同的兩個人互相見了面,完全不能交流信息。因而他們需要定義一些共通的東西來進行交流,TCP/IP 就是爲此而生。

TCP/IP 不是一個協議,而是一個協議族的統稱。裏面包括了 IP 協議,IMCP 協議,TCP 協議,以及我們更加熟悉的 http、ftp 協議等等。電腦有了這些,就好像學會了外語一樣,就可以和其他的計算機終端做自由的交流了。今天我們學習 TCP 協議。

什麼是 TCP

TCP(傳輸控制協議)是一種面向連接的、可靠的、基於字節流的傳輸層通信協議,它完成第四層傳輸層所指定的功能,網絡模型下面介紹。

TCP 協議的特點是:

網絡模型

七層模型

國際標準化組織 ISO ,在 1981 年正式推薦了一個網絡系統結構一七層參考模型,也叫作開放系統互連模。由於這個標準模型的建立,使得各種計算機網絡均向它靠攏,大大推動了網絡通信的發展。

這個 ISO 層網絡模型各層的名字、主要功能對應的典型設備和傳輸單位如下圖:

這個七層網絡模型在數據的傳輸過程中還會對數據進行封裝,如下圖:

ISO 層網絡模型中,當一臺主需要傳送用戶的數據 (data) 時,數據首先通過應用層的接口進入應用層。

先看幾個常見報頭術語簡寫:

在應用層,用戶的數據被加上應用層的報頭 AH,形成應用層協議數據單元 PDU,然後被遞交到下層表示層。

表示層並不關心上層應用層的數據格式,而是把整個應用層遞交的數據包,看成是一個整體進行封裝,即加上表示層的報頭 PH。然後,遞交到下層會話層。

同樣,會話層、傳輸層、網絡層(假設用 TCP 傳輸,則是 TCP 數據+ IP 包頭)、數據鏈路層(把上層的 TCP 數據+ IP 頭統一稱爲幀數據,即幀 +幀數據+幀尾(CRC)也都要分別給上層遞交下來的數據加上自己的報頭)。

它們是:會話層報頭 SH、傳輸層報頭 TH、網絡層報頭 NH 和數據鏈路層報頭 DH。其中,數據鏈路層還要給網絡層遞交的數據加上數據鏈路層報尾形成最終的一幀數據。

當一幀數據,通過物理層傳送到目標主機的物理層時,該主機的物理層把它遞交到上層一一數據鏈路層。數據鏈路層負責去掉數據幀的幀頭部和尾部(同時還進行數據校驗)。如果數據沒有出錯,則遞交到上層網絡層。

同樣,網絡層、傳輸層、會話層、表示層、應用層也要做類似的工作。最終 ,原始數據被遞交到目標主機的具體應用程序中。

五層網絡模型

五層模型的網絡體系也經常被提到,這五層的名字與功能分別如下所述:

四層網絡模型

前面的兩種模型都是學術上的概念,使用並不廣泛 還有一個四層模型,使用最爲廣泛一 TCP/IP 分層模型。幾種模型如下圖:

TCP/IP 分層的四模型的個協議層分別完成以下的功能:

綜上所述,我們需要知道 TCP 協議在網絡 OSI 的七層模型中的第四層傳輸層, IP 協議在第三層網絡層, ARP 協議在第二層數據鏈路層;在第二層上的數據叫 Frame ,在第三層上的數據叫 Packet ,第四層的數據叫 Segment 所有程序的數據首先會打包到 TCP 的 Segment 中。

然後 TCP 的 Segment 會打包到 IP 的 Packet ,然後再打包到以太網 Ethernet 的 Frame 中,傳到對端後,各個 解析自己的協議,然後把數據交給更高層的協議處理。

TCP 頭格式

在學習 TCP 連接之前,還要學習一下 TCP 頭部格式。因爲 TCP 連接建立,需要用 TCP 包來交換和管理數據,下面看一下 TCP 頭部格式。

TCP 頭部裏每個字段都爲管理 TCP 連接和控制數據流起了重要作用。

16 位端口號:告知主機該報文段是來自哪裏(源端口)以及傳給哪個上層協議或應用程序(目的端口)的。

進行 TCP 通信時,客戶端通常使用系統自動選擇的臨時端口號,而服務器則使用知名服務端口號。所有知名服務使用的端口號都定義在  /etc/services 文件中。

32 位序號(sequence number):一次 TCP 通信(從 TCP 連接建立到斷開)過程中個傳輸方向上的字節流的每個字節的編號。

32 位確認號(acknowledgement number):用作對另一方發送來的 TCP 報文段的響應其值是收到的 TCP 報文段的序號值加 1。

4 位頭部長度(header length):標識該 TCP 頭部有多少個 32bit ( 4 Byte 因爲最大能表示 15 ,所以 TCP 頭部最長是 60 Byte。

6 位標誌位包含如下幾項:

16 位窗口大小(window size):是 TCP 流量控制的一個手段。這裏說的窗口,指的是接收通告窗口( Receiver Window, RWND)。它告訴對方本端的 TCP 接收緩衝區還能容納多少字節的數據,這樣對方就可以控制發送數據的速度。

16 位校驗和(TCP checksum):由發送端填充,接收端對 TCP 報文段執行 CRC 算法,以檢驗 TCP 報文段在傳輸過程中是否損壞 。注意,這個校驗不僅包括 TCP 頭部,也包括數據部分。這也是 TCP 可靠傳輸的一個重要保障。

16 位緊急指針(urgent pointer):是一個正的偏移量。它和序號字段的值相加表示最後一個緊急數據的下一字節的序號 。確切地說,這個字段是緊急指針相對當前序號的偏移,不妨稱之爲 “緊急偏移”。TCP 的緊急指針是發送端向接收端發送緊急數據的方法。

綜上,你需要注意如下幾點:

TCP 的包是沒有 IP 地址的,那是 IP 層上的事,但是有源端口和目的端口。

一個 TCP 連接需要四元組( src_ip,src_port,dst_ip,dst_port )來表示是同一個連接 準確說是五元組,還有一個是協議 但因爲這裏只是強調 TCP 協議,所以,只說四元組。

Sequence Number 是包的序號,用來解決網絡包亂序(reordering )問題。

Acknowledgement Number 就是 ACK ,用於確認收到,用來解決不丟包的問題。

Window Advertised Window ,也就是著名 的滑動窗口 Sliding Window ),用於解決流量控制問題。

TCP Flag ,也就是包的類型,主要是用於操控 TCP 的狀態機的。

TCP 三次握手

其實,網絡上的傳輸是沒有連接的, TCP 是一樣的 TCP 所謂的 “連接”,其實只不過是在通信的雙方維護一個 “連接狀態”,讓它看上去好像有連接一樣 所以, TCP 的狀態變換是非常重要的 。

先來看一下著名的三次握手圖。

TCP 連接的建立可以簡單地稱爲三次握手,而連接的中止則可以稱爲四次揮手。

建立連接 TCP/IP 協議中, TCP 協議提供可靠的連接服務,採用三次握手建立一個連接。

完成 三次握手,客戶端與服務器開始傳送數據,也就是 ESTABLISHED 狀態。

連接建立中的異常

建連接時 SYN 超時問題

如果 server 端因爲某種情況沒有收到 client 回來的 ACK,那麼,這個連接處還處於一個未建立的狀態。於是,server 端如果在一定時間內沒有收到,則 server 端的 TCP 會重發 SYN_ACK。

在 Linux 下,默認重試次數爲 5 次,重試的間隔時間從 1s 開始每次都翻倍,5 次的重試時間間隔爲 1s, 2s, 4s, 8s, 16s,總共 31s,第 5 次發出後還要等 32s 都知道第 5 次也超時了。如果第五次重傳之後,還未收到客戶端的 ACK,server 端的 TCP 纔會把斷開這個連接。

關於 SYN Flood 攻擊

攻擊者短時間僞造不同 IP 地址的 SYN 報文,服務端每接收到一個 SYN 報文,就進入 SYN_RCVD 狀態,但服務端發送出去的 ACK + SYN 報文,無法得到未知 IP 主機的 ACK 應答,久而久之就會佔滿服務端的 SYN 接收隊列(未連接隊列),使得服務器不能爲正常用戶服務。

避免方式

設置 tcp_syncookies = 1。當 SYN 隊列滿了後,TCP 會通過源地址端口、目標地址端口和時間戳打造出一個特別的 Sequence Number 發回去(又叫 cookie)。

如果是攻擊者則不會有響應,如果是正常連接,則會把這個 SYN Cookie 發回來,然後服務端可以通過 cookie 建連接。

設置 netdev_max_backlog 的值,確定鏈接隊列的大小。當網卡接收數據包的速度大於內核處理的速度時,會有一個隊列保存這些數據包。

通過設置 netdev_max_backlog 的值,確定 SYN_RCVD 狀態連接的最大個數。

通過設置 tcp_abort_on_overflow 的值。當超出處理能時,對新的 SYN 直接回報 RST,丟棄連接。

TCP 四次揮手

TCP 的連接斷開

TCP 一個特別的概念叫作半關閉,這個概念是說, TCP 的連接是全雙工(可以同時發送和接收)連接,因此在關閉連接的時候,必須關閉傳和送兩個方向上的連接。

客戶機給服務器 FIN 的 TCP 報文,然後服務器返回給客戶端一個確認 ACK 報文,並且發送一個 FIN 報文,當客戶機回覆 ACK 報文後( 四次握手),連接就結束了。

在建立連接的時候,通信的雙方要互相確認對方的最大報文長度( MSS ),以便通信。

一般這個 SYN 長度是 MTU 減去固定 IP 首部和 TCP 首部長度。對於一個以太網,一般可以達 1460 Byte 。當然如果對於非本地的 IP ,這個 MSS 可能就只有 536 Byte ,而且,如果中間的傳輸網絡的 MSS 更加的小的話,這個值還會變得更小。

爲什麼建連接要三次握手,斷連接需要四次揮手?

對於建連接的三次握手,主要是要初始化 Sequence Number 的初始值。通信的雙方要互相通知對方自己的初始化的 Sequence Numbe,所以叫 SYN 。

這個號要作爲以後的數據通信的序號,以保證應用層接收到的數據不會因爲網絡上的傳輸問題而亂序( TCP 會用這個序號來拼接數據)。

對於四次揮手,其實仔細看則是兩次,因爲 TCP 是全雙工的,所以,發送方和接收方都需要 FIN 和 ACK。

只不過,有一方是被動的,所以看上去就成了所謂的四次揮手 。如果兩邊同時斷連接,那就會就進入到 CLOSING 狀態,接着就是 TIME_WAIT 狀態。

斷開連接中的異常

TIME_WAIT 數量太多

從上面的描述可以知道,TIME_WAIT 是個很重要的狀態,但是如果在大併發的短鏈接下,TIME_WAIT 就會太多。TIME_WAIT 過多會佔用大量的內存資源和端口資源。

優化法一:tcp_tw_reuse

設置 tcp_tw_reuse = 1,則可以複用處於 TIME_WAIT 的 socket 爲新的連接所用。

有一點需要注意的是,tcp_tw_reuse 功能只能用客戶端(連接發起方),因爲開啓了該功能,在調用 connect() 函數時,內核會隨機找一個 time_wait 狀態超過 1 秒的連接給新的連接複用。

使用 tcp_timestamps = 1 選項,還有一個前提,需要打開對 TCP 時間戳的支持,即這個時間戳的字段是在 TCP 頭部的「選項」裏,用於記錄 TCP 發送方的當前時間戳和從對端接收到的最新時間戳。

由於引入了時間戳,我們在前面提到的 2MSL 問題就不復存在了,因爲重複的數據包會因爲時間戳過期被自然丟棄。

優化法二:tcp_max_tw_buckets

這個值默認爲 18000,當系統中處於 TIME_WAIT 的連接一旦超過這個值時,系統就會將後面的 TIME_WAIT 連接狀態重置。

這個方法過於暴力,而且治標不治本,帶來的問題遠比解決的問題多,不推薦使用。

TCP 狀態流轉

接下來再看一下著名的 TCP 狀態流轉圖。

CLOSED 狀態:表示初始狀態。

LISTEN 狀態:表示服務器端的某個 socket 處於監聽狀態,可以接受連接。

SYN_SENT 狀態:在服務端監聽後,客戶端 socket 執行 CONNECT 連接時,客戶端發送 SYN 報文,此時客戶端就進入 SYN_SENT 狀態,等待服務端的確認。

SYN_RCVD 狀態:表示服務端接收到了 SYN 報文,在正常情況下,這個狀態是服務器端的 socket 在建立 TCP 連接時的三次握手會話過程中的一箇中間狀態,很短暫,基本上用網絡查詢工具 netstat 是很難看到這種狀態的。因此這種狀態時,當收到客戶端的 ACK 報文後,它會進入到 ESTABLISHED 狀態。

ESTABLISHED 狀態:表示連接已經建立了。

FIN_WAIT_1 狀態:這個是已經建立連接之後,其中一方請求終止連接,等待對方的 FIN 報文 。

FIN_WAIT_1 狀態是當 socket 在 ESTABLISHED 狀態時,它想主動關閉連接,向對方發送了 FIN 報文,此時該 socket 即進入到 FIN_WAIT_1 狀態。而當對方迴應 ACK 報文後,則進入到 FIN_WAIT_2 狀態。

當然在實際的正常情況下,無論對方處於何種情況,都應該馬上回應 ACK 報文,所以 FIN_WAIT_1 狀態一般是比較難見到的,而 FIN_WAIT_2 狀態還可以用 netstat 看到。

FIN_WAIT_2 狀態:實際上 FIN_WAIT_2 狀態下的 socket ,表示半連接,即有一方要求關閉連接,但另外還告訴對方:我暫時還有點數據需要傳送給你,請稍後再關閉連接。

**TIME_ WAIT 狀態**:表示收到了對方的 FIN 報文,併發送出了 ACK 報文,就等 2MSL 後即可回到 CLOSED 可用狀態了。如果在 FIN_WAIT_1 狀態下,收到了對方同時帶 FIN 標誌和 ACK 標誌的報文時,可以直接進入到 TIME_WAIT 狀態,而無需經過 FIN_WAIT_2 狀態。

CLOSING 狀態:這種狀態比較特殊,實際情況中應該是很少見。正常情況下,當發送 FIN 報文後,按理來說是應該先收到(或同時收到)對方的 ACK 報文,再收到對方的 FIN 報文 。但是 CLOSING 狀態表示你發送 FIN 報文後,並沒有收到對方的 ACK 報文,反而收到了對方的 FIN 報文 。

如果雙方几乎在同時關閉一個 socket 的話,那麼就出現了雙方同時發送 FIN 報文的情況,就會出現 CLOSING 狀態,表示雙方都正在關閉 socket 連接。

CLOSE_WAIT 狀態:表示在等待關閉。當對方關閉一個 socket 後發送 FIN 報文給自己時,系統將毫無疑問地會迴應 ACK 報文給對方,此時則進入到 CLOSE_WAIT 狀態。

接下來呢,實際上你真正需要考慮的事情是察看你是否還有數據發送給對方,如果沒有,那麼你也就可以關閉這個 socket 了,發送 FIN 報文給對方,即關閉連接 。CLOSE _WAIT 狀態下,需要完成的事情是等待你去關閉連接。

LAST_ACK 狀態:這個狀態還是比較好理解的,它是被動關閉 方在發送 FIN 報文後,最後等待對方的 ACK 報文。

CLOSED 狀態:當收到 ACK 報文後,也即可以進入到 CLOSED 可用狀態了。

2MSL 等待狀態:在 FIN_WAIT_2 發送了最後一個 ACK 數據報以後,要進入 TIME_WAIT 態,這個狀態是防止最後一次握手的數據報沒有傳送到對方那裏而準備的。

由於 socket 2MSL 狀態,使得應用程序在 2MSL 時間內無法再次使用同一個 socket ,對於客戶程序還好 些,但是對於服務程序(httpd),它總是要使用同一個端口來進行服務,而在 2MSL 時間內,啓動 httpd 就會出現錯誤(插口被使用)。

爲了避免這個錯誤,服務器給出了一個平靜時間的概念,這是說在 2MSL 的時間內,雖然可以重新啓動服務器,但是這個服務器還是要平靜地等待 2MSL 的時間才能進行下一次連接。

FIN WAIT_2 狀態:這就是著名的半關閉狀態了,這是在關閉連接時,客戶端和服務器兩次握手之後的狀態 。

在這個狀態下,應用程序還有接收數據的能力。已經無法發送數據,但是也有一種可能是,客戶端處於 FIN_WAIT_2 狀態,而服務器則一直處於 WAIT_CLOSE 狀態,直到應用層來決定關閉這個狀態。

RST 同時打開和同時關閉:RST 是另一種關閉連接的方式,應用程序應該可以判斷 RST 包的真實性,即是否爲異常中止 而同時打開和同時關閉則是兩種特殊的 TCP 狀態,發生的概率很小。

總結

本文主要講述了網絡分層模型,以及各層的作用,數據包是怎麼組裝和拆包的。TCP 包結構也大致學習了下,還有 TCP 連接的建立和斷開。

TCP 連接建立之後纔開始發數據包,所以 TCP 三次握手很重要。TCP 三次握手中也可能存在一些異常,只有徹底搞懂三次握手才能正確處理這些異常。

TCP 四次揮手也很重要,server 中經常要接受和斷開連接。對應斷開連接中的異常,以及服務器請求量過多,只有在搞懂 TCP 四次揮手以後,處理這些問題才能得心應手。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/b1Ai_PAz1_oAKoK-mjH-xA