預(yù)備知識
TCP 頭格式
TCP的包是沒有IP地址的,那是IP層上的事哈垢。但是有源端口和目標(biāo)端口槽卫。
一個TCP連接需要四個元組來表示是同一個連接(src_ip, src_port, dst_ip, dst_port)準(zhǔn)確說是五元組菠净,還有一個是協(xié)議甘改。但因為這里只是說TCP協(xié)議呛凶,所以男娄,這里我只說四元組。
注意上圖中的四個非常重要的東西:
- Sequence Number是包的序號把兔,用來解決網(wǎng)絡(luò)包亂序(reordering)問題沪伙。
- Acknowledgement Number就是ACK——用于確認(rèn)收到,用來解決不丟包的問題县好。
- Window又叫Advertised-Window围橡,也就是著名的滑動窗口(Sliding Window),用于解決流控的缕贡。
- TCP Flag 翁授,也就是包的類型,主要是用于操控TCP的狀態(tài)機(jī)的晾咪。
TCP 特點
- 三次握手
- 四次揮手
- 可靠連接
- 丟包重傳
- TCP連接狀態(tài)
TCP的一個核心是:可靠傳輸協(xié)議收擦。
三次握手
- 第一步:client 發(fā)送 syn 到server 發(fā)起握手;
- 第二步:server 收到 syn后回復(fù)syn+ack給client谍倦;
- 第三步:client 收到syn+ack后塞赂,回復(fù)server一個ack表示收到了server的syn+ack。
ack總是seq+len(包的大兄缰)宴猾,這樣發(fā)送方明確知道server收到那些東西了,但是特例是三次握手和四次揮手叼旋,雖然len都是0仇哆,但是syn和fin都要占用一個seq號,所以這里的ack都是seq+1夫植。
tcp在傳輸過程中都有一個ack讹剔,接收方通過ack告訴發(fā)送方收到那些包了。這樣發(fā)送方能知道有沒有丟包详民,進(jìn)而確定重傳
- 關(guān)于ISN的初始化延欠。ISN是不能hard code的,不然會出問題的——比如:如果連接建好后始終用1來做ISN阐斜,如果client發(fā)了30個segment過去衫冻,但是網(wǎng)絡(luò)斷了,于是 client重連谒出,又用了1做ISN隅俘,但是之前連接的那些包到了邻奠,于是就被當(dāng)成了新連接的包,此時为居,client的Sequence Number 可能是3床蜘,而Server端認(rèn)為client端的這個號是30了钢猛。全亂了执庐。RFC793中說榴啸,ISN會和一個假的時鐘綁在一起,這個時鐘會在每4微秒對ISN做加一操作膳凝,直到超過2^32碑隆,又從0開始。這樣蹬音,一個ISN的周期大約是4.55個小時上煤。因為,我們假設(shè)我們的TCP Segment在網(wǎng)絡(luò)上的存活時間不會超過Maximum Segment Lifetime(縮寫為MSL - Wikipedia語條)著淆,所以劫狠,只要MSL的值小于4.55小時,那么永部,我們就不會重用到ISN独泞。
- 關(guān)于建連接時SYN超時。試想一下苔埋,如果server端接到了clien發(fā)的SYN后回了SYN-ACK后client掉線了懦砂,server端沒有收到client回來的ACK,那么组橄,這個連接處于一個中間狀態(tài)孕惜,即沒成功,也沒失敗晨炕。于是,server端如果在一定時間內(nèi)沒有收到的TCP會重發(fā)SYN-ACK毫炉。在Linux下瓮栗,默認(rèn)重試次數(shù)為5次,重試的間隔時間從1s開始每次都翻售瞄勾,5次的重試時間間隔為1s, 2s, 4s, 8s, 16s费奸,總共31s,第5次發(fā)出后還要等32s都知道第5次也超時了进陡,所以愿阐,總共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 2^6 -1 = 63s,TCP才會把斷開這個連接趾疚。
當(dāng)len 不為0 時候缨历,Client 和Server是這樣保證數(shù)據(jù)傳輸?shù)摹?/p>
Seq的增加是和傳輸?shù)淖止?jié)數(shù)相關(guān)的以蕴。上圖中,三次握手后辛孵,先來了一個len:43的包丛肮,Server返回的ack=seq(Client):1000 + len:43 = 1043。表示服務(wù)器收到第一個數(shù)據(jù)包魄缚。TCP依靠Seq 和Ack 宝与,保障了TCP傳輸數(shù)據(jù)包的完整性。
四次揮手
因為TCP是全雙工的冶匹,所以习劫,發(fā)送方和接收方都需要Fin和Ack。 ?簡單說來是 “先關(guān)讀嚼隘,后關(guān)寫”诽里,一共需要四個階段。以CLIENT發(fā)起關(guān)閉連接為例:
- 服務(wù)器讀通道關(guān)閉? : client主動發(fā)送Fin包給server
- 客戶機(jī)寫通道關(guān)閉? : server回復(fù)Ack(對應(yīng)第一步fin包的ack)給client嗓蘑,表示server知道client要斷開了
- 客戶機(jī)讀通道關(guān)閉? : server發(fā)送Fin包給client须肆,表示server也可以斷開了
- 服務(wù)器寫通道關(guān)閉 :client回復(fù)Ack給server。
丟包重傳
重傳這部分完全摘抄自TCP 的那些事兒(上) 桩皿,總結(jié)實在太好了豌汇!
TCP要保證所有的數(shù)據(jù)包都可以到達(dá),所以泄隔,必需要有重傳機(jī)制拒贱。
注意佛嬉,接收端給發(fā)送端的Ack確認(rèn)只會確認(rèn)最后一個連續(xù)的包逻澳,比如,發(fā)送端發(fā)了1,2,3,4,5一共五份數(shù)據(jù)暖呕,接收端收到了1斜做,2,于是回ack 3湾揽,然后收到了4(注意此時3沒收到)瓤逼,此時的TCP會怎么辦?我們要知道库物,因為正如前面所說的霸旗,SeqNum和Ack是以字節(jié)數(shù)為單位,所以ack的時候戚揭,不能跳著確認(rèn)诱告,只能確認(rèn)最大的連續(xù)收到的包,不然民晒,發(fā)送端就以為之前的都收到了精居。
超時重傳機(jī)制
一種是不回ack锄禽,死等3,當(dāng)發(fā)送方發(fā)現(xiàn)收不到3的ack超時后箱蟆,會重傳3沟绪。一旦接收方收到3后,會ack 回 4——意味著3和4都收到了空猜。
但是绽慈,這種方式會有比較嚴(yán)重的問題,那就是因為要死等3辈毯,所以會導(dǎo)致4和5即便已經(jīng)收到了坝疼,而發(fā)送方也完全不知道發(fā)生了什么事,因為沒有收到Ack谆沃,所以钝凶,發(fā)送方可能會悲觀地認(rèn)為也丟了,所以有可能也會導(dǎo)致4和5的重傳唁影。
對此有兩種選擇:
- 一種是僅重傳timeout的包耕陷。也就是第3份數(shù)據(jù)。
- 另一種是重傳timeout后所有的數(shù)據(jù)据沈,也就是第3哟沫,4,5這三份數(shù)據(jù)锌介。
*這兩種方式有好也有不好嗜诀。第一種會節(jié)省帶寬,但是慢孔祸,第二種會快一點隆敢,但是會浪費帶寬,也可能會有無用功崔慧。但總體來說都不好拂蝎。因為都在等timeout,timeout可能會很長(在下篇會說TCP是怎么動態(tài)地計算出timeout的)
快速重傳機(jī)制
Fast Retransmit
于是惶室,TCP引入了一種叫Fast Retransmit 的算法匣屡,不以時間驅(qū)動,而以數(shù)據(jù)驅(qū)動重傳拇涤。也就是說,如果誉结,包沒有連續(xù)到達(dá)鹅士,就ack最后那個可能被丟了的包,如果發(fā)送方連續(xù)收到3次相同的ack惩坑,就重傳掉盅。Fast Retransmit的好處是不用等timeout了再重傳也拜。
比如:如果發(fā)送方發(fā)出了1,2趾痘,3慢哈,4,5份數(shù)據(jù)永票,第一份先到送了卵贱,于是就ack回2,結(jié)果2因為某些原因沒收到侣集,3到達(dá)了键俱,于是還是ack回2,后面的4和5都到了世分,但是還是ack回2编振,因為2還是沒有收到,于是發(fā)送端收到了三個ack=2的確認(rèn)臭埋,知道了2還沒有到踪央,于是就馬上重轉(zhuǎn)2。然后瓢阴,接收端收到了2畅蹂,此時因為3,4炫掐,5都收到了魁莉,于是ack回6。示意圖如下:
Fast Retransmit只解決了一個問題募胃,就是timeout的問題旗唁,它依然面臨一個艱難的選擇,就是痹束,是重傳之前的一個還是重傳所有的問題检疫。對于上面的示例來說,是重傳#2呢還是重傳#2祷嘶,#3屎媳,#4,#5呢论巍?因為發(fā)送端并不清楚這連續(xù)的3個ack(2)是誰傳回來的烛谊?也許發(fā)送端發(fā)了20份數(shù)據(jù),是#6嘉汰,#10丹禀,#20傳來的呢。這樣,發(fā)送端很有可能要重傳從2到20的這堆數(shù)據(jù)(這就是某些TCP的實際的實現(xiàn))双泪〕炙眩可見,這是一把雙刃劍焙矛。
SACK 方法
另外一種更好的方式叫:Selective Acknowledgment (SACK)(參看RFC 2018)葫盼,這種方式需要在TCP頭里加一個SACK的東西,ACK還是Fast Retransmit的ACK村斟,SACK則是匯報收到的數(shù)據(jù)碎版贫导。參看下圖:
這樣,在發(fā)送端就可以根據(jù)回傳的SACK來知道哪些數(shù)據(jù)到了邓梅,哪些沒有到脱盲。于是就優(yōu)化了Fast Retransmit的算法。當(dāng)然日缨,這個協(xié)議需要兩邊都支持钱反。在 Linux下,可以通過tcp_sack參數(shù)打開這個功能(Linux 2.4后默認(rèn)打開)匣距。
這里還需要注意一個問題——接收方Reneging面哥,所謂Reneging的意思就是接收方有權(quán)把已經(jīng)報給發(fā)送端SACK里的數(shù)據(jù)給丟了。這樣干是不被鼓勵的毅待,因為這個事會把問題復(fù)雜化了尚卫,但是,接收方這么做可能會有些極端情況尸红,比如要把內(nèi)存給別的更重要的東西吱涉。所以,發(fā)送方也不能完全依賴SACK外里,還是要依賴ACK怎爵,并維護(hù)Time-Out,如果后續(xù)的ACK沒有增長盅蝗,那么還是要把SACK的東西重傳鳖链,另外,接收端這邊永遠(yuǎn)不能把SACK的包標(biāo)記為Ack墩莫。
注意:SACK會消費發(fā)送方的資源芙委,試想,如果一個攻擊者給數(shù)據(jù)發(fā)送方發(fā)一堆SACK的選項狂秦,這會導(dǎo)致發(fā)送方開始要重傳甚至遍歷已經(jīng)發(fā)出的數(shù)據(jù)灌侣,這會消耗很多發(fā)送端的資源。詳細(xì)的東西請參看《TCP SACK的性能權(quán)衡》
Duplicate SACK – 重復(fù)收到數(shù)據(jù)的問題
Duplicate SACK又稱D-SACK裂问,其主要使用了SACK來告訴發(fā)送方有哪些數(shù)據(jù)被重復(fù)接收了侧啼。RFC-2883 里有詳細(xì)描述和示例玖姑。下面舉幾個例子(來源于RFC-2883)
D-SACK使用了SACK的第一個段來做標(biāo)志,
如果SACK的第一個段的范圍被ACK所覆蓋慨菱,那么就是D-SACK
如果SACK的第一個段的范圍被SACK的第二個段覆蓋,那么就是D-SACK
示例一:ACK丟包
下面的示例中戴甩,丟了兩個ACK符喝,所以,發(fā)送端重傳了第一個數(shù)據(jù)包(3000-3499)甜孤,于是接收端發(fā)現(xiàn)重復(fù)收到协饲,于是回了一個SACK=3000-3500,因為ACK都到了4000意味著收到了4000之前的所有數(shù)據(jù)缴川,所以這個SACK就是D-SACK——旨在告訴發(fā)送端我收到了重復(fù)的數(shù)據(jù)茉稠,而且我們的發(fā)送端還知道,數(shù)據(jù)包沒有丟把夸,丟的是ACK包而线。
Transmitted Received ACK Sent
Segment Segment (Including SACK Blocks)
3000-3499 3000-3499 3500 (ACK dropped)
3500-3999 3500-3999 4000 (ACK dropped)
3000-3499 3000-3499 4000, SACK=3000-3500
** 示例二,網(wǎng)絡(luò)延誤**
下面的示例中恋日,網(wǎng)絡(luò)包(1000-1499)被網(wǎng)絡(luò)給延誤了膀篮,導(dǎo)致發(fā)送方?jīng)]有收到ACK,而后面到達(dá)的三個包觸發(fā)了“Fast Retransmit算法”岂膳,所以重傳誓竿,但重傳時,被延誤的包又到了谈截,所以筷屡,回了一個SACK=1000-1500,因為ACK已到了3000簸喂,所以毙死,這個SACK是D-SACK——標(biāo)識收到了重復(fù)的包。
這個案例下娘赴,發(fā)送端知道之前因為“Fast Retransmit算法”觸發(fā)的重傳不是因為發(fā)出去的包丟了规哲,也不是因為回應(yīng)的ACK包丟了,而是因為網(wǎng)絡(luò)延時了诽表。
Transmitted Received ACK Sent
Segment Segment (Including SACK Blocks)
500-999 500-999 1000
1000-1499 (delayed)
1500-1999 1500-1999 1000, SACK=1500-2000
2000-2499 2000-2499 1000, SACK=1500-2500
2500-2999 2500-2999 1000, SACK=1500-3000
1000-1499 1000-1499 3000
1000-1499 3000, SACK=1000-1500
可見唉锌,引入了D-SACK,有這么幾個好處:
1)可以讓發(fā)送方知道竿奏,是發(fā)出去的包丟了袄简,還是回來的ACK包丟了。
2)是不是自己的timeout太小了泛啸,導(dǎo)致重傳绿语。
3)網(wǎng)絡(luò)上出現(xiàn)了先發(fā)的包后到的情況(又稱reordering)
4)網(wǎng)絡(luò)上是不是把我的數(shù)據(jù)包給復(fù)制了。
知道這些東西可以很好得幫助TCP了解網(wǎng)絡(luò)情況,從而可以更好的做網(wǎng)絡(luò)上的流控吕粹。
TCP連接狀態(tài)
圖片來源自TCP網(wǎng)絡(luò)關(guān)閉的狀態(tài)變換時序圖
建立連接
- 正常情況
1.1 S調(diào)用listen開啟監(jiān)聽种柑,S的狀態(tài)由CLOSED--->LISTEN。
1.2 C調(diào)用connect發(fā)起連接匹耕,即發(fā)送SYN_1給S聚请,C的狀態(tài)由CLOSED---->SYN_SENT。
1.3 S收到SYN_1稳其,把此連接信息放入未完成連接隊列(incomplete connection queue)驶赏,回復(fù)SYN_2和ACK_1(SYN_1 + 1),S的狀態(tài)由LISTEN--->SYN_RCVD
1.4 C收到ACK_1和SYN_2既鞠,回復(fù)ACK_2(SYN_2 + 1)煤傍,C的狀態(tài)由SYN_SEND--->ESTABLISHED。
1.5 S收到ACK_2嘱蛋,把此連接信息從未完成連接隊列移除蚯姆,并放入完成連接隊列(complete connection queue),由listen函數(shù)的backlog控制完成連接隊列大小浑槽。S的狀態(tài)由SYN_RCVD--->ESTABLISHED蒋失。
1.6 S可以調(diào)用accept從完成連接隊列獲取已建立好的連接。
- 同時建立連接
2.1 S1和S2同時發(fā)送SYN(其中S1發(fā)送SYN_1桐玻,S2發(fā)送SYN_2)篙挽,S1和S2同時由LISTEN--->SYN_SENT。
2.2 S1收到SYN_2镊靴,回復(fù)SYN_1和ACK_1(SYN_2 + 1)铣卡,S1的狀態(tài)由SYN_SEND--->SYN_RCVD。
2.3 S2收到SYN_1偏竟,回復(fù)SYN_2和ACK_2(SYN_1 + 1)煮落,S2的狀態(tài)由SYN_SEND--->SYN_RCVD。
2.4 S2收到ACK_1和SYN_1踊谋,S2的狀態(tài)由SYN_RCVD--->ESTABLISHED蝉仇。
2.5 S1收到ACK_2和SYN_2,S1的狀態(tài)由SYN_RCVD--->ESTABLISHED殖蚕。
斷開連接
- 正常情況
1.1 C調(diào)用close轿衔,發(fā)送FIN_1,C的狀態(tài)由ESTABLISHED--->FIN_WAIT_1睦疫。
1.2 S收到FIN_1害驹,發(fā)送ACK_1(FIN_1 + 1),S的狀態(tài)由ESTABLISHED--->CLOSE_WAIT蛤育。
1.3 C收到ACK_1宛官,C的狀態(tài)由FIN_WAIT_1--->FIN_WAIT_2葫松。
1.4 S調(diào)用close,發(fā)送FIN_2底洗,S的狀態(tài)由CLOSE_WAIT--->LAST_ACK腋么。
1.5 C收到FIN_2, 發(fā)送ACK_2(FIN_2 + 1)亥揖,C的狀態(tài)由FIN_WAIT_2--->TIME_WAIT党晋。
1.6 S收到ACK_2,S狀態(tài)由CLOSE_WAIT--->CLOSED徐块。
1.7 C經(jīng)過2個MSL(Maximum Segment Lifetime),之所以要等待2個MSL灾而,是因為S在發(fā)送FIN_2后胡控,會等待1個MSL,如果1個MSL內(nèi)未收到ACK_2旁趟,則S認(rèn)為FIN_2丟包了昼激,會重發(fā)FIN_2,S重發(fā)的FIN_2到達(dá)C最大的時間為2個MSL锡搜,因此如果C在2個MSL內(nèi)未收到FIN_2橙困,則認(rèn)為S已收到ACK_2。C的狀態(tài)由TIME_WAIT--->CLOSED耕餐。- 特殊情況
2.1 C調(diào)用close凡傅,發(fā)送FIN_1,C的狀態(tài)由ESTABLISHED--->FIN_WAIT_1肠缔。
2.2 S收到FIN_1夏跷,S調(diào)用close,發(fā)送FIN_2的同時帶上ACK_1(FIN_1 + 1)明未,S的狀態(tài)由ESTABLISHED--->LAST_ACK槽华。
2.3 C收到FIN_2和ACK_1,發(fā)送ACK_2(FIN_2 + 1)趟妥,C的狀態(tài)由FIN_WAIT_1--->TIME_WAIT猫态。
2.4 S收到ACK_2,S狀態(tài)由CLOSE_WAIT--->CLOSED披摄。
2.5 C經(jīng)過2MSL亲雪,C的狀態(tài)由TIME_WAIT--->CLOSED。- 同時關(guān)閉
3.1 S1和S2同時調(diào)用close行疏,發(fā)送FIN(其中S1發(fā)送FIN_1匆光,S2發(fā)送FIN_2),S1和S2的狀態(tài)都由ESTABLISHED--->FIN_WAIT_1酿联。
3.2 S1收到FIN_2终息,發(fā)送ACK_1(FIN_2 + 1)夺巩,S1的狀態(tài)由FIN_WAIT_1--->CLOSING。
3.3 S2收到FIN_1周崭,發(fā)送ACK_2(FIN_1 + 1)柳譬,S2的狀態(tài)由FIN_WAIT_2--->CLOSING。
3.4 S2收到ACK_1续镇,S2的狀態(tài)由CLOSING--->TIME_WAIT美澳。
3.5 S1收到ACK_2,S1狀態(tài)由CLOSING--->TIME_WAIT摸航。
3.6 S1和S2經(jīng)過2MSL制跟,S1和S2的狀態(tài)由TIME_WAIT--->CLOSED。
Ref:
TCP網(wǎng)絡(luò)關(guān)閉的狀態(tài)變換時序圖
TCP 的那些事兒(上)
https://nmap.org/book/tcpip-ref.html