參考:html
http://coolshell.cn/articles/11564.html算法
http://coolshell.cn/articles/11609.htmlshell
TCP頭格式服務器
接下來,咱們來看一下TCP頭的格式網絡
你須要注意這麼幾點:ssh
TCP的包是沒有IP地址的,那是IP層上的事。可是有源端口和目標端口。tcp
一個TCP鏈接須要四個元組來表示是同一個鏈接(src_ip, src_port, dst_ip, dst_port)準確說是五元組,還有一個是協議。但由於這裏只是說TCP協議,因此,這裏我只說四元組。ide
注意上圖中的四個很是重要的東西:優化
Sequence Number是包的序號,用來解決網絡包亂序(reordering)問題。網站
Acknowledgement Number就是ACK——用於確認收到,用來解決不丟包的問題。
Window又叫Advertised-Window,也就是著名的滑動窗口(Sliding Window),用於解決流控的。
TCP Flag ,也就是包的類型,主要是用於操控TCP的狀態機的。
關於其它的東西,能夠參看下面的圖示
TCP的狀態機
其實,網絡上的傳輸是沒有鏈接的,包括TCP也是同樣的。而TCP所謂的「鏈接」,其實只不過是在通信的雙方維護一個「鏈接狀態」,讓它看上去好像有鏈接同樣。因此,TCP的狀態變換是很是重要的。
下面是:「TCP協議的狀態機」(圖片來源)和 「TCP建連接」、「TCP斷連接」、「傳數據」 的對照圖,我把兩個圖並排放在一塊兒,這樣方便在你對照着看。另外,下面這兩個圖很是很是的重要,你必定要記牢。(吐個槽:看到這樣複雜的狀態機,就知道這個協議有多複雜,複雜的東西老是有不少坑爹的事情,因此TCP協議其實也挺坑爹的)
不少人會問,爲何建連接要3次握手,斷連接須要4次揮手?
對於建連接的3次握手,主要是要初始化Sequence Number 的初始值。通訊的雙方要互相通知對方本身的初始化的Sequence Number(縮寫爲ISN:Inital Sequence Number)——因此叫SYN,全稱Synchronize Sequence Numbers。也就上圖中的 x 和 y。這個號要做爲之後的數據通訊的序號,以保證應用層接收到的數據不會由於網絡上的傳輸的問題而亂序(TCP會用這個序號來拼接數據)。
對於4次揮手,其實你仔細看是2次,由於TCP是全雙工的,因此,發送方和接收方都須要Fin和Ack。只不過,有一方是被動的,因此看上去就成了所謂的4次揮手。若是兩邊同時斷鏈接,那就會就進入到CLOSING狀態,而後到達TIME_WAIT狀態。下圖是雙方同時斷鏈接的示意圖(你一樣能夠對照着TCP狀態機看):
爲何須要「三次握手」: 爲了防止已失效的鏈接請求報文段忽然又傳送到了服務端,於是產生錯誤
謝希仁版《計算機網絡》中的例子是這樣的,「已失效的鏈接請求報文段」的產生在這樣一種狀況下:client發出的第一個鏈接請求報文段並無丟失,而是在某個網絡結點長時間的滯留了,以至延誤到鏈接釋放之後的某個時間纔到達server。原本這是一個早已失效的報文段。但server收到此失效的鏈接請求報文段後,就誤認爲是client再次發出的一個新的鏈接請求。因而就向client發出確認報文段,贊成創建鏈接。假設不採用「三次握手」,那麼只要server發出確認,新的鏈接就創建了。因爲如今client並無發出創建鏈接的請求,所以不會理睬server的確認,也不會向server發送數據。但server卻覺得新的運輸鏈接已經創建,並一直等待client發來數據。這樣,server的不少資源就白白浪費掉了。採用「三次握手」的辦法能夠防止上述現象發生。例如剛纔那種狀況,client不會向server的確認發出確認。server因爲收不到確認,就知道client並無要求創建鏈接。」。主要目的防止server端一直等待,浪費資源。
爲何須要「四次揮手」: 由於TCP是全雙工模式,接收到FIN時意味將沒有數據再發來,可是仍是能夠繼續發送數據。
TCP的擁塞處理 – Congestion Handling
對此TCP的設計理念是:TCP不是一個自私的協議,當擁塞發生的時候,要作自我犧牲。就像交通阻塞同樣,每一個車都應該把路讓出來,而不要再去搶路了。
擁塞控制主要是四個算法:1)慢啓動,2)擁塞避免,3)擁塞發生,4)快速恢復。這四個算法不是一天都搞出來的,這個四算法的發展經歷了不少時間,到今天都還在優化中。備註:
· 1988年,TCP-Tahoe 提出了1)慢啓動,2)擁塞避免,3)擁塞發生時的快速重傳
· 1990年,TCP Reno 在Tahoe的基礎上增長了4)快速恢復
首先,咱們來看一下TCP的慢熱啓動。慢啓動的意思是,剛剛加入網絡的鏈接,一點一點地提速,不要一上來就像那些特權車同樣霸道地把路佔滿。新同窗上高速仍是要慢一點,不要把已經在高速上的秩序給搞亂了。
慢啓動的算法以下(cwnd全稱Congestion Window):
1)鏈接建好的開始先初始化cwnd = 1,代表能夠傳一個MSS大小的數據。
2)每當收到一個ACK,cwnd++; 呈線性上升
3)每當過了一個RTT,cwnd = cwnd*2; 呈指數讓升
4)還有一個ssthresh(slow start threshold),是一個上限,當cwnd >= ssthresh時,就會進入「擁塞避免算法」(後面會說這個算法)
因此,咱們能夠看到,若是網速很快的話,ACK也會返回得快,RTT也會短,那麼,這個慢啓動就一點也不慢。下圖說明了這個過程。
這裏,我須要提一下的是一篇Google的論文《An Argument for Increasing TCP’s Initial Congestion Window》Linux 3.0後採用了這篇論文的建議——把cwnd 初始化成了 10個MSS。 而Linux 3.0之前,好比2.6,Linux採用了RFC3390,cwnd是跟MSS的值來變的,若是MSS< 1095,則cwnd = 4;若是MSS>2190,則cwnd=2;其它狀況下,則是3。
前面說過,還有一個ssthresh(slow start threshold),是一個上限,當cwnd >= ssthresh時,就會進入「擁塞避免算法」。通常來講ssthresh的值是65535,單位是字節,當cwnd達到這個值時後,算法以下:
1)收到一個ACK時,cwnd = cwnd + 1/cwnd
2)當每過一個RTT時,cwnd = cwnd + 1
這樣就能夠避免增加過快致使網絡擁塞,慢慢的增長調整到網絡的最佳值。很明顯,是一個線性上升的算法。
前面咱們說過,當丟包的時候,會有兩種狀況:
1)等到RTO超時,重傳數據包。TCP認爲這種狀況太糟糕,反應也很強烈。
o sshthresh = cwnd /2
o cwnd 重置爲 1
o 進入慢啓動過程
2)Fast Retransmit算法,也就是在收到3個duplicate ACK時就開啓重傳,而不用等到RTO超時。
o TCP Tahoe的實現和RTO超時同樣。
o TCP Reno的實現是:
§ cwnd = cwnd /2
§ sshthresh = cwnd
§ 進入快速恢復算法——Fast Recovery
上面咱們能夠看到RTO超時後,sshthresh會變成cwnd的一半,這意味着,若是cwnd<=sshthresh時出現的丟包,那麼TCP的sshthresh就會減了一半,而後等cwnd又很快地以指數級增漲爬到這個地方時,就會成慢慢的線性增漲。咱們能夠看到,TCP是怎麼經過這種強烈地震盪快速而當心得找到網站流量的平衡點的。
TCP四種定時器
定時器在TCP可靠傳輸的過程當中起着舉足輕重的做用。TCP在創建鏈接以後可能(保活keep-alive定時器是可選的)會 啓動四個定時器。
TCP使用四種定時器(Timer,也稱爲「計時器」):
重傳計時器:Retransmission Timer
堅持計時器:Persistent Timer
保活計時器:Keeplive Timer
時間等待計時器:Time_Wait Timer。
(1)重傳計時器:Retransmission Timer
重傳定時器:爲了控制丟失的報文段或丟棄的報文段,也就是對報文段確認的等待時間。當TCP發送報文段時,就建立這個特定報文段的重傳計時器,可能發生兩種狀況:若在計時器超時以前收到對報文段的確認,則撤銷計時器;若在收到對特定報文段的確認以前計時器超時,則重傳該報文,並把計時器復位;
重傳時間=2*RTT;
RTT的值應該動態計算。經常使用的公式是:RTT=previous RTT*i + (1-i)*current RTT。i的值一般取90%,即新的RTT是之前的RTT值的90%加上當前RTT值的10%.
Karn算法:對重傳報文,在計算新的RTT時,不考慮重傳報文的RTT。由於沒法推理出:發送端所收到的確認是對上一次報文段的確認仍是對重傳報文段的確認。乾脆不計入。
(2)堅持計時器:persistent timer
專門爲對付零窗口通知而設立的。
當發送端收到零窗口的確認時,就啓動堅持計時器,當堅持計時器截止期到時,發送端TCP就發送一個特殊的報文段,叫探測報文段,這個報文段只有一個字節的數據。探測報文段有序號,但序號永遠不須要確認,甚至在計算對其餘部分數據的確認時這個序號也被忽略。探測報文段提醒接收端TCP,確認已丟失,必須重傳。
堅持計時器的截止期設置爲重傳時間的值,但若沒有收到從接收端來的響應,則發送另外一個探測報文段,並將堅持計時器的值加倍和並復位,發送端繼續發送探測報文段,將堅持計時器的值加倍和復位,知道這個值增大到閾值爲止(一般爲60秒)。以後,發送端每隔60s就發送一個報文段,直到窗口從新打開爲止;
補充:
堅持定時器的原理是簡單的,當TCP服務器收到了客戶端的0滑動窗口報文的時候,就啓動一個定時器來計時,並在定時器溢出的時候向向客戶端查詢窗口是否已經增大,若是獲得非零的窗口就從新開始發送數據,若是獲得0窗口就再開一個新的定時器準備下一次查詢。經過觀察能夠得知,TCP的堅持定時器使用1,2,4,8,16……64秒這樣的普通指數退避序列來做爲每一次的溢出時間。
糊塗窗口綜合症
TCP的窗口協議,會引發一種一般叫作糊塗窗口綜合症的問題,具體表現爲,當客戶端通告一個小的非零窗口時,服務器馬上發送小數據給客戶端並充滿其緩衝區,一來二去就會讓網絡中充滿小TCP數據報,從而影響網絡利用率。對於發送方和接收端的這種糊塗行爲。
再次補充:
TCP經過讓接收方指明但願從發送方接收的數據字節數(即窗口大小)來進行流量控制。若是窗口大小爲 0會發生什麼狀況呢?這將有效地阻止發送方傳送數據,直到窗口變爲非0爲止。
TCP不對ACK報文段進行確認, TCP只確認那些包含有數據的ACK報文段。
若是一個確認丟失了(這個確認是」接收方「向」發送方「發送的ACK,通知」發送方「本身的窗口已經非0了),則雙方就有可能由於等待對方而使鏈接終止:接收方等待接收數據(由於它已經向發送方通告了一個非 0的窗口),而發送方在等待容許它繼續發送數據的窗口更新。爲防止這種死鎖狀況的發生,發送方使用一個堅持定時器 (persist timer)來週期性地向接收方查詢,以便發現窗口是否已增大。這些從發送方發出的報文段稱爲窗口探查 (window probe)。
(3)保活計時器:keeplive timer
每當服務器收到客戶的信息,就將keeplive timer復位,超時一般設置2小時,若服務器超過2小時尚未收到來自客戶的信息,就發送探測報文段,若發送了10個探測報文段(沒75秒發送一個)還沒收到響應,則終止鏈接。
補充:
保活定時器更加的簡單,還記得FTP或者Http服務器都有Sesstion Time機制麼?由於TCP是面向鏈接的,因此就會出現只鏈接不傳送數據的「半開放鏈接」,服務器固然要檢測到這種鏈接而且在某些狀況下釋放這種鏈接,這就是保活定時器的做用。其時限根據服務器的實現不一樣而不通。另外要提到的是,當其中一端若是崩潰並從新啓動的狀況下,若是收到該端「前生」的保活探察,則要發送一個RST數據報文幫助另外一端結束鏈接。
(4)時間等待計時器:Time_Wait Timer
在鏈接終止期使用,當TCP關閉鏈接時,並不認爲這個鏈接就真正關閉了,在時間等待期間,鏈接還處於一種中間過分狀態。這樣就能夠時重複的fin報文段在到達終點後被丟棄,這個計時器的值一般設置爲一格報文段壽命指望值的兩倍。
補充:
2MSL定時器:MSL是報文段作大生存時間(Maximum Segment Lifetime),設置這個定時器有兩個目的:
其一是爲了測量鏈接處於TIME_WAIT狀態的時間.這樣可讓TCP再次發送最後的ACK以防止這個ACK丟失(若是丟失,另外一端會重傳FIN)。
其二,爲容許老的重複分節在網絡中消逝。具體能夠解釋爲,若是一個TCP鏈接在斷開以前有迷途分節還沒有消逝,在斷開該TCP鏈接以後馬上重啓一個一樣的鏈接(雙方的IP地址和端口port相同),這時以前的迷途的老分節可能對新的新的TCP鏈接接收,從而形成未定義的錯誤。爲了不這種狀況,TCP規定在TIME_WAIT狀態,不能啓動一個鏈接的化身。既然TIME_WAIT狀態維持2MSL,這就保證了一個鏈接上的分組及其應該在 2MSL內都會消失。