傳輸層
在傳輸層兩大關鍵的協議就是UDP和TCP協議了,除此之外,還有別的傳輸層協議,本文章將介紹UDP和TCP協議,重點介紹TCP協議。
首先回顧TCP和UDP 的特點:
UDP:不可靠傳輸,面向數據包,全雙工
TCP:可靠傳輸,面向字節流,全雙工
UDP 協議
UDP報文格式:
源端口號和目的端口號耳熟能詳了。
UDP長度是指整個UDP數據包的長度(報頭 + 載荷),由于是16位的長度,所以整個UDP數據包最大的長度就是 2^16-1 個字節(單位是字節)即 64KB
16位UDP校驗和是校驗UDP數據包有沒有出錯,這和 https 的校驗和不一樣,https 是為了防止有人篡改數據,這里的UDP校驗和是為了防止出現比特翻轉(0->1,1->0),也就是防止原始數據發生比特翻轉導致數據錯誤。
最后就是載荷內容
UDP 數據包的報頭固定是64個比特位也就是8個字節。
由于UDP最大長度是68KB,在現在的互聯網時代,遠遠不夠用,所以引入了下面的 TCP 協議。
TCP 協議
TCP報文的格式:
32位序號:該報文的載荷數據的第一個字節的起始編號
32位確認序號:說明該報文的確認報文,確認序號說明自己已經接收到的最后一個數據包的最后一個字節的編號+1,也就是你下一個數據包發送的序號應該湊夠這里開始。舉個例子:假設一個數據包序號為1000,載荷數據占1000字節,對方正常接收,之后需要返回一個確認報文,該報文的確認序號應該是2001。說明2001前面的全部的數據包已經被接收了。
4位首部長度是指首部最大的長度是多少,單位是(4字節)。說明TCP報頭的最大長度就是 (2^4 -1)* 4 = 60 字節
確認應答
確認應答是指當一方發送數據到達另一方之后,另一方需要返回一個確認數據包,這就是保證可靠傳輸的其中一種保證。
在TCP報頭中我們發現一個有六個標志位的部分:
當 ack 為 1 的時候,說明這是一個確認應答的數據包。
超時重傳
當一方發送完數據包之后,如果遲遲未等到確認應答的話,就會認為改數據包發生了丟包了,因此為了保證可靠傳輸,我們需要進行重新傳輸改數據包,這就是超時重傳。
保證可靠傳輸的關鍵就在于確認應答和超時重傳。
連接管理
三次握手
在客戶端和服務器建立連接的時候,TCP使用的是三次握手的流程。
首先服務器是被動接收客戶端的請求,因此建立連接的發起者一定是客戶端,客戶端首先發送一個同步報文,然后服務器接收到同步報文之后,就會返回一個確認應答和同步報文,最后客戶端返回確認應答數據包。
同步報文的特點是 六個標志位里的 syn 是 1.
為什么是三次握手???
首先TCP是有連接的,具體體現在客戶端和服務器分別保存了對端的信息。
連接的建立還需要確保雙方的發送能力和接收能力都沒問題,能正常進行數據通信。
因此客戶端發送syn,如果服務器正常接收,說明客戶端的發送能力沒有問題,這時候服務器發送syn+ack,此時客戶端如果額能正常接收的話,說明客戶端知道服務器可以正常接收和發送數據,但是服務器這邊不知道客戶端是否能正常接收數據,因此客戶端需要再次發送一個數據包也就是確認應答數據包ack,服務器接收完畢之后,雙方就都確定了對方的接收和發送能力正常,也保存了對端的信息,可以開啟會話。
由于我們把 syn 和 ack 合并在一起發送給客戶端,所以只需要一次,而不是兩次,因此四次握手就沒有很大的必要了。兩次握手又不足以驗證雙方各自的發送和接收能力正常
三次握手的作用:
首先初步驗證通信鏈路是否是通暢的
驗證雙方各自的發送能力和接收能力是否正常(這就是為什么兩次握手不行)
協商關鍵信息(序號是從哪里開始的)
三次握手的狀態:
當服務器程序一啟動,就會由 CLOSED 狀態 切換為 LISTEN 狀態。
LISTENED 狀態:服務器已經啟動,可以隨時連接客戶端
SYN_SEND 和 SYN_RCVD,分別是 syn 發送和接收狀態,一般很難看到,因為TCP連接的建立很快就能發生。
ESTABLISHED:是連接建立完畢的狀態,隨時可以進行數據通信
四次揮手
首先明確一點就是客戶端和服務器都可以主動斷開連接。
斷開連接一般是四次揮手:
主動申請斷開連接的需要先發送一個 FIN 數據包(標識位FIN 為1)
然后接收到 FIN 數據包之后需要返回 ACK
接著發送 FIN
最后 返回ACK
TCP連接就會釋放
為什么三次握手第二次的數據包SYN可以和ACK 一起,而四次揮手的中間的 FIN 和 ACK 不能合并呢?
因為三次握手的第二份數據包中SYN和ACK 是由內核決定的,當接收到客戶端的SYN 之后,會立即返回syn+ack
然而四次揮手的ack 是由內核決定的,但是 FIN 是由應用程序的代碼決定的,當程序執行到 socket.close 之后,才會發送 FIN 數據包
四次揮手的狀態:
這里要注意的是:誰先發送 FIN,誰就會進入到 TIME_WAIT 狀態
誰后發送FIN ,誰就會進入到 CLOSE_WAIT 狀態
異常情況的處理
這里列出四種異常情況。
一、進程崩潰了
進程崩潰和主動退出沒有本質區別,雖然進程崩潰了,但是只是應用層不行了,傳輸層的TCP連接還是存在的,因此這里會發生四次揮手來釋放連接
二、主機正常關機了
正常的主機關機流程,關機需要一定的時間,在這段時間中可以進行四次揮手
三、不正常關機
如果是不正常關機,例如臺式機斷電,那么四次揮手可能就沒有揮完。舉個例子,如果A關機之前發送了一個FIN,在收到 B 的 FIN 之后,A已經關機了,這就意味著B是永遠都收不到 ACK的了,那么由于 B 遲遲沒有收到 ACK,那么首先會認為發生了丟包,進行超時重傳,當超時重傳幾次之后還是沒有收到ACK,就會認為對端發生了嚴重的錯誤,B就會主動放連接。
上面的情況是A發出去 FIN 之后,如果A突然斷電,沒有發送FIN:
1)接收方突然斷電
那么這時候 發送方 發送的業務數據遲遲沒有得到確認報文,那么 發送 首先會認為發生丟包開始進行超時重傳,等到多次遲遲沒有得到回應,這時候就會發觸發 “重置TCP連接”,發送復位報文(標識位RST為1),【復位報文是指重新建立連接,從頭來過發送數據】如果沒有得到ACK,那么服務器會單方面斷開連接。
2)發送方突然斷電
這時候客戶端不知道是服務器掛了還是服務器只是沒有繼續發送數據了,那么客戶端就會先等著,等待到一定時間之后就會發送一個 “心跳包”,(心跳包只是為了觸發ack,確認一下服務器是否存活,不攜帶任何業務數據)【這個也叫做保活機制,但是由于TCP發送心跳包的時間過長,一般我們會在應用層重新實現心跳包的邏輯代碼】
四、網線斷開了
這種情況,站在 A 的視角會認為是第三種情況,A最后會放棄連接
站在B 的視角會認為是第三種情況,B會主動放棄連接。
滑動窗口
由于TCP是可靠傳輸,那么必定會影響到傳輸的效率,相比于UDP來說,TCP的傳輸效率是遠遠小于UDP的。為了在可靠傳輸的基礎上盡可能提高傳輸效率,我們使用滑動窗口的概念。
這里的窗口是指接收方的緩沖區的大小,最多可以容納多少個TCP數據包,當接收方接收完數據后一般會返回ack,其中 ack 里面就會包含接收方剩余窗口容量(對應的就是16位窗口大小),這樣發送方就可以根據窗口
大小來進行批量傳輸數據,就不用一個一個地慢慢傳輸了。
16 位窗口大小并不是窗口大小的極限值,在TCP報頭里還有一個叫做選項的數值,里面有一個數值是窗口擴展因子,假設窗口擴展因子為2,那么窗口大小要左移( << )兩位
滑動窗口不能無限大,大了會影響傳輸效率。
快速重傳
由于接收方有窗口(緩沖區),所以如果其中有一個數據包發生了丟包就會觸發快速重傳那個丟失的數據包,然后繼續之前的傳輸。因為數據包已經在緩沖區排列好了,不需要重新重傳已經發送過的數據包
流量控制
流量控制是調整發送方的傳輸速率,避免接收方不能及時處理數據就導致丟包,那么我們就會引入流量控制。
當接收方處理不過來的時候,發送方會降低發送的速率;當接收方還可以繼續處理更多的數據的時候,發送方會提高傳輸的速率,這就是流量控制。
擁塞控制
由于傳輸效率還與當前網絡的鏈路有關,如果當前網絡占用率高,那么發送方就要降低發送速率,如果當前網絡鏈路占用率低,可以提高發送速率。
慢啟動目前已經被棄用,現在使用的是快啟動。
首先有一個窗口的閥門值,先發送一個數據包,每輪傳輸的數據包數量等于前一輪傳輸的數據包數量 * 2,直到達到窗口的閥門值,之后每輪傳輸的數據包會 + 1,也就是開始是指數增長的,達到閥門值之后就變成線性增長了。
假設當前窗口大小調整到了24,如上圖所示,這時候已經到了網絡鏈路的極限的時候,也就是發生了丟包,發送方收到了連續的 3 個 ack 之后,就會進行快啟動,現在窗口閥門值會變成當前窗口的 1/2 也就是 24 / 2 = 12,發送數據量會降到新的閥門值,之后繼續進行 + 1 的線性增長
如果是慢啟動,新的窗口閥門值還是當前極限窗口的 1/2 也就是12 ,這時候傳輸的數據包的數量降為1,重新開始 * 2 的指數增長到達閥門值然后進行 + 1 的線性增長
從快啟動和慢啟動的區別來看,快啟動會效率高一些,所以現在我們使用的是快啟動。
窗口的實際大小等于 MIN(流量控制的窗口值,擁塞控制的窗口值)
延遲應答
當接收到數據的時候不立即返回 ack,而是等一會再返回 ack,這就是延遲應答
延遲應答可以讓接收方有一些時間先處理一下緩沖區(窗口)的數據,這樣等一會,窗口就會變得大一些,那么 ack 返回的窗口的大小就可以大一些,那么發送方就可以批量傳輸更多的數據了,再一定程度上提高了傳輸效率。
捎帶應答
就是在返回 ack 的時候順便把業務數據也一起帶上,捎帶應答是配合延遲應答使用的,延遲應答一般是等到接收方需要發送業務數據的時候把 ack 一起帶上,這樣可以減少網絡開銷,在一定程度上提高了傳輸效率。
面向字節流 —— 粘包問題
由于TCP是面向字節流的,所以在拆包之后,數據可能發生粘包問題,就是拆完包之后分不出哪些數據是一組,哪些數據是另一組。
舉個例子,發送方發送 aaa, bbb, ccc 這三個數據包,在接收方的傳輸層里進行拆包然后數據就可能變成這樣 aaabbbccc,交給應用層的時候就無法區分了,這就是粘包問題。
要解決粘包問題需要在應用層處理,在傳輸層是無解的,在設計應用層協議的時候,我們需要定義好如何區分這些數據,可以像之前我們寫TCP回顯服務器和客戶端一樣,使用 \n 這個換行符來進行數據的分割,或者其他的分割方法。