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