TCP是一個(gè)巨復(fù)雜的協(xié)議,因?yàn)樗鉀Q很多問(wèn)題,而這些問(wèn)題又帶出了很多子問(wèn)題和陰暗面。所以學(xué)習(xí)TCP本身是個(gè)比較痛苦的過(guò)程,但對(duì)于學(xué)習(xí)的過(guò)程卻能讓人有很多收獲。
一、TCP協(xié)議的定義
TCP在網(wǎng)絡(luò)OSI的七層模型中的第四層——Transport層,IP在第三層——Network層,ARP在第二層——Data Link層,在第二層上的數(shù)據(jù),我們叫Frame,在第三層上的數(shù)據(jù)叫Packet,第四層的數(shù)據(jù)叫Segment。
首先,我們需要知道,我們程序的數(shù)據(jù)首先會(huì)打到TCP的Segment中,然后TCP的Segment會(huì)打到IP的Packet中,然后再打到以太網(wǎng)Ethernet的Frame中,傳到對(duì)端后,各個(gè)層解析自己的協(xié)議,然后把數(shù)據(jù)交給更高層的協(xié)議處理。
TCP頭格式
TCP協(xié)議是面向連接的協(xié)議,把連接作為最基本的抽象。每一條TCP連接唯一的被通信兩端的兩個(gè)端點(diǎn)所確定。TCP協(xié)議是點(diǎn)對(duì)點(diǎn),而不是點(diǎn)對(duì)多點(diǎn)。端點(diǎn)又被稱為套接字,TCP協(xié)議規(guī)定,IP地址拼接端口號(hào)就構(gòu)成套接字。
接下來(lái),我們來(lái)看一下TCP頭的格式:
1、seq:序號(hào)。TCP協(xié)議是面向字節(jié)流的,在一個(gè)TCP連接中的傳送的字節(jié)流的每一個(gè)字節(jié)都是按照順序編號(hào),seq需要占用4個(gè)字節(jié),所以范圍是[0 4294967296],序號(hào)可以重復(fù)使用。TCP規(guī)定,首部中序號(hào)字段值是本報(bào)文段所發(fā)送數(shù)據(jù)的第一個(gè)字節(jié)的序號(hào)。序號(hào)用于跟蹤該端發(fā)送的數(shù)據(jù)量。4294967296個(gè)字節(jié)(2的32次方),如果不重復(fù)利用就是4G的數(shù)據(jù)量。序號(hào)為當(dāng)前端成功發(fā)送的數(shù)據(jù)字節(jié)數(shù),確認(rèn)號(hào)為當(dāng)前端成功接收的數(shù)據(jù)字節(jié)數(shù),SYN標(biāo)志位和FIN標(biāo)志位也各自要占用1個(gè)序號(hào)。TCP的標(biāo)準(zhǔn)規(guī)定,ACK報(bào)文段可以攜帶數(shù)據(jù),但如果不攜帶數(shù)據(jù)則不消耗序號(hào)。A(客戶端)把自己的初始化序列號(hào)放在SYN數(shù)據(jù)包中發(fā)送給B(服務(wù)器),B收到后會(huì)回發(fā)一個(gè)SYN+ACK數(shù)據(jù)包,該數(shù)據(jù)包中B會(huì)把收到的來(lái)自A的序列號(hào)加一后的值發(fā)送回去(確認(rèn)號(hào)),同時(shí)數(shù)據(jù)包中也包含B的初始化序列號(hào),當(dāng)A收到數(shù)據(jù)后發(fā)送一個(gè)ACK數(shù)據(jù)包其中包含了B發(fā)過(guò)來(lái)的序列號(hào)加一后所得的數(shù)值(確認(rèn)號(hào))。
2、ACK:僅當(dāng)ACK=1時(shí)確認(rèn)字段才有效,當(dāng)ACK=0時(shí)確認(rèn)字段無(wú)效,并且TCP規(guī)定,在連接建立后所有的傳送報(bào)文段都必須要把ACK置為1。
3、SYN:同步序列號(hào),用來(lái)發(fā)起一個(gè)連接。當(dāng)SYN=1而ACK=0時(shí)表明這是一個(gè)請(qǐng)求報(bào)文段;若對(duì)方同意連接,則響應(yīng)報(bào)文中SYN=1,ACK=1。
4、FIN :用來(lái)釋放一個(gè)連接,當(dāng)FIN=1表示此報(bào)文段的發(fā)送方已經(jīng)發(fā)送完畢。并要求釋放鏈接。
5、SYN、ACK、FIN是標(biāo)志位,在屬性flag中,flag占用一個(gè)字節(jié)。含有SYN或FIN標(biāo)志位的包并不攜帶有效數(shù)據(jù)。
注:SYN位被啟動(dòng)時(shí),向?qū)Ψ礁嬷约海蛻舳嘶蛘叻?wù)器)的初始序列號(hào)以便對(duì)方知道如何接收自己發(fā)送過(guò)來(lái)的數(shù)據(jù)包。如果是ACK比特位被啟動(dòng),它表明數(shù)據(jù)包用于通知接收方我收到了你上次發(fā)來(lái)的數(shù)據(jù)。
你需要注意這么幾點(diǎn):
1、TCP的包是沒(méi)有IP地址的,那是IP層上的事。但是有源端口和目標(biāo)端口。
2、一個(gè)TCP連接需要四個(gè)元組來(lái)表示是同一個(gè)連接(src_ip, src_port, dst_ip, dst_port)準(zhǔn)確說(shuō)是五元組,還有一個(gè)是協(xié)議。但因?yàn)檫@里只是說(shuō)TCP協(xié)議,所以,這里我只說(shuō)四元組。
3、注意上圖中的四個(gè)非常重要的東西:
- Sequence Number是包的序號(hào),用來(lái)解決網(wǎng)絡(luò)包亂序(reordering)問(wèn)題。
- Acknowledgement Number就是ACK——用于確認(rèn)收到,用來(lái)解決不丟包的問(wèn)題。
- Window又叫Advertised-Window,也就是著名的滑動(dòng)窗口(Sliding Window),用于解決流控的。
- TCP Flag ,也就是包的類(lèi)型,主要是用于操控TCP的狀態(tài)機(jī)的。
關(guān)于其它的東西,可以參看下面的圖示:
TCP的狀態(tài)機(jī)
其實(shí),網(wǎng)絡(luò)上的傳輸是沒(méi)有連接的,包括TCP也是一樣的。而TCP所謂的“連接”,其實(shí)只不過(guò)是在通訊的雙方維護(hù)一個(gè)“連接狀態(tài)”,讓它看上去好像有連接一樣。所以,TCP的狀態(tài)變換是非常重要的。
可靠數(shù)據(jù)運(yùn)輸原理:
1、TCP協(xié)議發(fā)送兩種數(shù)據(jù)包,一種數(shù)據(jù)包用來(lái)傳輸數(shù)據(jù),一種數(shù)據(jù)包用來(lái)發(fā)送控制信息。TCP數(shù)據(jù)都會(huì)有一個(gè)包頭,包頭中有相應(yīng)標(biāo)志位,標(biāo)志位的設(shè)定用于表明數(shù)據(jù)包是用于數(shù)據(jù)發(fā)送還是用于傳輸控制信息。
2、TCP建立一個(gè)連接需要三個(gè)報(bào)文段:
情況1:防止已失效的請(qǐng)求報(bào)文段突然又傳送到了服務(wù)端而產(chǎn)生連接的誤判。
客戶端發(fā)送了一個(gè)連接請(qǐng)求報(bào)文段A到服務(wù)端,但是在某些網(wǎng)絡(luò)節(jié)點(diǎn)上長(zhǎng)時(shí)間滯留了,而后客戶端又超時(shí)重發(fā)了一個(gè)連接請(qǐng)求報(bào)文段B該服務(wù)端,而后 正常建立連接,數(shù)據(jù)傳輸完畢,并釋放了連接。但是請(qǐng)求報(bào)文段A延遲了一段時(shí)間后,又到了服務(wù)端,這本是一個(gè)早已失效的報(bào)文段,但是服務(wù)端收到后會(huì)誤以為客戶端又發(fā)出了一次連接請(qǐng)求,于是向客戶端發(fā)出確認(rèn)報(bào)文段,并同意建立連接。那么問(wèn)題來(lái)了,假如這里沒(méi)有三次握手,這時(shí)服務(wù)端只要發(fā)送了確認(rèn),新的連接就建立了,但由于客戶端沒(méi)有發(fā)出建立連接的請(qǐng)求,因此不會(huì)理會(huì)服務(wù)端的確認(rèn),也不會(huì)向服務(wù)端發(fā)送數(shù)據(jù),而服務(wù)端卻認(rèn)為新的連接已經(jīng)建立了,并在 一直等待客戶端發(fā)送數(shù)據(jù),這樣服務(wù)端就會(huì)一直等待下去,直到超出?;钣?jì)數(shù)器的設(shè)定值,而將客戶端判定為出了問(wèn)題,才會(huì)關(guān)閉這個(gè)連接。這樣就浪費(fèi)了很多服務(wù) 器的資源。而如果采用三次握手,客戶端就不會(huì)向服務(wù)端發(fā)出確認(rèn),服務(wù)端由于收不到確認(rèn),就知道客戶端沒(méi)有要求建立連接,從而不建立該連接。
情況2:防止形成死鎖。
服務(wù)器的SYN和ACK報(bào)文段沒(méi)有發(fā)送到客戶端,服務(wù)器認(rèn)為連接已經(jīng)建立,但是客戶端不知道服務(wù)器是否已準(zhǔn)備好,不知道服務(wù)器建立什么樣的序列號(hào)??蛻舳苏J(rèn)為連接還未建立成功,將忽略服務(wù)器發(fā)來(lái)的任何數(shù)據(jù)分組,只等待連接確認(rèn)應(yīng)答分組。而服務(wù)器在發(fā)出的分組超時(shí)后,重復(fù)發(fā)送同樣的分組。這樣就形成了死鎖。
3、TCP釋放一個(gè)連接卻需要四個(gè)報(bào)文段
第一個(gè)報(bào)文段,客戶端向服務(wù)器發(fā)送釋放連接報(bào)文段,釋放連接報(bào)文段FIN置1,此時(shí)客戶端序列號(hào)為w
第二個(gè)報(bào)文段,服務(wù)器回復(fù)確認(rèn)收到客戶端釋放連接報(bào)文段的報(bào)文段,確認(rèn)號(hào)u+1,之后客戶端不會(huì)像服務(wù)器發(fā)送報(bào)文段,但是服務(wù)器可以向客戶端發(fā)送報(bào)文段,全部發(fā)送完成服務(wù)器的序列號(hào)為v,客戶端的確認(rèn)號(hào)為v,但是客戶端序列號(hào)為w+1
第三個(gè)報(bào)文段,服務(wù)器向客戶端發(fā)送釋放連接報(bào)文段,服務(wù)器序列號(hào)v+1,確認(rèn)號(hào)還是w+1
第四個(gè)報(bào)文段,客戶端回復(fù)確認(rèn)收到服務(wù)器的釋放連接報(bào)文段的報(bào)文段,,客戶端的確認(rèn)號(hào)+1,序列號(hào)+1。
服務(wù)器收到第四個(gè)報(bào)文段,就進(jìn)入CLOSED狀態(tài);客戶端要等待最長(zhǎng)報(bào)文段壽命(2MSL),才進(jìn)入到CLOSED狀態(tài)。
注:
1、MSL是任何IP數(shù)據(jù)報(bào)能夠在網(wǎng)絡(luò)中存活的最長(zhǎng)時(shí)間,每個(gè)數(shù)據(jù)報(bào)含有一個(gè)稱為跳限(hop limit)的8位字段,它的最大值是255,即最大為255跳。具有最大跳限的數(shù)據(jù)報(bào)在網(wǎng)絡(luò)中存在的時(shí)間不可能超過(guò)MSL秒。
2、客戶端在TIME-WAIT狀態(tài)必須2MSL的時(shí)間的兩個(gè)理由:
可靠地實(shí)現(xiàn)TCP全雙工連接的終止:第四個(gè)報(bào)文段如果丟失,在2MSL時(shí)間內(nèi)服務(wù)器會(huì)重新發(fā)送第三報(bào)文段客戶端接收到之后重新發(fā)送第四報(bào)文段。
防止“已失效的連接請(qǐng)求報(bào)文段”出現(xiàn)在本連接中,在2MSL時(shí)間之后,本連接持續(xù)的時(shí)間內(nèi)所產(chǎn)生的所有報(bào)文段都會(huì)網(wǎng)絡(luò)中消失。因?yàn)橐粋€(gè)TCP端口不能同時(shí)被打開(kāi)多次,如果沒(méi)有TIME-WAIT狀態(tài),TCP端口關(guān)閉之后在TIME-WAIT時(shí)間內(nèi)重新打開(kāi),可能會(huì)接受原來(lái)還沒(méi)有消失的報(bào)文段,這是不能發(fā)生的?。。?/p>
下面是:“TCP協(xié)議的狀態(tài)機(jī)” 和 “TCP建鏈接”、“TCP斷鏈接”、“傳數(shù)據(jù)” 的對(duì)照?qǐng)D,我把兩個(gè)圖并排放在一起,這樣方便在你對(duì)照著看。另外,下面這兩個(gè)圖非常非常的重要,你一定要記牢。(吐個(gè)槽:看到這樣復(fù)雜的狀態(tài)機(jī),就知道這個(gè)協(xié)議有多復(fù)雜,復(fù)雜的東西總是有很多坑爹的事情,所以TCP協(xié)議其實(shí)也挺坑爹的)
很多人會(huì)問(wèn),為什么建鏈接要3次握手,斷鏈接需要4次揮手?
對(duì)于建鏈接的3次握手,主要是要初始化Sequence Number 的初始值。通信的雙方要互相通知對(duì)方自己的初始化的Sequence Number(縮寫(xiě)為ISN:Inital Sequence Number)——所以叫SYN,全稱Synchronize Sequence Numbers。也就上圖中的 x 和 y。這個(gè)號(hào)要作為以后的數(shù)據(jù)通信的序號(hào),以保證應(yīng)用層接收到的數(shù)據(jù)不會(huì)因?yàn)榫W(wǎng)絡(luò)上的傳輸?shù)膯?wèn)題而亂序(TCP會(huì)用這個(gè)序號(hào)來(lái)拼接數(shù)據(jù))。
對(duì)于4次揮手,其實(shí)你仔細(xì)看是2次,因?yàn)門(mén)CP是全雙工的,所以,發(fā)送方和接收方都需要Fin和Ack。只不過(guò),有一方是被動(dòng)的,所以看上去就成了所謂的4次揮手。如果兩邊同時(shí)斷連接,那就會(huì)就進(jìn)入到CLOSING狀態(tài),然后到達(dá)TIME_WAIT狀態(tài)。下圖是雙方同時(shí)斷連接的示意圖(你同樣可以對(duì)照著TCP狀態(tài)機(jī)看):
兩端同時(shí)斷連接
另外,有幾個(gè)事情需要注意一下:
- 關(guān)于建連接時(shí)SYN超時(shí)。試想一下,如果server端接到了clien發(fā)的SYN后回了SYN-ACK后client掉線了,server端沒(méi)有收到client回來(lái)的ACK,那么,這個(gè)連接處于一個(gè)中間狀態(tài),即沒(méi)成功,也沒(méi)失敗。于是,server端如果在一定時(shí)間內(nèi)沒(méi)有收到的TCP會(huì)重發(fā)SYN-ACK。在Linux下,默認(rèn)重試次數(shù)為5次,重試的間隔時(shí)間從1s開(kāi)始每次都翻售,5次的重試時(shí)間間隔為1s, 2s, 4s, 8s, 16s,總共31s,第5次發(fā)出后還要等32s都知道第5次也超時(shí)了,所以,總共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 2^6 -1 = 63s,TCP才會(huì)把斷開(kāi)這個(gè)連接。
- 關(guān)于SYN Flood攻擊。一些惡意的人就為此制造了SYN Flood攻擊——給服務(wù)器發(fā)了一個(gè)SYN后,就下線了,于是服務(wù)器需要默認(rèn)等63s才會(huì)斷開(kāi)連接,這樣,攻擊者就可以把服務(wù)器的syn連接的隊(duì)列耗盡,讓正常的連接請(qǐng)求不能處理。于是,Linux下給了一個(gè)叫tcp_syncookies的參數(shù)來(lái)應(yīng)對(duì)這個(gè)事——當(dāng)SYN隊(duì)列滿了后,TCP會(huì)通過(guò)源地址端口、目標(biāo)地址端口和時(shí)間戳打造出一個(gè)特別的Sequence Number發(fā)回去(又叫cookie),如果是攻擊者則不會(huì)有響應(yīng),如果是正常連接,則會(huì)把這個(gè) SYN Cookie發(fā)回來(lái),然后服務(wù)端可以通過(guò)cookie建連接(即使你不在SYN隊(duì)列中)。請(qǐng)注意,請(qǐng)先千萬(wàn)別用tcp_syncookies來(lái)處理正常的大負(fù)載的連接的情況。因?yàn)?,synccookies是妥協(xié)版的TCP協(xié)議,并不嚴(yán)謹(jǐn)。對(duì)于正常的請(qǐng)求,你應(yīng)該調(diào)整三個(gè)TCP參數(shù)可供你選擇,第一個(gè)是:tcp_synack_retries 可以用他來(lái)減少重試次數(shù);第二個(gè)是:tcp_max_syn_backlog,可以增大SYN連接數(shù);第三個(gè)是:tcp_abort_on_overflow 處理不過(guò)來(lái)干脆就直接拒絕連接了。
- 關(guān)于ISN的初始化。ISN是不能hard code的,不然會(huì)出問(wèn)題的——比如:如果連接建好后始終用1來(lái)做ISN,如果client發(fā)了30個(gè)segment過(guò)去,但是網(wǎng)絡(luò)斷了,于是 client重連,又用了1做ISN,但是之前連接的那些包到了,于是就被當(dāng)成了新連接的包,此時(shí),client的Sequence Number 可能是3,而Server端認(rèn)為client端的這個(gè)號(hào)是30了。全亂了。RFC793中說(shuō),ISN會(huì)和一個(gè)假的時(shí)鐘綁在一起,這個(gè)時(shí)鐘會(huì)在每4微秒對(duì)ISN做加一操作,直到超過(guò)2^32,又從0開(kāi)始。這樣,一個(gè)ISN的周期大約是4.55個(gè)小時(shí)。因?yàn)?,我們假設(shè)我們的TCP Segment在網(wǎng)絡(luò)上的存活時(shí)間不會(huì)超過(guò)Maximum Segment Lifetime(縮寫(xiě)為MSL – Wikipedia語(yǔ)條),所以,只要MSL的值小于4.55小時(shí),那么,我們就不會(huì)重用到ISN。
- 關(guān)于 MSL 和 TIME_WAIT。通過(guò)上面的ISN的描述,相信你也知道MSL是怎么來(lái)的了。我們注意到,在TCP的狀態(tài)圖中,從TIME_WAIT狀態(tài)到CLOSED狀態(tài),有一個(gè)超時(shí)設(shè)置,這個(gè)超時(shí)設(shè)置是 2*MSL(RFC793定義了MSL為2分鐘,Linux設(shè)置成了30s)為什么要這有TIME_WAIT?為什么不直接給轉(zhuǎn)成CLOSED狀態(tài)呢?主要有兩個(gè)原因:1)TIME_WAIT確保有足夠的時(shí)間讓對(duì)端收到了ACK,如果被動(dòng)關(guān)閉的那方?jīng)]有收到Ack,就會(huì)觸發(fā)被動(dòng)端重發(fā)Fin,一來(lái)一去正好2個(gè)MSL,2)有足夠的時(shí)間讓這個(gè)連接不會(huì)跟后面的連接混在一起(你要知道,有些自做主張的路由器會(huì)緩存IP數(shù)據(jù)包,如果連接被重用了,那么這些延遲收到的包就有可能會(huì)跟新連接混在一起)。
- 關(guān)于TIME_WAIT數(shù)量太多。從上面的描述我們可以知道,TIME_WAIT是個(gè)很重要的狀態(tài),但是如果在大并發(fā)的短鏈接下,TIME_WAIT 就會(huì)太多,這也會(huì)消耗很多系統(tǒng)資源。只要搜一下,你就會(huì)發(fā)現(xiàn),十有八九的處理方式都是教你設(shè)置兩個(gè)參數(shù),一個(gè)叫tcp_tw_reuse,另一個(gè)叫tcp_tw_recycle的參數(shù),這兩個(gè)參數(shù)默認(rèn)值都是被關(guān)閉的,后者recyle比前者resue更為激進(jìn),resue要溫柔一些。另外,如果使用tcp_tw_reuse,必需設(shè)置tcp_timestamps=1,否則無(wú)效。這里,你一定要注意,打開(kāi)這兩個(gè)參數(shù)會(huì)有比較大的坑——可能會(huì)讓TCP連接出一些詭異的問(wèn)題(因?yàn)槿缟鲜鲆粯?,如果不等待超時(shí)重用連接的話,新的連接可能會(huì)建不上。正如官方文檔上說(shuō)的一樣“It should not be changed without advice/request of technical experts”)。
- 關(guān)于TIME_WAIT數(shù)量太多。從上面的描述我們可以知道,TIME_WAIT是個(gè)很重要的狀態(tài),但是如果在大并發(fā)的短鏈接下,TIME_WAIT 就會(huì)太多,這也會(huì)消耗很多系統(tǒng)資源。只要搜一下,你就會(huì)發(fā)現(xiàn),十有八九的處理方式都是教你設(shè)置兩個(gè)參數(shù),一個(gè)叫tcp_tw_reuse,另一個(gè)叫tcp_tw_recycle的參數(shù),這兩個(gè)參數(shù)默認(rèn)值都是被關(guān)閉的,后者recyle比前者resue更為激進(jìn),resue要溫柔一些。另外,如果使用tcp_tw_reuse,必需設(shè)置tcp_timestamps=1,否則無(wú)效。這里,你一定要注意,打開(kāi)這兩個(gè)參數(shù)會(huì)有比較大的坑——可能會(huì)讓TCP連接出一些詭異的問(wèn)題(因?yàn)槿缟鲜鲆粯?,如果不等待超時(shí)重用連接的話,新的連接可能會(huì)建不上。正如官方文檔上說(shuō)的一樣“It should not be changed without advice/request of technical experts”)。
- 關(guān)于tcp_tw_reuse。官方文檔上說(shuō)tcp_tw_reuse 加上tcp_timestamps(又叫PAWS, for Protection Against Wrapped Sequence Numbers)可以保證協(xié)議的角度上的安全,但是你需要tcp_timestamps在兩邊都被打開(kāi)(你可以讀一下tcp_twsk_unique的源碼 )。我個(gè)人估計(jì)還是有一些場(chǎng)景會(huì)有問(wèn)題。
- 關(guān)于tcp_tw_recycle。如果是tcp_tw_recycle被打開(kāi)了話,會(huì)假設(shè)對(duì)端開(kāi)啟了tcp_timestamps,然后會(huì)去比較時(shí)間戳,如果時(shí)間戳變大了,就可以重用。但是,如果對(duì)端是一個(gè)NAT網(wǎng)絡(luò)的話(如:一個(gè)公司只用一個(gè)IP出公網(wǎng))或是對(duì)端的IP被另一臺(tái)重用了,這個(gè)事就復(fù)雜了。建鏈接的SYN可能就被直接丟掉了(你可能會(huì)看到connection time out的錯(cuò)誤)
- 關(guān)于tcp_max_tw_buckets。這個(gè)是控制并發(fā)的TIME_WAIT的數(shù)量,默認(rèn)值是180000,如果超限,那么,系統(tǒng)會(huì)把多的給destory掉,然后在日志里打一個(gè)警告(如:time wait bucket table overflow),官網(wǎng)文檔說(shuō)這個(gè)參數(shù)是用來(lái)對(duì)抗DDoS攻擊的。也說(shuō)的默認(rèn)值180000并不小。這個(gè)還是需要根據(jù)實(shí)際情況考慮。
Again,使用tcp_tw_reuse和tcp_tw_recycle來(lái)解決TIME_WAIT的問(wèn)題是非常非常危險(xiǎn)的,因?yàn)檫@兩個(gè)參數(shù)違反了TCP協(xié)議(RFC 1122)
其實(shí),TIME_WAIT表示的是你主動(dòng)斷連接,所以,這就是所謂的“不作死不會(huì)死”。試想,如果讓對(duì)端斷連接,那么這個(gè)破問(wèn)題就是對(duì)方的了,呵呵。另外,如果你的服務(wù)器是于HTTP服務(wù)器,那么設(shè)置一個(gè)HTTP的KeepAlive有多重要(瀏覽器會(huì)重用一個(gè)TCP連接來(lái)處理多個(gè)HTTP請(qǐng)求),然后讓客戶端去斷鏈接(你要小心,瀏覽器可能會(huì)非常貪婪,他們不到萬(wàn)不得已不會(huì)主動(dòng)斷連接)。
數(shù)據(jù)傳輸中的Sequence Number
下圖是我從Wireshark中截了個(gè)我在訪問(wèn)coolshell.cn時(shí)的有數(shù)據(jù)傳輸?shù)膱D給你看一下,SeqNum是怎么變的。(使用Wireshark菜單中的Statistics ->Flow Graph… )
你可以看到,SeqNum的增加是和傳輸?shù)淖止?jié)數(shù)相關(guān)的。上圖中,三次握手后,來(lái)了兩個(gè)Len:1440的包,而第二個(gè)包的SeqNum就成了1441。然后第一個(gè)ACK回的是1441,表示第一個(gè)1440收到了。
注意:如果你用Wireshark抓包程序看3次握手,你會(huì)發(fā)現(xiàn)SeqNum總是為0,不是這樣的,Wireshark為了顯示更友好,使用了Relative SeqNum——相對(duì)序號(hào),你只要在右鍵菜單中的protocol preference 中取消掉就可以看到“Absolute SeqNum”了
TCP連接狀態(tài)總結(jié):
一、LISTENING
提供某種服務(wù),偵聽(tīng)遠(yuǎn)方TCP端口的連接請(qǐng)求,當(dāng)提供的服務(wù)沒(méi)有被連接時(shí),處于LISTENING狀態(tài),端口是開(kāi)放的,等待被連接。
二、SYN_SENT (客戶端狀態(tài))
客戶端調(diào)用connect,發(fā)送一個(gè)SYN請(qǐng)求建立一個(gè)連接,在發(fā)送連接請(qǐng)求后等待匹配的連接請(qǐng)求,此時(shí)狀態(tài)為SYN_SENT.
三、SYN_RECEIVED (服務(wù)端狀態(tài))
在收到和發(fā)送一個(gè)連接請(qǐng)求后,等待對(duì)方對(duì)連接請(qǐng)求的確認(rèn),當(dāng)服務(wù)器收到客戶端發(fā)送的同步信號(hào)時(shí),將標(biāo)志位ACK和SYN置1發(fā)送給客戶端,此時(shí)服務(wù)器端處于SYN_RCVD狀態(tài),如果連接成功了就變?yōu)镋STABLISHED,正常情況下SYN_RCVD狀態(tài)非常短暫。
四、ESTABLISHED
ESTABLISHED狀態(tài)是表示兩臺(tái)機(jī)器正在傳輸數(shù)據(jù)。
五、FIN-WAIT-1
等待遠(yuǎn)程TCP連接中斷請(qǐng)求,或先前的連接中斷請(qǐng)求的確認(rèn),主動(dòng)關(guān)閉端應(yīng)用程序調(diào)用close,TCP發(fā)出FIN請(qǐng)求主動(dòng)關(guān)閉連接,之后進(jìn)入FIN_WAIT1狀態(tài)。
六、FIN-WAIT-2
從遠(yuǎn)程TCP等待連接中斷請(qǐng)求,主動(dòng)關(guān)閉端接到ACK后,就進(jìn)入了FIN-WAIT-2 .這是在關(guān)閉連接時(shí),客戶端和服務(wù)器兩次握手之后的狀態(tài),是著名的半關(guān)閉的狀態(tài)了,在這個(gè)狀態(tài)下,應(yīng)用程序還有接受數(shù)據(jù)的能力,但是已經(jīng)無(wú)法發(fā)送數(shù)據(jù),但是也有一種可能是,客戶端一直處于FIN_WAIT_2狀態(tài),而服務(wù)器則一直處于WAIT_CLOSE狀態(tài),而直到應(yīng)用層來(lái)決定關(guān)閉這個(gè)狀態(tài)。
附半關(guān)閉例圖:
七、CLOSE-WAIT
等待從本地用戶發(fā)來(lái)的連接中斷請(qǐng)求 ,被動(dòng)關(guān)閉端TCP接到FIN后,就發(fā)出ACK以回應(yīng)FIN請(qǐng)求(它的接收也作為文件結(jié)束符傳遞給上層應(yīng)用程序),并進(jìn)入CLOSE_WAIT.
八、CLOSING
等待遠(yuǎn)程TCP對(duì)連接中斷的確認(rèn),處于此種狀態(tài)比較少見(jiàn)。
九、LAST-ACK
等待原來(lái)的發(fā)向遠(yuǎn)程TCP的連接中斷請(qǐng)求的確認(rèn),被動(dòng)關(guān)閉端一段時(shí)間后,接收到文件結(jié)束符的應(yīng)用程序?qū)⒄{(diào)用CLOSE關(guān)閉連接,TCP也發(fā)送一個(gè) FIN,等待對(duì)方的ACK.進(jìn)入LAST-ACK。
十、TIME-WAIT
在主動(dòng)關(guān)閉端接收到FIN后,TCP就發(fā)送ACK包,并進(jìn)入TIME-WAIT狀態(tài),等待足夠的時(shí)間以確保遠(yuǎn)程TCP接收到連接中斷請(qǐng)求的確認(rèn),很大程度上保證了雙方都可以正常結(jié)束,但是也存在問(wèn)題,須等待2MSL時(shí)間的過(guò)去才能進(jìn)行下一次連接。
**十一、CLOSED **
被動(dòng)關(guān)閉端在接受到ACK包后,就進(jìn)入了closed的狀態(tài),連接結(jié)束,沒(méi)有任何連接狀態(tài)。
附TCP正常連接建立和終止所對(duì)應(yīng)的狀態(tài)圖:
狀態(tài)遷移過(guò)程:
a、客戶端:
CLOSED->SYN_SENT->ESTABLISHED->FIN_WAIT_1->FIN_WAIT_2->TIME_WAIT->CLOSED
b、服務(wù)端
CLOSED->LISTEN->SYN_RECEIVED->ESTABLISHED->CLOSE_WAIT->LAST_ACK->CLOSE
在眾多狀態(tài)中,經(jīng)常關(guān)注的有兩個(gè):TIME_WAIT、CLOSE_WAIT。
附狀態(tài)遷移過(guò)程圖:
二、丟包重傳機(jī)制
TCP重傳機(jī)制
TCP要保證所有的數(shù)據(jù)包都可以到達(dá),所以,必需要有重傳機(jī)制。
注意,接收端給發(fā)送端的Ack確認(rèn)只會(huì)確認(rèn)最后一個(gè)連續(xù)的包,比如,發(fā)送端發(fā)了1,2,3,4,5一共五份數(shù)據(jù),接收端收到了1,2,于是回ack 3,然后收到了4(注意此時(shí)3沒(méi)收到),此時(shí)的TCP會(huì)怎么辦?我們要知道,因?yàn)檎缜懊嫠f(shuō)的,SeqNum和Ack是以字節(jié)數(shù)為單位,所以ack的時(shí)候,不能跳著確認(rèn),只能確認(rèn)最大的連續(xù)收到的包,不然,發(fā)送端就以為之前的都收到了。
超時(shí)重傳機(jī)制
一種是不回ack,死等3,當(dāng)發(fā)送方發(fā)現(xiàn)收不到3的ack超時(shí)后,會(huì)重傳3。一旦接收方收到3后,會(huì)ack 回 4——意味著3和4都收到了。
但是,這種方式會(huì)有比較嚴(yán)重的問(wèn)題,那就是因?yàn)橐赖?,所以會(huì)導(dǎo)致4和5即便已經(jīng)收到了,而發(fā)送方也完全不知道發(fā)生了什么事,因?yàn)闆](méi)有收到Ack,所以,發(fā)送方可能會(huì)悲觀地認(rèn)為也丟了,所以有可能也會(huì)導(dǎo)致4和5的重傳。
對(duì)此有兩種選擇:
- 一種是僅重傳timeout的包。也就是第3份數(shù)據(jù)。
- 另一種是重傳timeout后所有的數(shù)據(jù),也就是第3,4,5這三份數(shù)據(jù)。
這兩種方式有好也有不好。第一種會(huì)節(jié)省帶寬,但是慢,第二種會(huì)快一點(diǎn),但是會(huì)浪費(fèi)帶寬,也可能會(huì)有無(wú)用功。但總體來(lái)說(shuō)都不好。因?yàn)槎荚诘萾imeout,timeout可能會(huì)很長(zhǎng)(在下篇會(huì)說(shuō)TCP是怎么動(dòng)態(tài)地計(jì)算出timeout的)
快速重傳機(jī)制
于是,TCP引入了一種叫Fast Retransmit 的算法,不以時(shí)間驅(qū)動(dòng),而以數(shù)據(jù)驅(qū)動(dòng)重傳。也就是說(shuō),如果,包沒(méi)有連續(xù)到達(dá),就ack最后那個(gè)可能被丟了的包,如果發(fā)送方連續(xù)收到3次相同的ack,就重傳。Fast Retransmit的好處是不用等timeout了再重傳。
比如:如果發(fā)送方發(fā)出了1,2,3,4,5份數(shù)據(jù),第一份先到送了,于是就ack回2,結(jié)果2因?yàn)槟承┰驔](méi)收到,3到達(dá)了,于是還是ack回2,后面的4和5都到了,但是還是ack回2,因?yàn)?還是沒(méi)有收到,于是發(fā)送端收到了三個(gè)ack=2的確認(rèn),知道了2還沒(méi)有到,于是就馬上重轉(zhuǎn)2。然后,接收端收到了2,此時(shí)因?yàn)?,4,5都收到了,于是ack回6。示意圖如下:
Fast Retransmit只解決了一個(gè)問(wèn)題,就是timeout的問(wèn)題,它依然面臨一個(gè)艱難的選擇,就是,是重傳之前的一個(gè)還是重傳所有的問(wèn)題。對(duì)于上面的示例來(lái)說(shuō),是重傳#2呢還是重傳#2,#3,#4,#5呢?因?yàn)榘l(fā)送端并不清楚這連續(xù)的3個(gè)ack(2)是誰(shuí)傳回來(lái)的?也許發(fā)送端發(fā)了20份數(shù)據(jù),是#6,#10,#20傳來(lái)的呢。這樣,發(fā)送端很有可能要重傳從2到20的這堆數(shù)據(jù)(這就是某些TCP的實(shí)際的實(shí)現(xiàn))??梢?jiàn),這是一把雙刃劍。
SACK 方法
另外一種更好的方式叫:Selective Acknowledgment (SACK)(參看RFC 2018),這種方式需要在TCP頭里加一個(gè)SACK的東西,ACK還是Fast Retransmit的ACK,SACK則是匯報(bào)收到的數(shù)據(jù)碎版,參看下圖:
這樣,在發(fā)送端就可以根據(jù)回傳的SACK來(lái)知道哪些數(shù)據(jù)到了,哪些沒(méi)有到。于是就優(yōu)化了Fast Retransmit的算法。當(dāng)然,這個(gè)協(xié)議需要兩邊都支持。在 Linux下,可以通過(guò)tcp_sack參數(shù)打開(kāi)這個(gè)功能(Linux 2.4后默認(rèn)打開(kāi))。
這里還需要注意一個(gè)問(wèn)題——接收方Reneging,所謂Reneging的意思就是接收方有權(quán)把已經(jīng)報(bào)給發(fā)送端SACK里的數(shù)據(jù)給丟了。這樣干是不被鼓勵(lì)的,因?yàn)檫@個(gè)事會(huì)把問(wèn)題復(fù)雜化了,但是,接收方這么做可能會(huì)有些極端情況,比如要把內(nèi)存給別的更重要的東西。所以,發(fā)送方也不能完全依賴SACK,還是要依賴ACK,并維護(hù)Time-Out,如果后續(xù)的ACK沒(méi)有增長(zhǎng),那么還是要把SACK的東西重傳,另外,接收端這邊永遠(yuǎn)不能把SACK的包標(biāo)記為Ack。
注意:SACK會(huì)消費(fèi)發(fā)送方的資源,試想,如果一個(gè)攻擊者給數(shù)據(jù)發(fā)送方發(fā)一堆SACK的選項(xiàng),這會(huì)導(dǎo)致發(fā)送方開(kāi)始要重傳甚至遍歷已經(jīng)發(fā)出的數(shù)據(jù),這會(huì)消耗很多發(fā)送端的資源。
Duplicate SACK – 重復(fù)收到數(shù)據(jù)的問(wèn)題
Duplicate SACK又稱D-SACK,其主要使用了SACK來(lái)告訴發(fā)送方有哪些數(shù)據(jù)被重復(fù)接收了。RFC-2883 里有詳細(xì)描述和示例。下面舉幾個(gè)例子(來(lái)源于RFC-2883)
D-SACK使用了SACK的第一個(gè)段來(lái)做標(biāo)志,
- 如果SACK的第一個(gè)段的范圍被ACK所覆蓋,那么就是D-SACK
- 如果SACK的第一個(gè)段的范圍被SACK的第二個(gè)段覆蓋,那么就是D-SACK
示例一:ACK丟包
下面的示例中,丟了兩個(gè)ACK,所以,發(fā)送端重傳了第一個(gè)數(shù)據(jù)包(3000-3499),于是接收端發(fā)現(xiàn)重復(fù)收到,于是回了一個(gè)SACK=3000-3500,因?yàn)锳CK都到了4000意味著收到了4000之前的所有數(shù)據(jù),所以這個(gè)SACK就是D-SACK——旨在告訴發(fā)送端我收到了重復(fù)的數(shù)據(jù),而且我們的發(fā)送端還知道,數(shù)據(jù)包沒(méi)有丟,丟的是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á)的三個(gè)包觸發(fā)了“Fast Retransmit算法”,所以重傳,但重傳時(shí),被延誤的包又到了,所以,回了一個(gè)SACK=1000-1500,因?yàn)锳CK已到了3000,所以,這個(gè)SACK是D-SACK——標(biāo)識(shí)收到了重復(fù)的包。
這個(gè)案例下,發(fā)送端知道之前因?yàn)椤癋ast Retransmit算法”觸發(fā)的重傳不是因?yàn)榘l(fā)出去的包丟了,也不是因?yàn)榛貞?yīng)的ACK包丟了,而是因?yàn)榫W(wǎng)絡(luò)延時(shí)了。
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 ---------
可見(jiàn),引入了D-SACK,有這么幾個(gè)好處:
1)可以讓發(fā)送方知道,是發(fā)出去的包丟了,還是回來(lái)的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ò)上的流控。
Linux下的tcp_dsack參數(shù)用于開(kāi)啟這個(gè)功能(Linux 2.4后默認(rèn)打開(kāi))
三、TCP的RTT算法
TCP要解決一個(gè)很大的事,那就是要在一個(gè)網(wǎng)絡(luò)根據(jù)不同的情況來(lái)動(dòng)態(tài)調(diào)整自己的發(fā)包的速度,小則讓自己的連接更穩(wěn)定,大則讓整個(gè)網(wǎng)絡(luò)更穩(wěn)定。
從前面的TCP重傳機(jī)制我們知道Timeout的設(shè)置對(duì)于重傳非常重要。
- 設(shè)長(zhǎng)了,重發(fā)就慢,丟了老半天才重發(fā),沒(méi)有效率,性能差;
- 設(shè)短了,會(huì)導(dǎo)致可能并沒(méi)有丟就重發(fā)。于是重發(fā)的就快,會(huì)增加網(wǎng)絡(luò)擁塞,導(dǎo)致更多的超時(shí),更多的超時(shí)導(dǎo)致更多的重發(fā)。
而且,這個(gè)超時(shí)時(shí)間在不同的網(wǎng)絡(luò)的情況下,根本沒(méi)有辦法設(shè)置一個(gè)死的值。只能動(dòng)態(tài)地設(shè)置。為了動(dòng)態(tài)地設(shè)置,TCP引入了RTT——Round Trip Time,也就是一個(gè)數(shù)據(jù)包從發(fā)出去到回來(lái)的時(shí)間。這樣發(fā)送端就大約知道需要多少的時(shí)間,從而可以方便地設(shè)置Timeout——RTO(Retransmission TimeOut),以讓我們的重傳機(jī)制更高效。聽(tīng)起來(lái)似乎很簡(jiǎn)單,好像就是在發(fā)送端發(fā)包時(shí)記下t0,然后接收端再把這個(gè)ack回來(lái)時(shí)再記一個(gè)t1,于是RTT = t1 – t0。沒(méi)那么簡(jiǎn)單,這只是一個(gè)采樣,不能代表普遍情況。
經(jīng)典算法
RFC793 中定義的經(jīng)典算法是這樣的:
1)首先,先采樣RTT,記下最近好幾次的RTT值。
2)然后做平滑計(jì)算SRTT( Smoothed RTT)。公式為:(其中的 α 取值在0.8 到 0.9之間,這個(gè)算法英文叫Exponential weighted moving average,中文叫:加權(quán)移動(dòng)平均)
SRTT = ( α * SRTT ) + ((1- α) * RTT)
3)開(kāi)始計(jì)算RTO。公式如下:
RTO = min [ UBOUND, max [ LBOUND, (β * SRTT) ] ]
其中:
- UBOUND是最大的timeout時(shí)間,上限值
- LBOUND是最小的timeout時(shí)間,下限值
- β 值一般在1.3到2.0之間。
Karn / Partridge 算法
但是上面的這個(gè)算法在重傳的時(shí)候會(huì)出有一個(gè)終極問(wèn)題——你是用第一次發(fā)數(shù)據(jù)的時(shí)間和ack回來(lái)的時(shí)間做RTT樣本值,還是用重傳的時(shí)間和ACK回來(lái)的時(shí)間做RTT樣本值?
這個(gè)問(wèn)題無(wú)論你選那頭都是按下葫蘆起了瓢。如下圖所示:
- 情況(a)是ack沒(méi)回來(lái),所以重傳。如果你計(jì)算第一次發(fā)送和ACK的時(shí)間,那么,明顯算大了。
- 情況(b)是ack回來(lái)慢了,但是導(dǎo)致了重傳,但剛重傳不一會(huì)兒,之前ACK就回來(lái)了。如果你是算重傳的時(shí)間和ACK回來(lái)的時(shí)間的差,就會(huì)算短了。
所以1987年的時(shí)候,搞了一個(gè)叫Karn / Partridge Algorithm,這個(gè)算法的最大特點(diǎn)是——忽略重傳,不把重傳的RTT做采樣(你看,你不需要去解決不存在的問(wèn)題)。
但是,這樣一來(lái),又會(huì)引發(fā)一個(gè)大BUG——如果在某一時(shí)間,網(wǎng)絡(luò)閃動(dòng),突然變慢了,產(chǎn)生了比較大的延時(shí),這個(gè)延時(shí)導(dǎo)致要重轉(zhuǎn)所有的包(因?yàn)橹暗腞TO很?。?,于是,因?yàn)橹剞D(zhuǎn)的不算,所以,RTO就不會(huì)被更新,這是一個(gè)災(zāi)難。于是Karn算法用了一個(gè)取巧的方式——只要一發(fā)生重傳,就對(duì)現(xiàn)有的RTO值翻倍(這就是所謂的 Exponential backoff),很明顯,這種死規(guī)矩對(duì)于一個(gè)需要估計(jì)比較準(zhǔn)確的RTT也不靠譜。
Jacobson / Karels 算法
前面兩種算法用的都是“加權(quán)移動(dòng)平均”,這種方法最大的毛病就是如果RTT有一個(gè)大的波動(dòng)的話,很難被發(fā)現(xiàn),因?yàn)楸黄交袅?。所以?988年,又有人推出來(lái)了一個(gè)新的算法,這個(gè)算法叫Jacobson / Karels Algorithm(參看RFC6289)。這個(gè)算法引入了最新的RTT的采樣和平滑過(guò)的SRTT的差距做因子來(lái)計(jì)算。公式如下:(其中的DevRTT是Deviation RTT的意思)
SRTT = SRTT + α (RTT – SRTT) —— 計(jì)算平滑RTT
DevRTT = (1-β) DevRTT + β (|RTT-SRTT|) ——計(jì)算平滑RTT和真實(shí)的差距(加權(quán)移動(dòng)平均)
RTO= μ * SRTT + ? *DevRTT —— 神一樣的公式
(其中:在Linux下,α = 0.125,β = 0.25, μ = 1,? = 4 ——這就是算法中的“調(diào)得一手好參數(shù)”,nobody knows why, it just works…) 最后的這個(gè)算法在被用在今天的TCP協(xié)議中。
四、TCP滑動(dòng)窗口
需要說(shuō)明一下,如果你不了解TCP的滑動(dòng)窗口這個(gè)事,你等于不了解TCP協(xié)議。我們都知道,TCP必需要解決的可靠傳輸以及包亂序(reordering)的問(wèn)題,所以,TCP必需要知道網(wǎng)絡(luò)實(shí)際的數(shù)據(jù)處理帶寬或是數(shù)據(jù)處理速度,這樣才不會(huì)引起網(wǎng)絡(luò)擁塞,導(dǎo)致丟包。
所以,TCP引入了一些技術(shù)和設(shè)計(jì)來(lái)做網(wǎng)絡(luò)流控,Sliding Window是其中一個(gè)技術(shù)。前面我們說(shuō)過(guò),TCP頭里有一個(gè)字段叫Window,又叫Advertised-Window,這個(gè)字段是接收端告訴發(fā)送端自己還有多少緩沖區(qū)可以接收數(shù)據(jù)。于是發(fā)送端就可以根據(jù)這個(gè)接收端的處理能力來(lái)發(fā)送數(shù)據(jù),而不會(huì)導(dǎo)致接收端處理不過(guò)來(lái)。為了說(shuō)明滑動(dòng)窗口,我們需要先看一下TCP緩沖區(qū)的一些數(shù)據(jù)結(jié)構(gòu):
上圖中,我們可以看到:
- 接收端LastByteRead指向了TCP緩沖區(qū)中讀到的位置,NextByteExpected指向的地方是收到的連續(xù)包的最后一個(gè)位置,LastByteRcved指向的是收到的包的最后一個(gè)位置,我們可以看到中間有些數(shù)據(jù)還沒(méi)有到達(dá),所以有數(shù)據(jù)空白區(qū)。
- 發(fā)送端的LastByteAcked指向了被接收端Ack過(guò)的位置(表示成功發(fā)送確認(rèn)),LastByteSent表示發(fā)出去了,但還沒(méi)有收到成功確認(rèn)的Ack,LastByteWritten指向的是上層應(yīng)用正在寫(xiě)的地方。
于是:
- 接收端在給發(fā)送端回ACK中會(huì)匯報(bào)自己的AdvertisedWindow = MaxRcvBuffer – LastByteRcvd – 1;
- 而發(fā)送方會(huì)根據(jù)這個(gè)窗口來(lái)控制發(fā)送數(shù)據(jù)的大小,以保證接收方可以處理。
下面我們來(lái)看一下發(fā)送方的滑動(dòng)窗口示意圖:
上圖中分成了四個(gè)部分,分別是:(其中那個(gè)黑模型就是滑動(dòng)窗口)
- #1已收到ack確認(rèn)的數(shù)據(jù)。
- #2發(fā)還沒(méi)收到ack的。
- #3在窗口中還沒(méi)有發(fā)出的(接收方還有空間)。
- #4窗口以外的數(shù)據(jù)(接收方?jīng)]空間)
下面是個(gè)滑動(dòng)后的示意圖(收到36的ack,并發(fā)出了46-51的字節(jié)):
下面我們來(lái)看一個(gè)接受端控制發(fā)送端的圖示:
Zero Window
上圖,我們可以看到一個(gè)處理緩慢的Server(接收端)是怎么把Client(發(fā)送端)的TCP Sliding Window給降成0的。此時(shí),你一定會(huì)問(wèn),如果Window變成0了,TCP會(huì)怎么樣?是不是發(fā)送端就不發(fā)數(shù)據(jù)了?是的,發(fā)送端就不發(fā)數(shù)據(jù)了,你可以想像成“Window Closed”,那你一定還會(huì)問(wèn),如果發(fā)送端不發(fā)數(shù)據(jù)了,接收方一會(huì)兒Window size 可用了,怎么通知發(fā)送端呢?
解決這個(gè)問(wèn)題,TCP使用了Zero Window Probe技術(shù),縮寫(xiě)為ZWP,也就是說(shuō),發(fā)送端在窗口變成0后,會(huì)發(fā)ZWP的包給接收方,讓接收方來(lái)ack他的Window尺寸,一般這個(gè)值會(huì)設(shè)置成3次,第次大約30-60秒(不同的實(shí)現(xiàn)可能會(huì)不一樣)。如果3次過(guò)后還是0的話,有的TCP實(shí)現(xiàn)就會(huì)發(fā)RST把鏈接斷了。
注意:只要有等待的地方都可能出現(xiàn)DDoS攻擊,Zero Window也不例外,一些攻擊者會(huì)在和HTTP建好鏈發(fā)完GET請(qǐng)求后,就把Window設(shè)置為0,然后服務(wù)端就只能等待進(jìn)行ZWP,于是攻擊者會(huì)并發(fā)大量的這樣的請(qǐng)求,把服務(wù)器端的資源耗盡。
另外,Wireshark中,你可以使用tcp.analysis.zero_window來(lái)過(guò)濾包,然后使用右鍵菜單里的follow TCP stream,你可以看到ZeroWindowProbe及ZeroWindowProbeAck的包。
Silly Window Syndrome
Silly Window Syndrome翻譯成中文就是“糊涂窗口綜合癥”。正如你上面看到的一樣,如果我們的接收方太忙了,來(lái)不及取走Receive Windows里的數(shù)據(jù),那么,就會(huì)導(dǎo)致發(fā)送方越來(lái)越小。到最后,如果接收方騰出幾個(gè)字節(jié)并告訴發(fā)送方現(xiàn)在有幾個(gè)字節(jié)的window,而我們的發(fā)送方會(huì)義無(wú)反顧地發(fā)送這幾個(gè)字節(jié)。
要知道,我們的TCP+IP頭有40個(gè)字節(jié),為了幾個(gè)字節(jié),要達(dá)上這么大的開(kāi)銷(xiāo),這太不經(jīng)濟(jì)了。
另外,你需要知道網(wǎng)絡(luò)上有個(gè)MTU,對(duì)于以太網(wǎng)來(lái)說(shuō),MTU是1500字節(jié),除去TCP+IP頭的40個(gè)字節(jié),真正的數(shù)據(jù)傳輸可以有1460,這就是所謂的MSS(Max Segment Size)注意,TCP的RFC定義這個(gè)MSS的默認(rèn)值是536,這是因?yàn)?RFC 791里說(shuō)了任何一個(gè)IP設(shè)備都得最少接收576尺寸的大小(實(shí)際上來(lái)說(shuō)576是撥號(hào)的網(wǎng)絡(luò)的MTU,而576減去IP頭的20個(gè)字節(jié)就是536)。
如果你的網(wǎng)絡(luò)包可以塞滿MTU,那么你可以用滿整個(gè)帶寬,如果不能,那么你就會(huì)浪費(fèi)帶寬。(大于MTU的包有兩種結(jié)局,一種是直接被丟了,另一種是會(huì)被重新分塊打包發(fā)送) 你可以想像成一個(gè)MTU就相當(dāng)于一個(gè)飛機(jī)的最多可以裝的人,如果這飛機(jī)里滿載的話,帶寬最高,如果一個(gè)飛機(jī)只運(yùn)一個(gè)人的話,無(wú)疑成本增加了,也而相當(dāng)二。
所以,Silly Windows Syndrome這個(gè)現(xiàn)像就像是你本來(lái)可以坐200人的飛機(jī)里只做了一兩個(gè)人。要解決這個(gè)問(wèn)題也不難,就是避免對(duì)小的window size做出響應(yīng),直到有足夠大的window size再響應(yīng),這個(gè)思路可以同時(shí)實(shí)現(xiàn)在sender和receiver兩端。
- 如果這個(gè)問(wèn)題是由Receiver端引起的,那么就會(huì)使用 David D Clark’s 方案。在receiver端,如果收到的數(shù)據(jù)導(dǎo)致window size小于某個(gè)值,可以直接ack(0)回sender,這樣就把window給關(guān)閉了,也阻止了sender再發(fā)數(shù)據(jù)過(guò)來(lái),等到receiver端處理了一些數(shù)據(jù)后windows size 大于等于了MSS,或者,receiver buffer有一半為空,就可以把window打開(kāi)讓send 發(fā)送數(shù)據(jù)過(guò)來(lái)。
- 如果這個(gè)問(wèn)題是由Sender端引起的,那么就會(huì)使用著名的 Nagle’s algorithm。這個(gè)算法的思路也是延時(shí)處理,他有兩個(gè)主要的條件:1)要等到 Window Size>=MSS 或是 Data Size >=MSS,2)收到之前發(fā)送數(shù)據(jù)的ack回包,他才會(huì)發(fā)數(shù)據(jù),否則就是在攢數(shù)據(jù)。
另外,Nagle算法默認(rèn)是打開(kāi)的,所以,對(duì)于一些需要小包場(chǎng)景的程序——比如像telnet或ssh這樣的交互性比較強(qiáng)的程序,你需要關(guān)閉這個(gè)算法。你可以在Socket設(shè)置TCP_NODELAY選項(xiàng)來(lái)關(guān)閉這個(gè)算法(關(guān)閉Nagle算法沒(méi)有全局參數(shù),需要根據(jù)每個(gè)應(yīng)用自己的特點(diǎn)來(lái)關(guān)閉)
setsockopt(sock_fd, IPPROTO_TCP, TCP_NODELAY, (char *)&value,sizeof(int));
另外,網(wǎng)上有些文章說(shuō)TCP_CORK的socket option是也關(guān)閉Nagle算法,這不對(duì)。TCP_CORK其實(shí)是更新激進(jìn)的Nagle算漢,完全禁止小包發(fā)送,而Nagle算法沒(méi)有禁止小包發(fā)送,只是禁止了大量的小包發(fā)送。最好不要兩個(gè)選項(xiàng)都設(shè)置。
五、擁塞處理 Congestion Handling
上面我們知道了,TCP通過(guò)Sliding Window來(lái)做流控(Flow Control),但是TCP覺(jué)得這還不夠,因?yàn)镾liding Window需要依賴于連接的發(fā)送端和接收端,其并不知道網(wǎng)絡(luò)中間發(fā)生了什么。TCP的設(shè)計(jì)者覺(jué)得,一個(gè)偉大而牛逼的協(xié)議僅僅做到流控并不夠,因?yàn)榱骺刂皇蔷W(wǎng)絡(luò)模型4層以上的事,TCP的還應(yīng)該更聰明地知道整個(gè)網(wǎng)絡(luò)上的事。
具體一點(diǎn),我們知道TCP通過(guò)一個(gè)timer采樣了RTT并計(jì)算RTO,但是,如果網(wǎng)絡(luò)上的延時(shí)突然增加,那么,TCP對(duì)這個(gè)事做出的應(yīng)對(duì)只有重傳數(shù)據(jù),但是,重傳會(huì)導(dǎo)致網(wǎng)絡(luò)的負(fù)擔(dān)更重,于是會(huì)導(dǎo)致更大的延遲以及更多的丟包,于是,這個(gè)情況就會(huì)進(jìn)入惡性循環(huán)被不斷地放大。試想一下,如果一個(gè)網(wǎng)絡(luò)內(nèi)有成千上萬(wàn)的TCP連接都這么行事,那么馬上就會(huì)形成“網(wǎng)絡(luò)風(fēng)暴”,TCP這個(gè)協(xié)議就會(huì)拖垮整個(gè)網(wǎng)絡(luò)。這是一個(gè)災(zāi)難。
所以,TCP不能忽略網(wǎng)絡(luò)上發(fā)生的事情,而無(wú)腦地一個(gè)勁地重發(fā)數(shù)據(jù),對(duì)網(wǎng)絡(luò)造成更大的傷害。對(duì)此TCP的設(shè)計(jì)理念是:TCP不是一個(gè)自私的協(xié)議,當(dāng)擁塞發(fā)生的時(shí)候,要做自我犧牲。就像交通阻塞一樣,每個(gè)車(chē)都應(yīng)該把路讓出來(lái),而不要再去搶路了。
擁塞控制主要是四個(gè)算法:1)慢啟動(dòng),2)擁塞避免,3)擁塞發(fā)生,4)快速恢復(fù)。這四個(gè)算法不是一天都搞出來(lái)的,這個(gè)四算法的發(fā)展經(jīng)歷了很多時(shí)間,到今天都還在優(yōu)化中。備注:
- 1988年,TCP-Tahoe 提出了1)慢啟動(dòng),2)擁塞避免,3)擁塞發(fā)生時(shí)的快速重傳
- 1990年,TCP Reno 在Tahoe的基礎(chǔ)上增加了4)快速恢復(fù)
慢熱啟動(dòng)算法 – Slow Start
首先,我們來(lái)看一下TCP的慢熱啟動(dòng)。慢啟動(dòng)的意思是,剛剛加入網(wǎng)絡(luò)的連接,一點(diǎn)一點(diǎn)地提速,不要一上來(lái)就像那些特權(quán)車(chē)一樣霸道地把路占滿。新同學(xué)上高速還是要慢一點(diǎn),不要把已經(jīng)在高速上的秩序給搞亂了。
慢啟動(dòng)的算法如下(cwnd全稱Congestion Window):
1)連接建好的開(kāi)始先初始化cwnd = 1,表明可以傳一個(gè)MSS大小的數(shù)據(jù)。
2)每當(dāng)收到一個(gè)ACK,cwnd++; 呈線性上升
3)每當(dāng)過(guò)了一個(gè)RTT,cwnd = cwnd*2; 呈指數(shù)讓升
4)還有一個(gè)ssthresh(slow start threshold),是一個(gè)上限,當(dāng)cwnd >= ssthresh時(shí),就會(huì)進(jìn)入“擁塞避免算法”(后面會(huì)說(shuō)這個(gè)算法)
所以,我們可以看到,如果網(wǎng)速很快的話,ACK也會(huì)返回得快,RTT也會(huì)短,那么,這個(gè)慢啟動(dòng)就一點(diǎn)也不慢。下圖說(shuō)明了這個(gè)過(guò)程。
這里,我需要提一下,Linux 3.0后采用了這篇論文的建議——把cwnd 初始化成了 10個(gè)MSS。而Linux 3.0以前,比如2.6,Linux采用了RFC3390,cwnd是跟MSS的值來(lái)變的,如果MSS< 1095,則cwnd = 4;如果MSS>2190,則cwnd=2;其它情況下,則是3。
擁塞避免算法 – Congestion Avoidance
前面說(shuō)過(guò),還有一個(gè)ssthresh(slow start threshold),是一個(gè)上限,當(dāng)cwnd >= ssthresh時(shí),就會(huì)進(jìn)入“擁塞避免算法”。一般來(lái)說(shuō)ssthresh的值是65535,單位是字節(jié),當(dāng)cwnd達(dá)到這個(gè)值時(shí)后,算法如下:
1)收到一個(gè)ACK時(shí),cwnd = cwnd + 1/cwnd
2)當(dāng)每過(guò)一個(gè)RTT時(shí),cwnd = cwnd + 1
這樣就可以避免增長(zhǎng)過(guò)快導(dǎo)致網(wǎng)絡(luò)擁塞,慢慢的增加調(diào)整到網(wǎng)絡(luò)的最佳值。很明顯,是一個(gè)線性上升的算法。
擁塞狀態(tài)時(shí)的算法
前面我們說(shuō)過(guò),當(dāng)丟包的時(shí)候,會(huì)有兩種情況:
1)等到RTO超時(shí),重傳數(shù)據(jù)包。TCP認(rèn)為這種情況太糟糕,反應(yīng)也很強(qiáng)烈。
- sshthresh = cwnd /2
- cwnd 重置為 1
- 進(jìn)入慢啟動(dòng)過(guò)程
2)Fast Retransmit算法,也就是在收到3個(gè)duplicate ACK時(shí)就開(kāi)啟重傳,而不用等到RTO超時(shí)。
TCP Tahoe的實(shí)現(xiàn)和RTO超時(shí)一樣。
TCP Reno的實(shí)現(xiàn)是:
- 進(jìn)入快速恢復(fù)算法——Fast Recovery
- sshthresh = cwnd
- cwnd = cwnd /2
上面我們可以看到RTO超時(shí)后,sshthresh會(huì)變成cwnd的一半,這意味著,如果cwnd<=sshthresh時(shí)出現(xiàn)的丟包,那么TCP的sshthresh就會(huì)減了一半,然后等cwnd又很快地以指數(shù)級(jí)增漲爬到這個(gè)地方時(shí),就會(huì)成慢慢的線性增漲。我們可以看到,TCP是怎么通過(guò)這種強(qiáng)烈地震蕩快速而小心得找到網(wǎng)站流量的平衡點(diǎn)的。
快速恢復(fù)算法 – Fast Recovery
TCP Reno
這個(gè)算法定義在RFC5681??焖僦貍骱涂焖倩謴?fù)算法一般同時(shí)使用??焖倩謴?fù)算法是認(rèn)為,你還有3個(gè)Duplicated Acks說(shuō)明網(wǎng)絡(luò)也不那么糟糕,所以沒(méi)有必要像RTO超時(shí)那么強(qiáng)烈。注意,正如前面所說(shuō),進(jìn)入Fast Recovery之前,cwnd 和 sshthresh已被更新:
- cwnd = cwnd /2
- sshthresh = cwnd
然后,真正的Fast Recovery算法如下:
- cwnd = sshthresh + 3 * MSS (3的意思是確認(rèn)有3個(gè)數(shù)據(jù)包被收到了)
- 重傳Duplicated ACKs指定的數(shù)據(jù)包
- 如果再收到 duplicated Acks,那么cwnd = cwnd +1
- 如果收到了新的Ack,那么,cwnd = sshthresh ,然后就進(jìn)入了擁塞避免的算法了。
如果你仔細(xì)思考一下上面的這個(gè)算法,你就會(huì)知道,上面這個(gè)算法也有問(wèn)題,那就是——它依賴于3個(gè)重復(fù)的Acks。注意,3個(gè)重復(fù)的Acks并不代表只丟了一個(gè)數(shù)據(jù)包,很有可能是丟了好多包。但這個(gè)算法只會(huì)重傳一個(gè),而剩下的那些包只能等到RTO超時(shí),于是,進(jìn)入了惡夢(mèng)模式——超時(shí)一個(gè)窗口就減半一下,多個(gè)超時(shí)會(huì)超成TCP的傳輸速度呈級(jí)數(shù)下降,而且也不會(huì)觸發(fā)Fast Recovery算法了。
通常來(lái)說(shuō),正如我們前面所說(shuō)的,SACK或D-SACK的方法可以讓Fast Recovery或Sender在做決定時(shí)更聰明一些,但是并不是所有的TCP的實(shí)現(xiàn)都支持SACK(SACK需要兩端都支持),所以,需要一個(gè)沒(méi)有SACK的解決方案。而通過(guò)SACK進(jìn)行擁塞控制的算法是FACK(后面會(huì)講)
TCP New Reno
于是,1995年,TCP New Reno(參見(jiàn) RFC 6582 )算法提出來(lái),主要就是在沒(méi)有SACK的支持下改進(jìn)Fast Recovery算法的——
- 當(dāng)sender這邊收到了3個(gè)Duplicated Acks,進(jìn)入Fast Retransimit模式,開(kāi)發(fā)重傳重復(fù)Acks指示的那個(gè)包。如果只有這一個(gè)包丟了,那么,重傳這個(gè)包后回來(lái)的Ack會(huì)把整個(gè)已經(jīng)被sender傳輸出去的數(shù)據(jù)ack回來(lái)。如果沒(méi)有的話,說(shuō)明有多個(gè)包丟了。我們叫這個(gè)ACK為Partial ACK。
- 一旦Sender這邊發(fā)現(xiàn)了Partial ACK出現(xiàn),那么,sender就可以推理出來(lái)有多個(gè)包被丟了,于是乎繼續(xù)重傳sliding window里未被ack的第一個(gè)包。直到再也收不到了Partial Ack,才真正結(jié)束Fast Recovery這個(gè)過(guò)程
我們可以看到,這個(gè)“Fast Recovery的變更”是一個(gè)非常激進(jìn)的玩法,他同時(shí)延長(zhǎng)了Fast Retransmit和Fast Recovery的過(guò)程。
算法示意圖
下面我們來(lái)看一個(gè)簡(jiǎn)單的圖示以同時(shí)看一下上面的各種算法的樣子:
FACK算法
FACK全稱Forward Acknowledgment 算法,論文地址在這里(PDF)Forward Acknowledgement: Refining TCP Congestion Control 這個(gè)算法是其于SACK的,前面我們說(shuō)過(guò)SACK是使用了TCP擴(kuò)展字段Ack了有哪些數(shù)據(jù)收到,哪些數(shù)據(jù)沒(méi)有收到,他比Fast Retransmit的3 個(gè)duplicated acks好處在于,前者只知道有包丟了,不知道是一個(gè)還是多個(gè),而SACK可以準(zhǔn)確的知道有哪些包丟了。所以,SACK可以讓發(fā)送端這邊在重傳過(guò)程中,把那些丟掉的包重傳,而不是一個(gè)一個(gè)的傳,但這樣的一來(lái),如果重傳的包數(shù)據(jù)比較多的話,又會(huì)導(dǎo)致本來(lái)就很忙的網(wǎng)絡(luò)就更忙了。所以,F(xiàn)ACK用來(lái)做重傳過(guò)程中的擁塞流控。
- 這個(gè)算法會(huì)把SACK中最大的Sequence Number 保存在snd.fack這個(gè)變量中,snd.fack的更新由ack帶秋,如果網(wǎng)絡(luò)一切安好則和snd.una一樣(snd.una就是還沒(méi)有收到ack的地方,也就是前面sliding window里的category #2的第一個(gè)地方)
- 然后定義一個(gè)awnd = snd.nxt – snd.fack(snd.nxt指向發(fā)送端sliding window中正在要被發(fā)送的地方——前面sliding windows圖示的category#3第一個(gè)位置),這樣awnd的意思就是在網(wǎng)絡(luò)上的數(shù)據(jù)。(所謂awnd意為:actual quantity of data outstanding in the network)
- 如果需要重傳數(shù)據(jù),那么,awnd = snd.nxt – snd.fack + retran_data,也就是說(shuō),awnd是傳出去的數(shù)據(jù) + 重傳的數(shù)據(jù)。
- 然后觸發(fā)Fast Recovery 的條件是:( ( snd.fack – snd.una ) > (3*MSS) ) || (dupacks == 3) ) 。這樣一來(lái),就不需要等到3個(gè)duplicated acks才重傳,而是只要sack中的最大的一個(gè)數(shù)據(jù)和ack的數(shù)據(jù)比較長(zhǎng)了(3個(gè)MSS),那就觸發(fā)重傳。在整個(gè)重傳過(guò)程中cwnd不變。直到當(dāng)?shù)谝淮蝸G包的snd.nxt<=snd.una(也就是重傳的數(shù)據(jù)都被確認(rèn)了),然后進(jìn)來(lái)?yè)砣苊鈾C(jī)制——cwnd線性上漲。
我們可以看到如果沒(méi)有FACK在,那么在丟包比較多的情況下,原來(lái)保守的算法會(huì)低估了需要使用的window的大小,而需要幾個(gè)RTT的時(shí)間才會(huì)完成恢復(fù),而FACK會(huì)比較激進(jìn)地來(lái)干這事。但是,F(xiàn)ACK如果在一個(gè)網(wǎng)絡(luò)包會(huì)被 reordering的網(wǎng)絡(luò)里會(huì)有很大的問(wèn)題。
其它擁塞控制算法簡(jiǎn)介
TCP Vegas 擁塞控制算法
這個(gè)算法1994年被提出,它主要對(duì)TCP Reno 做了些修改。這個(gè)算法通過(guò)對(duì)RTT的非常重的監(jiān)控來(lái)計(jì)算一個(gè)基準(zhǔn)RTT。然后通過(guò)這個(gè)基準(zhǔn)RTT來(lái)估計(jì)當(dāng)前的網(wǎng)絡(luò)實(shí)際帶寬,如果實(shí)際帶寬比我們的期望的帶寬要小或是要多的活,那么就開(kāi)始線性地減少或增加cwnd的大小。如果這個(gè)計(jì)算出來(lái)的RTT大于了Timeout后,那么,不等ack超時(shí)就直接重傳。(Vegas 的核心思想是用RTT的值來(lái)影響擁塞窗口,而不是通過(guò)丟包) 這個(gè)算法的論文是《TCP Vegas: End to End Congestion Avoidance on a Global Internet》這篇論文給了Vegas和 New Reno的對(duì)比:
關(guān)于這個(gè)算法實(shí)現(xiàn),你可以參看Linux源碼:/net/ipv4/tcp_vegas.h「鏈接」, /net/ipv4/tcp_vegas.c「鏈接」
HSTCP(High Speed TCP) 算法
這個(gè)算法來(lái)自RFC 3649(Wikipedia詞條)。其對(duì)最基礎(chǔ)的算法進(jìn)行了更改,他使得Congestion Window漲得快,減得慢。其中:
- 擁塞避免時(shí)的窗口增長(zhǎng)方式:cwnd = cwnd + α(cwnd) / cwnd
- 丟包后窗口下降方式:cwnd = (1- β(cwnd))*cwnd
注:α(cwnd)和β(cwnd)都是函數(shù),如果你要讓他們和標(biāo)準(zhǔn)的TCP一樣,那么讓?duì)?cwnd)=1,β(cwnd)=0.5就可以了。對(duì)于α(cwnd)和β(cwnd)的值是個(gè)動(dòng)態(tài)的變換的東西。關(guān)于這個(gè)算法的實(shí)現(xiàn),你可以參看Linux源碼:/net/ipv4/tcp_highspeed.c「鏈接」
TCP BIC 算法
2004年,產(chǎn)內(nèi)出BIC算法?,F(xiàn)在你還可以查得到相關(guān)的新聞《Google:美科學(xué)家研發(fā)BIC-TCP協(xié)議 速度是DSL六千倍》 BIC全稱Binary Increase Congestion control,在Linux 2.6.8中是默認(rèn)擁塞控制算法。BIC的發(fā)明者發(fā)這么多的擁塞控制算法都在努力找一個(gè)合適的cwnd – Congestion Window,而且BIC-TCP的提出者們看穿了事情的本質(zhì),其實(shí)這就是一個(gè)搜索的過(guò)程,所以BIC這個(gè)算法主要用的是Binary Search——二分查找來(lái)干這個(gè)事。關(guān)于這個(gè)算法實(shí)現(xiàn),你可以參看Linux源碼:/net/ipv4/tcp_bic.c「鏈接」
TCP WestWood算法
westwood采用和Reno相同的慢啟動(dòng)算法、擁塞避免算法。westwood的主要改進(jìn)方面:在發(fā)送端做帶寬估計(jì),當(dāng)探測(cè)到丟包時(shí),根據(jù)帶寬值來(lái)設(shè)置擁塞窗口、慢啟動(dòng)閾值。那么,這個(gè)算法是怎么測(cè)量帶寬的?每個(gè)RTT時(shí)間,會(huì)測(cè)量一次帶寬,測(cè)量帶寬的公式很簡(jiǎn)單,就是這段RTT內(nèi)成功被ack了多少字節(jié)。因?yàn)椋@個(gè)帶寬和用RTT計(jì)算RTO一樣,也是需要從每個(gè)樣本來(lái)平滑到一個(gè)值的——也是用一個(gè)加權(quán)移平均的公式。另外,我們知道,如果一個(gè)網(wǎng)絡(luò)的帶寬是每秒可以發(fā)送X個(gè)字節(jié),而RTT是一個(gè)數(shù)據(jù)發(fā)出去后確認(rèn)需要的時(shí)候,所以,X * RTT應(yīng)該是我們緩沖區(qū)大小。所以,在這個(gè)算法中,ssthresh的值就是est_BD * min-RTT(最小的RTT值),如果丟包是Duplicated ACKs引起的,那么如果cwnd > ssthresh,則 cwin = ssthresh。如果是RTO引起的,cwnd = 1,進(jìn)入慢啟動(dòng)。
-
數(shù)據(jù)
+關(guān)注
關(guān)注
8文章
7256瀏覽量
91932 -
服務(wù)器
+關(guān)注
關(guān)注
13文章
9797瀏覽量
88024 -
端口
+關(guān)注
關(guān)注
4文章
1046瀏覽量
32963 -
TCP協(xié)議
+關(guān)注
關(guān)注
1文章
101瀏覽量
12465
發(fā)布評(píng)論請(qǐng)先 登錄
TCP協(xié)議詳細(xì)解析

TCP/IP協(xié)議連接指南
Q2406B內(nèi)嵌TCP協(xié)議模塊測(cè)試
tcp ip協(xié)議_什么是tcp ip協(xié)議

基于WRED協(xié)議的TCP連接初始化的優(yōu)化方法

tcp和udp協(xié)議的異同

ISO on TCP協(xié)議通信的連接配置
什么是TCP狀態(tài)轉(zhuǎn)移

TCP協(xié)議和UDP協(xié)議的區(qū)別
TCP狀態(tài)機(jī)設(shè)計(jì)與實(shí)現(xiàn)

TCP連接的建立與中止
如何理解HTTP協(xié)議是無(wú)狀態(tài)的

評(píng)論