注意:本文摘抄自陳皓的:TCP那些事
關(guān)于TCP/IP協(xié)議層次劃分
我們需要知道TCP在網(wǎng)絡(luò)OSI的七層模型中的第四層——Transport層忿檩,IP在第三層——Network層蒸苇,ARP在第二層——Data Link層得封,在第二層上的數(shù)據(jù),我們叫Frame顽频,在第三層上的數(shù)據(jù)叫Packet威酒,第四層的數(shù)據(jù)叫Segment骗卜。
首先,我們需要知道震庭,我們程序的數(shù)據(jù)首先會打到TCP的Segment中瑰抵,然后TCP的Segment會打到IP的Packet中,然后再打到以太網(wǎng)Ethernet的Frame中器联,傳到對端后二汛,各個層解析自己的協(xié)議,然后把數(shù)據(jù)交給更高層的協(xié)議處理拨拓。
TCP頭格式
接下來肴颊,我們來看一下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ī)的担神。
很多人會問楼吃,為什么建鏈接要3次握手,斷鏈接需要4次揮手妄讯?
對于建鏈接的3次握手孩锡,主要是要初始化Sequence Number的初始值。通信的雙方要互相通知對方自己的初始化的Sequence Number(縮寫為ISN:Inital Sequence Number)——所以叫SYN亥贸,全稱Synchronize Sequence Numbers躬窜。這個號要作為以后的數(shù)據(jù)通信的序號,以保證應(yīng)用層接收到的數(shù)據(jù)不會因為網(wǎng)絡(luò)上的傳輸?shù)膯栴}而亂序(TCP會用這個序號來拼接數(shù)據(jù))炕置。
對于4次揮手荣挨,其實你仔細(xì)看是2次,因為TCP是全雙工的朴摊,所以默垄,發(fā)送方和接收方都需要Fin和Ack。只不過甚纲,有一方是被動的口锭,所以看上去就成了所謂的4次揮手。如果兩邊同時斷連接介杆,那就會就進(jìn)入到CLOSING狀態(tài)鹃操,然后到達(dá)TIME_WAIT狀態(tài)。下圖是雙方同時斷連接的示意圖(你同樣可以對照著TCP狀態(tài)機(jī)看):
另外春哨,有幾個事情需要注意一下:
關(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才會把斷開這個連接箫爷。
關(guān)于SYN Flood攻擊嚷节。一些惡意的人就為此制造了SYN Flood攻擊——給服務(wù)器發(fā)了一個SYN后,就下線了虎锚,于是服務(wù)器需要默認(rèn)等63s才會斷開連接硫痰,這樣,攻擊者就可以把服務(wù)器的syn連接的隊列耗盡窜护,讓正常的連接請求不能處理效斑。于是,Linux下給了一個叫tcp_syncookies的參數(shù)來應(yīng)對這個事——當(dāng)SYN隊列滿了后柱徙,TCP會通過源地址端口缓屠、目標(biāo)地址端口和時間戳打造出一個特別的Sequence Number發(fā)回去(又叫cookie),如果是攻擊者則不會有響應(yīng)护侮,如果是正常連接敌完,則會把這個 SYN Cookie發(fā)回來,然后服務(wù)端可以通過cookie建連接(即使你不在SYN隊列中)羊初。請注意蠢挡,請先千萬別用tcp_syncookies來處理正常的大負(fù)載的連接的情況。因為凳忙,synccookies是妥協(xié)版的TCP協(xié)議,并不嚴(yán)謹(jǐn)禽炬。對于正常的請求涧卵,你應(yīng)該調(diào)整三個TCP參數(shù)可供你選擇,第一個是:tcp_synack_retries 可以用他來減少重試次數(shù)腹尖;第二個是:tcp_max_syn_backlog柳恐,可以增大SYN連接數(shù);第三個是:tcp_abort_on_overflow 處理不過來干脆就直接拒絕連接了热幔。
關(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)于 MSL 和 TIME_WAIT。通過上面的ISN的描述葱色,相信你也知道MSL是怎么來的了递宅。我們注意到,在TCP的狀態(tài)圖中苍狰,從TIME_WAIT狀態(tài)到CLOSED狀態(tài)办龄,有一個超時設(shè)置,這個超時設(shè)置是 2*MSL(RFC793定義了MSL為2分鐘淋昭,Linux設(shè)置成了30s)為什么要這有TIME_WAIT俐填?為什么不直接給轉(zhuǎn)成CLOSED狀態(tài)呢?主要有兩個原因:1)TIME_WAIT確保有足夠的時間讓對端收到了ACK翔忽,如果被動關(guān)閉的那方?jīng)]有收到Ack英融,就會觸發(fā)被動端重發(fā)Fin,一來一去正好2個MSL歇式,2)有足夠的時間讓這個連接不會跟后面的連接混在一起(你要知道驶悟,有些自做主張的路由器會緩存IP數(shù)據(jù)包,如果連接被重用了材失,那么這些延遲收到的包就有可能會跟新連接混在一起)痕鳍。你可以看看這篇文章《TIME_WAIT and its design implications for protocols and scalable client server systems》
關(guān)于TIME_WAIT數(shù)量太多。從上面的描述我們可以知道龙巨,TIME_WAIT是個很重要的狀態(tài)额获,但是如果在大并發(fā)的短鏈接下够庙,TIME_WAIT 就會太多,這也會消耗很多系統(tǒng)資源抄邀。只要搜一下耘眨,你就會發(fā)現(xiàn),十有八九的處理方式都是教你設(shè)置兩個參數(shù)境肾,一個叫tcp_tw_reuse剔难,另一個叫tcp_tw_recycle的參數(shù),這兩個參數(shù)默認(rèn)值都是被關(guān)閉的奥喻,后者recyle比前者resue更為激進(jìn)偶宫,resue要溫柔一些。另外环鲤,如果使用tcp_tw_reuse纯趋,必需設(shè)置tcp_timestamps=1,否則無效冷离。這里吵冒,你一定要注意,打開這兩個參數(shù)會有比較大的坑——可能會讓TCP連接出一些詭異的問題(因為如上述一樣西剥,如果不等待超時重用連接的話痹栖,新的連接可能會建不上。正如官方文檔上說的一樣“It should not be changed without advice/request of technical experts”)瞭空。
TCP重傳機(jī)制
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ù)。
這種等待機(jī)制會帶來事件開銷.
快速重傳機(jī)制
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的問題,它依然面臨一個艱難的選擇盗忱,就是重轉(zhuǎn)之前的一個還是重裝所有的問題酱床。對于上面的示例來說,是重傳#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)衡》