文章目錄
- 應用層
- 自定義協議
- 傳輸層
- udp協議
- TCP協議
- 1.確認應答
- 2.超時重傳
- 3.連接管理
- 建立連接, 三次握手
- 斷開連接, 四次揮手
- tcp的狀態
- 4.滑動窗口
- 5.流量控制
- 6.擁塞控制
- 7.延時應答
- 8.攜帶應答
- 9.面向字節流
- 10.異常情況
應用層
自定義協議
客戶端和服務器之間往往要進行交互的是“結構化數據”,網絡傳輸的數據其實是字符串/二進制bit流。約定協議的過程中,就是把結構化數據轉成字符串/二進制bit流的過程。
- 把結構化數據,轉成字符串/二進制比特流這個操作,稱為序列化
- 把字符串/二進制比特流還原成結構化數據這個操作,稱為反序列化
序列化/反序列化具體要組織成什么樣的格式,這里包含哪些信息,約定這兩件事的過程就是 自定義協議的過程。
自定義協議,要約定好兩方面內容
- 服務器和客戶端之間要交互哪些信息
- 數據的具體格式
客戶端按照約定發送請求,服務器按照約定來解析請求。
服務器按照約定構造響應,客戶端按照約定來解析請求。
傳輸層
udp協議
UDP的特點是無連接,不可靠傳輸,面向數據報,全雙工的。
研究一個協議主要是研究它的報文格式。下面是大學課本中常見的報文格式。
下面是比較準確的報文格式
- udp報頭中一共有四個字段,每個字段2個字節。由于一個報頭中使用2個字節表示端口號,端口號的取值范圍就是0~65535,即64kb。同理一個數據報長度也是64kb,不能在長了。然而在當今互聯網時代,64kb是非常小的。一旦數據報長度超過64kb,后面的數據可能會出現截斷。如果我們需要傳輸的數據超過64K,就需要在應用層手動的分包,多次發送,并在接收端手動拼裝。
- 校驗和起到的效果,就是去嘗試檢查當前傳輸的數據是否存在問題?是否出現了bit翻轉?如果發現出現問題,就可以把這個錯誤的數據報丟掉,避免將錯就錯。
校驗和可以采用下面幾個方法
- CRC算法,即循環冗余算法。UDP 數據報發送方,在發送之前,先計算一遍 CRC,把算好的 CRC 值放到 UDP 數據報中,(設這個 CRC 值為value1)。接下來這個數據包通過網絡傳輸到達接收端,接收端收到這個數據之后,也會按照同樣的算法,再算一遍 CRC的值,得到的結果是value2. 比較自己計算的 value2 和收到的 value1 是否一致 如果是一致的,就說明的數據是 ok的.如果不一致,傳輸過程中發生了比特翻轉了. 上述 CRC 算法中,如果只有一個 bit 位發生翻轉,此時100%能夠發現問題。如果有兩個/多個bit位發生翻轉,有可能恰好校驗和和之前一樣!!這樣的情況概率比較低,可以忽略不計。
- md5算法/sha1算法。這兩個算法類似,都具有下面幾個特點。
1.定長。無論原始數據多長, 算出來的 md5 的最終值都是固定長度
常見的 md5 有 16 位版本(2字節),32位版本(4字節),64位版本 (8字節)。
2.分散。計算 md5 的過程中, 原始數據,只要變化一點點,算出來的 md5 值就會差異很大.網絡傳輸中,如果出現 bit 翻轉, 意味著只是極少的 bit 翻轉了即使就只是翻轉 1 bit, 最終得到的 md5 都會差異非常大。
3.不可逆。給你一個源字符串,計算 md5 值很簡單。但給一個計算好的md5值計算源字符串是很難的。也常用于加密。
TCP協議
TCP的特點是有連接的,可靠傳輸,面向字節流和全雙工。下面先看看tcp的報文格式。
- 四位首部長度:即報頭的長度。tcp報頭的前20字節都是固定不變的,而后面的選項長度是可變的。可以有也可以沒有。4個比特位表示的范圍是0~15,這里設置的單位是4字節,而不是字節。這里的4位表示的就是15*4為60字節。報頭的長度是60字節,選項的長度是20字節。
- 保留(6位):在udp中數據報的長度是2個字節,無法在進行擴展,tcp在設置報頭的時候保留6位,在擴展的時候可以使用保留位避免擴展不兼容的問題。
- 六位標志位,tcp最核心的部分。
1.確認應答
- tcp中核心的功能是可靠傳輸,而在網絡通信中,不可能100%把數據發送出去,只能盡量將數據發送出去,發送方能夠知道對方是否收到數據,就認為是可靠了。
- 為了確保可靠性,最核心的機制就是“確認應答”,發送方發送一條消息,接收方收到消息回復一條應答消息。但在網絡通信中經常會出現發送方或者接收方連續發送多條消息,出現“后發先至”的情況。
- 出現“后發先至”的原因。在網絡通信中,網絡通信環境錯綜復雜,兩臺設備通過多個路由器交換機連接,一個數據報從發送方到接收方走的路徑可能是不一樣的,可能出現后發的先到達的情況。
- 為了解決“后發先至”的情況,引入了序號和確認序號,對于數據進行編號,應答報文就可以告訴發送方,我這次應答的是那個數據。tcp是面向字節流的,是以字節位單位進行傳輸的,tcp的序號和確認序號都是以字節位單位進行編號的。
序號:針對tcp的每一個字節進行編號> 確認序號:應答報文中的確認序號是發送過去的最后一個序號加1 >
在確認應答中,通過應答報文反饋給發送方。**應答報文也叫做ack報文 **,平時報頭中ack值為0,如果當前報文是應答報文,此時報頭中ack的值為1tcp可靠性的核心機制是有確認應答,而不是“進行了三次握手”
2.超時重傳
- 在網絡通信中,若傳輸順利,通過應答報文就可以告訴發送方,當前數據是否收到,但在網絡上可能出現 “丟包” 的情況,如果數據丟了,沒有到達接收方,也不會有Ack報文。
- tcp的可靠性就是在對抗丟包,發送方在發送數據之后會等待一定的時間,若等了好久(超時),ack還沒有等到,此時發送方認為出現丟包,當認為丟包后,就會把剛才的數據重新發送一次(重傳)
- 出現 “ 丟包” 的原因:在網絡中,可能存在某個時刻,某個路由器/交換機的負載量過高,短時間內有大量數據要經過這個設備進行轉發,當高負載超過這個設備的極限的時候就會出現丟包。
- 上面的過程中,認為沒有收到ack就是丟包了,但可能是應答中的數據報丟了,也會重新發送。
Tcp socket在內核中存在接收緩沖區(一塊內存空間)發送方發來的數據會先放到緩沖區中,然后應用程序調用read/sanner.next才能讀到數據。當數據到達緩沖區中
接收方就會先判定當前緩沖區是否已有這個數據(或者是否曾經存在過),如果有或曾經存在直接把這個重復發來的數據丟棄,就能確保應用程序在調用read/scanner.next出現重復數據。接收緩沖區不僅能夠進行去重,還能進行排序。接收方如何判斷這個數據是不是重復數據
- 數據還在緩沖區中,還沒有被read走。 此時就拿著新收到的序號和緩沖區中的序號對一下,看有沒有一樣的,若有,就是重復的數據,把新收的的數據丟掉。
- 數據在緩沖區中已經被read走,此時新來的數據無法再緩沖區中查到。應用程序在讀取數據的時候是按照序號的先后順序讀取的,先讀1-1000,1001-2000, 2001-3000一定是先讀序號小的在讀序號大的,比如上一次讀到的是3000,新收到的數據是1001,則這個1001一定是已經被讀過的樹,此時可以判定這個新的數據包是“重復的包”,可以直接丟棄。
超時會重傳,也不是無限重傳
- 重傳次數也是有上限的,重傳到一定程度還沒有ack,就嘗試重置連接,如果重置也失效,就放棄連接。
- 重傳的超時時間的閾值,會隨著重傳次數的增加而增大。
3.連接管理
建立連接, 三次握手
tcp是有連接的,在建立連接的過程中,應用程序只是調用socket api,而真正建立連接的過程是由操作系統的內核來完成的。操作系統完成建立連接的操作叫做 “三次握手” 。建立連接的目的是讓通信雙方保存對方的相關信息。下面是三次握手的具體過程。
- 第一次握手是向服務器發送一個syn同步報文段,所謂的syn是一個特殊的tcp數據報。此時的作用是告訴服務器我要和你建立連接。
- 沒有載荷,不會攜帶應用層數據,但有ip報頭,以太網數據幀頭,以及tcp報頭。tcp報頭中就保存了客戶端自己的端口,IP報頭中就包含了客戶端自己的IP。
- 六個標志位中的第五位,為1.
- 服務器在收到syn后,會返回ack(確認應答),接下來還會在返回syn,此時的作用是告訴客戶端我收到請求并同意建立連接。
- 最后客戶端向服務器返回一個確認應答。
所謂的建立連接就是通信雙方各自給對方發送一個syn,各自給對方回應一個ack。
- 在上圖的建立連接過程中,是有四次交互,但服務器給客戶端發送到的兩條數據可以合并. syn是第五個標志位為1, ack是第二個標志位為1,這樣完全可以用一個tcp數據包就可以發送,既能應答上個請求,也能發起syn,網絡傳輸設計到封裝回和分用,兩個合并既提高了效率,有降低了成本,最終形成三次握手.
為什么要加進行三次握手
- 可以先針對通信路徑進行投石問路,初步確認通信鏈路是否正常(可靠性的前提條件)
- 可以驗證通信雙方,發送數據的能力和接受數據能力是否正常(為啥要進行三次握手?兩次行不行(服務器無法確認自己的發送能力和客戶端的接受能力)?四次行不行(多余了))
- 三次握手的過程中也會協商一些必要的參數
tcp中有很多參數需要協商,都是在數據包 “選項” 中體現的. 其中,TCP的 “序號” 和 “確認序號” 還是聽重要的,tcp的序號 和 確認序號的初始值都不是從0 或 1開始的,而是從一個較大值開始往后計算的.即使是同一個客戶端和服務器每次連接的初始值都是不一樣的.
斷開連接, 四次揮手
在建立連接的過程中,是通信雙方保存對端的信息,斷開連接的目的是將對端的信息從數據結構中刪除/釋放掉. 下面是四次揮手的具體過程.
四次揮手和三次握手的方式大致相同,只是發送的數據不同,但四次揮手不能像三次握手一樣將中間的兩步合并,三次握手中間兩不觸發機制完全相同,和應用程序代碼無關,可以合并,但四次揮手服務器調用ack是內核來完成的,而服務器發送fin是服務器代碼調用close()方法才能發送fin,在這個時間段,間隔時間可能很長,也有可能很短,不一定能將兩步合并為一步.
總結三次握手和四次揮手的相同之處和不同之處
- 相同之處: 都是通信雙方給對方發送一個syn/fin, 給對方回應一個ack.
- 不同之處: 三次握手可以合并,四次揮手不一定能合并.
三次握手,必須是客戶端主動,四次揮手,客戶端/服務器都可以主動發起.
tcp的狀態
tcp服務器和客戶端之間通過一定的數據結構來保存對端的信息, 在這個數據結構中有一個屬性叫 “狀態”, 操作系統內核根據當前狀態的不同,
決定了當前應該干什么.下面是具體的tcp狀態圖.
上面的圖只需要簡單了解就可以,下面看看幾個重要的狀態.
三次握手中重要的狀態
- LISTEN狀態: 表示服務器這邊已經創建好seversocket了,并且已經綁定好端口號. 此時表示可以開始建立連接了.
- ESTABLISHED狀態: 表示客戶端和服務器建立連接完成,即三次握手完成.
四次揮手中的重要狀態
- CLOSE_WAIT狀態:誰主動斷開連接就進入CLOSE_WAIT狀態,表示接下來代碼中藥調用close狀態來發起fin. (收到對方發起的fin之后進入這個狀態)
- TIME_WAIT轉態:表示 本端發起fin之后,對端也發起fin之后,進入TIME_WAIT狀態,這個狀態的作用是給最后一個ack重傳留有一定的時間.
TIME_WAIT的等待也不是無休止的等待,最多等待2MSL(MSL是一個系統內核的配置項,表示客戶端到服務器消耗的最長時間,常見設置有2min),超過這個時間都不重傳,意思是不會在重傳了.雙方都進入CLOSED狀態.
4.滑動窗口
在確認應答,超時重傳和連接管理的三個機制中, 雖然保證了可靠傳輸, 但等待ack的過程消耗的時間是挺多的, 滑動窗口就在保證可靠傳輸的前提下, 讓消耗的時間成本也降低了.
上述圖中把多次請求等待ack的時間, 使用同一份時間來等了, 減少了總的等待時間. 當有一個請求的ack返回以后, 窗口的大小不變, 但位置向右移動, 就出現了滑動的效果, 就叫做滑動窗口.
但上述操作也會出現丟包的情況, 具體處理情況如下.
- ack丟了
像ack丟了的情況,就無需進行處理,對于可靠性沒有任何影響,也不需要進行重傳.后面的ack會覆蓋前面ack中的信息,如果是2001先到/或者1001丟了,說明1-1000,1001-2000都是已經到達了的,滑動窗口會直接往后移動兩個格子.- 數據丟了
數據都是按順序排序發送的,當接收方發現某個數據丟失后,會反復向發送方索要這個數據的ack, 當發送方在多次受到某個ack的確認報文后,則認為是這個數據丟了,于是重傳這個數據. 反復索要的目的就是再給這個數據留有等待時間,多次索要還沒等到,應該是丟了,就相當于進行了 "超時時間"判定. (沒有丟包的數據已經拍好序列放在接受緩沖區中)
在上述過程中,這里的重傳做到了 “針對性” 的重傳,那個數據丟了就重傳那個,已經收到的數據,不必重復發送,整體的效率沒有額外的損失,把這種方式叫做"快速重傳"
當短時間傳輸的數據較多是,會使用快速重傳,短時間數據較少還是會按照超時重傳進行發送.
5.流量控制
通過滑動窗口可以提高傳輸的效率, 窗口大小越大, 更多的數據復用同一塊時間等待,效率就越高. 但窗口的大小也并不是越大越好,當發送數據過快,接收方處理能力低,接受緩沖區滿了,就出現丟包情況,這種情況,即使重傳,也還是會丟包.通過流量控制,讓發送方發送數據速度和接收方處理數據的速度保持一致,避免出現丟包的情況.
在ack報頭中中有一個 16位窗口大小,通過這個字段來給發送方反饋發送速度.接收方會按照自己接受緩沖區剩余空間的大小,作為ack中窗口大小的數值,發送方根據這個數值調整自己窗口的大小.
當窗口大小為0時, 說明接收緩沖區已經滿了, 此時發送方就暫定發送數據.發送方會周期性的發送 " 窗口探測包", 這個包不攜帶載荷,只是為了觸發ack, 當查詢結果非0就繼續發送數據.
6.擁塞控制
流量控制是站在接收方來控制發送方發送速率,擁塞控制是控制數據傳輸路徑上發生擁堵的問題. 針對這種情況,核心思路是把中間路徑經過的所有設備視為一個整體,然后通過" 實驗 "的方法找到一個合適的傳輸速率.
如何進行實驗的?
- 慢啟動: 剛開始傳輸數據的時候,速率較小,采用較小的擁塞窗口,此時不知道網絡路徑的具體情況,一上來就太大,可能導致網絡路徑更加擁擠
- 若慢啟動沒有出現丟包,就增大擁塞窗口的大小,按指數增長來增大擁塞窗口.
- 當指數增長到達一定 “閾值” 的時候,指數增長變為現行增長.
- 當現行增長出現丟包的情況,就把擁塞窗口的閾值設置成較小的值,回到最初的慢啟動,并且此時會重新設置指數增長的 " 閾值".
上述過程可以用圖展示
現在更新之后有新的版本,一個是當發生丟包后重新回到新訂的閾值不在進行慢啟動,直接進入指數增長的模式,這樣更加提高了傳輸的速率.
7.延時應答
延時應答也是基于滑動窗口在提高一些效率,結合滑動窗口和流量控制,能夠延時應答ack的方式,把反饋的窗口大小弄大一些.接收方在接受數據放到接受緩沖區之后,不會立刻返回ack,而是等一段時間, 讓應用程序將這個數據使用掉, 這樣, 接受緩沖區的空間就變大, 返回的窗口大小就是一個更大的值了.
在滑動窗口中,ack丟了, 不會影響可靠信, 正常情況每個數據都有ack,
這樣可以每隔幾個數據在返回一個ack(每隔幾個數據也能起到延時應答的效果),另外也能減少ack傳輸的數量,起到節省空間的效果.
8.攜帶應答
攜帶應答是盡可能把能合并的數據包進行合并,從而起到提高效率的效果.
在延時應答的機制下, ack會延遲發送, 就可能在返回響應的時候和響應的數據包一起返回, 本身ack也不攜帶載荷, 只是在報頭中ack標志位設置為1, 并且設置確認序號以及窗口大小. 這幾個屬性,在reponse報文中也用不到, 不會發生沖突.
在攜帶應答的加持下, 后續每次傳輸請求響應,都有可能把傳輸的業務數據和上次的ack合二為一.
9.面向字節流
粘包問題 , 此處的包是"TCP載荷中的應用層數據包"
TCP是面向字節流的,站在應用層的角度,每次收到的數據是一串連續的字節數據,不知道從哪一部分到那一部分是一個完整的應用層數據包,多個數據包混淆不清,像這種情況就是" 粘包問題".
如何解決粘包問題?
核心方法就是 “明確包之間的邊界”.
- 通過特殊符號, 作為分隔符, 見到分隔符就視為一個包結束了.
- 指定出包的長度,比如在包開始的位置, 加上一個特殊的空間來表示整個數據的長度.
UDP存在粘包問題嗎?
UDP傳輸的基本單位是udp數據包, 在udp這一層就已經分開了,只需約定好, 每個udp數據包值承載一個人應用層數據包, 不需要額外的手段來區分,就不存在粘包問題.
10.異常情況
考慮的情況是考慮比丟包更嚴重的情況, 如網絡直接出現故障等情況,該如何處理?
- 接收方或發送方有一方出現 進程崩潰.
進程無論是正常結束還是異常崩潰,系統都會自動觸發回收資源文件, 關閉文件這樣的效果,就會觸發四次揮手. tcp的連接是有生命周期的,可以比進程更長一些,雖然進程退出,但tcp連接還在,任然可以通過四次揮手斷開連接- 接收方或發送方出現關機(正常關機)
關機操作會終止系統所有的進程,和上面一樣會觸發四次揮手的操作,若揮手速度夠快,就能刪除本端和對端的信息, 但可能存在四次揮手還沒有結束系統就已經關閉了,此時fin已經發送,無法收到ack,此時會進入超時重傳的流程中,若還是沒有收到ack, 就會單方面釋放連接信息.- 其中一方斷電(更突然的關機)
- 斷電的是接收方: 發送方突然出現沒有ack了,就會進入超時重傳,重傳之后還是不行, 就會進入 "復位連接 ".(相當于清除原來tcp中的臨時數據,重新開始). 此時會用到tcp中的復位報文段(RST),
當rst也收不到ack,就當方面釋放連接.- 斷電的是發送方: 當接收方一段時間沒有收到對方的信息,就會觸發心跳包,來判斷發送方是掛了,還是當前沒有發送數據,如果發現對端沒有心跳了,本端也會嘗試復位連接并且當方面釋放連接.
心跳包是不攜帶應用層數據的特殊數據包,是周期性的,沒有心跳視為對端掛了.
- 網線斷開.
網線斷開,數據就無法發送出去,此時發送方收不到ack就會超時重傳并且觸發復位連接,從而單方面釋放連接,接收方收不到數據,就會觸發心跳包判斷對方是不是掛了,若沒有心跳就會當方面釋放連接.