之前講了「從輸入 URL 再到瀏覽器成功看到界面」中的域名是如何變成 IP 地址的,了解了 DNS 相關(guān)的東西。這篇文章就聊聊發(fā)生在 DNS 解析之后的操作——建立連接。也就是我們常說的三次握手。
看到三次握手你可能會說,這不是面試都被問爛了的題嗎?
三次握手不就是:
- 服務(wù)器開始為 CLOSE 狀態(tài),然后監(jiān)聽某個端口,此時服務(wù)器會進(jìn)入 LISTEN 狀態(tài)
- 客戶端最初也是 CLOSE 狀態(tài),客戶端會向服務(wù)器發(fā)送一個帶 SYN 標(biāo)志位的數(shù)據(jù)包,主動發(fā)起連接。此時客戶端會變成 SYN-SENT 狀態(tài)
- 服務(wù)器接收到客戶端的數(shù)據(jù)包之后,通過標(biāo)志位判斷出了客戶端想要建立連接。然后返回一個 SYN 和 ACK ,此時服務(wù)器的狀態(tài)變?yōu)榱?SYN-RCVD
- 客戶端收到了服務(wù)器的 ACK 之后,會回一個 ACK 給服務(wù)器,回完這個 ACK 之后,服務(wù)器的狀態(tài)就變?yōu)榱?ESTABLISH
- 服務(wù)器收到了客戶端回復(fù)的 ACK 之后,服務(wù)器的狀態(tài)也變成了 ESTABLISH
這不就完了嗎?還有什么好聊的?
這篇文章不會涉及到上面提到的什么各種狀態(tài)的變化,包內(nèi)的標(biāo)志位是什么,而是會更加關(guān)注于底層的東西,也就是上面那些發(fā)來發(fā)去的數(shù)據(jù)包是如何發(fā)送出去的。
其實不僅僅是建立連接時的三次握手,像瀏覽器中調(diào)用的很多 HTTP 接口,都會和服務(wù)器進(jìn)行通信。
那這些個請求到底都是怎么發(fā)送給服務(wù)器的呢?
這還用問?不就是發(fā)個 HTTP 請求就過去了嗎?
當(dāng)然,這個答案可能是很多不了解網(wǎng)絡(luò)的人可能會說出的答案。
其實更具體、更準(zhǔn)確的說法是通過協(xié)議棧和網(wǎng)卡發(fā)送出去的。
其中,協(xié)議棧負(fù)責(zé)對數(shù)據(jù)進(jìn)行打包,打包完成之后就由網(wǎng)卡將數(shù)據(jù)轉(zhuǎn)換成電信號,通過光纖發(fā)送出去了。
網(wǎng)卡自不必說,用來和其他的計算機(jī)進(jìn)行通訊的硬件,我們常說的 MAC(Medium Access Control) 地址,其實就是網(wǎng)卡的編號,從其被生產(chǎn)出來的那一刻就被確定的一個唯一編號。MAC 地址長為 48 個比特,也就是 6 個字節(jié),用十六進(jìn)制進(jìn)行表示。
當(dāng)我們知道了和我們通信的 IP 地址之后,就可以委托操作系統(tǒng)中的協(xié)議棧將來來自應(yīng)用程序的數(shù)據(jù),打包成數(shù)據(jù)包然后發(fā)送出去。那協(xié)議棧,具體是啥呢?協(xié)議棧其實是一系列網(wǎng)絡(luò)協(xié)議的總和,例如:
- TCP
- UDP
- IP
不同的應(yīng)用程序在進(jìn)行數(shù)據(jù)傳輸?shù)臅r候,可能會選擇不同的協(xié)議。例如我們使用的瀏覽器就是使用的 TCP 協(xié)議,而像之前講過的 DNS 解析就用的 UDP 協(xié)議。
那數(shù)據(jù)在協(xié)議棧中到底經(jīng)歷了什么?才變成了一個一個的數(shù)據(jù)包?
就拿我們向服務(wù)器發(fā)送一個 HTTP 請求作為例子,我們知道 HTTP 請求中有:
- 請求行
- 請求頭
- 請求體
HTTP 是屬于應(yīng)用層的協(xié)議,而應(yīng)用層還有很多其他的協(xié)議,每個協(xié)議所涉及到的數(shù)據(jù)也都不同,協(xié)議棧要怎么去兼容不同協(xié)議之間的數(shù)據(jù)呢?
答案是不做兼容。對于協(xié)議棧來說,所有的數(shù)據(jù)都只不過是一堆二進(jìn)制序列。
那協(xié)議棧收到了這一堆二進(jìn)制序列之后是不是就直接交給網(wǎng)卡發(fā)送了呢?
我都這么問了,那顯然不是了...
其實協(xié)議棧在收到數(shù)據(jù)之后并不會馬上就會就發(fā)送出去,而是會先寫入位于內(nèi)存的 Buffer 中。那為啥不直接發(fā)出呢?
其實很簡單,假設(shè)你現(xiàn)在正在公交車的起始站,你覺得公交車會來一個人就立馬發(fā)車嗎?
顯然不是,它會等一段時間,有更多的乘客上車之后再發(fā)車。但是它又不能等太長的時間,不然后續(xù)站臺的乘客就會等的很久。
協(xié)議棧之所以不立即發(fā)出去,其實也是同樣的道理。其實這背后無非基礎(chǔ)兩種考慮:
- 數(shù)據(jù)的長度
- 等待的時間
應(yīng)用層的程序發(fā)送過來的數(shù)據(jù)可能長度都不太一樣,有的可能一個字節(jié)一個字節(jié)的發(fā), 有的可能一次性就傳入所有的數(shù)據(jù)。
如果收到數(shù)據(jù)就發(fā)送出去,會導(dǎo)致在網(wǎng)絡(luò)中傳輸著很多小包,而這會降低網(wǎng)絡(luò)傳輸?shù)男省?
所以,協(xié)議棧在收到數(shù)據(jù)之后會等待一段時間,等數(shù)據(jù)達(dá)到一定量之后,再執(zhí)行發(fā)送操作。
但是,協(xié)議棧又不能等的太久是吧?等太久了你讓正在電腦面前操作的用戶情何以堪,這種發(fā)送延遲會讓用戶體驗刷刷的往下掉。
但是吧,想做到對這兩者的平衡卻不是一件簡單的事。數(shù)據(jù)包太短,降低網(wǎng)絡(luò)傳輸效率,等待太長時間,又會造成發(fā)送延遲。所以協(xié)議棧索性就把控制權(quán)交給了應(yīng)用程序。
應(yīng)用程序可以自己控制到底采取哪種措施,例如我們常用的瀏覽器,因為和用戶實時的在進(jìn)行交互,用戶對整個頁面的響應(yīng)速度也相當(dāng)敏感,所以一般都會采用直接發(fā)送數(shù)據(jù)的方式,即使其數(shù)據(jù)并沒有達(dá)到「一定的量」
這一個「一定的量」到底是啥?
的確,上面都只說一定的量、一定的量,那這個量到底是多少?
要了解這個我們需要知道兩個參數(shù),分別是:
- MTU(Maximum Transmission Unit)最大傳輸單元
- MSS(Maximum Segment Size)最大分段大小
MTU 其實就代表了上面途中數(shù)據(jù)包的最大長度,一般來說是 1500 字節(jié)。而我們需要知道數(shù)據(jù)包是由以下部分組成的:
- 各種頭部信息
- 真實數(shù)據(jù)
而從 MTU 中減去各種頭部數(shù)據(jù)的大小,剩下的就是 MSS 了,也就是實際的數(shù)據(jù)。
知道了數(shù)據(jù)包的組成和 MTU、MSS 的概念之后,我們就可以繼續(xù)接下來的步驟了。某次發(fā)送的數(shù)據(jù),沒有超過 MSS 還好,就可以直接發(fā)送出去了。
那如果超過了 MSS 咋辦?例如我發(fā)這篇文章時所發(fā)請求的數(shù)據(jù)長度就可能超過 MSS 。
過長數(shù)據(jù)包拆分
此時就需要對數(shù)據(jù)進(jìn)行拆分,按照 MSS 的長度為單位進(jìn)行拆分,將拆出來的數(shù)據(jù)分別裝進(jìn)不同的數(shù)據(jù)包中。拆分好之后,就可以發(fā)送給目標(biāo)服務(wù)器了。
TCP 會確保通信的服務(wù)器能夠收到數(shù)據(jù)包。傳輸時對每個字節(jié)都進(jìn)行了編號,舉個例子,假設(shè)此次傳輸?shù)臄?shù)據(jù)是 1 - 1000 字節(jié),然后服務(wù)器回的 ACK 就會是 1001,這就代表沒有丟包。
這些發(fā)送過的包都會暫存在 Buffer 中,如果傳輸?shù)倪^程中出錯,則可以進(jìn)行重發(fā)的補(bǔ)償措施。這也是為什么在數(shù)據(jù)鏈路層(例如網(wǎng)卡、路由器、集線器)等等都沒有補(bǔ)償機(jī)制,它們一旦檢測到錯誤會直接將包丟棄。然后由傳輸層重發(fā)就好。
那要是網(wǎng)絡(luò)很擁堵,服務(wù)器一直沒有返回怎么辦?
在服務(wù)器端,我們?nèi)ズ推渌谌l(fā)進(jìn)行交互時,是不是都會設(shè)定一個超時的時間?如果不設(shè)置超時時間那難道一直在這等下去嗎?
TCP 也同理。客戶端在等待服務(wù)器響應(yīng)時,會有一個時間叫 ACK 等待時間,其實也是超時時間。
當(dāng)網(wǎng)絡(luò)發(fā)生擁堵時,其實你完全也可以把網(wǎng)絡(luò)擁堵理解成路上堵車。此時,ACK 的返回就會變慢。如果返回時間長到了讓客戶端認(rèn)為服務(wù)器沒有收到,就有可能會重發(fā)。
并且有可能剛剛重發(fā)完,ACK 就到了。雖然服務(wù)器端可以通過序號來對包進(jìn)行判重,不會造成錯誤,但是這種沒有意義的重復(fù)包,在本身網(wǎng)絡(luò)負(fù)擔(dān)已經(jīng)很重的情況下,你還往里懟重復(fù)的無用的數(shù)據(jù)包,這不是扯淡嗎?這明顯不行的。
那怎么避免上面的這個情況呢?答案很簡單,稍微延長一點 ACK等待時間,這樣一來就能一定程度上避免上述的問題。但是用屁股想想應(yīng)該也知道,這個時間肯定不是越長越好,再長用戶那又該等爆炸了。
除了網(wǎng)絡(luò)波動會影響到 ACK 的返回時間,通信的物理距離也是一個影響的因素。說白了就是這玩意兒不可能設(shè)置一個固定的時間。所以,實際上,這個等待時間是動態(tài)調(diào)整的,這次稍微返回慢了點,那我下次就稍微延長一點等待時間。返回 ACK 的速度如果很給力,那么就會相應(yīng)的減少等待。
上面的概念也有一個大家很熟悉的名字,叫——超時重傳。
我們來設(shè)想一個更加極端的情況,假設(shè)你們通信的網(wǎng)線被挖斷了,甚至機(jī)房起火了,這個時候無論你重發(fā)多少次都沒用。那 TCP 不就一直無限循環(huán)的把請求發(fā)下去了?
當(dāng)然 TCP 設(shè)計時也考慮到了這種情況,其在重傳幾次無效之后,就會強(qiáng)制中斷通信,并拋出錯誤給應(yīng)用程序。
問題又來了,客戶端在向服務(wù)器發(fā)送數(shù)據(jù)包之后,等待 ACK 的過程中,真的就只是等 ACK,其他的什么也不做嗎?
當(dāng)然不是,這樣極其的浪費資源,降低通信效率。發(fā)送完一個數(shù)據(jù)包之后,不用等待 ACK 的返回,會直接繼續(xù)發(fā)送下一個包,這就是滑動窗口。
但是這樣會有一個問題,應(yīng)用程序發(fā)送包發(fā)送的過于頻繁,導(dǎo)致服務(wù)器接收不過來了。
因為剛剛說過,應(yīng)用程序發(fā)送的時候,會將發(fā)送過的數(shù)據(jù)存儲在 buffer 中。而對于接收方也是一樣的,接收方收到消息之后,會將數(shù)據(jù)存儲在 Buffer 中,然后在 Buffer 中對收到的數(shù)據(jù)進(jìn)行重組,還原成最初的應(yīng)用程序發(fā)送的數(shù)據(jù)。
但是如果發(fā)送的數(shù)據(jù)太快,超過了重組的速度,緩沖區(qū)就會被填滿。而緩沖區(qū)一旦被填滿,后續(xù)的數(shù)據(jù)就無法再接收了,然后丟包就出現(xiàn)了。
那 TCP 是如何解決這個問題的呢?答案是 流量控制。為了防止傳輸方發(fā)送的過快直接造成丟包,繼而觸發(fā)上面的超時重傳機(jī)制,根據(jù)接收方的接受能力,來決定發(fā)送方的傳輸速度,這個機(jī)制就是流量控制。
該機(jī)制作用于接受方。在TCP報文頭部中會用一個16位的字段來表示窗口大小,非常重要的調(diào)優(yōu)參數(shù)。這個數(shù)字越大,則說明接收方的緩沖區(qū)越大,能夠接收更多的數(shù)據(jù)。接收方會在確認(rèn)應(yīng)答的時候,將自己的剩余窗口大小寫入,隨ACK一起發(fā)送給發(fā)送方。
TCP流量控制
如果發(fā)送方接收到的大小為0,那么此時就會停止發(fā)送數(shù)據(jù)。這樣會有一個問題,如果下一個應(yīng)答(也就是窗口大小不為0)在過程中丟了,那么發(fā)送方就會進(jìn)入死鎖,相互等待。所以發(fā)送方會定期的向接收方發(fā)送窗口探測的數(shù)據(jù)段。
好了,關(guān)于數(shù)據(jù)包的發(fā)送就介紹到這里。之后有機(jī)會再聊聊 TCP 的擁塞控制相關(guān)的東西。
原文鏈接:https://mp.weixin.qq.com/s/JYPhwwuBPPHH8GgJtoCyug