漫談TCP


不得不承認,tcp是一個非常復雜的協議。它包含了RFC793及之后的一些協議。能把tcp的所有方面面面具到地說清楚,本身就是個很復雜的事情。如果再講得枯燥,那么就會更讓人昏昏欲睡了。本文希望能盡量用稍顯通俗的話把tcp描述清楚。

關於分層

請忘掉大學課本上學的七層模型,我們使用四層模型更為貼合我們的實際網絡。應用層,傳輸層,網絡層,網絡接入層。

分層是為什么,其實和公司中職位是一樣的,不同職位的人做不同的事情,然后不同職位的人合起來,一起完成了數據傳輸的事情。

網絡傳輸層

網絡傳輸層負責最底層的底層鏈路連接。兩台主機之間進行互聯,基於網線的物理硬件上的協議。在這個層面,主機與主機的交互只認得硬件mac編號,並不認識IP。這個層需要了解的一個概念是MTU,網絡中每個路由都會設置一個MTU,代表這個路由中能通過的最大的包的大小。那么整個網絡鏈路的MTU值就是由網絡中所有路由的最小MTU決定。這個就好比水路管道,水流量是由管道鏈路中管子最小的那個鏈路來決定的。

網絡層

IP的出現是很有必要的,就像給網絡上每個機器一個門牌號,網絡層,你可以把它理解為郵件運輸工,它的職能就是負責把一包東西,從這個門牌運輸到另外一個門牌。

傳輸層

傳輸層相比於網絡層最大的不同就是引入了端口的概念。網絡層只管發送地址和目的地址。但是發送主機上有可能有多個程序和同一個接收主機進行傳輸數據,怎么區分這多個程序呢?就引入了端口的概念。(發送IP地址,發送端口,接收IP地址,接收端口)四元組標示了一個主機的程序到另一個主機程序的唯一標示。傳輸層的職能,就是維護這個四元組。

其實傳輸層還有一個職能是定義發送方和接收方基本處理包的行為。上面說到網絡層就相當於郵件運輸工,它只負責把一包東西從一個地方放到另外一個地方,但是,這包東西是否送達了,送達之后接收方又有什么行為。這些都可以在傳輸層進行定義。注意,這里說的是可以,你也可以在傳輸層布不管這些,只做簡單的基本封裝四元組。你懂的,我說的就是UDP。

應用層

應用層,就更抽象一層了。我們這個端口和那個端口的連接是用來干什么的,傳輸文件?那么可以使用FTP。傳輸文本?那么可以使用HTTP。應用層就是實際上對具體的程序之間的交互功能進行定義的層。

如果你比較鑽牛角尖的話,可能會提出的問題是:為什么要分為四層?為什么不是分為兩層,五層?

這里分為幾個問題解釋:

為什么要分層?

分層實際上是一個抽象的過程,和我們寫代碼的時候的封裝是一樣的。你要說,我只有一層,一個協議把所有從程序到物理網絡的所有東西都描述一遍,這個可以不可以?可以。但是,這樣你協議寫多了以后,就會發現,物理網絡部分幾乎所有協議都一樣,那么我倒不如另外寫一個協議,然后其他協議使用include的方式來包含這個協議。好了,網絡傳輸層就出來了。后面的原理也幾乎一樣了。所以分層是必要的。

為什么分為四層?

代碼中有個過度設計的概念,分層中也照樣有個過度設計的概念。基本上,分層層數越多,是越符合抽象的。但是到最后我們會發現,有一個協議整篇都是include,自己實際上並沒有任何實質的東西。這個就是過度設計。基本上我們琢磨來琢磨去,按照上面說的,分為四層,是個很合適的設計。四層中的每一層都有自己負責的一塊內容,內容大小適中,又沒有相互耦合的地方。四層中的每一層都不可缺少。

TCP的協議結構

說到一個協議,最先應該展示的是它的結構。

整個結構正如車子的零部件一樣,每個字段都有對應的作用:

Source Port和Destination Port

這兩個字段表示的就是發送地址和目的地址的端口號。或許有人會問,那四元組中的其他兩個發送主機IP和目的主機IP呢?ok,那是IP層的事情,請查看IP協議頭。

Sequence Number和Acknowledgment Number

這兩個字段就有的聊了。

首先第一個問題,序列號做什么用呢?

序列號是用來標記包的順序的,假設有一段要傳輸的內容大小有9000字節,按照1460字節一個包的大小,假設初始序號為10000,那么我們就把這段內容分為10000-11460, 11460-12920,... 18760-19000 一共7個包。網絡包由於網絡問題,可能並不是順序到達接收端的,那么接收端可以按照序列號來重新組裝這段內容。

第二個問題,序列號有兩個說明什么?

說明tcp是全雙工的,就是說,tcp的任意一端可以發送數據,也可以接收數據。那么需要有個發送序列號seqence Number和接收序列號 Acknowledgement Number。

Data offset 和 Reserved

由於tcp頭可能是不固定大小的(因為存在可選字段),所以需要有這個值來表示當前這個包的tcp頭有多大。

Reserved就是保留字段

Tag位

就是上圖中的URG,ACK,PSH,RST,SYN,FIN位,每個位置一表示的意思是:

URG:緊急位,RFC已經建議廢棄
ACK:說明這個包中帶有回復信息
PSH:說明這個包中有傳輸數據
RST:重置位,說明這個包是用來要對方重置連接
SYN:建立連接,說明發送方向另一方發送建立連接的請求
FIN:結束位,說明發送一方告知另外一方,要請求中斷連接

熟悉這些Tag位是非常必要的,我們一般討論包請求的時候,使用的術語一般就是:
發送方請求一個SYN,接收方返回一個ACK。每每看到這種字眼,請不要傻眼。

還有一個誤區,一個包是不是只能包含一個tag位?不是的。一個包可以包含一個或者多個tag位。比如一個包可以有ACK的功能,也能同時有SYN的請求功能。(在TCP三次握手的第二次握手的時候就是攜帶了ACK+SYN的標志位)。

Window

這個值就是著名的滑動窗口值。滑動窗口是接收端告訴發送端下次可以發送多少包。好吧,這里也需要面對幾個問題:

避免誤區:發送方和接受方的請求-響應並不是一一對應的。

網絡上並不是只有發送方發一個請求,接收方回復一個ACK這種模式的。他們交互的模式更可能是:發送方一次發送多個請求包,接收方回復一個ACK,把這些請求包都回復了。這個使用前面的Acknowledgment Number是可以做到的。

但是基本上,在接收方的角度,ACK包一定是收到一個包之后,才返回一個ACK,就是說,沒有無緣無故的發送重復ACK,沒有一個請求,多個ACK這種情況。但是有多個請求,多個重復ACK的情況,這個時候,往往說明某個請求的包丟失了。

為什么需要有滑動窗口存在?

滑動窗口的存在是為了控制網絡上包的數量。如果沒有滑動窗口,那么就是一個很理想很理想的情況,發送方一有數據,加上包頭達到MTU大小,直接發送,就和沖鋒槍一樣,突突突突。但是呢?這樣子,實際上,沒有考慮到接收方是否能接收完。接收端就像一個一直在吃飯的胖子,他的吃飯速度是固定的,它一次性最多能吃10碗飯,某個時刻可能已經吃了兩碗飯,但是還沒消化。所以這個時候,它只能再吃8碗飯了,如果這個時候你一下子給它80碗,必然導致它堵死了,吃不下不吃下。這個滑動窗口就是接收端告訴發送端我還能吃幾碗飯的通話器。

總結下,這里已經有兩個條件限制發送方的效率了,一個是MTU,全鏈路MTU大小,限制每次最大發送的包的大小。另一個是滑動窗口,限制發送方一次發送的包數量。

為什么叫滑動窗口?

滑動窗口我更願意理解為發送方和接收方共同維護的。分別有發送窗口和接收窗口區別。

發送方數據有幾個狀態:數據已發送未收到ACK,數據已發送收到ACK
接收方數據有幾個狀態:數據已收到未被應用層消費,數據已收到已被應用層消費

把發送數據橫拍做長列狀,發送方一但有數據收到ACK,那么滑動窗口左側邊就進行左移。同樣,一旦接收方有數據被應用層消費,那么,滑動窗口的右側邊就進行右移。整個過程,就好比努力爬行的蚯蚓,尾巴向前挪一寸,頭部再向前走一寸,直到把整個數據都從頭到尾移動完畢。

回到tcp的windows字段,這個字段是接收端回復給發送端,告訴發送端接收端的窗口大小的。我們其實默認也把這個窗口大小叫做滑動窗口大小。

關於滑動窗口的概念的理解,我的感觸是網上各種各樣對這個滑動窗口的描述,不要陷入到咬文嚼字中,頭腦中形象有這個滑動窗口的滑動過程,就可以了,很多文章很多描述可能是前后矛盾的。比如,下面兩個關於發送窗口的描述:

  • 發送窗口是由滑動窗口和擁塞窗口共同決定的。
  • 發送窗口是由接收窗口決定的。

CheckSum

校驗和。就是對TCP的頭部和數據部分進行檢驗,是否在中途被篡改過。它和IP頭中的校驗和的算法是一樣的,只是IP校驗內容中不包括數據,但是TCP是包括頭部和數據兩個部分的。

Urgent Pointer

緊急數據指針。緊急數據指的是發送端告訴接收端,這個數據是非常緊急的,請優先讀取,設計初期可能是由於考慮到中斷或者異常等情況,但是在RFC6093中已經明確,緊急數據已經是廢棄功能了。不建議使用。只為舊程序兼容而使用。

所以,對於Urgent Porinter和tag中的URG標示就不要使用了。

Options 和 padding

options字段相當於擴展使用的,RFC有哪些信息要傳輸,而頭部沒有安排的字段,就可以放在options中進行傳輸。padding是為了對其字節位。

它根據kind+length+ value的形式來定義存儲哪些屬性。具體看下圖的例子:

比如kind =2 代表存儲的是MSS值(最大內容大小,MTU-IP頭-TCP頭),它有四個字節的長度,具體值為1460(05 b4)

具體kind和對應的值的映射可以參考 http://www.iana.org/assignments/tcp-parameters/tcp-parameters.xhtml

總兒言之,這個options字段增加了TCP的可擴展性。但是確實,到現在,它包含的內容也越來越復雜了。

握手連接

一個最常遇到的月經雞湯面試題就是,UDP和TCP有什么不同。嗯,猴子和老虎就是不一樣的。經常我們會提及的一點就是TCP是可靠的,UDP是不可靠的。TCP的可靠體現在哪里呢?握手連接的建立和消失就是其中一個體現。

TCP著名的三次握手和四次揮手

這個圖里面的client和server應該理解為發送方和接收方。下面這一串描述請熟練練習到像串口相聲一樣:發送方發送一個SYN到接收方請求建立連接,接收方返回一個ACK確認收到請求,並攜帶一個SYN給發送方請求建立雙向連接,發送方再返回一個ACK給接收方確認,這個時候連接就建立了。

順勢說下四次揮手吧。發送方發送一個FIN給接收方主動請求斷開連接,接收方返回一個ACK確認,接着接收方再發送一個FIN請求斷開另一方向的連接,發送方收到之后返回一個ACK確認。這個時候,連接就中斷了。

在三次握手和四次揮手的時候,發送方和接收方的socket是有狀態的,對,就是你使用netstat 能看到機器上socket的狀態。
SYN_SENT/SYN_RCVD/ESTABLISH/FIN_WAIT1/CLOSE_WAIT/FIN_WAIT2/LAST_ACK/TIME_WAIT

backlog

linux的TCP模塊維護兩個隊列,半連接隊列和鏈接隊列,當三次握手的時候,收到第一個SYN,發送完ACK之后,就會把這個連接放入到半連接隊列中,當第三步完成的時候,連接建立了,就把連接從半連接隊列放到連接隊列中。

半連接隊列長度是由net.ipv4.tcp_max_syn_backlog進行設置的。
連接隊列的長度由我們創建socket的時候指定的backlog和net.core.somaxconn其中的較小值確定的。

如果backlog或者net.core.somaxconn設置過小,那么很多連接就無法建立,服務端會發送RST拒絕連接,這個也是很多服務器性能上不去或斷開連接的原因。

SYN_FLOOD攻擊

三次握手,我們假設server端是按照正常的流程走的,但是client端是邪惡的,它發送了SYN之后,server端返回了ACK+SYN,但是client端一直hold住,或者直接掉線,不發送ack了,那么這個時候,server端就一直保持在SYN_RCVD狀態。

如果這種client端非常多,就會把前面說的半連接隊列塞滿,后面的連接就無法建立了,這個server的服務也就給中止了。

這種攻擊就叫做SYN_FLOOD攻擊。

TIME_WAIT狀態

說到四次揮手,主動發起請求的一方會在TIME_WAIT狀態持續2MSL。MSL就是Maximum Segment Lifetime,一個包在網絡上存活的最長時間,linux設置的MSL是30s。這個是為了防止最后一個ACK可能被丟失了,那么在2MSL中如果收到對方重復發送的FIN包,就需要重新發送ACK來關閉連接。TCP的這種行為,我們可以看作是一種負責任的行為,主動請求關閉的一方在很大程度上確保了對方收到斷開確認請求之后才關閉這個連接的。當然,這也能保證了如果我這個端口被其他程序復用了,舊的請求不會發一個莫名其妙的FIN過來。

首先確認,TIME_WAIT不是邪惡的,我們可能在服務器上很經常會看到TIME_WAIT的連接,不必驚慌,除非這種連接數已經超過了系統的fd數,如果沒有超過的話,我的建議,不要在壓TIME_WAIT數量上太下功夫,找出什么導致出現大量TIME_WAIT的原因比較靠譜(大多數是網絡問題)。

不過,服務端確實應當盡量避免TIME_WAIT留在服務端,不管怎樣,這個會消耗一些資源的。所以,把TIME_WAIT留在客戶端,服務端不主動斷開連接是一個很好的方法,比如HTTP協議提供的KEEP_ALIVE就是在這個方面做的很好的,客戶端連接的時候告訴服務器不要主動斷開連接。

四次揮手的第二次請求和第三次請求能不能一次發送?

很容易會出現這個問題,第二次請求和第三次請求能不能一起發送呢?這樣是不是能節約性能?答案是可以的。而linux也確實是這么實現的。你具體抓一個包,就會發現第二次請求和第三次請求是一起發送的。

關於ACK

在實際網絡中抓包,你會發現,除了SYN之外,所有的請求包都帶有ACK標示。即使是上圖中的#197條已經對seq為177的消息發送了ACK,# 325也還會發送一個ACK seq=177。

這個是RFC的建議

建立連接之后,每個請求都要帶上ACK標志。我們可以這么理解,由於在TCP的機制中,ACK是無法確認中途有沒有丟失的,那么本着不發白不發的原則,每個請求都順帶帶上當前已經ACK的信息,

擁塞阻塞

TCP不是一個自私的協議,它的設計充分考慮了互聯網的大環境。試想,如果所有的網絡發送方都不管網絡的情況,明明網絡已經堵塞了,還一個勁地發送大量包,甚至重發,那么這個時候,大家都沒得玩了。於是,漸漸的,TCP引入了擁塞窗口(cwnd)的概念。擁塞窗口的存在單純是為了避免網絡上有超過當前網絡能力而造成堵塞。擁塞窗口的單位是報文段個數。比如我們平時會說,現在的擁塞窗口為3,代表發送端可以一次性發送3個報文段。當然,實際上,發送端的最大發送窗口數取決於擁塞窗口(cwnd)和滑動窗口(win)的最小值。

控制網絡擁塞的算法為擁塞算法,這個算法在不斷演變,在不同操作系統中也有不同實現。

慢啟動

控制擁塞我們首先會想到在剛剛連接網絡的時候,是不是最好先慢慢檢測網絡情況,再確定發送包的數量。這就是我們說的慢啟動算法。發送方從1個包開始,收到ACK,下次就發送2個包,收到這兩個包的ACK(請注意,這里有可能只有一個ACK),下次就發送4個包。

“每收到一個ACK,擁塞窗口就增加一個報文段”。

這句話我更願意理解為“每確認一個包被ACK了,擁塞窗口就增加一個報文段”

這句話的理解就是,由於有“延遲ACK”算法,很有可能,當發送方發送兩個請求包過來的時候,我只發送一個ACK。確認你發送的兩個包,這個時候,cwnd實際上是加2,而不是加1。如下圖中的cwnd為4的ACK。

當然上圖的情況太理想,實際的情況,坑cwnd為2的請求發出去兩個報文包的時候,先返回了一個ACK,然后cwnd這個時候就為3,發送方就會繼續發送請求包。。。更貼近實際的正如這個圖:

擁塞避免算法

慢啟動使得cwnd是呈指數增長。一定不可能是無限增長的,這里就有個閥值,超過這個閥值,就進入擁塞避免算法。

先說擁塞避免算法,擁塞避免算法說的是擁塞窗口的增加不再是“每收到一個ACK,擁塞窗口就增加一個報文段”。 而是“每收到一個ACK,cwnd = cwnd + 1/cwnd”。 這個就代表,

判斷擁塞

我們怎么判斷擁塞呢?有兩種判斷方法:

a 超時重傳(發出去的包在指定時間內沒有收到ACK)

這個指定時間是通過超時定時器來計算的,發出去一個包,超時定時器就開始計時,當超時定時器到時間之后,沒有收到ACK,那么這個時候就判斷為擁堵了。需要進行重傳。

當被這個情況觸發,TCP認為網絡情況非常糟糕,所以會直接把cwnd調整為1,sshthread 調整為cwnd/2 。 重新進入到慢啟動流程。

b 快速重傳(重復收到ACK)

這個是由於發送方一次性發送多個請求(比如5個請求,但是第二個請求丟失了,第一三四五請求到了接收端)三四五請求觸發了三個ACK返回,但是由於接收端沒有收到請求一,返回的三個ACK都是ACK一的,所以發送方就表現為收到重復ACK。當連續收到三條重復ACK的時候就進行重傳,不需要等待重傳計時器

這個時候TCP會覺得網絡還是可以的,反應不會那么激烈,cwnd調整為cwnd/2, sshthresh調整為cwnd大小,進入快速恢復算法。

快速恢復算法

快速恢復算法是為了不要有一個重傳就那么大響應。能盡快恢復到網絡流暢時候穩定的狀態。

  • cwnd = sshthresh + 3 * MSS (3的意思是確認有3個數據包被收到了)
  • 重傳Duplicated ACKs指定的數據包
  • 如果再收到 duplicated Acks,那么cwnd = cwnd +1
  • 如果收到了新的Ack,那么,cwnd = sshthresh ,然后就進入了擁塞避免的算法了。

定時器

TCP中有四個定時器,有的定時器之前已經說過了。

重傳定時器

是為了重傳的時候使用的。

2MSL定時器

在上面說到TCP揮手的時候,四次揮手中最后一次揮手,主動發起的一方會進入TIME_WAIT狀態2MSL的時常,這個定時器就是用來計算這個的。

堅持定時器

當滑動窗口為0的時候,發送方不會再發送包給接收方了。但是不發送包怎么知道接收方現在的窗口是不是還為0呢。這個時候就需要不定時去接收方咨詢是否滑動窗口還為0。這個不定時的算法就是使用堅持定時器來進行咨詢的。

這個算法是使用TCP指數退避方法,第一次1.5秒,第二次1.5x2秒,第三次1.5x4... 以此規律來進行輪詢的。

保活定時器

tcp有個keepalive機制,這個只有在一定時間內(tcp_keepalive_time,默認每2個小時),沒有數據包傳遞了,發送方在發送心跳檢測,如果發送成功,則連接繼續,如果沒有正常返回,則在指定次數內(tcp_keepalive_probes,默認是9次),指定間隔(tcp_keepalive_intvl,默認是17s)發送心跳包。

名詞回顧

  • 滑動窗口
  • 阻塞窗口
  • MTU
  • MSS
  • MSL
  • 重傳定時器
  • 2MSL定時器
  • 堅持定時器
  • 保活定時器
  • 慢啟動
  • 擁塞避免算法
  • 快速恢復算法
  • 快速重傳算法
  • backlog
  • 三次握手
  • 四次揮手

后記

tcp的東西真是比較復雜。好吧,這玩意根本沒法講通俗。我的理解或許有偏差,如果有錯誤,希望能幫忙留言提出。同時,強烈建議,看完這篇再看一遍耗子哥的《TCP的那些事兒》(http://coolshell.cn/articles/11564.html)。或許還能回來告訴下我哪些地方我理解有問題。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM