tcp前4字節消息長度_網絡基礎篇之TCP

?網絡分層

a7faa0ea6dc3f5eea690bcc792c809eb.png

什么是 TCP

TCP 是面向連接的、可靠的、基于字節流的傳輸層通信協議。

- 面向連接:通過三次握手建立一對一的連接( UDP 協議 可以一個主機同時向多個主機發送消息,即一對多);

- 可靠的:通過序號、校驗和、滑動窗口、超時重傳等機制,保證一個報文一定能夠到達接收端;

- 字節流:消息是沒有邊界的,所以無論我們消息有多大都可以進行傳輸。并且消息是有序的,當前一個消息沒有收到的時候,即使它先收到了后面的字節已經收到,那么也不能扔給應用層去處理,同時對重復的報文會自動丟棄。

流式服務&數據報服務

013bf3d043f82380f7100b04871b6b99.png
  • 流式服務:TCP的字節流服務體現在,發送端執行的寫操作數和接收端執行的讀操作次數之間沒有任何數量關系,當發送端應用程序連續執行多次寫操作的時,TCP模塊先將這些數據放入TCP發送緩沖區中。當TCP模塊真正開始發送數據的時候,發送緩沖區中這些等待發送的數據可能被封裝成一個或多個TCP報文段發出。
  • UPD的數據報服務:發送端應用程序每執行一次寫操作,UDP模塊就將其封裝成一個UDP數據報并發送之。接收端必須及時針對每一個UDP數據報執行讀操作(通過recvfrom系統調用),否則就會丟包(這經常發生在較慢的服務器上)。并且,如果沒有指定足夠的應用程序緩沖區來讀取UDP數據,則UDP數據將被截斷。


TCP頭部

d1c74f026b67bf815fd7bdb54048a3ba.png

序列號:在建立連接時由計算機生成的隨機數作為其初始值,通過 SYN 包傳給接收端主機,之后每次發送累加一次該數據字節數的大小。用來解決網絡包亂序問題

確認應答號:指下一次期望收到的數據的序列號(也就是收到的序列號+1),發送端收到這個確認應答以后可以認為在這個序號以前的數據都已經被正常接收。用來解決不丟包的問題

控制位

ACK:該位為 1 時,「確認應答」的字段變為有效,TCP 規定除了最初建立連接時的 SYN 包之外該位必須設置為 1 。

RST:該位為 1 時,表示 TCP 連接中出現異常必須強制斷開連接。

SYC:該位為 1 時,表示希望建立連接,并在其「序列號」的字段進行序列號初始值的設定。

FIN:該位為 1 時,表示今后不會再有數據發送,希望斷開連接。當通信結束希望斷開連接時,通信雙方的主機之間就可以相互交換 FIN 位置為 1 的 TCP 段。

如何唯一確定一個 TCP 連接

TCP 四元組可以唯一的確定一個連接,四元組包括如下:源地址、源端口、目的地址、目的端口。

源地址和目的地址的字段(32位)是在 IP 頭部中,作用是通過 IP 協議發送報文給對方主機。

源端口和目的端口的字段(16位)是在 TCP 頭部中,作用是告訴 TCP 協議應該把報文發給哪個進程。

【 IP 層是不可靠的,它不保證網絡包的交付、不保證網絡包的按序交付、也不保證網絡包中的數據的完整性。如果需要保障網絡數據包的可靠性,那么就需要由上層(傳輸層)的 TCP 協議來負責。】

TCP最大連接數

有一個 IP 的服務器監聽了一個端口,它的 TCP 的最大連接數是多少?

服務器通常固定在某個本地端口上監聽,等待客戶端的連接請求。

因此,客戶端 IP 和 端口是可變的,其理論值計算公式如下:

f5a5f84343f772bd45818fdee1623131.png

對 IPv4,客戶端的 IP 數最多為 2 的 32 次方,客戶端的端口數最多為 2 的 16 次方,也就是服務端單機最大 TCP 連接數,約為 2 的 48 次方。

當然,服務端最大并發 TCP 連接數遠不能達到理論上限。

  • 首先主要是文件描述符限制,Socket 都是文件,所以首先要通過 ulimit 配置文件描述符的數目;
  • 另一個是內存限制,每個 TCP 連接都要占用一定內存,操作系統是有限的。

UDP 和 TCP 區別及應用場景

UDP 不提供復雜的控制機制,利用 IP 提供面向無連接的通信服務。

UDP 頭部格式如下:

024bc0610d56cc88602bed8b567fee2e.png

目標和源端口:主要是告訴 UDP 協議應該把報文發給哪個進程。

包長度:該字段保存了 UDP 首部的長度跟數據的長度之和。

校驗和:校驗和是為了提供可靠的 UDP 首部和數據而設計。

TCP 和 UDP 區別

  • 連接方式

TCP需要三次握手建立連接。

UDP不需要連接直接傳輸數據。

  • 首部開銷

TCP 首部長度較長,會有一定的開銷,首部在沒有使用選項字段時是 20 個字節,如果使用了選項字段則會變長的。

UDP 首部只有 8 個字節,并且是固定不變的,開銷較小。

  • 服務對象

TCP是一對一;UDP支持一對一、一對多、多對多通信。

  • 可靠性

TCP 是可靠交付數據的,數據可以無差錯、不丟失、不重復、按需到達。

UDP 是盡最大努力交付,不保證可靠交付數據。

  • 擁塞控制、流量控制

TCP 有擁塞控制和流量控制機制,保證數據傳輸的安全性。

UDP 則沒有,即使網絡非常擁堵了,也不會影響 UDP 的發送速率。

TCP 和 UDP 應用場景

由于 TCP 是面向連接,能保證數據的可靠性交付,因此經常用于:

- FTP 文件傳輸

- HTTP / HTTPS

由于 UDP 面向無連接,它可以隨時發送數據,再加上UDP本身的處理既簡單又高效,因此經常用于:

- 包總量較少的通信,如 DNS 、SNMP 等

- 視頻、音頻等多媒體通信

- 廣播通信

為什么 UDP 頭部沒有「首部長度」字段,而 TCP 頭部有「首部長度」字段呢?

原因是 TCP 有可變長的「選項」字段,而 UDP 頭部長度則是不會變化的,無需多一個字段去記錄 UDP 的首部長度。

為什么 UDP 頭部有「包長度」字段,而 TCP 頭部則沒有「包長度」字段呢?

TCP 計算負載數據長度:

f2b2727af47727b69306b7df2a4bf26f.png

其中 IP 總長度 和 IP 首部長度,在 IP 首部格式是已知的。TCP 首部長度,則是在 TCP 首部格式已知的,所以就可以求得 TCP 數據的長度。

大家這時就奇怪了問:“ UDP 也是基于 IP 層的呀,那 UDP 的數據長度也可以通過這個公式計算呀?為何還要有包長度呢?”

這么一問,確實感覺 UDP 「包長度」是冗余的。

因為為了網絡設備硬件設計和處理方便,首部長度需要是 4字節的整數倍。

如果去掉 UDP 「包長度」字段,那 UDP 首部長度就不是 4 字節的整數倍了,所以小林覺得這可能是為了補全 UDP 首部長度是 4 字節的整數倍,才補充了「包長度」字段。

TCP 連接建立

TCP 三次握手過程和狀態變遷

TCP 是面向連接的協議,所以使用 TCP 前必須先建立連接,而建立連接是通過三次握手而進行的。

c7b7dc54bf957f6c7f994e7b865e2ce5.png

一開始,客戶端和服務端都處于 CLOSED 狀態。先是服務端主動監聽某個端口,處于 LISTEN 狀態

60f1bf6079b871d92d10d23d1966bc90.png

客戶端會隨機初始化序號(client_isn),將此序號置于 TCP 首部的序號字段中,同時把 SYN 標志位置為 1 ,表示 SYN 報文。接著把第一個 SYN 報文發送給服務端,表示向服務端發起連接,該報文不包含應用層數據,之后客戶端處于 SYN-SENT 狀態。

f740b814150f5b85569242af3ab59491.png

服務端收到客戶端的 SYN 報文后,首先服務端也隨機初始化自己的序號(server_isn),將此序號填入 TCP 首部的「序號」字段中,其次把 TCP 首部的「確認應答號」字段填入 client_isn + 1, 接著把 SYN 和 ACK 標志位置為 1。最后把該報文發給客戶端,該報文也不包含應用層數據,之后服務端處于 SYN-RCVD 狀態。

f66e2285a59ceb83bdc85eb2a250f0ec.png

客戶端收到服務端報文后,還要向服務端回應最后一個應答報文,首先該應答報文 TCP 首部 ACK 標志位置為 1 ,其次「確認應答號」字段填入 server_isn + 1 ,最后把報文發送給服務端,這次報文可以攜帶客戶到服務器的數據,之后客戶端處于 ESTABLISHED 狀態。

服務器收到客戶端的應答報文后,也進入 ESTABLISHED 狀態。

從上面的過程可以發現第三次握手是可以攜帶數據的,前兩次握手是不可以攜帶數據的。

一旦完成三次握手,雙方都處于 ESTABLISHED 狀態,此致連接就已建立完成,客戶端和服務端就可以相互發送數據了。

Linux 系統中查看 TCP 狀態命令

TCP 的連接狀態查看,在 Linux 可以通過 netstat -napt 命令查看。

acae04e93cb8547ca92276fa626fd237.png

為什么是三次握手?不是兩次、四次?

  • 避免歷史連接

簡單來說,三次握手的首要原因是為了防止舊的重復連接初始化造成混亂。

網絡環境是錯綜復雜的,往往并不是如我們期望的一樣,先發送的數據包,就先到達目標主機,反而它很騷,可能會由于網絡擁堵等亂七八糟的原因,會使得舊的數據包,先到達目標主機,那么這種情況下 TCP 三次握手是如何避免的呢?

b50a2cb7895c347ec7761cd16f1e347e.png

客戶端連續發送多次 SYN 建立連接的報文,在網絡擁堵等情況下:

一個「舊 SYN 報文」比「最新的 SYN 」 報文早到達了服務端;

那么此時服務端就會回一個 SYN + ACK 報文給客戶端;

客戶端收到后可以根據自身的上下文,判斷這是一個歷史連接(序列號過期或超時),那么客戶端就會發送 RST 報文給服務端,表示中止這一次連接。

如果是兩次握手連接,就不能判斷當前連接是否是歷史連接,三次握手則可以在客戶端(發送方)準備發送第三次報文時,客戶端因有足夠的上下文來判斷當前連接是否是歷史連接:

如果是歷史連接(序列號過期或超時),則第三次握手發送的報文是 RST 報文,以此中止歷史連接;

如果不是歷史連接,則第三次發送的報文是 ACK 報文,通信雙方就會成功建立連接;

所以, TCP 使用三次握手建立連接的最主要原因是防止歷史連接初始化了連接。

  • 同步雙方初始序列號

TCP 協議的通信雙方, 都必須維護一個「序列號」, 序列號是可靠傳輸的一個關鍵因素,它的作用:

接收方可以去除重復的數據;

接收方可以根據數據包的序列號按序接收;

可以標識發送出去的數據包中, 哪些是已經被對方收到的;

可見,序列號在 TCP 連接中占據著非常重要的作用,所以當客戶端發送攜帶「初始序列號」的 SYN 報文的時候,需要服務端回一個 ACK 應答報文,表示客戶端的 SYN 報文已被服務端成功接收,那當服務端發送「初始序列號」給客戶端的時候,依然也要得到客戶端的應答回應,這樣一來一回,才能確保雙方的初始序列號能被可靠的同步。

b1e6b72435dba58b0a884c338b97dbeb.png

四次握手其實也能夠可靠的同步雙方的初始化序號,但由于第二步和第三步可以優化成一步,所以就成了「三次握手」。

  • 避免資源浪費

如果只有「兩次握手」,當客戶端的 SYN 請求連接在網絡中阻塞,客戶端沒有接收到 ACK 報文,就會重新發送 SYN ,由于沒有第三次握手,服務器不清楚客戶端是否收到了自己發送的建立連接的 ACK 確認信號,所以每收到一個 SYN 就只能先主動建立一個連接,這會造成什么情況呢?

如果客戶端的 SYN 阻塞了,重復發送多次 SYN 報文,那么服務器在收到請求后就會建立多個冗余的無效鏈接,造成不必要的資源浪費。

bccc08307074f7a7cde832db3a7b0e2f.png

即兩次握手會造成消息滯留情況下,服務器重復接受無用的連接請求 SYN 報文,而造成重復分配資源。

為什么客戶端和服務端的初始序列號 ISN 是不相同的?

因為網絡中的報文會延遲、會復制重發、也有可能丟失,這樣會造成的不同連接之間產生互相影響,所以為了避免互相影響,客戶端和服務端的初始序列號是隨機且不同的。

初始序列號 ISN 是如何隨機產生的?

起始 ISN 是基于時鐘的,每 4 毫秒 + 1,轉一圈要 4.55 個小時。

RFC1948 中提出了一個較好的初始化序列號 ISN 隨機生成算法。

ISN = M + F (localhost, localport, remotehost, remoteport)

M 是一個計時器,這個計時器每隔 4 毫秒加 1。

F 是一個 Hash 算法,根據源 IP、目的 IP、源端口、目的端口生成一個隨機數值。要保證 Hash 算法不能被外部輕易推算得出,用 MD5 算法是一個比較好的選擇。

既然 IP 層會分片,為什么 TCP 層還需要 MSS 呢?

我們先來認識下 MTU 和 MSS

47ef631b2bfac7f1bb0c509cd8a5e31d.png

- MTU:一個網絡包的最大長度,以太網中一般為 1500 字節;

- MSS:除去 IP 和 TCP 頭部之后,一個網絡包所能容納的 TCP 數據的最大長度;

如果TCP 的整個報文(頭部 + 數據)交給 IP 層進行分片,會有什么異常呢?

當 IP 層有一個超過 MTU 大小的數據(TCP 頭部 + TCP 數據)要發送,那么 IP 層就要進行分片,把數據分片成若干片,保證每一個分片都小于 MTU。把一份 IP 數據報進行分片以后,由目標主機的 IP 層來進行重新組裝后,在交給上一層 TCP 傳輸層。

這看起來井然有序,但這存在隱患的,那么當如果一個 IP 分片丟失,整個 IP 報文的所有分片都得重傳。

因為 IP 層本身沒有超時重傳機制,它由傳輸層的 TCP 來負責超時和重傳。

當接收方發現 TCP 報文(頭部 + 數據)的某一片丟失后,則不會響應 ACK 給對方,那么發送方的 TCP 在超時后,就會重發「整個 TCP 報文(頭部 + 數據)」。

因此,可以得知由 IP 層進行分片傳輸,是非常沒有效率的。

所以,為了達到最佳的傳輸效能 TCP 協議在建立連接的時候通常要協商雙方的 MSS 值,當 TCP 層發現數據超過 MSS 時,則就先會進行分片,當然由它形成的 IP 包的長度也就不會大于 MTU ,自然也就不用 IP 分片了。

cb171acb046cccbb0a8ab47a5d3b355f.png

經過 TCP 層分片后,如果一個 TCP 分片丟失后,進行重發時也是以 MSS 為單位,而不用重傳所有的分片,大大增加了重傳的效率。

SYN 攻擊

我們都知道 TCP 連接建立是需要三次握手,假設攻擊者短時間偽造不同 IP 地址的 SYN 報文,服務端每接收到一個 SYN 報文,就進入SYN_RCVD 狀態,但服務端發送出去的 ACK + SYN 報文,無法得到未知 IP 主機的 ACK 應答,久而久之就會占滿服務端的 SYN 接收隊列(未連接隊列),使得服務器不能為正常用戶服務。

  • 避免 SYN 攻擊方式一:通過修改 Linux 內核參數,控制隊列大小和當隊列滿時應做什么處理。

當網卡接收數據包的速度大于內核處理的速度時,會有一個隊列保存這些數據包。控制該隊列的最大值如下參數:net.core.netdev_max_backlog

SYN_RCVD 狀態連接的最大個數:net.ipv4.tcp_max_syn_backlog

超出處理能時,對新的 SYN 直接回 RST,丟棄連接:net.ipv4.tcp_abort_on_overflow

  • 避免 SYN 攻擊方式二:tcp_syncookies 的方式

我們先來看下Linux 內核的 SYN (未完成連接建立)隊列與 Accpet (已完成連接建立)隊列是如何工作的?

ed66c5ce6feac4e28955b954246ee1f4.png

正常流程:

- 當服務端接收到客戶端的 SYN 報文時,會將其加入到內核的「 SYN 隊列」;

- 接著發送 SYN + ACK 給客戶端,等待客戶端回應 ACK 報文;

- 服務端接收到 ACK 報文后,從「 SYN 隊列」移除放入到「 Accept 隊列」;

- 應用通過調用 accpet() socket 接口,從「 Accept 隊列」取出的連接。

743d28e9c00bea4b82d8da36f1a2e663.png

應用程序過慢:

如果應用程序過慢時,就會導致「 Accept 隊列」被占滿。

f8dc10039fbdfbc43852263a7a4b2142.png

受到 SYN 攻擊:

如果不斷受到 SYN 攻擊,就會導致「 SYN 隊列」被占滿。

tcp_syncookies 的方式可以應對 SYN 攻擊的方法:net.ipv4.tcp_syncookies = 1

f7479a370896f5dfebe4574c94facb67.png

- 當 「 SYN 隊列」滿之后,后續服務器收到 SYN 包,不進入「 SYN 隊列」;

- 計算出一個 cookie 值,再以 SYN + ACK 中的「序列號」返回客戶端,

- 服務端接收到客戶端的應答報文時,服務器會檢查這個 ACK 包的合法性。如果合法,直接放入到「 Accept 隊列」。

- 最后應用通過調用 accpet() socket 接口,從「 Accept 隊列」取出的連接。

TCP 連接斷開

TCP 四次揮手過程和狀態變遷

天下沒有不散的宴席,對于 TCP 連接也是這樣, TCP 斷開連接是通過四次揮手方式。

雙方都可以主動斷開連接,斷開連接后主機中的「資源」將被釋放。

0b9eb0fb4d966a655034e497872e75c9.png

- 客戶端打算關閉連接,此時會發送一個 TCP 首部 FIN 標志位被置為 1 的報文,也即 FIN 報文,之后客戶端進入 FIN_WAIT_1 狀態。

- 服務端收到該報文后,就向客戶端發送 ACK 應答報文,接著服務端進入 CLOSED_WAIT 狀態。

- 客戶端收到服務端的 ACK 應答報文后,之后進入 FIN_WAIT_2 狀態。

- 等待服務端處理完數據后,也向客戶端發送 FIN 報文,之后服務端進入 LAST_ACK 狀態。

- 客戶端收到服務端的 FIN 報文后,回一個 ACK 應答報文,之后進入 TIME_WAIT 狀態

- 服務器收到了 ACK 應答報文后,就進入了 CLOSE 狀態,至此服務端已經完成連接的關閉。

- 客戶端在經過 2MSL 一段時間后,自動進入 CLOSE 狀態,至此客戶端也完成連接的關閉。

你可以看到,每個方向都需要一個 FIN 和一個 ACK,因此通常被稱為四次揮手。

這里一點需要注意是:主動關閉連接的,才有 TIME_WAIT 狀態。

為什么揮手需要四次?

- 關閉連接時,客戶端向服務端發送 FIN 時,僅僅表示客戶端不再發送數據了但是還能接收數據。

- 服務器收到客戶端的 FIN 報文時,先回一個 ACK 應答報文,而服務端可能還有數據需要處理和發送,等服務端不再發送數據時,才發送 FIN 報文給客戶端來表示同意現在關閉連接。

從上面過程可知,服務端通常需要等待完成數據的發送和處理,所以服務端的 ACK 和 FIN 一般都會分開發送,從而比三次握手導致多了一次。

為什么 TIME_WAIT 等待的時間是 2MSL?

MSL 是 Maximum Segment Lifetime,報文最大生存時間,它是任何報文在網絡上存在的最長時間,超過這個時間報文將被丟棄。因為 TCP 報文基于是 IP 協議的,而 IP 頭中有一個 TTL 字段,是 IP 數據報可以經過的最大路由數,每經過一個處理他的路由器此值就減 1,當此值為 0 則數據報將被丟棄,同時發送 ICMP 報文通知源主機。

MSL 與 TTL 的區別:MSL 的單位是時間,而 TTL 是經過路由跳數。所以 MSL 應該要大于等于 TTL 消耗為 0 的時間,以確保報文已被自然消亡。

TIME_WAIT 等待 2 倍的 MSL,比較合理的解釋是:網絡中可能存在來自發送方的數據包,當這些發送方的數據包被接收方處理后又會向對方發送響應,所以一來一回需要等待 2 倍的時間。

比如,如果被動關閉方沒有收到斷開連接的最后的 ACK 報文,就會觸發超時重發 Fin 報文,另一方接收到 FIN 后,會重發 ACK 給被動關閉方, 一來一去正好 2 個 MSL。

2MSL 的時間是從客戶端接收到 FIN 后發送 ACK 開始計時的。如果在 TIME-WAIT 時間內,因為客戶端的 ACK 沒有傳輸到服務端,客戶端又接收到了服務端重發的 FIN 報文,那么 2MSL 時間將重新計時。

在 Linux 系統里 2MSL 默認是 60 秒,那么一個 MSL 也就是 30 秒。Linux 系統停留在 TIME_WAIT 的時間為固定的 60 秒。

其定義在 Linux 內核代碼里的名稱為 TCP_TIMEWAIT_LEN:

#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT state, about 60 seconds  */

如果要修改 TIME_WAIT 的時間長度,只能修改 Linux 內核代碼里 TCP_TIMEWAIT_LEN 的值,并重新編譯 Linux 內核。

為什么需要 TIME_WAIT 狀態?

主動發起關閉連接的一方,才會有 TIME-WAIT 狀態。

需要 TIME-WAIT 狀態,主要是兩個原因:

原因一:防止舊連接的數據包

假設 TIME-WAIT 沒有等待時間或時間過短,被延遲的數據包抵達后會發生什么呢?

7c075aabe8003e044718bc042738cb27.png

- 如上圖黃色框框服務端在關閉連接之前發送的 SEQ = 301 報文,被網絡延遲了。

- 這時有相同端口的 TCP 連接被復用后,被延遲的 SEQ = 301 抵達了客戶端,那么客戶端是有可能正常接收這個過期的報文,這就會產生數據錯亂等嚴重的問題。

所以,TCP 就設計出了這么一個機制,經過 2MSL 這個時間,足以讓兩個方向上的數據包都被丟棄,使得原來連接的數據包在網絡中都自然消失,再出現的數據包一定都是新建立連接所產生的。

原因二:保證連接正確關閉

也就是說,TIME-WAIT 作用是等待足夠的時間以確保最后的 ACK 能讓被動關閉方接收,從而幫助其正常關閉。

假設 TIME-WAIT 沒有等待時間或時間過短,斷開連接會造成什么問題呢?

6ea900b7235f954f94d8953177f29408.png

- 如上圖紅色框框客戶端四次揮手的最后一個 ACK 報文如果在網絡中被丟失了,此時如果客戶端 TIME-WAIT 過短或沒有,則就直接進入了 CLOSE 狀態了,那么服務端則會一直處在 LASE-ACK 狀態。

- 當客戶端發起建立連接的 SYN 請求報文后,服務端會發送 RST 報文給客戶端,連接建立的過程就會被終止。

如果 TIME-WAIT 等待足夠長的情況就會遇到兩種情況:

- 服務端正常收到四次揮手的最后一個 ACK 報文,則服務端正常關閉連接。

- 服務端沒有收到四次揮手的最后一個 ACK 報文時,則會重發 FIN 關閉連接報文并等待新的 ACK 報文。

所以客戶端在 TIME-WAIT 狀態等待 2MSL 時間后,就可以保證雙方的連接都可以正常的關閉。

TIME_WAIT 過多有什么危害?

如果服務器有處于 TIME-WAIT 狀態的 TCP,則說明是由服務器方主動發起的斷開請求。

過多的 TIME-WAIT 狀態主要的危害有兩種:

- 第一是內存資源占用;

- 第二是對端口資源的占用,一個 TCP 連接至少消耗一個本地端口;

第二個危害是會造成嚴重的后果的,要知道,端口資源也是有限的,一般可以開啟的端口為 32768~61000,也可以通過如下參數設置指定

net.ipv4.ip_local_port_range

如果服務端 TIME_WAIT 狀態過多,占滿了所有端口資源,則會導致無法創建新連接。

如何優化 TIME_WAIT?

這里給出優化 TIME-WAIT 的幾個方式,都是有利有弊:

方式一:net.ipv4.tcp_tw_reuse 和 tcp_timestamps

如下的 Linux 內核參數開啟后,則可以復用處于 TIME_WAIT 的 socket 為新的連接所用。

net.ipv4.tcp_tw_reuse = 1

使用這個選項,還有一個前提,需要打開對 TCP 時間戳的支持,即

net.ipv4.tcp_timestamps=1(默認即為 1)

這個時間戳的字段是在 TCP 頭部的「選項」里,用于記錄 TCP 發送方的當前時間戳和從對端接收到的最新時間戳。

由于引入了時間戳,我們在前面提到的 2MSL 問題就不復存在了,因為重復的數據包會因為時間戳過期被自然丟棄。

溫馨提醒:net.ipv4.tcp_tw_reuse要慎用,因為使用了它就必然要打開時間戳的支持 net.ipv4.tcp_timestamps,當客戶端與服務端主機時間不同步時,客戶端的發送的消息會被直接拒絕掉。小林在工作中就遇到過。。。排查了非常的久

方式二:net.ipv4.tcp_max_tw_buckets

這個值默認為 18000,當系統中處于 TIME_WAIT 的連接一旦超過這個值時,系統就會將所有的 TIME_WAIT 連接狀態重置。

這個方法過于暴力,而且治標不治本,帶來的問題遠比解決的問題多,不推薦使用。

方式三:程序中使用 SO_LINGER

我們可以通過設置 socket 選項,來設置調用 close 關閉連接行為。

struct linger so_linger;

so_linger.l_onoff = 1;

so_linger.l_linger = 0;

setsockopt(s, SOL_SOCKET, SO_LINGER, &so_linger,sizeof(so_linger));

如果l_onoff為非 0, 且l_linger值為 0,那么調用close后,會立該發送一個RST標志給對端,該 TCP 連接將跳過四次揮手,也就跳過了TIME_WAIT狀態,直接關閉。

但這為跨越TIME_WAIT狀態提供了一個可能,不過是一個非常危險的行為,不值得提倡。

如果已經建立了連接,但是客戶端突然出現故障了怎么辦?

TCP 有一個機制是保活機制。這個機制的原理是這樣的:

定義一個時間段,在這個時間段內,如果沒有任何連接相關的活動,TCP 保活機制會開始作用,每隔一個時間間隔,發送一個探測報文,該探測報文包含的數據非常少,如果連續幾個探測報文都沒有得到響應,則認為當前的 TCP 連接已經死亡,系統內核將錯誤信息通知給上層應用程序。

在 Linux 內核可以有對應的參數可以設置保活時間、保活探測的次數、保活探測的時間間隔,以下都為默認值:

net.ipv4.tcp_keepalive_time=7200

net.ipv4.tcp_keepalive_intvl=75

net.ipv4.tcp_keepalive_probes=9

tcp_keepalive_time=7200:表示保活時間是 7200 秒(2小時),也就 2 小時內如果沒有任何連接相關的活動,則會啟動保活機制

tcp_keepalive_intvl=75:表示每次檢測間隔 75 秒;

tcp_keepalive_probes=9:表示檢測 9 次無響應,認為對方是不可達的,從而中斷本次的連接。

也就是說在 Linux 系統中,最少需要經過 2 小時 11 分 15 秒才可以發現一個「死亡」連接。

f47e16db23a6c1c9dcfb36acba2e83dd.png

這個時間是有點長的,我們也可以根據實際的需求,對以上的保活相關的參數進行設置。

如果開啟了 TCP 保活,需要考慮以下幾種情況:

第一種,對端程序是正常工作的。當 TCP 保活的探測報文發送給對端, 對端會正常響應,這樣 TCP 保活時間會被重置,等待下一個 TCP 保活時間的到來。

第二種,對端程序崩潰并重啟。當 TCP 保活的探測報文發送給對端后,對端是可以響應的,但由于沒有該連接的有效信息,會產生一個 RST 報文,這樣很快就會發現 TCP 連接已經被重置。

第三種,是對端程序崩潰,或對端由于其他原因導致報文不可達。當 TCP 保活的探測報文發送給對端后,石沉大海,沒有響應,連續幾次,達到保活探測次數后,TCP 會報告該 TCP 連接已經死亡。

Socket 編程

針對 TCP 應該如何 Socket 編程?

4bb4b61df90b995a3a4ea365dab62750.png

服務端和客戶端初始化 socket,得到文件描述符;

服務端調用 bind,將綁定在 IP 地址和端口;

服務端調用 listen,進行監聽;

服務端調用 accept,等待客戶端連接;

客戶端調用 connect,向服務器端的地址和端口發起連接請求;

服務端 accept 返回用于傳輸的 socket 的文件描述符;

客戶端調用 write 寫入數據;服務端調用 read 讀取數據;

客戶端斷開連接時,會調用 close,那么服務端 read 讀取數據的時候,就會讀取到了 EOF,待處理完數據后,服務端調用 close,表示連接關閉。

這里需要注意的是,服務端調用 accept 時,連接成功了會返回一個已完成連接的 socket,后續用來傳輸數據。

所以,監聽的 socket 和真正用來傳送數據的 socket,是「兩個」 socket,一個叫作監聽 socket,一個叫作已完成連接 socket。

成功連接建立之后,雙方開始通過 read 和 write 函數來讀寫數據,就像往一個文件流里面寫東西一樣。

listen 時候參數 backlog 的意義?

Linux內核中會維護兩個隊列:

未完成連接隊列(SYN 隊列):接收到一個 SYN 建立連接請求,處于 SYN_RCVD 狀態;

已完成連接隊列(Accpet 隊列):已完成 TCP 三次握手過程,處于 ESTABLISHED 狀態;

542d0b2f5eb88528fc096ffe4cdc4abe.png

int listen (int socketfd, int backlog)

- 參數一 socketfd 為 socketfd 文件描述符

- 參數二 backlog,這參數在歷史有一定的變化

在早期 Linux 內核 backlog 是 SYN 隊列大小,也就是未完成的隊列大小。

在 Linux 內核 2.2 之后,backlog 變成 accept 隊列,也就是已完成連接建立的隊列長度,所以現在通常認為 backlog 是 accept 隊列。

accept 發送在三次握手的哪一步?

我們先看看客戶端連接服務端時,發送了什么?

02bcf6a88f32458e7adf150cdbeaba12.png

- 客戶端的協議棧向服務器端發送了 SYN 包,并告訴服務器端當前發送序列號 client_isn,客戶端進入 SYNC_SENT 狀態;

- 服務器端的協議棧收到這個包之后,和客戶端進行 ACK 應答,應答的值為 client_isn+1,表示對 SYN 包 client_isn 的確認,同時服務器也發送一個 SYN 包,告訴客戶端當前我的發送序列號為 server_isn,服務器端進入 SYNC_RCVD 狀態;

- 客戶端協議棧收到 ACK 之后,使得應用程序從 connect 調用返回,表示客戶端到服務器端的單向連接建立成功,客戶端的狀態為 ESTABLISHED,同時客戶端協議棧也會對服務器端的 SYN 包進行應答,應答數據為 server_isn+1;

- 應答包到達服務器端后,服務器端協議棧使得 accept 阻塞調用返回,這個時候服務器端到客戶端的單向連接也建立成功,服務器端也進入 ESTABLISHED 狀態。

從上面的描述過程,我們可以得知客戶端 connect 成功返回是在第二次握手,服務端 accept 成功返回是在三次握手成功之后。

客戶端調用 close 了,連接斷開的流程是什么?

- 客戶端調用 close,表明客戶端沒有數據需要發送了,則此時會向服務端發送 FIN 報文,進入 - FIN_WAIT_1 狀態;

- 服務端接收到了 FIN 報文,TCP 協議棧會為 FIN 包插入一個文件結束符 EOF 到接收緩沖區中,應用程序可以通過 read 調用來感知這個 FIN 包。這個 EOF 會被放在已排隊等候的其他已接收的數據之后,這就意味著服務端需要處理這種異常情況,因為 EOF 表示在該連接上再無額外數據到達。此時,服務端進入 CLOSE_WAIT 狀態;

- 接著,當處理完數據后,自然就會讀到 EOF,于是也調用 close 關閉它的套接字,這會使得會發出一個 FIN 包,之后處于 LAST_ACK 狀態;

- 客戶端接收到服務端的 FIN 包,并發送 ACK 確認包給服務端,此時客戶端將進入 TIME_WAIT 狀態;

- 服務端收到 ACK 確認包后,就進入了最后的 CLOSE 狀態;

- 客戶端進過 2MSL 時間之后,也進入 CLOSED 狀態;

重傳機制

在錯綜復雜的網絡環境中,萬一數據在傳輸過程中丟失,TCP針對這種情況用重傳機制解決。常見的重傳機制:

- 超時重傳

- 快速重傳

- SACK

- D-SACK

超時重傳

發送數據是設定一個定時器,當超過指定時間沒有收到對方的確認報文,就會重發該數據。TCP會在一下兩種情況發生超時重傳:

- 數據包丟失

- 確認應答丟失

cf0d64b8218cc3934ac6ce9dc8a5c0cd.png

超時時間設置為多少呢?

RTT(Round-Trip Time 往返時延)

91d6d7d2dbe87e99bce4d0cc87648670.png

RTT 就是數據從網絡一端傳送到另一端并收到應答所需的時間,也就是包的往返時間。超時重傳時間是以 RTO (Retransmission Timeout 超時重傳時間)表示。

假設在重傳的情況下,超時時間 RTO 「較長或較短」時,會發生什么事情呢?

b38142689d7936f291baa908d20f90f9.png

上圖中有兩種超時時間不同的情況:

- 當超時時間 RTO 較大時,重發就慢,丟了老半天才重發,沒有效率,性能差;

- 當超時時間 RTO 較小時,會導致可能并沒有丟就重發,于是重發的就快,會增加網絡擁塞,導致更多的超時,更多的超時導致更多的重發。

精確的測量超時時間 RTO 的值是非常重要的,這可讓我們的重傳機制更高效。

根據上述的兩種情況,我們可以得知,超時重傳時間 RTO 的值應該略大于報文往返 RTT 的值。

8a9c094d0cb5c46659aaaa3f000404c0.png

至此,可能大家覺得超時重傳時間 RTO 的值計算,也不是很復雜嘛。

好像就是在發送端發包時記下 t0 ,然后接收端再把這個 ack 回來時再記一個 t1,于是 RTT = t1 – t0。沒那么簡單,這只是一個采樣,不能代表普遍情況。

實際上「報文往返 RTT 的值」是經常變化的,因為我們的網絡也是時常變化的。也就因為「報文往返 RTT 的值」 是經常波動變化的,所以「超時重傳時間 RTO 的值」應該是一個動態變化的值。

我們來看看 Linux 是如何計算 RTO 的呢?

估計往返時間,通常需要采樣以下兩個:

需要 TCP 通過采樣 RTT 的時間,然后進行加權平均,算出一個平滑 RTT 的值,而且這個值還是要不斷變化的,因為網絡狀況不斷地變化。

除了采樣 RTT,還要采樣 RTT 的波動范圍,這樣就避免如果 RTT 有一個大的波動的話,很難被發現的情況。

RFC6289 建議使用以下的公式計算 RTO:

70b2929ca567d6ca5b34c0c59c592f86.png

其中 SRTT 是計算平滑的RTT ,DevRTR 是計算平滑的RTT 與 最新 RTT 的差距。

在 Linux 下,α = 0.125,β = 0.25, μ = 1,? = 4。別問怎么來的,問就是大量實驗中調出來的。

如果超時重發的數據,再次超時的時候,又需要重傳的時候,TCP 的策略是超時間隔加倍。

也就是每當遇到一次超時重傳的時候,都會將下一次超時時間間隔設為先前值的兩倍。兩次超時,就說明網絡環境差,不宜頻繁反復發送。

超時觸發重傳存在的問題是,超時周期可能相對較長。那是不是可以有更快的方式呢?

于是就可以用「快速重傳」機制來解決超時重發的時間等待。

快速重傳

TCP 還有另外一種快速重傳(Fast Retransmit)機制,它不以時間為驅動,而是以數據驅動重傳。

快速重傳機制,是如何工作的呢?其實很簡單,一圖勝千言。

0bc4a9ba276c5f2ea5ee83a4c0518df7.png

在上圖,發送方發出了 1,2,3,4,5 份數據:

第一份 Seq1 先送到了,于是就 Ack 回 2;

結果 Seq2 因為某些原因沒收到,Seq3 到達了,于是還是 Ack 回 2;

后面的 Seq4 和 Seq5 都到了,但還是 Ack 回 2,因為 Seq2 還是沒有收到;

發送端收到了三個 Ack = 2 的確認,知道了 Seq2 還沒有收到,就會在定時器過期之前,重傳丟失的 Seq2。

最后,接收到收到了 Seq2,此時因為 Seq3,Seq4,Seq5 都收到了,于是 Ack 回 6 。

所以,快速重傳的工作方式是當收到三個相同的 ACK 報文時,會在定時器過期之前,重傳丟失的報文段。

快速重傳機制只解決了一個問題,就是超時時間的問題,但是它依然面臨著另外一個問題。就是重傳的時候,是重傳之前的一個,還是重傳所有的問題。

比如對于上面的例子,是重傳 Seq2 呢?還是重傳 Seq2、Seq3、Seq4、Seq5 呢?因為發送端并不清楚這連續的三個 Ack 2 是誰傳回來的。

根據 TCP 不同的實現,以上兩種情況都是有可能的。可見,這是一把雙刃劍。

為了解決不知道該重傳哪些 TCP 報文,于是就有 SACK 方法。

SACK( Selective Acknowledgment 選擇性確認)

這種方式需要在 TCP 頭部「選項」字段里加一個 SACK 的東西,它可以將緩存的地圖發送給發送方,這樣發送方就可以知道哪些數據收到了,哪些數據沒收到,知道了這些信息,就可以只重傳丟失的數據。

如下圖,發送方收到了三次同樣的 ACK 確認報文,于是就會觸發快速重發機制,通過 SACK 信息發現只有 200~299 這段數據丟失,則重發時,就只選擇了這個 TCP 段進行重復。

17f5a5c2f6da36627c2675d365affdba.png

如果要支持 SACK,必須雙方都要支持。在 Linux 下,可以通過 net.ipv4.tcp_sack 參數打開這個功能(Linux 2.4 后默認打開)。

D-SACK(Duplicate SACK)

其主要使用了 SACK 來告訴「發送方」有哪些數據被重復接收了。

下面舉例兩個栗子,來說明 D-SACK 的作用。

栗子一號:ACK 丟包

9d76e2da1b30de9efd128f4ea83e5ede.png

- 「接收方」發給「發送方」的兩個 ACK 確認應答都丟失了,所以發送方超時后,重傳第一個數據包(3000 ~ 3499)

- 于是「接收方」發現數據是重復收到的,于是回了一個 SACK = 3000~3500,告訴「發送方」 3000~3500 的數據早已被接收了,因為 ACK 都到了 4000 了,已經意味著 4000 之前的所有數據都已收到,所以這個 SACK 就代表著 D-SACK。

- 這樣「發送方」就知道了,數據沒有丟,是「接收方」的 ACK 確認報文丟了。

栗子二號:網絡延時

b56be25fb8b3f64e4e2410eeb56369fb.png

數據包(1000~1499) 被網絡延遲了,導致「發送方」沒有收到 Ack 1500 的確認報文。

而后面報文到達的三個相同的 ACK 確認報文,就觸發了快速重傳機制,但是在重傳后,被延遲的數據包(1000~1499)又到了「接收方」;

所以「接收方」回了一個 SACK=1000~1500,因為 ACK 已經到了 3000,所以這個 SACK 是 D-SACK,表示收到了重復的包。

這樣發送方就知道快速重傳觸發的原因不是發出去的包丟了,也不是因為回應的 ACK 包丟了,而是因為網絡延遲了。

可見,D-SACK 有這么幾個好處:

可以讓「發送方」知道,是發出去的包丟了,還是接收方回應的 ACK 包丟了;

可以知道是不是「發送方」的數據包被網絡延遲了;

可以知道網絡中是不是把「發送方」的數據包給復制了;

在 Linux 下可以通過 net.ipv4.tcp_dsack 參數開啟/關閉這個功能(Linux 2.4 后默認打開)。

滑動窗口

我們都知道 TCP 是每發送一個數據,都要進行一次確認應答。當上一個數據包收到了應答了, 再發送下一個。

這個模式就有點像我和你面對面聊天,你一句我一句。但這種方式的缺點是效率比較低的。

如果你說完一句話,我在處理其他事情,沒有及時回復你,那你不是要干等著我做完其他事情后,我回復你,你才能說下一句話,很顯然這不現實。

所以,這樣的傳輸方式有一個缺點:數據包的往返時間越長,通信的效率就越低。

為解決這個問題,TCP 引入了窗口這個概念。即使在往返時間較長的情況下,它也不會降低網絡通信的效率。

那么有了窗口,就可以指定窗口大小,窗口大小就是指無需等待確認應答,而可以繼續發送數據的最大值。

窗口的實現實際上是操作系統開辟的一個緩存空間,發送方主機在等到確認應答返回之前,必須在緩沖區中保留已發送的數據。如果按期收到確認應答,此時數據就可以從緩存區清除。

假設窗口大小為 3 個 TCP 段,那么發送方就可以「連續發送」 3 個 TCP 段,并且中途若有 ACK 丟失,可以通過「下一個確認應答進行確認」。如下圖

5a5886751e6f4f7762279618756f1865.png

圖中的 ACK 600 確認應答報文丟失,也沒關系,因為可以通話下一個確認應答進行確認,只要發送方收到了 ACK 700 確認應答,就意味著 700 之前的所有數據「接收方」都收到了。這個模式就叫累計確認或者累計應答。

窗口大小由哪一方決定?

TCP 頭里有一個字段叫 Window,也就是窗口大小。

這個字段是接收端告訴發送端自己還有多少緩沖區可以接收數據。于是發送端就可以根據這個接收端的處理能力來發送數據,而不會導致接收端處理不過來。

所以,通常窗口的大小是由接收方的決定的。

發送方發送的數據大小不能超過接收方的窗口大小,否則接收方就無法正常接收到數據。

發送方的滑動窗口

我們先來看看發送方的窗口,下圖就是發送方緩存的數據,根據處理的情況分成四個部分,其中深藍色方框是發送窗口,紫色方框是可用窗口:

04756719872255f810bec9a4c869cc19.png


在下圖,當發送方把數據「全部」都一下發送出去后,可用窗口的大小就為 0 了,表明可用窗口耗盡,在沒收到 ACK 確認之前是無法繼續發送數據了。

0526682e8c25f37aff50cf17212f0c6f.png


在下圖,當收到之前發送的數據 32~36 字節的 ACK 確認應答后,如果發送窗口的大小沒有變化,則滑動窗口往右邊移動 5 個字節,因為有 5 個字節的數據被應答確認,接下來 52~56 字節又變成了可用窗口,那么后續也就可以發送 52~56 這 5 個字節的數據了。

d725d2e66ee67a9934a1682696d388c8.png

程序是如何表示發送方的四個部分的呢?

TCP 滑動窗口方案使用三個指針來跟蹤在四個傳輸類別中的每一個類別中的字節。其中兩個指針是絕對指針(指特定的序列號),一個是相對指針(需要做偏移)。

1e26913d7293494c750f55bd8f4bbf8c.png


SND.WND:表示發送窗口的大小(大小是由接收方指定的);

SND.UNA:是一個絕對指針,它指向的是已發送但未收到確認的第一個字節的序列號,也就是 #2 的第一個字節。

SND.NXT:也是一個絕對指針,它指向未發送但可發送范圍的第一個字節的序列號,也就是 #3 的第一個字節。

指向 #4 的第一個字節是個相對指針,它需要 SND.UNA 指針加上 SND.WND 大小的偏移量,就可以指向 #4 的第一個字節了。

那么可用窗口大小的計算就可以是:

可用窗口大 = SND.WND -(SND.NXT - SND.UNA)

接收方的滑動窗口

接下來我們看看接收方的窗口,接收窗口相對簡單一些,根據處理的情況劃分成三個部分:

#1 + #2 是已成功接收并確認的數據(等待應用進程讀取);

#3 是未收到數據但可以接收的數據;

#4 未收到數據并不可以接收的數據;

b4649ae90df5b609fc4314b299c52e36.png


其中三個接收部分,使用兩個指針進行劃分:

RCV.WND:表示接收窗口的大小,它會通告給發送方。

RCV.NXT:是一個指針,它指向期望從發送方發送來的下一個數據字節的序列號,也就是 #3 的第一個字節。

指向 #4 的第一個字節是個相對指針,它需要 RCV.NXT 指針加上 RCV.WND 大小的偏移量,就可以指向 #4 的第一個字節了。

接收窗口和發送窗口的大小是相等的嗎?

并不是完全相等,接收窗口的大小是約等于發送窗口的大小的。

因為滑動窗口并不是一成不變的。比如,當接收方的應用進程讀取數據的速度非常快的話,這樣的話接收窗口可以很快的就空缺出來。那么新的接收窗口大小,是通過 TCP 報文中的 Windows 字段來告訴發送方。那么這個傳輸過程是存在時延的,所以接收窗口和發送窗口是約等于的關系。

流量控制

發送方不能無腦的發數據給接收方,要考慮接收方處理能力。

如果一直無腦的發數據給對方,但對方處理不過來,那么就會導致觸發重發機制,從而導致網絡流量的無端的浪費。

為了解決這種現象發生,TCP 提供一種機制可以讓「發送方」根據「接收方」的實際接收能力控制發送的數據量,這就是所謂的流量控制。

下面舉個栗子,為了簡單起見,假設以下場景:

  • 客戶端是接收方,服務端是發送方
  • 假設接收窗口和發送窗口相同,都為 200
  • 假設兩個設備在整個傳輸過程中都保持相同的窗口大小,不受外界影響

9bc35f5a0b782d34672d44142bb9f990.png


根據上圖的流量控制,說明下每個過程:

客戶端向服務端發送請求數據報文。這里要說明下,本次例子是把服務端作為發送方,所以沒有畫出服務端的接收窗口。

服務端收到請求報文后,發送確認報文和 80 字節的數據,于是可用窗口 Usable 減少為 120 字節,同時 SND.NXT 指針也向右偏移 80 字節后,指向 321,這意味著下次發送數據的時候,序列號是 321。

客戶端收到 80 字節數據后,于是接收窗口往右移動 80 字節,RCV.NXT 也就指向 321,這意味著客戶端期望的下一個報文的序列號是 321,接著發送確認報文給服務端。

服務端再次發送了 120 字節數據,于是可用窗口耗盡為 0,服務端無法在繼續發送數據。

客戶端收到 120 字節的數據后,于是接收窗口往右移動 120 字節,RCV.NXT 也就指向 441,接著發送確認報文給服務端。

服務端收到對 80 字節數據的確認報文后,SND.UNA 指針往右偏移后指向 321,于是可用窗口 Usable 增大到 80。

服務端收到對 120 字節數據的確認報文后,SND.UNA 指針往右偏移后指向 441,于是可用窗口 Usable 增大到 200。

服務端可以繼續發送了,于是發送了 160 字節的數據后,SND.NXT 指向 601,于是可用窗口 Usable 減少到 40。

客戶端收到 160 字節后,接收窗口往右移動了 160 字節,RCV.NXT 也就是指向了 601,接著發送確認報文給服務端。

服務端收到對 160 字節數據的確認報文后,發送窗口往右移動了 160 字節,于是 SND.UNA 指針偏移了 160 后指向 601,可用窗口 Usable 也就增大至了 200。

操作系統緩沖區與滑動窗口的關系
前面的流量控制例子,我們假定了發送窗口和接收窗口是不變的,但是實際上,發送窗口和接收窗口中所存放的字節數,都是放在操作系統內存緩沖區中的,而操作系統的緩沖區,會被操作系統調整。

當應用進程沒辦法及時讀取緩沖區的內容時,也會對我們的緩沖區造成影響。

那操心系統的緩沖區,是如何影響發送窗口和接收窗口的呢?

我們先來看看第一個例子。

當應用程序沒有及時讀取緩存時,發送窗口和接收窗口的變化。

考慮以下場景:

客戶端作為發送方,服務端作為接收方,發送窗口和接收窗口初始大小為 360;

服務端非常的繁忙,當收到客戶端的數據時,應用層不能及時讀取數據。

acc2c56108952eb59acafed41f753d5c.png


根據上圖的流量控制,說明下每個過程:

客戶端發送 140 字節數據后,可用窗口變為 220 (360 - 140)。

服務端收到 140 字節數據,但是服務端非常繁忙,應用進程只讀取了 40 個字節,還有 100 字節占用著緩沖區,于是接收窗口收縮到了 260 (360 - 100),最后發送確認信息時,將窗口大小通過給客戶端。

客戶端收到確認和窗口通告報文后,發送窗口減少為 260。

客戶端發送 180 字節數據,此時可用窗口減少到 80。

服務端收到 180 字節數據,但是應用程序沒有讀取任何數據,這 180 字節直接就留在了緩沖區,于是接收窗口收縮到了 80 (260 - 180),并在發送確認信息時,通過窗口大小給客戶端。

客戶端收到確認和窗口通告報文后,發送窗口減少為 80。

客戶端發送 80 字節數據后,可用窗口耗盡。

服務端收到 80 字節數據,但是應用程序依然沒有讀取任何數據,這 80 字節留在了緩沖區,于是接收窗口收縮到了 0,并在發送確認信息時,通過窗口大小給客戶端。

客戶端收到確認和窗口通告報文后,發送窗口減少為 0。

可見最后窗口都收縮為 0 了,也就是發生了窗口關閉。當發送方可用窗口變為 0 時,發送方實際上會定時發送窗口探測報文,以便知道接收方的窗口是否發生了改變,這個內容后面會說,這里先簡單提一下。

我們先來看看第二個例子。

當服務端系統資源非常緊張的時候,操心系統可能會直接減少了接收緩沖區大小,這時應用程序又無法及時讀取緩存數據,那么這時候就有嚴重的事情發生了,會出現數據包丟失的現象。

d0b2682371ce1c01541da6772163287d.png


說明下每個過程:

客戶端發送 140 字節的數據,于是可用窗口減少到了 220。

服務端因為現在非常的繁忙,操作系統于是就把接收緩存減少了 100 字節,當收到 對 140 數據確認報文后,又因為應用程序沒有讀取任何數據,所以 140 字節留在了緩沖區中,于是接收窗口大小從 360 收縮成了 100,最后發送確認信息時,通告窗口大小給對方。

此時客戶端因為還沒有收到服務端的通告窗口報文,所以不知道此時接收窗口收縮成了 100,客戶端只會看自己的可用窗口還有 220,所以客戶端就發送了 180 字節數據,于是可用窗口減少到 40。

服務端收到了 180 字節數據時,發現數據大小超過了接收窗口的大小,于是就把數據包丟失了。

客戶端收到第 2 步時,服務端發送的確認報文和通告窗口報文,嘗試減少發送窗口到 100,把窗口的右端向左收縮了 80,此時可用窗口的大小就會出現詭異的負值。

所以,如果發生了先減少緩存,再收縮窗口,就會出現丟包的現象。
為了防止這種情況發生,TCP 規定是不允許同時減少緩存又收縮窗口的,而是采用先收縮窗口,過段時間在減少緩存,這樣就可以避免了丟包情況。

窗口關閉

在前面我們都看到了,TCP 通過讓接收方指明希望從發送方接收的數據大小(窗口大小)來進行流量控制。

如果窗口大小為 0 時,就會阻止發送方給接收方傳遞數據,直到窗口變為非 0 為止,這就是窗口關閉。

窗口關閉潛在的危險

接收方向發送方通告窗口大小時,是通過 ACK 報文來通告的。

那么,當發生窗口關閉時,接收方處理完數據后,會向發送方通告一個窗口非 0 的 ACK 報文,如果這個通告窗口的 ACK 報文在網絡中丟失了,那麻煩就大了。

a032afe2403a9d6dd4e364f6acd85677.png


這會導致發送方一直等待接收方的非 0 窗口通知,接收方也一直等待發送方的數據,如不不采取措施,這種相互等待的過程,會造成了死鎖的現象。

TCP 是如何解決窗口關閉時,潛在的死鎖現象呢?

為了解決這個問題,TCP 為每個連接設有一個持續定時器,只要 TCP 連接一方收到對方的零窗口通知,就啟動持續計時器。

如果持續計時器超時,就會發送窗口探測 ( Window probe ) 報文,而對方在確認這個探測報文時,給出自己現在的接收窗口大小。

ea2a212a3ed0d8586fe9f48ec189405a.png


如果接收窗口仍然為 0,那么收到這個報文的一方就會重新啟動持續計時器;

如果接收窗口不是 0,那么死鎖的局面就可以被打破了。

窗口探查探測的次數一般為 3 此次,每次次大約 30-60 秒(不同的實現可能會不一樣)。如果 3 次過后接收窗口還是 0 的話,有的 TCP 實現就會發 RST 報文來中斷連接。

糊涂窗口綜合癥
如果接收方太忙了,來不及取走接收窗口里的數據,那么就會導致發送方的發送窗口越來越小。

到最后,如果接收方騰出幾個字節并告訴發送方現在有幾個字節的窗口,而發送方會義無反顧地發送這幾個字節,這就是糊涂窗口綜合癥。

要知道,我們的 TCP + IP 頭有 40 個字節,為了傳輸那幾個字節的數據,要達上這么大的開銷,這太不經濟了。

就好像一個可以承載 50 人的大巴車,每次來了一兩個人,就直接發車。除非家里有礦的大巴司機,才敢這樣玩,不然遲早破產。要解決這個問題也不難,大巴司機等乘客數量超過了 25 個,才認定可以發車。

現舉個糊涂窗口綜合癥的栗子,考慮以下場景:

接收方的窗口大小是 360 字節,但接收方由于某些原因陷入困境,假設接收方的應用層讀取的能力如下:

接收方每接收 3 個字節,應用程序就只能從緩沖區中讀取 1 個字節的數據;

在下一個發送方的 TCP 段到達之前,應用程序
還從緩沖區中讀取了 40 個額外的字節;

67b32e97cd73fa5e3ac3f26aade20e16.png


每個過程的窗口大小的變化,在圖中都描述的很清楚了,可以發現窗口不斷減少了,并且發送的數據都是比較小的了。

所以,糊涂窗口綜合癥的現象是可以發生在發送方和接收方:

接收方可以通告一個小的窗口

而發送方可以發送小數據

于是,要解決糊涂窗口綜合癥,就解決上面兩個問題就可以了

讓接收方不通告小窗口給發送方

讓發送方避免發送小數據

怎么讓接收方不通告小窗口呢?

接收方通常的策略如下:

當「窗口大小」小于 min( MSS,緩存空間/2 ) ,也就是小于 MSS 與 1/2 緩存大小中的最小值時,就會向發送方通告窗口為 0,也就阻止了發送方再發數據過來。

等到接收方處理了一些數據后,窗口大小 >= MSS,或者接收方緩存空間有一半可以使用,就可以把窗口打開讓發送方發送數據過來。

怎么讓發送方避免發送小數據呢?

發送方通常的策略:

使用 Nagle 算法,該算法的思路是延時處理,它滿足以下兩個條件中的一條才可以發送數據:

要等到窗口大小 >= MSS 或是 數據大小 >= MSS

收到之前發送數據的 ack 回包

只要沒滿足上面條件中的一條,發送方一直在囤積數據,直到滿足上面的發送條件。

另外,Nagle 算法默認是打開的,如果對于一些需要小數據包交互的場景的程序,比如,telnet 或 ssh 這樣的交互性比較強的程序,則需要關閉 Nagle 算法。

可以在 Socket 設置 TCP_NODELAY 選項來關閉這個算法(關閉 Nagle 算法沒有全局參數,需要根據每個應用自己的特點來關閉)

setsockopt(sock_fd, IPPROTO_TCP, TCP_NODELAY, (char *)&value, sizeof(int));

擁塞控制


為什么要有擁塞控制呀,不是有流量控制了嗎?

前面的流量控制是避免「發送方」的數據填滿「接收方」的緩存,但是并不知道網絡的中發生了什么。

一般來說,計算機網絡都處在一個共享的環境。因此也有可能會因為其他主機之間的通信使得網絡擁堵。

在網絡出現擁堵時,如果繼續發送大量數據包,可能會導致數據包時延、丟失等,這時 TCP 就會重傳數據,但是一重傳就會導致網絡的負擔更重,于是會導致更大的延遲以及更多的丟包,這個情況就會進入惡性循環被不斷地放大….

所以,TCP 不能忽略網絡上發生的事,它被設計成一個無私的協議,當網絡發送擁塞時,TCP 會自我犧牲,降低發送的數據量。

于是,就有了擁塞控制,控制的目的就是避免「發送方」的數據填滿整個網絡。

為了在「發送方」調節所要發送數據的量,定義了一個叫做「擁塞窗口」的概念。

什么是擁塞窗口?和發送窗口有什么關系呢?

擁塞窗口 cwnd是發送方維護的一個 的狀態變量,它會根據網絡的擁塞程度動態變化的。

我們在前面提到過發送窗口 swnd 和接收窗口 rwnd 是約等于的關系,那么由于入了擁塞窗口的概念后,此時發送窗口的值是swnd = min(cwnd, rwnd),也就是擁塞窗口和接收窗口中的最小值。

擁塞窗口 cwnd 變化的規則:

只要網絡中沒有出現擁塞,cwnd 就會增大;

但網絡中出現了擁塞,cwnd 就減少;

那么怎么知道當前網絡是否出現了擁塞呢?

其實只要「發送方」沒有在規定時間內接收到 ACK 應答報文,也就是發生了超時重傳,就會認為網絡出現了用擁塞。

擁塞控制有哪些控制算法?

擁塞控制主要是四個算法:

慢啟動

擁塞避免

擁塞發生

快速恢復

慢啟動
TCP 在剛建立連接完成后,首先是有個慢啟動的過程,這個慢啟動的意思就是一點一點的提高發送數據包的數量,如果一上來就發大量的數據,這不是給網絡添堵嗎?

慢啟動的算法記住一個規則就行:當發送方每收到一個 ACK,就擁塞窗口 cwnd 的大小就會加 1。

這里假定擁塞窗口 cwnd 和發送窗口 swnd 相等,下面舉個栗子:

連接建立完成后,一開始初始化 cwnd = 1,表示可以傳一個 MSS 大小的數據。

當收到一個 ACK 確認應答后,cwnd 增加 1,于是一次能夠發送 2 個

當收到 2 個的 ACK 確認應答后, cwnd 增加 2,于是就可以比之前多發2 個,所以這一次能夠發送 4 個

當這 4 個的 ACK 確認到來的時候,每個確認 cwnd 增加 1, 4 個確認 cwnd 增加 4,于是就可以比之前多發 4 個,所以這一次能夠發送 8 個。

e3c32823486a5bf2c1548fcc70af1731.png


可以看出慢啟動算法,發包的個數是指數性的增長。

那慢啟動漲到什么時候是個頭呢?

有一個叫慢啟動門限 ssthresh (slow start threshold)狀態變量。

當 cwnd < ssthresh 時,使用慢啟動算法。

當 cwnd >= ssthresh 時,就會使用「擁塞避免算法」。

擁塞避免算法
前面說道,當擁塞窗口 cwnd 「超過」慢啟動門限 ssthresh 就會進入擁塞避免算法。

一般來說 ssthresh 的大小是 65535 字節。

那么進入擁塞避免算法后,它的規則是:每當收到一個 ACK 時,cwnd 增加 1/cwnd。

接上前面的慢啟動的栗子,現假定 ssthresh 為 8:

當 8 個 ACK 應答確認到來時,每個確認增加 1/8,8 個 ACK 確認 cwnd 一共增加 1,于是這一次能夠發送 9 個 MSS 大小的數據,變成了線性增長。

bb8ddc98313b364938cbc584110fad5e.png


所以,我們可以發現,擁塞避免算法就是將原本慢啟動算法的指數增長變成了線性增長,還是增長階段,但是增長速度緩慢了一些。

就這么一直增長著后,網絡就會慢慢進入了擁塞的狀況了,于是就會出現丟包現象,這時就需要對丟失的數據包進行重傳。

當觸發了重傳機制,也就進入了「擁塞發生算法」。

擁塞發生
當網絡出現擁塞,也就是會發生數據包重傳,重傳機制主要有兩種:

超時重傳

快速重傳

這兩種使用的擁塞發送算法是不同的,接下來分別來說說。

發生超時重傳的擁塞發生算法

當發生了「超時重傳」,則就會使用擁塞發生算法。

這個時候,sshresh 和 cwnd 的值會發生變化:

ssthresh 設為 cwnd/2,

cwnd 重置為 1

a6a42e24570f2da6725b2a5319647ec5.png


接著,就重新開始慢啟動,慢啟動是會突然減少數據流的。這真是一旦「超時重傳」,馬上回到解放前。但是這種方式太激進了,反應也很強烈,會造成網絡卡頓。

就好像本來在秋名山高速漂移著,突然來個緊急剎車,輪胎受得了嗎。。。

發生快速重傳的擁塞發生算法

還有更好的方式,前面我們講過「快速重傳算法」。當接收方發現丟了一個中間包的時候,發送三次前一個包的 ACK,于是發送端就會快速地重傳,不必等待超時再重傳。

TCP 認為這種情況不嚴重,因為大部分沒丟,只丟了一小部分,則 ssthresh 和 cwnd 變化如下:

cwnd = cwnd/2 ,也就是設置為原來的一半;

ssthresh = cwnd;

進入快速恢復算法

快速恢復
快速重傳和快速恢復算法一般同時使用,快速恢復算法是認為,你還能收到 3 個重復 ACK 說明網絡也不那么糟糕,所以沒有必要像 RTO 超時那么強烈。

正如前面所說,進入快速恢復之前,cwnd 和 ssthresh 已被更新了:

cwnd = cwnd/2 ,也就是設置為原來的一半;

ssthresh = cwnd;

然后,進入快速恢復算法如下:

擁塞窗口 cwnd = ssthresh + 3 ( 3 的意思是確認有 3 個數據包被收到了)

重傳丟失的數據包

如果再收到重復的 ACK,那么 cwnd 增加 1

如果收到新數據的 ACK 后,設置 cwnd 為 ssthresh,接著就進入了擁塞避免算法

1500b2ba3b3388b8ed505198328b2f2b.png

也就是沒有像「超時重傳」一夜回到解放前,而是還在比較高的值,后續呈線性增長。

參考:https://mp.weixin.qq.com/s?__biz=MzAxODQxMDM0Mw==&mid=2247485167&idx=2&sn=19facbf79be561aee497e36d61d4c3a3&chksm=9bd7f8e7aca071f1d0330bf4aa8850e6ac050da7aad2af5a1b609e6a9c330b5b9c710ca6ddde&scene=21#wechat_redirect

https://mp.weixin.qq.com/s/xe3dEu17mGTqM46LRFxzhg

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/457686.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/457686.shtml
英文地址,請注明出處:http://en.pswp.cn/news/457686.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

使用Servlet實現用戶注冊

1、用戶注冊頁面代碼 <% page language"java" contentType"text/html; charsetUTF-8"pageEncoding"UTF-8"%><!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd&q…

年輕人的第一篇V語言筆記

V語言極限學習 我聽說V語言看文檔半小時就能完全掌握&#xff1f;&#xff1f;&#xff1f;&#xff1f;以我的智商一小時掌握不了我就給各位科普一下廣告法&#xff1f;&#xff1f;&#xff1f; 宇宙慣例hello world // first v code fn main(){printIn("hello world…

android studio運行手機時出錯怎么解決_小程序 android ios h5解決方案

你現在開發android,ios,小程序用什么工具&#xff0c;怎么開發的&#xff1f;還在單個端的開發嗎&#xff1f;今天我們主要討論的是一次開發多端使用的技術&#xff0c;也是這兩年比較流行的開發方向。現在的終端太多了&#xff0c;app兩個端android和ios,小程序有微信&#xf…

Android SDK上手指南:應用程序數據

版權聲明&#xff1a;本文為博主原創文章&#xff0c;轉載請標明出處。 https://blog.csdn.net/chaoyu168/article/details/52996965 在本系列教程當中&#xff0c;我們將學習如何從零開始進行Android SDK開發。我們已經熟悉了Android應用程序的結構與基本組成元素&#xff0c;…

設計微服務架構需要解決的問題

問題&#xff1a; 劃分服務的原則是什么服務之間選擇何種輕量級的通信協議如何做到服務的獨立部署如何確定使用何種編程語言?控制多語言帶來的復雜度如何做到服務的去中心化如何解決大量微服務引入的運維成本轉載于:https://www.cnblogs.com/fight-tao/p/5641286.html

Django Model設計詳解

Django Model 設計 Django Model設計是Django五項基礎核心設計之一&#xff08;Model設計&#xff0c;URL配置&#xff0c;View編寫&#xff0c;Template設計&#xff0c;From使用&#xff09;&#xff0c;也是MVC模式中重要的環節。 如果圖片無法訪問&#xff0c;大家可以移…

python設置全局變量失敗_Python全局變量與global關鍵字常見錯誤解決方案

在Python的變量使用中&#xff0c;經常會遇到這樣的錯誤:local variable a referenced before assignment它的意思是&#xff1a;局部變量“a”在賦值前就被引用了。比如運行下面的代碼就會出現這樣的問題&#xff1a;a 3def Fuc():print (a)a a 1Fuc()? 但是如果把 a a …

Atititi tesseract使用總結

Atititi tesseract使用總結 消除bug&#xff0c;優化&#xff0c;重新發布。當前版本為3.02 項目下載地址為&#xff1a;http://code.google.com/p/tesseract-ocr。 Windows cmd命令行使用Tesseract-OCR引擎識別驗證碼: 1、下載安裝Tesseract-OCR引擎(3.0版本才支持中文識別) t…

Javascipt數組去重的幾種方式

方法一 function unique(arr) {var retArr [];for (var i 0; i < arr.length; i) {(retArr.indexOf(arr[i]) -1) && retArr.push(arr[i]);}return retArr; } 方法二 function unique(arr) {return arr.filter(function(item, index, array) {return array.indexO…

01_JS語法

JS語法 嚴格區分大小寫以;結尾&#xff0c;不寫瀏覽器會自動加&#xff0c;但不準確&#xff0c;且會占用瀏覽器資源自動忽略多個空格和換行 寫在哪 所有JS代碼都必須依托網頁運行 內嵌 寫在html的script標簽中 <script>// JS代碼 </script>事件 寫在某個ht…

pythonwhile循環love_python基礎之while循環及編碼

while 條件&#xff1a;循環體死循環&#xff1a;沒有終止條件(修改方法&#xff1a;1.改變條件2.使用break)break 終止當前循環contiune&#xff1a;跳出本次循環&#xff0c;繼續下次循環break和contione必須在循環體里while 條件&#xff1a;循環體else&#xff1a;結果當wh…

css頁面布局

居中布局 水平居中 父元素和子元素的寬度都未知 inline-block text-ailgn .child{display:inline-block;} .parent{text-align:center;} 優點&#xff1a;兼容性好 缺點&#xff1a;子元素文本繼承了text-align屬性&#xff0c;子元素要額外加text-align:left; table ma…

02_JS變量

JS變量 字面量 常量&#xff0c;不可變量 變量 變量用 var 變量名聲明 命名 變量命名以數字字母下劃線和$組成&#xff0c;不能以數字開頭&#xff0c;還可以是utf-8的任意字符&#xff0c;包括中文&#xff0c;一般采用駝峰命名法 常用的幾個函數 alert():瀏覽器彈窗d…

Rotate String

Given a string and an offset, rotate string by offset. (rotate from left to right) Example Given "abcdefg". offset0 > "abcdefg" offset1 > "gabcdef" offset2 > "fgabcde" offset3 > "efgabcd"分析&am…

音視頻播放、錄音、拍照

音頻 在iOS中音頻播放從形式上可以分為音效播放和音樂播放。前者主要指的是一些短音頻播放&#xff0c;通常作為點綴音頻&#xff0c;對于這類音頻不需要進行進度、循環等控制。后者指的是一些較長的音頻&#xff0c;通常是主音頻&#xff0c;對于這些音頻的播放通常需要進行精…

python 遞歸函數與循環的區別_提升Python效率之使用循環機制代替遞歸函數

斐波那契數列當年&#xff0c;典型的遞歸題目&#xff0c;斐波那契數列還記得嗎&#xff1f;def fib(n):if n1 or n2:return 1else:return fib(n-1)fib(n-2)當然, 為了程序健壯性&#xff0c;加上try...except...def fib(n):if isinstance(n, int):print(兄弟,輸入正整數哈)ret…

03_JS數據類型

JS數據類型 基本數據類型 String 字符串類型&#xff0c;申明時用單引號或雙引號引起來&#xff0c;兩種引號不可嵌套&#xff0c;不可混用 Number 數值型&#xff0c;有兩個特殊的數字 Infint:無窮大NaN&#xff1a;非數值型數字&#xff0c;不與任何類型相等 Boolean …

7.5

姓名 崔巍 時間 2016年7月5日 學習內容 最后一次確定同步控制力度等實現細節。 學習了Visual Studio C#軟件測試方面的工具。鞏固了等價類黑盒測試方法的相關理論&#xff0c;并且學習了集成測試、回歸測試的相關內容&#xff0c;并進行了測試。 集成測試&#xff0c;…

python scratch ev3_如何在scratch上連接樂高ev3?

樂高教育的官網有關于EV3使用Python的詳細介紹https://education.lego.com/zh-cn/support/mindstorms-ev3/python-for-ev3?education.lego.com來自網易有道Scratch是現在小朋友們最熱的編程工具&#xff0c;也是各學校和培訓機構對小學生編程的入門首選。網易有道Kada平臺是一…

04_JS運算符

JS運算符 一元運算符 -,正負號&#xff0c;對非數值類型做正負操作會先轉換成數值型&#xff0c;可以用快速進行類型轉換 邏輯運算符 且 &&&#xff0c;從左到右看&#xff0c;一旦返現值為false的表達式立刻返回false&#xff0c;全真為真或 ||&#xff0c;從左到右…