表白失敗后,我明白了TCP實(shí)現(xiàn)原理
前幾天發(fā)了一個(gè)朋友圈,發(fā)現(xiàn)暗戀已久的女生給我點(diǎn)了個(gè)贊,于是我當(dāng)晚輾轉(zhuǎn)反側(cè)、徹夜未眠!想著妹子是不是對(duì)我有感覺呢?不然怎么會(huì)突然給我點(diǎn)贊呢?要不趁機(jī)表個(gè)白?
于是第二天我在心中模擬了多次表白的話語,連呼吸都反復(fù)練習(xí)。
到了晚上,我撥通了妹子的微信語音,還沒等對(duì)方開口我就按捺不住內(nèi)心的想法,開始自說自話,一陣狂亂的表達(dá)…
足足五分鐘一氣呵成,一切都是那么自然!可是在我說完之后卻半天都沒有等到妹子的回應(yīng)…
過了好一會(huì)兒才聽到對(duì)方的聲音:“喂!喂!我這邊信號(hào)不好,你剛剛在說啥我一句都沒聽到,我在跟我男朋友逛街呢…”。
我掛斷了電話,我也對(duì)我這次失敗的表白進(jìn)行了深度的總結(jié)!原因就是因?yàn)槲覜]有學(xué)好 TCP!
如果我懂 TCP,那我在表白之前至少要先問一句“在嗎?”!先建立可靠的連接,確保連接正常才能開始表白!
如果我懂 TCP,那我在我說話的過程中需要對(duì)方不斷的確認(rèn),這樣才能保證我說的每一句話對(duì)方都能聽到!這樣我才能表白成功!
所以一切都是因?yàn)槲覜]有學(xué)好 TCP,于是我走進(jìn)了圖書館…
我們先來看下 TCP 的定義:
TCP 全稱為 Transmission Control Protocol(傳輸控制協(xié)議),是一種面向連接的、可靠的、基于字節(jié)流的傳輸層通信協(xié)議。TCP 是為了在不可靠的互聯(lián)網(wǎng)絡(luò)上提供可靠的端到端字節(jié)流而專門設(shè)計(jì)的一個(gè)傳輸協(xié)議。
這里面每一個(gè)字我們都認(rèn)識(shí),但是連在一塊就不是那么好理解了!那我們就提煉一些關(guān)鍵的詞,也就是我上面高亮的那些:面向連接、可靠、基于字節(jié)流、傳輸層、協(xié)議、端到端!
理解了這些關(guān)鍵字也就理解了 TCP 的實(shí)現(xiàn)原理,那我們就來從這些關(guān)鍵字開始進(jìn)行分析!
傳輸層
我們先講傳輸層,因?yàn)榭梢詮谋容^高的層面去看 TCP,我們先看下經(jīng)典的 OSI 七層網(wǎng)絡(luò)參考模型:
當(dāng)我們需要在網(wǎng)絡(luò)上進(jìn)行數(shù)據(jù)交換的時(shí)候,就需要經(jīng)過這么幾層。每一層都有相關(guān)落地的實(shí)現(xiàn),我們今天要講的 TCP 就是傳輸層的一種落地實(shí)現(xiàn)。
可能我們平時(shí)在說到傳輸層的時(shí)候自然而然的就想到的 TCP,但是 TCP 只是傳輸層的一種實(shí)現(xiàn),其他比較常見的傳輸層協(xié)議還有 UDP 等!
我知道干巴巴的文字對(duì)你來說太抽象,那我就抓個(gè)包來看看,讓這幾層更加具象!
本文中所有的包都是通過 postman 發(fā)送請(qǐng)求,然后用 wireShark 來抓的!如果對(duì)這兩款軟件還不了解的盆友可以先去了解下哈,這里不過多說明。
我們?cè)?postman 中輸入 www.17coding.info 的域名,然后發(fā)送請(qǐng)求,wireshark 就能抓到數(shù)據(jù)包了。
圖上已經(jīng)標(biāo)明每一層與抓到的數(shù)據(jù)包對(duì)應(yīng)的關(guān)系了!咦!我們上面不是說的 7 層網(wǎng)絡(luò)參考模型么?為什么數(shù)據(jù)包只有 5 層呢?
注意參考二字,7 層模型是一個(gè)理論模型,實(shí)際的網(wǎng)絡(luò)中往往都把應(yīng)用層、會(huì)話層、表示層統(tǒng)為應(yīng)用層!
什么是協(xié)議?
說到協(xié)議,就是雙方共同遵守的一種約定!比如我寫的這篇文章里,你能夠看懂我寫的每一個(gè)字并明白我的意思,那就是因?yàn)槲覀兌甲裱藵h語的語法,這本身也就是一種協(xié)議。
還有比如我們寫代碼就必須按照規(guī)定的語法進(jìn)行編寫,這樣編譯器才能進(jìn)行正確編譯。
在計(jì)算機(jī)網(wǎng)絡(luò)中也有很多協(xié)議,比如常見的應(yīng)用層協(xié)議 http、ftp、dns 協(xié)議等等。
常見的傳輸層協(xié)議有 TCP、UDP 等等,其實(shí)這些協(xié)議都是發(fā)送方和接收方都在遵循的一種規(guī)范。
如果我們遵循了其規(guī)范,也能成為協(xié)議的實(shí)現(xiàn)者,比如自己寫一個(gè) web 服務(wù)器處理用戶請(qǐng)求。甚至我們還能自己規(guī)定一套協(xié)議,供別人使用!
TCP 頭部格式
我們前面說了協(xié)議的定義,那 TCP 協(xié)議肯定也有一定的規(guī)范咯!這樣通信雙方才能識(shí)別對(duì)方的數(shù)據(jù)報(bào)文,進(jìn)行數(shù)據(jù)交換。
我們先看下 TCP 的報(bào)文格式:
TCP 報(bào)文包含數(shù)據(jù)頭和數(shù)據(jù)體,頭部有 5 行的固定長度以及 1 行可變長度!圖上前面 5 行就是固定長度!
固定長度的每一行占有 4 個(gè)字節(jié)(32 位)。因此頭部固定長度就為 5*4=20 個(gè)字節(jié)!
到這里我們可以抓個(gè)包來看下加深印象,我們依然向 www.17coding.info 發(fā)送一個(gè)請(qǐng)求,然后看看其 TCP 部分的數(shù)據(jù)包:
接下來那我們就一行一行的來分析 TCP 的頭部:
第一行:
1、源端口:發(fā)送方端口
2、目標(biāo)端口:接收方端口
前面我們說到 TCP 是端到端的,這里就能很好的體現(xiàn)了!每個(gè)數(shù)據(jù)包中都有發(fā)送方和接收方的端口。這里每個(gè)端口占用 2 個(gè)字節(jié)(16 位)。
第二行、第三行:
1、序號(hào):TCP 是面向字節(jié)流的,數(shù)據(jù)分塊在緩存存放及發(fā)送,序號(hào)用來標(biāo)記某個(gè)數(shù)據(jù)包最開始的字節(jié)是整個(gè)數(shù)據(jù)的第多少個(gè)字節(jié)。
2、確認(rèn)號(hào):每次收到請(qǐng)求后,接收方都會(huì)回復(fù)發(fā)送方,告訴對(duì)方自己已經(jīng)接收了多少字節(jié),下一個(gè)數(shù)據(jù)包需要從第多少字節(jié)開始發(fā)送。這里的值一般等于接收到的序號(hào)+接收到的數(shù)據(jù)包數(shù)據(jù)部分長度。
這里的序號(hào)和確認(rèn)號(hào)是保證 TCP 可靠特性所不可或缺的,我們后面會(huì)通過抓包來詳細(xì)分析!序號(hào)和確認(rèn)號(hào)分別都占用了 4 個(gè)字節(jié)(32 位)!
第四行:
1、數(shù)據(jù)偏移:這里叫頭部長度更為合適。前面說過 TCP 頭部長度有部分是可變的,所以需要標(biāo)識(shí)數(shù)據(jù)包數(shù)據(jù)部分從哪里開始。這個(gè)值占用了 4 位。
2、保留:未使用,供擴(kuò)展使用。這個(gè)值占了 3 位。
3、標(biāo)志:標(biāo)志一共有 9 個(gè),每個(gè)標(biāo)識(shí)占 1 位,共占 9 位。上面的抓包截圖就能看到這 9 個(gè)標(biāo)識(shí)位!
3.1、NS:Nonce,與 ECN 顯式擁塞通知相關(guān)。
3.2、CWR:CWR 標(biāo)志與后面的 ECE 標(biāo)志都用于 IP 首部的 ECN 字段,ECE 標(biāo)志為 1 時(shí),則通知對(duì)方已將擁塞窗口縮小。
3.3、ECE:ECN-Echo,若設(shè)置了該標(biāo)識(shí),則會(huì)通知對(duì)方,從對(duì)方到這邊的網(wǎng)絡(luò)有阻塞。
3.4、URG:Urgent,用于在發(fā)送方加塞。比如在下載文件的時(shí)候,下到一半了需要停止下載,就需要發(fā)送一個(gè)緊急的請(qǐng)求告訴對(duì)方停止發(fā)送數(shù)據(jù)。數(shù)據(jù)包不排隊(duì)。
3.5、ACK:Acknowledgment,標(biāo)記為一個(gè)確認(rèn)。
3.6、PSH:Push,與 URG 對(duì)應(yīng)的,用于接收方加塞。
3.7、RST:Reset,表示出現(xiàn)嚴(yán)重差錯(cuò),可能需要重新創(chuàng)建 TCP 連接。如果我們打開某個(gè)網(wǎng)站一直沒刷出來,我們F5進(jìn)行刷新,那之前的數(shù)據(jù)包就要拒絕。
3.8、SYN:用于同步,建立請(qǐng)求的時(shí)候用。在握手時(shí)候會(huì)帶這個(gè)標(biāo)記!
3.9、FIN:通信結(jié)束,釋放連接的時(shí)候用。在揮手時(shí)候會(huì)帶這個(gè)標(biāo)記!
4、窗口:不管是發(fā)送方還是接收方,都有對(duì)應(yīng)的發(fā)送窗口和接收窗口。在通信之前,通信雙方會(huì)協(xié)商窗口的大小。
發(fā)送方按照接收方的接收窗口設(shè)置自己的發(fā)送窗口,同時(shí)發(fā)送窗口還受擁塞窗口的限制,這個(gè)在擁塞控制部分會(huì)提到!
在發(fā)送過程中窗口會(huì)根據(jù)接收方的處理能力調(diào)整。這個(gè)值對(duì) TCP 的可靠傳輸及流量控制起了很大的作用!這個(gè)值占了 16 位。
第五行:
1、校驗(yàn)和:用于校驗(yàn)數(shù)據(jù)包是否完整或者被修改。這個(gè)值占了 16 位。
2、緊急指針:用來標(biāo)記本報(bào)文段中緊急數(shù)據(jù)的指針,也就是指明了從數(shù)據(jù)包數(shù)據(jù)部分的頭部到指定位置的數(shù)據(jù)為緊急數(shù)據(jù),只有在設(shè)置了標(biāo)志位 URG 的時(shí)候才起作用。這個(gè)值占了 16 位。
第六行:
1、選項(xiàng):選項(xiàng)里面也有些重要的數(shù)據(jù),我們挑幾個(gè)講一下。
1.1、MSS:MSS 的全稱為 Maximum segment size,雙方協(xié)商的每一個(gè)報(bào)文段所能承載的最大數(shù)據(jù)長度(不包括文段頭)。
1.2、WS:WS 的全稱為 Window scale,也叫窗口因子!是用來調(diào)整窗口大小的。前面我們說到過窗口大小的字段,那這個(gè)窗口因子又是做什么用的呢?
早期的網(wǎng)絡(luò)帶寬、硬件配置都比較差,所以窗口大小最大只預(yù)留了 16 個(gè) bit,也就是最大能設(shè)置的值為 65535。
隨著硬件和網(wǎng)絡(luò)的發(fā)展,65535 已經(jīng)不能滿足。所以就增加了一個(gè) WS 的選項(xiàng)來擴(kuò)展!如果設(shè)置了 WS,那實(shí)際的窗口大小就等于窗口大小乘以窗口因子。
1.3、SACK:SACK 的全稱為 Selective ACK,選擇性確認(rèn)是建立在累計(jì)確認(rèn)(后面講) 的基礎(chǔ)上的!
只有收到失序的分組時(shí)才會(huì)可能會(huì)發(fā)送 SACK,如果接收方接收到了后面的數(shù)據(jù)包,而發(fā)現(xiàn)前面的數(shù)據(jù)包丟失,則會(huì)通知發(fā)送方哪些報(bào)文段丟失,需要重發(fā)!
2、填充:這個(gè)字段是為了讓整個(gè)頭部為 4 個(gè)字節(jié)的倍數(shù)。Java 中也有很多類似的用法!
我們找到一個(gè)數(shù)據(jù)包,看看其詳細(xì)的頭部數(shù)據(jù):
- 紅色部分顯示了 TCP 頭部的長度為 32byte,以及選項(xiàng)部分為 12byte。前面我們說了 TCP 首部固定長度為 20byte,所以 20+12=32。
- 黃線部分的窗口大小為 259byte,窗口因子為 256。所以實(shí)際的窗口大小為 259*256=66304!
面向連接怎么理解
從我表白失敗的例子就能看到,我還未確保連接的正常就開始表白,導(dǎo)致我說完了對(duì)方卻因?yàn)樾盘?hào)不好沒有聽到。
如果我事先確保連接正常,就不會(huì)出現(xiàn)這樣的情況了!我們前面說了 TCP 是面向連接的,那 TCP 是怎么面向連接的呢?
三次握手交代了什么?
沒錯(cuò),都是從握手開始!我們都知道,TCP 建立連接需要經(jīng)過三次握手,那每次握手都交代了什么呢?如果只進(jìn)行兩次握手行不行?
我們先看一個(gè)電話接通的場景:
A:你好,你能聽到嗎? B:我能聽到,你能聽到嗎? A:我也能聽到。 …….
在正式通話之前,為了確保通話的可靠,往往都需要經(jīng)過上面的三次對(duì)話進(jìn)行確認(rèn)。
那這三次對(duì)話是必須的嗎?每一次對(duì)話的必要性又是什么呢?
A:你好,你能聽到嗎?(讓B知道A能說話) B:我能聽到,你能聽到嗎?(讓A知道B能聽到,且能說話) A:我也能聽到。(讓B知道A能聽到) …….
只有經(jīng)過三次的對(duì)話,才能確認(rèn)自己的聲音能被對(duì)方聽到且能聽到對(duì)方的聲音。這也才能開展后續(xù)的對(duì)話。
這里我們就不得不祭出經(jīng)典的三次握手圖了:
我們分析三次握手過程及每次握手后的狀態(tài)如下:
- A 主機(jī)發(fā)送標(biāo)識(shí) SYN=1(SYN 表示 A 請(qǐng)求跟 B 建立連接,前面在講 TCP 頭部時(shí)候有說到過),序號(hào) Seq=x,第一次握手請(qǐng)求發(fā)送后 A 的狀態(tài)為 SYN_SENT,B 在接收到請(qǐng)求后狀態(tài)由 LISTEN 變?yōu)?SYN_RCVD!
- B 主機(jī)收到連接請(qǐng)求后向 A 主機(jī)發(fā)送標(biāo)識(shí) SYN=1,ACK=1(SYN 表示 B 請(qǐng)求跟 A 建立連接,ACK 表示對(duì) A 的連接請(qǐng)求進(jìn)行應(yīng)答),序號(hào) Seq=y,確認(rèn)號(hào) Ack=(x+1),A 接收到 B 的確認(rèn)后,狀態(tài)變?yōu)?ESTABLISHED,B 的狀態(tài)依然為 SYN_RCVD!
- 主機(jī) A 收到后檢查 ACK 是否正確,若正確,則發(fā)送標(biāo)識(shí) ACK=1(表示對(duì) B 的連接請(qǐng)求進(jìn)行應(yīng)答),序號(hào) Seq=(x+1),確認(rèn)號(hào) Ack=(y+1)。B 接收到 A 的確認(rèn)后,A 和 B 的狀態(tài)都變?yōu)?ESTABLISHED!
這里我們要注意的幾點(diǎn)是:
- 圖中的發(fā)送請(qǐng)求中中括號(hào)里面的 SYN、ACK 就是前面說 TCP 頭部中的那幾個(gè)標(biāo)志位!而 Seq 和 ACK 分別代表序號(hào)和確認(rèn)號(hào)。
- 接收方在接收到發(fā)送方發(fā)送的 Seq 后,應(yīng)答一個(gè) ACK,ACK 的值等于 Seq+1,表示已要發(fā)送方開始發(fā)送 Seq+1 位置的數(shù)據(jù)。
- B 在接收到了 A 的連接請(qǐng)求后回復(fù)中同時(shí)發(fā)送了 SYN、ACK 兩個(gè)標(biāo)識(shí)位,將建立連接的請(qǐng)求和對(duì) A 的應(yīng)答在同一個(gè)包中發(fā)送了,這也是為什么只需要三次握手,就能建立連接。
我們依然向 www.17coding.info 發(fā)送請(qǐng)求,下面為三次握手的包:
在 info 那一欄,我們很明顯的能看到發(fā)送的數(shù)據(jù)包頭部有我們上面說到的那些標(biāo)志位,還有 Seq、ACK 等頭部信息,還有 Win、MSS 等頭部選項(xiàng)數(shù)據(jù)!因此三次握手不僅僅是單純建立連接,還會(huì)協(xié)商一些參數(shù)!
當(dāng)我鼠標(biāo)選擇某一行時(shí),如果這個(gè)數(shù)據(jù)包包含了對(duì)某個(gè)數(shù)據(jù)包的確認(rèn)(也就是有 ACK 的標(biāo)記),就能在對(duì)應(yīng)的數(shù)據(jù)包的 No 列上面看到一個(gè)小勾勾,比如上面圖中我鼠標(biāo)選擇的是第三次握手的數(shù)據(jù)包,在第二次握手的數(shù)據(jù)包前面就有個(gè)小勾勾。
為什么握手只需要三次而揮手需要四次?
通過三次握手,雙方就建立了一個(gè)可靠的連接,就能進(jìn)行數(shù)據(jù)的傳輸了!當(dāng)數(shù)據(jù)傳輸完成,就得將連接關(guān)閉,因?yàn)檫B接也是一種資源!連接的關(guān)閉需要經(jīng)過四次揮手!
為什么握手可以三次完成,但是揮手卻需要四次呢?我偏要三次行不行?其實(shí)也沒啥不可以的!
比如下面的對(duì)話場景:
A:我說完了,你說完就掛電話吧! B:好嘞,我也說完了,可以掛電話了! A:好嘞,拜拜。 掛斷……
這樣三次對(duì)話就可以實(shí)現(xiàn)揮手了,但是在實(shí)際的網(wǎng)絡(luò)中,當(dāng)我發(fā)出一個(gè)請(qǐng)求的時(shí)候,可能服務(wù)器的響應(yīng)體比較大,需要較長時(shí)間的傳輸!
所以當(dāng)客戶端主動(dòng)發(fā)起斷開請(qǐng)求的時(shí)候,服務(wù)器先回應(yīng)一個(gè)確認(rèn),等所有數(shù)據(jù)傳輸完畢后再發(fā)送服務(wù)器斷開的請(qǐng)求。
A:我說完了,你說完就掛電話吧! B:好嘞… B:…… B:我也說完了,可以掛電話了 A:好嘞,拜拜 掛斷……
所以大部分情況下都需要進(jìn)行四次揮手!但是,在我個(gè)人的抓包實(shí)踐中,也會(huì)有三次揮手就能完成斷開連接的情況。
這里我們又不得不祭出經(jīng)典的四次揮手圖了:
我們分析四次揮手過程及每次揮手后的狀態(tài)如下:
- 主機(jī) A 發(fā)送標(biāo)識(shí) FIN=1(FIN 表示 A 請(qǐng)求關(guān)閉連接)用來關(guān)閉 A 到 B 的數(shù)據(jù)傳輸。此時(shí) A 的狀態(tài)為 FIN_WAIT_1!
- 主機(jī) B 收到關(guān)閉請(qǐng)求后向 A 發(fā)送 ACK(ACK 表示應(yīng)答 A 的關(guān)閉連接請(qǐng)求),A 不再向 B 發(fā)送數(shù)據(jù)。此時(shí) A 的狀態(tài)為 FIN_WAIT_2,B 為 CLOSE_WAIT!
- 主機(jī) B 發(fā)送標(biāo)識(shí) FIN=1 用來關(guān)閉 B 到 A 的數(shù)據(jù)傳輸。此時(shí) A 的狀態(tài)為 TIME_WAIT,B 為 LAST_ACK!
- 主機(jī) A 收到關(guān)閉請(qǐng)求后向 B 發(fā)送 ACK,此時(shí) B 不再向 A 發(fā)送數(shù)據(jù)。此時(shí) A、B 都關(guān)閉了,狀態(tài)變?yōu)?CLOSED。
在圖中我們能看到,A 的 TIME_WAIT 狀態(tài)會(huì)持續(xù) 2MSL 再變成 CLOSED。
MSL(Maximum Segment Lifetime)的中文可以譯為“報(bào)文最大生存時(shí)間”!他是任何報(bào)文在網(wǎng)絡(luò)上存在的最長時(shí)間,超過這個(gè)時(shí)間報(bào)文將被丟棄。
那 TIME_WAIT 維持 2MSL 的作用是什么呢?
- 第 4 次揮手的時(shí)候主機(jī) A 發(fā)送 ACK 到主機(jī) B,如果發(fā)送完成后就直接就關(guān)閉連接,那如果由于網(wǎng)絡(luò)原因 B 沒有收到 ACK,那 B 就沒法關(guān)閉連接了!因此 A 在回復(fù)確認(rèn)后,還需要等待,萬一 B 沒有收到應(yīng)答還會(huì)繼續(xù)發(fā)送 FIN 的請(qǐng)求。
- 如果不等待 2MSL,那客戶端的端口可能會(huì)被重用,如果再次用這個(gè)端口建立與服務(wù)器的連接,那前后兩個(gè)使用相同四元組的的連接之間會(huì)形成干擾!
我們看上面向 www.17coding.info 發(fā)送請(qǐng)求的揮手?jǐn)?shù)據(jù)包:
可能大家在抓包的時(shí)候不能立馬看到四次揮手的數(shù)據(jù)包!那是因?yàn)樵?HTTP 1.1 及之后,默認(rèn)都開啟了長連接!
也就是在一次請(qǐng)求之后,建立的連接并不會(huì)立馬關(guān)閉,而是供后續(xù)的其他請(qǐng)求繼續(xù)使用,以減少每次重新建立連接的資源消耗!
如果想發(fā)出請(qǐng)求后立馬能抓到四次揮手的數(shù)據(jù)包,可以設(shè)置 HTTP 的頭部 Connection:close。
這樣每次發(fā)送請(qǐng)求都能看到完整的三次握手四次揮手的過程啦!
TCP 是怎么保證可靠傳輸?shù)?
保證傳輸?shù)目煽课覀兦懊嬉呀?jīng)說到了面向連接,建立連接是保證數(shù)據(jù)傳輸?shù)牡谝徊?。那在連接建立之后的數(shù)據(jù)傳輸怎么保證可靠呢?
我們?cè)俅位氐轿覀兇螂娫挼膱鼍?,一般在?duì)話的過程中,都是得雙方都有互動(dòng),給與對(duì)方回應(yīng)。而不是一個(gè)人一個(gè)勁的說而另一方?jīng)]有任何回應(yīng)!
比如下面場景:
A:跟你講哦,我上周網(wǎng)上認(rèn)識(shí)了一個(gè)妹子 B:嚯,牛逼啊! A:然后我昨天約出來見面了 B:666啊!然后呢? A:然后我們@#¥%……& B:臥槽,你剛剛說啥我沒聽清,你再說一遍?...
這樣的確認(rèn)和應(yīng)答就確保了雙方的通信能夠完整可靠。TCP 也采用了這種 y 應(yīng)答和確認(rèn)重傳的機(jī)制,保證在不可靠的網(wǎng)絡(luò)上實(shí)現(xiàn)可靠的傳輸。只要我沒有收到確認(rèn),我就認(rèn)為沒有發(fā)送成功,就會(huì)重發(fā)。
停止等待協(xié)議
停止等待協(xié)議就是每次給對(duì)方發(fā)送數(shù)據(jù)包后,需要等待對(duì)方的回應(yīng)然后再發(fā)送下一個(gè)數(shù)據(jù)包!
停止等待協(xié)議會(huì)出現(xiàn)如下幾種情況:
①無差錯(cuò)情況:A 發(fā)送 M1 包到 B,B 收到后會(huì)給 A 一個(gè)確認(rèn),當(dāng) A 收到 B 的確認(rèn)后再發(fā)送包 M2。
②超時(shí)重傳:A 發(fā)送 M1 包到 B,如果發(fā)送過程中包丟失,A 會(huì)重新發(fā)送。A 等待重發(fā)的時(shí)間是比一個(gè)報(bào)文的往返時(shí)間(RTT)稍微多一點(diǎn)。
③確認(rèn)丟失:如果 B 在給 A 發(fā)送確認(rèn)的時(shí)候丟失,A 會(huì)重新發(fā)送 M1 包給 B,由于 B 已經(jīng)處理過 M1 的數(shù)據(jù)包所以 B 會(huì)丟棄報(bào)文,然后重傳確認(rèn) M1 給 A。
④確認(rèn)遲到:如果 A 發(fā)送數(shù)據(jù)包 M1 給 B,B 回復(fù)確認(rèn)的時(shí)候延遲了。這時(shí) A 又會(huì)重新發(fā)送包 M1 給 B,B 收到后丟棄數(shù)據(jù)包,然后重傳確認(rèn) M1 給 A。這時(shí) A 會(huì)收到多次確認(rèn),當(dāng)?shù)诙问盏竭t到的確認(rèn)后 A 也會(huì)丟棄該確認(rèn)。
我們從上面能看到,停止等待協(xié)議每次都是等到收到確認(rèn)后再發(fā)下一個(gè)數(shù)據(jù)包。
只要我沒收到你給我的確認(rèn),我就認(rèn)為你沒有收到我發(fā)的數(shù)據(jù)包,我就會(huì)進(jìn)行重發(fā)!這樣雖然可靠,但是會(huì)導(dǎo)致信道利用率較低!
流水線傳輸
流水線傳輸就是每次發(fā)送多組數(shù)據(jù)包,不必每次發(fā)完一組就停下來等待對(duì)方的確認(rèn)。由于信道上一直有數(shù)據(jù)不間斷的傳輸,因此可以獲得較高的信道利用率!
流水線傳輸如何保證可靠的呢?需要發(fā)送方維持發(fā)送窗口,假如發(fā)送窗口是 5,那 5 個(gè)數(shù)據(jù)包會(huì)同時(shí)發(fā)送,然后等確認(rèn)!
如果有收到接收方的確認(rèn),窗口就會(huì)滑動(dòng),進(jìn)行第 6 個(gè)數(shù)據(jù)包的發(fā)送。如果都是單個(gè)確認(rèn),可能效率會(huì)比較低,所以有了累計(jì)確認(rèn)!
也就是說假如發(fā)送方發(fā)送了數(shù)據(jù)包 1、2、3、4,接收方只需要回復(fù)對(duì)數(shù)據(jù)包 4 的確認(rèn),那表示 1234 數(shù)據(jù)包都已經(jīng)收到了,就可以進(jìn)行第五個(gè)數(shù)據(jù)包的發(fā)送了!
假如發(fā)送了數(shù)據(jù)包 1、2、3、4,其中第三個(gè)數(shù)據(jù)包丟失,那該怎么確認(rèn)呢?
TCP 只會(huì)回復(fù)對(duì)數(shù)據(jù)包 2 的確認(rèn),并且對(duì)數(shù)據(jù)包 4 進(jìn)行選擇性確認(rèn)(TCP 頭部選項(xiàng)講到過的 SACK),這樣發(fā)送方就知道數(shù)據(jù)包 4 已經(jīng)成功發(fā)送,只需要重發(fā)數(shù)據(jù)包 3。
繼續(xù)前面抓包的例子,接收方并不是對(duì)每個(gè)數(shù)據(jù)包都進(jìn)行確認(rèn),而是對(duì)多個(gè)數(shù)據(jù)包進(jìn)行累計(jì)確認(rèn):
這里我們能看到服務(wù)器發(fā)送多個(gè)數(shù)據(jù)包后,客戶端才進(jìn)行了一次確認(rèn)。
流量控制和擁塞控制
通過前面我們知道了,通過建立可靠的連接和確認(rèn)機(jī)制,保證了 TCP 的連接的可靠!
但是每個(gè)人使用的計(jì)算機(jī)的處理能力都是不一樣的,我發(fā)送太快了對(duì)方處理不過來怎么辦呢?通信雙方怎么去協(xié)調(diào)發(fā)送和接收數(shù)據(jù)的頻率呢?
以字節(jié)為單位的滑動(dòng)窗口技術(shù)
在介紹 TCP 頭部的時(shí)候,我們已經(jīng)提到過滑動(dòng)窗口,并且介紹了相關(guān)的控制參數(shù) Win!也說到了接收窗口和發(fā)送窗口!那他們的關(guān)系是怎么樣的呢?
假設(shè)現(xiàn)在 A 需要傳輸數(shù)據(jù)給 B,B 就先要告訴 A 自己的接收窗口有多大。A 根據(jù) B 的接收窗口設(shè)置自己的發(fā)送窗口!A 的發(fā)送窗口時(shí)不能大于 B 的接收窗口的!
在開始傳輸數(shù)據(jù)之前,初始的窗口設(shè)置如下圖:
如上圖我們能否看到,B 的接收窗口設(shè)置為 10 個(gè)字節(jié),那 A 的發(fā)送窗口設(shè)置不能超過 10 個(gè)字節(jié)!
如果開始傳送數(shù)據(jù),A 會(huì)將數(shù)據(jù)封裝成多個(gè)數(shù)據(jù)包進(jìn)行傳輸,如下圖:
在沒有收到 B 的確認(rèn)之前,A 的窗口不會(huì)滑動(dòng),也就是說最多能發(fā) 10 個(gè)字節(jié)的數(shù)據(jù)。
如果 B 接受到數(shù)據(jù)且回復(fù)確認(rèn)給了 A,那 A 的窗口則進(jìn)行滑動(dòng),如下圖:
這樣,A 又可以進(jìn)行第 11、12 個(gè)字節(jié)的發(fā)送啦!如果 B 的處理能力變?nèi)趿?,也可以通?A 將發(fā)送窗口調(diào)小!
這樣也也就很好的協(xié)調(diào)了雙方的接收和發(fā)送能力!這也就很好的實(shí)現(xiàn)了 TCP 的可靠傳輸和流量控制!
上面的數(shù)據(jù)包繼續(xù)發(fā)送,如果在發(fā)送過程中,3、4、5 這三個(gè)字節(jié)組成的數(shù)據(jù)包丟了,但是后面的數(shù)據(jù)卻收到了,這時(shí)候 A 的發(fā)送窗口會(huì)移動(dòng)么?
如果是這種情況,A 的發(fā)送窗口是不會(huì)移動(dòng)的。B 在接收到后面數(shù)據(jù)包的時(shí)候回復(fù)給 A 的 ACK 會(huì)設(shè)置為 3,且在選項(xiàng)中設(shè)置一個(gè) SACK(在 TCP 頭部選項(xiàng)里面有描述),告訴 A 哪部分?jǐn)?shù)據(jù)收到了,而哪部分?jǐn)?shù)據(jù)需要進(jìn)行重發(fā)!
擁塞控制
利用滑動(dòng)窗口技術(shù),可以很好的協(xié)調(diào)雙方的收發(fā)能力。但是,網(wǎng)絡(luò)狀況是非常復(fù)雜的,且在同一個(gè)網(wǎng)絡(luò)上可能有千千萬萬個(gè)發(fā)送方和接收方!
如果大家都需要傳輸數(shù)據(jù)都需要占用網(wǎng)絡(luò),不做好控制措施,就會(huì)導(dǎo)致整個(gè)網(wǎng)絡(luò)會(huì)堵塞甚至癱瘓。
如果我要從深圳開車去廣州,我就會(huì)走高速。如果只有我一個(gè)人開車,那肯定能暢通無阻!但是高速公路不是我家的,大家都能通行!
所以一到了節(jié)假日,大家都蜂擁而上,而高速的承運(yùn)能力不會(huì)因?yàn)楣?jié)假日而調(diào)整!
這時(shí)候往往就需要交通管制、限流等措施去舒緩交通!
- 綠線代表理想狀況下,如果高速公路的吞吐量為 100!當(dāng)需要通過的車輛不超過 100 時(shí),所有車輛都能順利通過!當(dāng)需要通過的車輛超過 100,那每次通行的車輛為 100,能提供的負(fù)載比較穩(wěn)定。
- 紅色代表沒有任何交通管制情況下,如果高速公路的吞吐量為 100!當(dāng)需要通過的車輛不超過 100 時(shí),會(huì)出現(xiàn)輕微的塞車現(xiàn)象!但是隨著車輛的增多,就會(huì)出現(xiàn)嚴(yán)重的阻塞,甚至癱瘓!
- 藍(lán)色代表在交通管制下,如果高速公路的吞吐量為 100!當(dāng)需要通過的車輛不超過 100 時(shí),會(huì)出現(xiàn)輕微的塞車現(xiàn)象!但是隨著車輛的增多,交通一直保存較高的負(fù)載,不會(huì)出現(xiàn)癱瘓的情況!
網(wǎng)絡(luò)就好比高速公路,傳輸?shù)臄?shù)據(jù)包就好比要通過的車輛,而 TCP 則就更像一個(gè)交警,維護(hù)著數(shù)據(jù)傳輸?shù)闹刃?那 TCP 是怎么做的呢?
慢開始與擁塞避免
發(fā)送方維持一個(gè) cwnd(擁塞窗口,注意這里的擁塞窗口不能大于前面說到的發(fā)送窗口!),剛開始擁塞窗口設(shè)置為 1。
如果發(fā)現(xiàn)這個(gè)包沒有丟失,則調(diào)整擁塞窗口為 2!如果又沒有丟包,則調(diào)整擁塞窗口為 4!
這樣每次以 2 倍的速度一直增長到 16!然后 17、18、19 這樣一個(gè)一個(gè)的增加,直到大小與發(fā)送窗口一致。
這就是所謂的慢開始和擁塞避免,16 就是慢開始門限……
有沒有得寸進(jìn)尺的感覺!
我就蹭蹭不進(jìn)去… … 我就進(jìn)去不動(dòng)… … 我就..
如果在發(fā)送的過程中發(fā)現(xiàn)有丟包現(xiàn)象,則會(huì)調(diào)整擁塞窗口大小為 1,并且設(shè)置新的慢開始門限為出現(xiàn)擁塞時(shí)的二分之一,也就是說當(dāng)擁塞窗口為 24 的時(shí)候出現(xiàn)丟包現(xiàn)象,那新的慢開始門限就調(diào)整為 12!
如果理解了上面的文字描述,下面的圖就不難理解了:
快重傳
前面說過累計(jì)確認(rèn),還說到了選擇性確認(rèn)。這個(gè)就跟快重傳有關(guān)!
接收方如果發(fā)現(xiàn)丟包,不會(huì)等到累計(jì)確認(rèn),就通知發(fā)送方三個(gè)重復(fù)的確認(rèn)通知對(duì)方重新發(fā)送丟失的包。當(dāng)接收方收到三個(gè)重復(fù)的確認(rèn),則意識(shí)到數(shù)據(jù)包丟失,進(jìn)行重傳!
通過下圖能看到,當(dāng)出現(xiàn)丟包的情況,接收方的 ACK 都是等于 50,而 SACK 分別對(duì) 60~89 之間的字節(jié)都進(jìn)行了選擇性的確認(rèn)!
這時(shí)候發(fā)送方也就知道 50~59 這部分?jǐn)?shù)據(jù)丟失而進(jìn)行重傳!
快恢復(fù)
如果一旦發(fā)生丟包,擁塞窗口就變成 1,這種方式也太傻了吧。如果能有個(gè)快速恢復(fù)的機(jī)制就好了!TCP 就使用了快恢復(fù)機(jī)制!
當(dāng)出現(xiàn)丟包時(shí),不會(huì)再次進(jìn)行慢開始,而是直接轉(zhuǎn)入擁塞避免!也就是從新的慢開始門限進(jìn)行加法增加!
看完全文,我們?cè)倩氐?TCP 的定義,你是不是又能有更多的理解了呢?
TCP 全稱為 Transmission Control Protocol(傳輸控制協(xié)議),是一種面向連接的、可靠的、基于字節(jié)流的傳輸層通信協(xié)議。TCP 是為了在不可靠的互聯(lián)網(wǎng)絡(luò)上提供可靠的端到端字節(jié)流而專門設(shè)計(jì)的一個(gè)傳輸協(xié)議。
作者:sullivan06
編輯:陶家龍
出處:轉(zhuǎn)載自公眾號(hào) 17coding 技術(shù)博客,17coding.info 是一個(gè)秉持著勤記錄、齊分享的理念的公眾號(hào),用于記錄平時(shí)所學(xué),分享編程心得。