來源丨經(jīng)授權(quán)轉(zhuǎn)自 假裝懂編程(ID:suntalkrobot)
作者丨康師傅
hi,大家好,我是康師傅,今天和大家聊聊網(wǎng)絡(luò)協(xié)議那些常見的知識點,為什么要聊這個知識呢?主要是因為自己快忘完了,同時這不今年快要結(jié)束了,可能很多同學(xué)都在開始準(zhǔn)備明年的面試了,那么我想不管你是前端、后端還是客戶端,網(wǎng)絡(luò)協(xié)議這塊的復(fù)習(xí)應(yīng)該是少不了的。
網(wǎng)絡(luò)協(xié)議離不開我們常說的http、tcp這些,在網(wǎng)絡(luò)分層模型中 http 屬于應(yīng)用層協(xié)議,tcp屬于傳輸層協(xié)議,其實應(yīng)用層協(xié)議還有像smtp、ftp等協(xié)議,傳輸層還有udp協(xié)議,當(dāng)然我們今天重點說下http和tcp相關(guān)的知識,http離不開tcp,因此我們先說說tcp、ip相關(guān)的知識。
某一天,你和你的同事正在用微信聊天,不知道你是否思考過,你們的電腦上裝了很多軟件,比如有網(wǎng)易云、QQ...等等,為什么你通過微信發(fā)的消息會正確的發(fā)送到對方的微信上,而不是發(fā)送到其他應(yīng)用軟件上?同時再說夸大一點,為什么你發(fā)送的消息會發(fā)送你同事的電腦上,而不是隔壁老王的電腦上,這么問題看起來有點傻,其實這些離不開我們今天要說的TCP、IP協(xié)議。首先IP大家肯定都能明白,每個電腦都有一個IP,這也是為什么我們的信息可以精準(zhǔn)的發(fā)給我們的同事而不是隔壁老王,因為我們知道同事電腦的IP,這就是IP層干的的事。
當(dāng)通過IP找到了你同事的電腦后,還要找到你同事正在運行的微信軟件,電腦上軟件這么多,而且大家的IP都是一樣的,這可怎么辦?答案是端口,這也就是TCP層干的事,在數(shù)據(jù)經(jīng)過TCP層的時候,會加上目標(biāo)端口也就是我們微信進程占用的端口,然后數(shù)據(jù)包到達你同事的電腦上時,在TCP層會拆包,拆包后會發(fā)現(xiàn)目標(biāo)端口號,然后把數(shù)據(jù)丟給我們的微信進程(電腦視角:端口號是10086,哦,這個數(shù)據(jù)丟給微信進程處理吧)。
其實TCP層不僅僅會加上目標(biāo)的端口號,還會加上發(fā)送者的端口號,IP層不僅僅會加上目標(biāo)IP,還會加上發(fā)送者的IP,發(fā)現(xiàn)沒,這就是我們常說的socket四元組:發(fā)送端IP+發(fā)送端端口+接收端IP+接收端端口。一個socket四元組就可以確定一個連接。
我們常說TCP協(xié)議是一種基于字節(jié)流、面向連接的、可靠的傳輸層通信協(xié)議,這里我們需要思考定義中的三個抽象描述:
我們先說第一個問題,TCP 協(xié)議是基于字節(jié)流傳輸?shù)模@是什么意思呢?舉個例子,其實當(dāng)我們往 socket 中寫入1000個字節(jié)的時候,會分很多情況的,這時1000個字節(jié)會被 copy 到內(nèi)核緩沖區(qū)的,但是1000個字節(jié)具體是怎么通過網(wǎng)卡發(fā)出去是不確定的,有可能一次性發(fā)出去,也可能分成2次,分別是300、700,也有可能是500、500,但是不管怎么分,每個字節(jié)都有自己的序號。
造成這么多情況的原因是因為受到路徑最大傳輸單元 MTU、發(fā)送窗口大小、擁塞窗口大小等因素的影響(這些概念后面會講,just follow me),在這里我們也說下可靠性,因為數(shù)據(jù)包已經(jīng)在 TCP 層分段了,等于一塊數(shù)據(jù)被打散了,這些打散的數(shù)據(jù)包被接收的順序可能不一樣,但是內(nèi)核在收到亂序的數(shù)據(jù)包后,并不會直接丟給上層應(yīng)用(http等),需要按照數(shù)據(jù)包的順序組裝好,這個組裝依賴的就是序列號,那基于字節(jié)流方式傳輸?shù)臄?shù)據(jù)包,如何確定這個數(shù)據(jù)包的序列號呢?其實這個序列號就是這個包的第一個字節(jié)的序號。
再說第二個問題:面向連接。對沒錯,我們還是要說說老掉牙的問題:三次握手、四次揮手。三次握手、四次揮手其實也是一種可靠性的表現(xiàn)。因為需要可靠,所以在建立連接的時候需要先確認雙方是否都o(jì)k,也就是三次握手。我們先看看三次握手干了什么?同時我們看看為什么三次就行了,兩次或者十次行不行?
看到上圖中的一堆玩意比如syn、seq、ack、isn等等,先不要害怕,我們一一解釋下,然后你就會明白了,上面我們也說到了,因為需要可靠,不能一上來就直接發(fā)送數(shù)據(jù),萬一對方不在線,那數(shù)據(jù)豈不是丟失了,因此握手的目的就是先確認兩邊的狀態(tài)都o(jì)k,那如何區(qū)分這次通信是握手而不是正常的發(fā)送數(shù)據(jù)呢?這就是SYN包的作用,SYN相當(dāng)于一個雙方通信中附帶的一個標(biāo)志,當(dāng)數(shù)據(jù)包中有它的時候,說明這次通信的目的是握手。
三次握手發(fā)SYN包之后,還有一個重要的事情:交換彼此的初始序列號seq,這是因為基于字節(jié)流的TCP其實每個字節(jié)的數(shù)據(jù)都有序號,在握手確認彼此的初始序列號之后,接下來所有的字節(jié)數(shù)據(jù)都是基于初始序列號向后累加的,初始序列號的生成方法就是ISN函數(shù),它大概會隨機生成一個數(shù)字,需要注意的是它的值并不是從0開始的。當(dāng)一端發(fā)送了自己的初始序列號之后,并且收到了對端的ack就說明此次交互通暢,其中ack的值就是自己發(fā)過去的序列號加1。
ok,搞懂了幾個名詞的概念和意義之后我們再來看看三次握手的過程。
我們總結(jié)下,由于TCP是可靠的傳輸層通信協(xié)議,握手的目的主要是確認雙方都有收發(fā)包的能力,從上文的描述來看三次剛剛好,如果少了,首先某一端的收發(fā)包能力就無法得到確認,比如最后一次如果發(fā)動端不發(fā)送最后的ack,那么接收端就不知道它是不是收到了數(shù)據(jù)包。當(dāng)然超過3次肯定也是沒問題的,但是沒必要,因為3次已經(jīng)可以知道雙方的狀況了。
不知道你發(fā)現(xiàn)沒有,建立連接的過程雙方都消耗了一個序列號,這里可不可以不消耗一個序列號呢?答案不可以,必須要消耗一個,關(guān)于這一點你先記住:不占用序列號的段是不需要確認的,比如ack,凡是消耗序列號的 TCP 報文段,一定需要對端確認。如果這個段沒有收到確認,會一直重傳直到達到指定的次數(shù)為止,像SYN 包就是需要確認的報文段。
看完了三次握手,我們再來看看四次揮手的過程,四次揮手的過程雙方會處于某種狀態(tài),這是需要注意的,這也是面試考察點。依然一樣我們來看看為什么需要四次揮手,以及每次揮手的過程干了什么?
為了方便描述,這里定義下主動斷開方叫「A」,被動斷開方叫做「B」。
能不能三次揮手?
看流程四次揮手絕對沒問題,那問題來了,三次行不行?其實某些情況下三次也是可以的,比如被動斷開方?jīng)]有要處理的數(shù)據(jù)也就不存在DATA那一部分,那其實ACK和FIN一起發(fā)過去問題也是沒問題的,如果存在DATA,非要把ACK+DATA+FIN合并在一起發(fā)過去會發(fā)生什么呢?首先處理DATA需要時間,那么為了等DATA處理完再發(fā)ACK,可能會導(dǎo)致主動斷開方因為遲遲沒收到ack,而重發(fā)FIN包。
為啥最后一步主動斷開方需要處于TIME_WAIT狀態(tài),這個狀態(tài)代表什么?
TIME_WAIT是主動關(guān)閉方最后進入的一種狀態(tài),TIME_WAIT是2MSL的,MSL是報文最大的生命周期,正常來說一個數(shù)據(jù)包如果在網(wǎng)絡(luò)中超過MSL之后還沒被對端收到就會被丟棄,那為什么主動斷開方需要2MSL呢?
因此2MSL = 去向 ACK 消息最大存活時間(MSL) + 來向 FIN 消息的最大存活時間(MSL)。
為什么FIN包也需要消耗一個序列號?
上圖并沒有說到序列號的事,其實 FIN 包和 SYN 包一樣的,也是需要消耗序列號的,如果要問為什么?只是回答“因為 FIN 包需要對端確認,而需要確認的報文段都是消耗序列號的”難免有些牽強,我們來看個圖你就知道了。
因此無論是 SYN 包還是 FIN 包,為了和正常的數(shù)據(jù)區(qū)分,都需要消耗一個序列號。
我是大哥我來分段-MTU和MSS
通過上文我們知道在 TCP 中數(shù)據(jù)的傳輸是基于字節(jié)流的,數(shù)據(jù)塊會被拆分成一個個報文段然后發(fā)出去,決定報文段大小的因素很多,比如路徑MTU、發(fā)送窗口大小、接收窗口大小等因素的影響,這里我們一起來看看這些因素是什么?首先我們來看看這個路徑MTU,在網(wǎng)絡(luò)分層中,我們知道最終數(shù)據(jù)是要通過鏈路層發(fā)出去的,這個鏈路層的通道其實是有限制的,這個限制我們就叫做MTU,那這個MTU是多少呢?一般是1500,你可以通過netstat -i
查看你本機網(wǎng)卡的MTU。
netstat -i
en0 1500 <Link#6> 08:f8:bc:6f:6a:03 34427890 0 37802460 26255 0
注意這只是本機的MTU,真實的網(wǎng)絡(luò)中,你的數(shù)據(jù)從電腦網(wǎng)卡出去之后,可能要經(jīng)過一系列的路由器、交換機等物理硬件,其中每個物理硬件都有自己的MTU,那在這漫長的網(wǎng)絡(luò)路徑中,起關(guān)鍵作用的MTU是哪個?答案是最小的那個,最小的那個就叫做路徑MTU,這就像木桶效應(yīng),桶的容量是由最短的那一塊板決定的,當(dāng)你的數(shù)據(jù)包大于MTU時,會被拆成一個一個合適的網(wǎng)絡(luò)包發(fā)出去。IP層發(fā)現(xiàn)鏈路層的數(shù)據(jù)包有大小限制,因此IP層就說:'既然鏈路層有大小限制,發(fā)再大的數(shù)據(jù)包過去也是會被拆解的,還不如我自己做,在把數(shù)據(jù)發(fā)給鏈路老弟之前,我直接按照它的要求把數(shù)據(jù)分好段,就不麻煩它了。' IP層干了數(shù)據(jù)分段的事情之后,TCP層不高興了,'弄啥嘞,弄啥嘞,我在他們的上層,數(shù)據(jù)竟然還要 IP 層小弟分段,我顏面何存!',于是 TCP 層為了避免數(shù)據(jù)被發(fā)送方分片,會主動把數(shù)據(jù)分割成小段再交給IP 層,TCP 能分的最大段我們稱之為 MSS (Max Segment Size),這個 MSS 的值是多少呢?其實它的值是這個:
MSS = MTU - IP 頭大小 - TCP 頭大小
其中 IP 頭和 TCP 頭各占 20 個字節(jié),以 MTU=1500來說,那么 MSS = 1500-20-20=1460。就這樣 TCP 層主動的把數(shù)據(jù)分好,從而得到了 IP 層和鏈路層的一致好評。IP 層:'大哥靠譜'。鏈路層:'大哥的大哥靠譜'。
我只能吃這么多-滑動窗口
在socket通信中,我們知道發(fā)送端有發(fā)送端的緩沖區(qū),接收端有接收端的緩沖區(qū),發(fā)送端把數(shù)據(jù)寫入到socket緩沖區(qū)中,待緩沖區(qū)滿了或者過了一段時間后,緩沖區(qū)的數(shù)據(jù)會被網(wǎng)卡一段一段的發(fā)出去,當(dāng)數(shù)據(jù)到到達對端后,并不是直接等著對方處理,這樣效率會很低,而是會先把數(shù)據(jù)放入到緩沖區(qū)中,也就是我們的接收緩沖區(qū),然后應(yīng)用程序不斷的從接收緩沖區(qū)中取數(shù)據(jù)。緩沖區(qū)起到一個緩沖的作用,很合理,然后如果發(fā)送端發(fā)送的太快,或者說接收端的應(yīng)用程序處理的太慢,導(dǎo)致接收端的緩沖區(qū)很快被填滿,這時候該怎么辦?直接發(fā)肯定不行,得告訴發(fā)送端先不要發(fā)了。這就要說到了 TCP 中的「滑動窗口」的概念。我們知道 TCP 是基于字節(jié)流來發(fā)送數(shù)據(jù)的,也就說每個字節(jié)其實都是有序號的,有了序號可以干什么呢?首先通過序號可以重組數(shù)據(jù),其次ack之前的序號表示都已經(jīng)收到,滑動窗口和ack的情況有關(guān),我們來站在 TCP 的角度看看數(shù)據(jù)包的狀態(tài)。
這是站在發(fā)送端的角度來看數(shù)據(jù)包的狀態(tài)的,其中的滑動窗口部分可以看作是發(fā)送端的滑動窗口,對于已發(fā)送已確認的部分,算是過去時了,它只會使滑動窗口向右移,真正影響滑動窗口大小的是「已發(fā)送未確認」和「未發(fā)送可發(fā)送」部分,剩下的「不能發(fā)送」是因為接收端沒有足夠的空間了。我們再來站在接收端角度看看滑動窗口是什么樣的。
可以發(fā)現(xiàn)窗口的大小其實是一樣的,唯一的區(qū)別是對于接收端來說要么已接收,要么未接收,不能接收的話說明沒有足夠的空間了。那發(fā)送端怎么知道當(dāng)前接收端剩余空間的大?。科鋵嵔邮斩嗽贏CK的時候會帶上自己窗口的大小,這樣發(fā)送端就知道了接收端窗口的大小。以上圖為例,當(dāng)接收端拿到了32-35的數(shù)據(jù)后,就會ACK=36告訴發(fā)送端,同時接收端的滑動窗口會向后移動4位,發(fā)送端收到ACK=36后,就知道36之前的數(shù)據(jù)接收端都收到了,因此會把發(fā)送端的窗口也向后移動4位。
滑動窗口很棒,可以在能力范圍內(nèi)處理數(shù)據(jù),但是有個問題呀:如果發(fā)送端能力極強,發(fā)的很快,接收端能力極弱,處理的很慢,這會導(dǎo)致什么問題?某一刻滑動窗口為0了,這時候接收端就會告訴發(fā)送端:'你奶奶的,消停會吧,沒空間了'。發(fā)送端收到了通知之后:'原來是個弱雞,休息會吧,等它下次ack通知我吧',正常來說,接收端在處理完數(shù)據(jù)之后可以告訴發(fā)送端可以繼續(xù)發(fā)數(shù)據(jù)了,然而意外出現(xiàn)了,由于接收端所在的主機的主人正在聽網(wǎng)易云音樂、玩著2k,同時還尼瑪欣賞著b站舞蹈區(qū)up主娥羅多姿的舞蹈,導(dǎo)致網(wǎng)卡壓力很大,最后一個ack丟失了,這樣發(fā)送端就不知道接收端其實已經(jīng)處理了一部分?jǐn)?shù)據(jù),這可怎么辦,如果一直丟失,豈不是要一直傻等,得主動出擊呀,于是搞了個「零窗口探測定時器」,這個定時器的功能相信大家也知道了,就是當(dāng)接收方的接收窗口為0時,每隔一段時間,發(fā)送方會主動發(fā)送探測包,通過迫使對端響應(yīng)來得知其接收窗口的狀態(tài),不得不說零窗口探測夠穩(wěn)。
悠著點慢慢來-擁塞控制
上面我們說到滑動窗口可以合理的控制接收端能處理數(shù)據(jù)的量,注意這里說的只是量,如果網(wǎng)絡(luò)狀況很差,發(fā)送端一次性發(fā)了很多數(shù)據(jù),并且窗口未被填滿(此處的意思就是和窗口的大小沒關(guān)系),這時會發(fā)生什么?我想大概率是發(fā)送端瘋狂的重傳(因為網(wǎng)絡(luò)差,未收到ack),那么我們反過來想一想網(wǎng)絡(luò)狀態(tài)差,我們還有必要發(fā)送大量的數(shù)據(jù)過去嗎,大量的重試不是給發(fā)送端自己找麻煩,因此需要悠著點,這里的悠著點說的就是「擁塞窗口(cwnd)」,擁塞窗口指的是在收到對端 ACK 之前自己還能傳輸?shù)淖畲?MSS 段數(shù),那它和之前說的發(fā)送窗口有什么關(guān)系?其實真正的發(fā)送窗口大小是擁塞窗口和接收端那個窗口之間的最小值。MSS 我們知道在 MTU=1500 的情況下它的值是1460,擁塞窗口指的就是能發(fā)多少個1460,由于在連接建立之初,發(fā)送端并不知道網(wǎng)絡(luò)狀況,如果網(wǎng)絡(luò)狀態(tài)很差,一口氣傳過去很多數(shù)據(jù)是不明智的,緩慢啟動才是正確的選擇,緩慢啟動可以及時止損,同時緩慢啟動也不并是說一直緩慢,如果網(wǎng)絡(luò)ok,會隨著時間慢慢增長,這就是緩慢啟動的目的,那怎么個增長法呢?在通信之初,只要發(fā)送端收到一個 ACK 就會把 cwnd 翻倍,比如一開始是10,收到一個 ACK 后,下次就是20,再收到一個 ACK 后,就是40...,這個做法很聰明,我們只需忍受剛啟動的時的慢速,隨著時間的增長 cwnd 會以指數(shù)級的增長快速趕上來。喝茶聊天,萬事大吉。不對,這指數(shù)級的增長若不控制,一會便要超過了馬斯克的財富了,這可不得了,于是搞了個慢啟動閾值(ssthresh),當(dāng) cwnd 達到 ssthresh 時,這時說明 cwnd 不小了,在翻倍的漲下去可能會危險,這時可以選擇小漲,不翻倍,每次在cwnd的基礎(chǔ)上再加個1 MSS 就行了。
即使是加1個 MSS,隨著時間的推移也可能無限大,但是為什么現(xiàn)實中沒出現(xiàn)問題?我想其中之一就是上面的說到的真正的發(fā)送窗口的大小是兩者中的最小的那個,畢竟接收窗口不可能無限大。其二就是隨著網(wǎng)絡(luò)包的越來越大,會發(fā)生網(wǎng)絡(luò)擁堵,這時候 ssthresh 會降級, 也就是 ssthresh = cwnd / 2,然后 cwnd 會被設(shè)置為1個報文段,重頭重新開始緩慢啟動和擁塞避免,關(guān)于第二點,我借鑒網(wǎng)上一個例子:'假設(shè) TCP 的 ssthresh 的初始值為 8。當(dāng)擁塞窗口上升到 12 時網(wǎng)絡(luò)發(fā)生了超時,于是TCP 開始使用慢開始和擁塞避免。試分別求出第 1 次到第 15 次傳輸?shù)母鲹砣翱诖笮 ?
一開始cwnd是1,然后不停的翻倍,直至到達 ssthresh,也就是8,這時開始每次加1個 MSS,當(dāng)?shù)?2的時候,發(fā)生超時,也就是 ssthresh 會變成6,然后 cwnd 重新從1開始,也是不停的翻倍,當(dāng)?shù)?,準(zhǔn)備翻倍到8的時候,發(fā)現(xiàn)sthresh=6,因此會變成6,然后開始每次加一個 MSS。因此第1次和第15次分別是1和9。
牛逼的算法讓我不由的手舞足蹈,ちょっと待って(等一下),怎么判斷網(wǎng)絡(luò)擁堵超時了?這個其實很好判斷,當(dāng)超過一定時間之后,發(fā)送端沒收到ack,可能就是網(wǎng)絡(luò)超時了,正常來說,這時候,發(fā)送端會使用退避策略來重新發(fā)送,每次重傳的間隔大概是幾百毫秒,這幾百毫秒毫秒對人類來說還挺快的,但是對計算機來說其實挺慢的,那有沒有什么更快的方法?我們先來看個例子:假設(shè)現(xiàn)在要發(fā)送4個數(shù)據(jù)包分別是[1,100],[101,200],[201,300],[301,400],正常來說發(fā)完第一個數(shù)據(jù)包之后,會回復(fù)ACK=101,沒毛病,但是在發(fā)第二個數(shù)據(jù)包的時候,網(wǎng)絡(luò)超時了,丟包了,當(dāng)發(fā)送端繼續(xù)發(fā)送第三個、第四包的時候,并不會回復(fù) ACK=301,401,而是會繼續(xù)回復(fù) ACK=101,這里請再記住 ACK 代表這個序列號之前的數(shù)據(jù)都已收到。正如上文說到的,正常來說,此時要等幾百毫秒才會意識到丟包,重發(fā),而如果想要更快點,比如收到三次重復(fù)的ACK說明就是丟包了,這樣是不是快很多,這就是「快速重傳(SACK)」,但是只是單純的告訴101之前的數(shù)據(jù)收到了(第一個數(shù)據(jù)包)有點低效,萬一第三個也丟了怎么辦,因此SACK做了進一步的優(yōu)化:在通知ACK的同時也告訴比如第三個包也丟了、第四個數(shù)據(jù)包我收到了,這樣發(fā)送端就知道了此刻除了第二個數(shù)據(jù)包丟失了,第三個包也丟失了,重傳第二個、第三個即可。
聯(lián)系客服