Transmission Control Protocol,傳輸控制協(xié)議,是一種面向連接的、可靠的、基于字節(jié)流的傳輸層通信協(xié)議
簡述
- 通過三次握手,四次揮手達到面向連接
- 通過校驗和確保數(shù)據(jù)在傳輸過程中沒有被修改
- 通過序號確保順序
- 通過確認機制+重傳機制確保數(shù)據(jù)到達
- 通過流量窗口+擁塞控制機制確保通信質(zhì)量
原理與應用
TCP協(xié)議的目的是:在不可靠傳輸?shù)腎P層之上建立一套可靠傳輸?shù)臋C制。 TCP的可靠只是對于它自身來說的, 甚至是對于socket接口層, 兩個系統(tǒng)就不是可靠的了, 因為發(fā)送出去的數(shù)據(jù), 沒有確保對方真正的讀到(所以要在業(yè)務層做重傳和確認機制)。
可靠傳輸?shù)牡谝灰厥?strong>確認, 第二要素是重傳, 第三要素是順序。 任何一個可靠傳輸?shù)南到y(tǒng), 都必須包含這三個要素。數(shù)據(jù)校驗也是必要的。
傳輸是一個廣義的概念, 不局限于狹義的網(wǎng)絡傳輸, 應該理解為通信和交互. 任何涉及到通信和交互的東西, 都可以借鑒TCP的思想。無論是在UDP上實現(xiàn)可靠傳輸或者創(chuàng)建自己的通信系統(tǒng),無論這個系統(tǒng)是以API方式還是服務方式,只要是一個通信系統(tǒng),就要考慮這三個要素。
TCP頭結(jié)構(gòu)


- 需要四個元組(src_ip, src_port, dest_ip, dest_port)來表示同一個連接。準確的說是五個元組,還有一個是協(xié)議,可以同一個IP,同一臺機器,同時使用TCP和UDP監(jiān)聽同一個端口,因為IP header中有個字段是指定使用哪個協(xié)議的
- Sequence Number:數(shù)據(jù)包的序號,用來解決網(wǎng)絡亂序問題
- Acknowledgement Number:簡稱ACK,用來確認數(shù)據(jù)包是否已收到,解決數(shù)據(jù)包丟失問題
- Window:又叫Advertised-Window,也就是著名的滑動窗口(Sliding Window),用于解決流控
- Checksum:校驗和,用于檢查數(shù)據(jù)包在傳輸過程中是否被修改過
- TCP Flag:數(shù)據(jù)包的類型,主要是用于操控TCP的狀態(tài)機
數(shù)據(jù)傳輸中的序號

SeqNum的增加是和傳輸?shù)淖止?jié)數(shù)相關的。上圖中,三次握手后,來了兩個Len:1440的包,而第二個包的SeqNum就成了1441。然后第一個ACK回的是1441(下一個待接收的字節(jié)號),表示第一個1440收到了。
TCP狀態(tài)機
網(wǎng)絡上的傳輸是沒有連接的,包括TCP也是一樣的。而TCP所謂的“連接”,其實只不過是在通訊的雙方維護一個“連接狀態(tài)”,讓它看上去好像有連接一樣。所以,TCP的狀態(tài)變換是非常重要的。


狀態(tài)表
| 狀態(tài) | 說明 |
|---|---|
| CLOSED | 關閉狀態(tài),沒有連接活動 |
| LISTEN | 監(jiān)聽狀態(tài),服務器正在等待連接進入 |
| SYN_SENT | 已經(jīng)發(fā)出連接請求,等待確認 |
| SYN_RCVD | 收到一個連接請求,尚未確認 |
| ESTABLISHED | 連接建立,正常數(shù)據(jù)傳輸狀態(tài) |
| FIN_WAIT_1 | (主動關閉)已經(jīng)發(fā)送關閉請求,等待確認 |
| FIN_WAIT_2 | (主動關閉)收到對方關閉確認,等待對方關閉請求 |
| CLOSE_WAIT | (被動關閉)收到對方關閉請求,已經(jīng)確認 |
| LAST_ACK | (被動關閉)等待最后一個關閉確認,并等待所有分組死掉 |
| TIMED_WAIT | 完成雙向關閉,等待所有分組死掉 |
| CLOSING | 雙方同時嘗試關閉,等待對方確認 |
查看各種狀態(tài)的數(shù)量
ss -ant | awk '{++s[$1]} END {for(k in s) print k,s[k]}'
建立連接
三次握手
通過三次握手完成連接的建立
- 客戶端發(fā)送SYN(x)
- 服務端返回ACK(x+1), SYN(y)
- 客戶端返回ACK(y+1)
三次握手的目的是交換通信雙方的初始化序號,以保證應用層接收到的數(shù)據(jù)不會亂序,所以叫SYN(Synchronize Sequence Numbers)。
為什么要三次握手
- 問題的本質(zhì)是:信道不可靠,但通信雙方需要就某個問題達成一致。而要解決這個問題,三次通信是理論的最小值。原則上任何數(shù)據(jù)傳輸都無法確保絕對可靠,三次握手只是確??煽康幕拘枰ㄈ我呀?jīng)能夠保證足夠高的可靠性概率,四次就有點多余)
-
通信雙方都需要確認自己的發(fā)信和收信功能正常,其中發(fā)信功能需要發(fā)出信息并得到對方確認,收信功能通過接收對方信息得到確認。最簡單的例子就是雙方打電話,一方先問:“喂,聽到嗎?”,另外一方收到后(此時確定自己接聽沒有問題,但不知說話是否有問題),回復:“聽得到,你呢,能聽到我說話嗎?“,第一個人聽到后(確定自己的發(fā)送和接收都沒有問題)回復:”聽得到“,另外一方收到后確定自己的發(fā)送也是沒有問題的,之后雙方就可以愉快地談話了。
- 只通信兩次不行,因為只通信兩次,有可能造成已失效的連接請求報文段突然又傳送到了服務端,而產(chǎn)生錯誤的問題:client發(fā)出的第一個連接請求報文段并沒有丟失,而是在某個網(wǎng)絡結(jié)點長時間的滯留了,以致延誤到連接釋放以后的某個時間才到達server。本來這是一個早已失效的報文段。但server收到此失效的連接請求報文段后,就誤認為是client再次發(fā)出的一個新的連接請求。于是就向client發(fā)出確認報文段,同意建立連接。假設不采用“三次握手”,那么只要server發(fā)出確認,新的連接就建立了。由于現(xiàn)在client并沒有發(fā)出建立連接的請求,因此不會理睬server的確認,也不會向server發(fā)送數(shù)據(jù)。但server卻以為新的運輸連接已經(jīng)建立,并一直等待client發(fā)來數(shù)據(jù)。這樣,server的很多資源就白白浪費掉了。而采用三次握手可以避免此問題,client不會向server的確認發(fā)出確認。server由于收不到確認,就知道client并沒有要求建立連接。
ISN初始化
ISN是不能hard code的,不然會出問題的。比如:如果連接建好后始終用1來做ISN,如果client發(fā)了30個segment過去,但是網(wǎng)絡斷了,于是client重連,又用了1做ISN,但是之前連接的那些包到了,于是就被當成了新連接的包,此時,client的Sequence Number可能是3,而Server端認為client端的這個號是30了。全亂了。RFC793中說,ISN會和一個假的時鐘綁在一起,這個時鐘會在每4微秒對ISN做加一操作,直到超過232,又從0開始。這樣,一個ISN的周期大約是4.55個小時。因為,我們假設我們的TCP Segment在網(wǎng)絡上的存活時間不會超過Maximum Segment Lifetime(MSL),所以,只要MSL的值小于4.55小時,那么,我們就不會重用到ISN。
SYN超時
如果Server端接到了Clien發(fā)的SYN后回了SYN-ACK,之后Client掉線了,Server端沒有收到Client返回的ACK,那么,這個連接就處于一個中間狀態(tài),即沒成功,也沒失敗。于是,Server端如果在一定時間內(nèi)沒有收到的ACK會重發(fā)SYN-ACK。在Linux下,默認重試次數(shù)為5次,重試的間隔時間從1s開始每次都翻番,5次的重試時間間隔為1s, 2s, 4s, 8s, 16s,總共31s,第5次發(fā)出后還要等32s都知道第5次也超時了,所以,總共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 26 -1 = 63s,TCP才會斷開這個連接。
SYN Flood攻擊
客戶端給服務器發(fā)了一個SYN后,就下線了,于是服務器需要默認等63s才會斷開連接,這樣,攻擊者就可以把服務器的SYN連接的隊列耗盡,讓正常的連接請求不能處理。
于是,Linux下給了一個叫tcp_syncookies的參數(shù)來應對這個事:當SYN隊列滿了后,TCP會通過源地址端口、目標地址端口和時間戳打造出一個特別的Sequence Number發(fā)回去(又叫cookie),此時服務器并沒有保留客戶端的SYN包。如果是攻擊者則不會有響應,如果是正常連接,則會把這個SYN Cookie發(fā)回來,然后服務端可以通過cookie建連接(即使你不在SYN隊列中)。
千萬別用tcp_syncookies來處理正常的大負載的連接的情況。因為sync cookies是妥協(xié)版的TCP協(xié)議,并不嚴謹。應該調(diào)整三個TCP參數(shù):tcp_synack_retries減少重試次數(shù),tcp_max_syn_backlog增大SYN連接數(shù),tcp_abort_on_overflow處理不過來干脆就直接拒絕連接
斷開連接
因為TCP是全雙工的,因此斷開連接需要4次揮手,發(fā)送方和接收方都需要發(fā)送Fin和Ack。如果兩邊同時斷連接,那就會就進入到CLOSING狀態(tài),然后到達TIME_WAIT狀態(tài)。

MSL
指的是報文段的最大生存時間,如果報文段在網(wǎng)絡中活動了MSL時間,還沒有被接收,那么會被丟棄。關于MSL的大小,RFC 793協(xié)議中給出的建議是兩分鐘,不過實際上不同的操作系統(tǒng)可能有不同的設置,以Linux為例,通常是半分鐘,兩倍的MSL就是一分鐘,也就是60秒
TIME_WAIT狀態(tài)
主動關閉的一方會進入TIME_WAIT狀態(tài),并且在此狀態(tài)停留兩倍的MSL時長。由于TIME_WAIT的存在,大量短連接會占有大量的端口,造成無法新建連接。
存在的意義
- TIME_WAIT確保有足夠的時間讓對端收到了ACK,如果被動關閉的那方?jīng)]有收到Ack,就會觸發(fā)被動端重發(fā)Fin,一來一去正好2個MSL
- 有足夠的時間讓這個連接不會跟后面的連接混在一起(有些自做主張的路由器會緩存IP數(shù)據(jù)包,如果連接被重用了,那么這些延遲收到的包就有可能會跟新連接混在一起)??梢钥纯催@篇文章《TIME_WAIT and its design implications for protocols and scalable client server systems》
解決辦法
- 盡量重用已有連接:不要頻繁地創(chuàng)建和關閉連接,例如啟用KeepAlive機制或者使用連接池機制
- 增大可用端口數(shù)
sysctl -a | grep portnet.ipv4.ip_local_port_range = 32768 61000
那么可用的端口數(shù)為 (61000-32768+1)=28223
可通過設置sysctl net.ipv4.ip_local_port_range=10240 61000來增大可用端口的范圍 - 啟用SO_LINGER
這個選項有點危險,會導致數(shù)據(jù)丟失。
關閉選項,close調(diào)用會立刻返回給調(diào)用者,如果發(fā)送緩沖區(qū)還有數(shù)據(jù),系統(tǒng)將繼續(xù)發(fā)送。這是默認情況
啟用選項,并設置linger=0,TCP將丟棄保留在發(fā)送緩沖區(qū)的數(shù)據(jù)并發(fā)送一個RST給對方,而不是正常的4次揮手,從而避免了TIME_WAIT狀態(tài)。注意的是只有發(fā)送緩沖區(qū)有數(shù)據(jù)的情況下才發(fā)送RST,沒有數(shù)據(jù)的情況還是走正常流程。
啟用選項,并設置linger的值大于0,當關閉連接時,將延遲一段時間(linger的值)。如果發(fā)送緩沖區(qū)還有數(shù)據(jù),進程將處于等待狀態(tài),直到1)所有數(shù)據(jù)都發(fā)送完且被對方確認,之后正常關閉2)延遲時間到,此時數(shù)據(jù)會被丟棄。 - 啟用tcp_tw_recyle,回收TIME_WAIT連接
TCP有一種行為,可以緩存每個主機最新的時間戳,后續(xù)請求中如果時間戳小于緩存的時間戳,即視為無效,相應的數(shù)據(jù)包會被丟棄。要啟用這種行為,要同時設置tcp_timestamps=1和tcp_tw_recycle=1,缺一不可。啟用后,60s內(nèi)同一源ip主機的socket connect請求中的timestamp必須是遞增的。單獨啟用tcp_tw_recycle,而關閉tcp_timestamps,是不起作用的。這個設置會導致一些問題:當多個客戶端通過NAT方式聯(lián)網(wǎng)并與服務端交互時,服務端看到的是同一個IP,也就是說對服務端而言這些客戶端實際上等同于一個,可惜由于這些客戶端的時間戳可能存在差異,于是乎從服務端的視角看,便可能出現(xiàn)時間戳錯亂的現(xiàn)象,進而直接導致時間戳小的數(shù)據(jù)包被丟棄。具體的表現(xiàn)通常是有些客戶端連接成功,有些失敗。具體有多少被drop的包呢?使用netstat -s的其中一行7439 packets rejects in established connections because of timestamp - 啟用tcp_tw_reuse,復用TIME_WAIT連接
當創(chuàng)建新連接的時候,如果可能的話會考慮復用相應的TIME_WAIT連接
1)TIME_WAIT創(chuàng)建時間必須超過一秒才可能會被復用
2)只有連接的時間戳是遞增的時候才會被復用。
如果使用tcp_tw_reuse,必需設置tcp_timestamps=1
要復用連接,應該在連接的發(fā)起方使用,而不能在被連接方使用。舉例來說:客戶端向服務端發(fā)起HTTP請求,服務端響應后主動關閉連接,于是TIME_WAIT便留在了服務端,此類情況使用「tcp_tw_reuse」是無效的,因為服務端是被連接方,所以不存在復用連接一說。比如說服務端是PHP,它查詢另一個MySQL服務端,然后主動斷開連接,于是TIME_WAIT就落在了PHP一側(cè),此類情況下使用「tcp_tw_reuse」是有效的 - 設置tcp_max_tw_buckets,控制TIME_WAIT總數(shù)
默認值是180000,如果超限,那么系統(tǒng)會把多的給destory掉,然后在日志里打一個警告(如:time wait bucket table overflow)官網(wǎng)文檔說這個參數(shù)是用來對抗DDoS攻擊的,平常不要人為的降低它。
CLOSE_WAIT狀態(tài)
主動關閉的一方發(fā)出 FIN包,被動關閉的一方響應ACK包,此時,被動關閉的一方就進入了CLOSE_WAIT狀態(tài)。如果一切正常,稍后被動關閉的一方也會發(fā)出FIN包,然后遷移到LAST_ACK狀態(tài)。
CLOSE_WAIT狀態(tài)在服務器停留時間很短,如果你發(fā)現(xiàn)大量的 CLOSE_WAIT狀態(tài),那么就意味著被動關閉的一方?jīng)]有及時發(fā)出FIN包。
解決辦法
- 程序問題:如果代碼層面忘記了close相應的連接,那么自然不會發(fā)出FIN包,從而導致CLOSE_WAIT累積
- 響應太慢或者超時設置過?。喝绻B接雙方不和諧,一方不耐煩直接 timeout,另一方卻還在忙于耗時邏輯,就會導致close被延后
- accept backlog太大:如果backlog太大的話,設想突然遭遇大訪問量的話,即便響應速度不慢,也可能出現(xiàn)來不及消費的情況,導致多余的請求還在隊列里就被對方關閉了
重傳機制
TCP要保證所有的數(shù)據(jù)包都可以到達,所以,必需要有重傳機制。
接收端給發(fā)送端的Ack確認只會確認最后一個連續(xù)的包,比如,發(fā)送端發(fā)了1,2,3,4,5一共五份數(shù)據(jù),接收端收到了1,2,于是回ack 3,然后收到了4(注意此時3沒收到),此時的TCP會怎么辦?我們要知道,因為正如前面所說的,SeqNum和Ack是以字節(jié)數(shù)為單位,所以ack的時候,不能跳著確認,只能確認最大的連續(xù)收到的包,不然,發(fā)送端就以為之前的都收到了
超時重傳機制
- 一種是僅重傳timeout的包。也就是第3份數(shù)據(jù)。節(jié)省帶寬,但是慢(后面的也有可能是超時了)
- 一種是重傳timeout后所有的數(shù)據(jù),也就是第3,4,5這三份數(shù)據(jù)??煲稽c,但是會浪費帶寬,也可能會有無用功
但總體來說都不好。因為都在等timeout,timeout可能會很長
快速重傳機制
不以時間驅(qū)動,而以數(shù)據(jù)驅(qū)動重傳
如果包沒有連續(xù)到達,就ack最后那個可能被丟了的包,如果發(fā)送方連續(xù)收到3次相同的ack,就重傳
SACK方法
Selective Acknowledgment, 需要在TCP頭里加一個SACK的東西,ACK還是Fast Retransmit的ACK,SACK則是匯報收到的數(shù)據(jù)碎版,在發(fā)送端就可以根據(jù)回傳的SACK來知道哪些數(shù)據(jù)到了,哪些沒有收到
Duplicate SACK
重復收到數(shù)據(jù)的問題,使用了SACK來告訴發(fā)送方有哪些數(shù)據(jù)被重復接收了
Timeout的設置
- 設長了,重發(fā)就慢,丟了老半天才重發(fā),沒有效率,性能差
- 設短了,會導致可能并沒有丟就重發(fā)。于是重發(fā)的就快,會增加網(wǎng)絡擁塞,導致更多的超時,更多的超時導致更多的重發(fā)
經(jīng)典算法:Karn/Partridge算法,Jacobson/Karels算法
流量控制
滑動窗口
TCP必需要知道網(wǎng)絡實際的數(shù)據(jù)處理帶寬或是數(shù)據(jù)處理速度,這樣才不會引起網(wǎng)絡擁塞,導致丟包
Advertised-Window:接收端告訴發(fā)送端自己還有多少緩沖區(qū)可以接收數(shù)據(jù)。于是發(fā)送端就可以根據(jù)這個接收端的處理能力來發(fā)送數(shù)據(jù),而不會導致接收端處理不過來

接收端LastByteRead指向了TCP緩沖區(qū)中讀到的位置,NextByteExpected指向的地方是收到的連續(xù)包的最后一個位置,LastByteRcved指向的是收到的包的最后一個位置,我們可以看到中間有些數(shù)據(jù)還沒有到達,所以有數(shù)據(jù)空白區(qū)。
發(fā)送端的LastByteAcked指向了被接收端Ack過的位置(表示成功發(fā)送確認),LastByteSent表示發(fā)出去了,但還沒有收到成功確認的Ack,LastByteWritten指向的是上層應用正在寫的地方。
接收端在給發(fā)送端回ACK中會匯報自己的AdvertisedWindow = MaxRcvBuffer – LastByteRcvd – 1;

- 黑模型就是滑動窗口
- 1已收到ack確認的數(shù)據(jù)
- 2發(fā)還沒收到ack的
- 3在窗口中還沒有發(fā)出的(接收方還有空間)
- 4窗口以外的數(shù)據(jù)(接收方?jīng)]空間)
收到36的ack,并發(fā)出了46-51的字節(jié)

Zero Window
如果Window變成0了,發(fā)送端就不發(fā)數(shù)據(jù)了
如果發(fā)送端不發(fā)數(shù)據(jù)了,接收方一會兒Window size 可用了,怎么通知發(fā)送端呢:TCP使用了Zero Window Probe技術(shù),縮寫為ZWP,也就是說,發(fā)送端在窗口變成0后,會發(fā)ZWP的包給接收方,讓接收方來ack他的Window尺寸,一般這個值會設置成3次,每次大約30-60秒。如果3次過后還是0的話,有的TCP實現(xiàn)就會發(fā)RST把鏈接斷了。
Silly Window Syndrome(糊涂窗口綜合癥)
如果你的網(wǎng)絡包可以塞滿MTU,那么你可以用滿整個帶寬,如果不能,那么你就會浪費帶寬。避免對小的window size做出響應,直到有足夠大的window size再響應。
如果這個問題是由Receiver端引起的,那么就會使用David D Clark’s 方案。在receiver端,如果收到的數(shù)據(jù)導致window size小于某個值,可以直接ack(0)回sender,這樣就把window給關閉了,也阻止了sender再發(fā)數(shù)據(jù)過來,等到receiver端處理了一些數(shù)據(jù)后windows size大于等于了MSS,或者receiver buffer有一半為空,就可以把window打開讓send 發(fā)送數(shù)據(jù)過來。
如果這個問題是由Sender端引起的,那么就會使用著名的 Nagle’s algorithm。這個算法的思路也是延時處理,他有兩個主要的條件:1)要等到 Window Size >= MSS 或是 Data Size >= MSS,2)等待時間或是超時200ms,這兩個條件有一個滿足,他才會發(fā)數(shù)據(jù),否則就是在攢數(shù)據(jù)。
TCP_CORK是禁止小包發(fā)送,而Nagle算法沒有禁止小包發(fā)送,只是禁止了大量的小包發(fā)送
擁塞控制
TCP不是一個自私的協(xié)議,當擁塞發(fā)生的時候,要做自我犧牲
擁塞控制的論文請參看《Congestion Avoidance and Control》
主要算法有:慢啟動,擁塞避免,擁塞發(fā)生,快速恢復,TCP New Reno,F(xiàn)ACK算法,TCP Vegas擁塞控制算法
參考
TCP網(wǎng)絡協(xié)議及其思想的應用
TCP 的那些事兒(上)
TCP 的那些事兒(下)
tcp為什么是三次握手,為什么不是兩次或四次?
記一次TIME_WAIT網(wǎng)絡故障
再敘TIME_WAIT
tcp_tw_recycle和tcp_timestamps導致connect失敗問題
tcp短連接TIME_WAIT問題解決方法大全(1)- 高屋建瓴
tcp短連接TIME_WAIT問題解決方法大全(2)- SO_LINGER
tcp短連接TIME_WAIT問題解決方法大全(3)- tcp_tw_recycle
tcp短連接TIME_WAIT問題解決方法大全(4)- tcp_tw_reuse
tcp短連接TIME_WAIT問題解決方法大全(5)- tcp_max_tw_buckets
TCP的TIME_WAIT快速回收與重用
淺談CLOSE_WAIT
又見CLOSE_WAIT
PHP升級導致系統(tǒng)負載過高問題分析
Coping with the TCP TIME-WAIT state on busy Linux servers
