實戰!我用「大白鯊」讓你看見 TCP,TCP三次握手四次揮手!

顯形「不可見」的網絡包

網絡世界中的數據表交互咱們肉眼是看不見的,它們就好像隱形了同樣,咱們對着課本學習計算機網絡的時候就會以爲很是的抽象,加大了學習的難度。web

還別說,我本身在大學的時候,也是如此。算法

直到工做後,認識了兩大分析網絡的利器:tcpdump 和 Wireshark,這兩大利器把咱們「看不見」的數據表,呈如今咱們眼前,一目瞭然。apache

唉,當初大學學習計網的時候,要是能知道這兩個工具,就不會學的一臉懵逼。緩存

tcpdump 和 Wireshark 有什麼區別?

tcpdump 和 Wireshark 就是最經常使用的網絡抓包和分析工具,更是分析網絡性能必不可少的利器。服務器

  • tcpdump 僅支持命令行格式使用,經常使用在 Linux 服務器中抓取和分析網絡包。
  • Wireshark 除了能夠抓包外,還提供了可視化分析網絡包的圖形頁面。

因此,這二者其實是搭配使用的,先用 tcpdump 命令在 Linux 服務器上抓包,接着把抓包的文件拖出到 Windows 電腦後,用 Wireshark 可視化分析。網絡

固然,若是你是在 Windows 上抓包,只須要用 Wireshark 工具就能夠。ssh

tcpdump 在 Linux 下如何抓包?

tcpdump 提供了大量的選項以及各式各樣的過濾表達式,來幫助你抓取指定的數據包,不過不要擔憂,只須要掌握一些經常使用選項和過濾表達式,就能夠知足大部分場景的須要了。curl

假設咱們要抓取下面的 ping 的數據包:tcp

要抓取上面的 ping 命令數據包,首先咱們要知道 ping 的數據包是 icmp協議,接着在使用 tcpdump 抓包的時候,就能夠指定只抓 icmp 協議的數據表:工具

那麼當 tcpdump 抓取到 icmp 數據包後, 輸出格式以下:

從 tcpdump 抓取的 icmp 數據包,咱們很清楚的看到 icmp echo 的交互過程了,首先發送方發起了 ICMP echo request 請求報文,接收方收到後回了一個 ICMP echo reply 相應報文,以後 seq 是遞增的。

我在這裏也幫你整理了一些最多見的用法,而且繪製成了表格,你能夠參考使用。

首先,先來看看經常使用的選項類,在上面的 ping 例子中,咱們用過 -i 選項指定網口,用過 -nn 選項不對 IP 地址和端口名稱解析。其它經常使用的選項,以下表格:

tcpdump 經常使用選項類

接下來,咱們再來看看經常使用的過濾表用法,在上面的 ping 例子中,咱們用過的是 icmp and host 183.232.231.174,表示抓取 icmp 協議的數據包,以及源地址或目標地址爲 183.232.231.174 的包。其餘經常使用的過濾選項,我也整理成了下面這個表格。

tcpdump 經常使用過濾表達式類

說了這麼多,你應該也發現了,tcpdump 雖然功能強大,可是輸出的格式並不直觀。

因此,在工做中 tcpdump 只是用來抓取數據包,不用來分析數據包,而是把 tcpdump 抓取的數據包保存成 pcap 後綴的文件,接着用 Wireshark 工具進行數據包分析。

Wireshark 工具如何分析數據包?

Wireshark 除了能夠抓包外,還提供了可視化分析網絡包的圖形頁面,同時,還內置了一系列的彙總分析工具。

好比,拿上面的 ping 例子來講,咱們可使用下面的命令,把抓取的數據包保存到 ping.pcap 文件

接着把 ping.pcap 文件拖到電腦,再用 Wireshark 打開它。打開後,你就能夠看到下面這個界面:

是吧?在 Wireshark 的頁面裏,能夠更加直觀的分析數據包,不只展現各個網絡包的頭部信息,還會用不一樣的顏色來區分不一樣的協議,因爲此次抓包只有 ICMP 協議,因此只有紫色的條目。

接着,在網絡包列表中選擇某一個網絡包後,在其下面的網絡包詳情中,能夠更清楚的看到,這個網絡包在協議棧各層的詳細信息。好比,以編號 1 的網絡包爲例子:

ping 網絡包

  • 能夠在數據鏈路層,看到 MAC 包頭信息,如源 MAC 地址和目標 MAC 地址等字段;
  • 能夠在 IP 層,看到 IP 包頭信息,如源 IP 地址和目標 IP 地址、TTL、IP 包長度、協議等 IP 協議各個字段的數值和含義;
  • 能夠在 ICMP 層,看到 ICMP 包頭信息,好比 Type、Code 等 ICMP 協議各個字段的數值和含義;

Wireshark 用了分層的方式,展現了各個層的包頭信息,把「不可見」的數據包,清清楚楚的展現了給咱們,還有理由學很差計算機網絡嗎?是不是相見恨晚

從 ping 的例子中,咱們能夠看到網絡分層就像有序的分工,每一層都有本身的責任範圍和信息,上層協議完成工做後就交給下一層,最終造成一個完整的網絡包。

解密 TCP 三次握手和四次揮手

既然學會了 tcpdump 和 Wireshark 兩大網絡分析利器,那咱們馬不停蹄,接下用它倆抓取和分析 HTTP 協議網絡包,並理解 TCP 三次握手和四次揮手的工做原理。

本次例子,咱們將要訪問的 http://192.168.3.200 服務端。在終端一用 tcpdump 命令抓取數據包:

接着,在終端二執行下面的 curl 命令:

最後,回到終端一,按下 Ctrl+C 中止 tcpdump,並把獲得的 http.pcap 取出到電腦。

使用 Wireshark 打開 http.pcap 後,你就能夠在 Wireshark 中,看到以下的界面:

HTTP 網絡包

咱們都知道 HTTP 是基於 TCP 協議進行傳輸的,那麼:

  • 最開始的 3 個包就是 TCP 三次握手創建鏈接的包
  • 中間是 HTTP 請求和響應的包
  • 而最後的 3 個包則是 TCP 斷開鏈接的揮手包

Wireshark 能夠用時序圖的方式顯示數據包交互的過程,從菜單欄中,點擊 統計 (Statistics) -> 流量圖 (Flow Graph),而後,在彈出的界面中的「流量類型」選擇 「TCP Flows」,你能夠更清晰的看到,整個過程當中 TCP 流的執行過程:

TCP 流量圖

你可能會好奇,爲何三次握手鍊接過程的 Seq 是 0 ?

其實是由於 Wireshark 工具幫咱們作了優化,它默認顯示的是序列號 seq 是相對值,而不是真實值。

若是你想看到實際的序列號的值,能夠右鍵菜單, 而後找到「協議首選項」,接着找到「Relative Seq」後,把它給取消,操做以下:

取消序列號相對值顯示

取消後,Seq 顯示的就是真實值了:

TCP 流量圖

可見,客戶端和服務端的序列號其實是不一樣的,序列號是一個隨機值。

這其實跟咱們書上看到的 TCP 三次握手和四次揮手很相似,做爲對比,你一般看到的 TCP 三次握手和四次揮手的流程,基本是這樣的:

TCP 三次握手和四次揮手的流程

爲何抓到的 TCP 揮手是三次,而不是書上說的四次?

由於服務器端收到客戶端的 FIN 後,服務器端同時也要關閉鏈接,這樣就能夠把 ACK 和 FIN 合併到一塊兒發送,節省了一個包,變成了「三次揮手」。

而一般狀況下,服務器端收到客戶端的 FIN 後,極可能還沒發送完數據,因此就會先回復客戶端一個 ACK 包,稍等一下子,完成全部數據包的發送後,纔會發送 FIN 包,這也就是四次揮手了。

以下圖,就是四次揮手的過程:

四次揮手

TCP 三次握手異常狀況實戰分析

TCP 三次握手的過程相信你們都背的倒背如流,那麼你有沒有想過這三個異常狀況:

  • TCP 第一次握手的 SYN 丟包了,會發生了什麼?
  • TCP 第二次握手的 SYN、ACK 丟包了,會發生什麼?
  • TCP 第三次握手的 ACK 包丟了,會發生什麼?

有的小夥伴可能說:「很簡單呀,包丟了就會重傳嘛。」

那我在繼續問你:

  • 那會重傳幾回?
  • 超時重傳的時間 RTO 會如何變化?
  • 在 Linux 下如何設置重傳次數?
  • ….

是否是啞口無言,沒法回答?

不知道不要緊,接下里我用三個實驗案例,帶你們一塊兒探究探究這三種異常。

實驗場景

本次實驗用了兩臺虛擬機,一臺做爲服務端,一臺做爲客戶端,它們的關係以下:

實驗環境

  • 客戶端和服務端都是 CentOs 6.5 Linux,Linux 內核版本 2.6.32
  • 服務端 192.168.12.36,apache web 服務
  • 客戶端 192.168.12.37

實驗一:TCP 第一次握手 SYN 丟包

爲了模擬 TCP 第一次握手 SYN 丟包的狀況,我是在拔掉服務器的網線後,馬上在客戶端執行 curl 命令:

其間 tcpdump 抓包的命令以下:

過了一會, curl 返回了超時鏈接的錯誤:

從 date 返回的時間,能夠發如今超時接近 1 分鐘的時間後,curl 返回了錯誤。

接着,把 tcp_sys_timeout.pcap 文件用 Wireshark 打開分析,顯示以下圖:

SYN 超時重傳五次

從上圖能夠發現, 客戶端發起了 SYN 包後,一直沒有收到服務端的 ACK ,因此一直超時重傳了 5 次,而且每次 RTO 超時時間是不一樣的:

  • 第一次是在 1 秒超時重傳
  • 第二次是在 3 秒超時重傳
  • 第三次是在 7 秒超時重傳
  • 第四次是在 15 秒超時重傳
  • 第五次是在 31 秒超時重傳

能夠發現,每次超時時間 RTO 是指數(翻倍)上漲的,當超過最大重傳次數後,客戶端再也不發送 SYN 包。

在 Linux 中,第一次握手的 SYN 超時重傳次數,是以下內核參數指定的:

$ cat /proc/sys/net/ipv4/tcp_syn_retries
5

tcp_syn_retries 默認值爲 5,也就是 SYN 最大重傳次數是 5 次。

接下來,咱們繼續作實驗,把 tcp_syn_retries 設置爲 2 次:

$ echo 2 > /proc/sys/net/ipv4/tcp_syn_retries

重傳抓包後,用 Wireshark 打開分析,顯示以下圖:

SYN 超時重傳兩次

實驗一的實驗小結

經過實驗一的實驗結果,咱們能夠得知,當客戶端發起的 TCP 第一次握手 SYN 包,在超時時間內沒收到服務端的 ACK,就會在超時重傳 SYN 數據包,每次超時重傳的 RTO 是翻倍上漲的,直到 SYN 包的重傳次數到達tcp_syn_retries 值後,客戶端再也不發送 SYN 包。

SYN 超時重傳

實驗二:TCP 第二次握手 SYN、ACK 丟包

爲了模擬客戶端收不到服務端第二次握手 SYN、ACK 包,個人作法是在客戶端加上防火牆限制,直接粗暴的把來自服務端的數據都丟棄,防火牆的配置以下:

接着,在客戶端執行 curl 命令:

從 date 返回的時間先後,能夠算出大概 1 分鐘後,curl 報錯退出了。

客戶端在這其間抓取的數據包,用 Wireshark 打開分析,顯示的時序圖以下:

從圖中能夠發現:

  • 客戶端發起 SYN 後,因爲防火牆屏蔽了服務端的全部數據包,因此 curl 是沒法收到服務端的 SYN、ACK 包,當發生超時後,就會重傳 SYN 包
  • 服務端收到客戶的 SYN 包後,就會回 SYN、ACK 包,可是客戶端一直沒有回 ACK,服務端在超時後,重傳了 SYN、ACK 包,接着一會,客戶端超時重傳的 SYN 包又抵達了服務端,服務端收到後,超時定時器就從新計時,而後回了 SYN、ACK 包,因此至關於服務端的超時定時器只觸發了一次,又被重置了。
  • 最後,客戶端 SYN 超時重傳次數達到了 5 次(tcp_syn_retries 默認值 5 次),就再也不繼續發送 SYN 包了。

因此,咱們能夠發現,當第二次握手的 SYN、ACK 丟包時,客戶端會超時重發 SYN 包,服務端也會超時重傳 SYN、ACK 包。

咦?客戶端設置了防火牆,屏蔽了服務端的網絡包,爲何 tcpdump 還能抓到服務端的網絡包?

添加 iptables 限制後, tcpdump 是否能抓到包 ,這要看添加的 iptables 限制條件:

  • 若是添加的是 INPUT 規則,則能夠抓獲得包
  • 若是添加的是 OUTPUT 規則,則抓不到包

網絡包進入主機後的順序以下:

  • 進來的順序 Wire -> NIC -> tcpdump -> netfilter/iptables
  • 出去的順序 iptables -> tcpdump -> NIC -> Wire
tcp_syn_retries 是限制 SYN 重傳次數,那第二次握手 SYN、ACK 限制最大重傳次數是多少?

TCP 第二次握手 SYN、ACK 包的最大重傳次數是經過tcp_synack_retries 內核參數限制的,其默認值以下:

$ cat /proc/sys/net/ipv4/tcp_synack_retries
5

是的,TCP 第二次握手 SYN、ACK 包的最大重傳次數默認值是 5 次。

爲了驗證 SYN、ACK 包最大重傳次數是 5 次,咱們繼續作下實驗,咱們先把客戶端的 tcp_syn_retries 設置爲 1,表示客戶端 SYN 最大超時次數是 1 次,目的是爲了防止屢次重傳 SYN,把服務端 SYN、ACK 超時定時器重置。

接着,仍是如上面的步驟:

  1. 客戶端配置防火牆屏蔽服務端的數據包
  2. 客戶端 tcpdump 抓取 curl 執行時的數據包

把抓取的數據包,用 Wireshark 打開分析,顯示的時序圖以下:

從上圖,咱們能夠分析出:

  • 客戶端的 SYN 只超時重傳了 1 次,由於 tcp_syn_retries 值爲 1
  • 服務端應答了客戶端超時重傳的 SYN 包後,因爲一直收不到客戶端的 ACK 包,因此服務端一直在超時重傳 SYN、ACK 包,每次的 RTO 也是指數上漲的,一共超時重傳了 5 次,由於 tcp_synack_retries 值爲 5

接着,我把 tcp_synack_retries 設置爲 2,tcp_syn_retries 依然設置爲 1:

$ echo 2 > /proc/sys/net/ipv4/tcp_synack_retries
$ echo 1 > /proc/sys/net/ipv4/tcp_syn_retries

依然保持同樣的實驗步驟進行操做,接着把抓取的數據包,用 Wireshark 打開分析,顯示的時序圖以下:

可見:

  • 客戶端的 SYN 包只超時重傳了 1 次,符合 tcp_syn_retries 設置的值;
  • 服務端的 SYN、ACK 超時重傳了 2 次,符合 tcp_synack_retries 設置的值
實驗二的實驗小結

經過實驗二的實驗結果,咱們能夠得知,當 TCP 第二次握手 SYN、ACK 包丟了後,客戶端 SYN 包會發生超時重傳,服務端 SYN、ACK 也會發生超時重傳。

客戶端 SYN 包超時重傳的最大次數,是由 tcp_syn_retries 決定的,默認值是 5 次;服務端 SYN、ACK 包時重傳的最大次數,是由 tcp_synack_retries 決定的,默認值是 5 次。

實驗三:TCP 第三次握手 ACK 丟包

爲了模擬 TCP 第三次握手 ACK 包丟,個人實驗方法是在服務端配置防火牆,屏蔽客戶端 TCP 報文中標誌位是 ACK 的包,也就是當服務端收到客戶端的 TCP ACK 的報文時就會丟棄,iptables 配置命令以下:

接着,在客戶端執行以下 tcpdump 命令:

而後,客戶端向服務端發起 telnet,由於 telnet 命令是會發起 TCP 鏈接,因此用此命令作測試:

此時,因爲服務端收不到第三次握手的 ACK 包,因此一直處於SYN_RECV 狀態:

而客戶端是已完成 TCP 鏈接創建,處於 ESTABLISHED 狀態:

過了 1 分鐘後,觀察發現服務端的 TCP 鏈接不見了:

過了 30 分別,客戶端依然仍是處於 ESTABLISHED 狀態:

接着,在剛纔客戶端創建的 telnet 會話,輸入 123456 字符,進行發送:

持續「好長」一段時間,客戶端的 telnet 才斷開鏈接:

以上就是本次的實現三的現象,這裏存在兩個疑點:

  • 爲何服務端本來處於 SYN_RECV 狀態的鏈接,過 1 分鐘後就消失了?
  • 爲何客戶端 telnet 輸入 123456 字符後,過了好長一段時間,telnet 才斷開鏈接?

不着急,咱們把剛抓的數據包,用 Wireshark 打開分析,顯示的時序圖以下:

上圖的流程:

  • 客戶端發送 SYN 包給服務端,服務端收到後,回了個 SYN、ACK 包給客戶端,此時服務端的 TCP 鏈接處於 SYN_RECV 狀態;
  • 客戶端收到服務端的  SYN、ACK 包後,給服務端回了個 ACK 包,此時客戶端的 TCP 鏈接處於 ESTABLISHED 狀態;
  • 因爲服務端配置了防火牆,屏蔽了客戶端的 ACK 包,因此服務端一直處於 SYN_RECV 狀態,沒有進入  ESTABLISHED 狀態,tcpdump 之因此能抓到客戶端的 ACK 包,是由於數據包進入系統的順序是先進入 tcpudmp,後通過 iptables;
  • 接着,服務端超時重傳了 SYN、ACK 包,重傳了 5 次後,也就是超過 tcp_synack_retries 的值(默認值是 5),而後就沒有繼續重傳了,此時服務端的 TCP 鏈接主動停止了,因此剛纔處於 SYN_RECV 狀態的 TCP 鏈接斷開了,而客戶端依然處於ESTABLISHED 狀態;
  • 雖然服務端 TCP 斷開了,但過了一段時間,發現客戶端依然處於ESTABLISHED 狀態,因而就在客戶端的 telnet 會話輸入了 123456 字符;
  • 此時因爲服務端已經斷開鏈接,客戶端發送的數據報文,一直在超時重傳,每一次重傳,RTO 的值是指數增加的,因此持續了好長一段時間,客戶端的 telnet 才報錯退出了,此時共重傳了 15 次。

經過這一波分析,剛纔的兩個疑點已經解除了:

  • 服務端在重傳 SYN、ACK 包時,超過了最大重傳次數tcp_synack_retries,因而服務端的 TCP 鏈接主動斷開了。
  • 客戶端向服務端發送數據包時,因爲服務端的 TCP 鏈接已經退出了,因此數據包一直在超時重傳,共重傳了 15 次, telnet 就 斷開了鏈接。
TCP 第一次握手的 SYN 包超時重傳最大次數是由 tcp_syn_retries 指定,TCP 第二次握手的 SYN、ACK 包超時重傳最大次數是由 tcp_synack_retries 指定,那 TCP 創建鏈接後的數據包最大超時重傳次數是由什麼參數指定呢?

TCP 創建鏈接後的數據包傳輸,最大超時重傳次數是由 tcp_retries2 指定,默認值是 15 次,以下:

$ cat /proc/sys/net/ipv4/tcp_retries2
15

若是 15 次重傳都作完了,TCP 就會告訴應用層說:「搞不定了,包怎麼都傳不過去!」

那若是客戶端不發送數據,何時纔會斷開處於 ESTABLISHED 狀態的鏈接?

這裏就須要提到 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 秒才能夠發現一個「死亡」鏈接。

這個時間是有點長的,因此若是我抓包足夠久,或許能抓到探測報文。

實驗三的實驗小結

在創建 TCP 鏈接時,若是第三次握手的 ACK,服務端沒法收到,則服務端就會短暫處於 SYN_RECV 狀態,而客戶端會處於 ESTABLISHED 狀態。

因爲服務端一直收不到 TCP 第三次握手的 ACK,則會一直重傳 SYN、ACK 包,直到重傳次數超過 tcp_synack_retries 值(默認值 5 次)後,服務端就會斷開 TCP 鏈接。

而客戶端則會有兩種狀況:

  • 若是客戶端沒發送數據包,一直處於 ESTABLISHED 狀態,而後通過 2 小時 11 分 15 秒才能夠發現一個「死亡」鏈接,因而客戶端鏈接就會斷開鏈接。
  • 若是客戶端發送了數據包,一直沒有收到服務端對該數據包的確認報文,則會一直重傳該數據包,直到重傳次數超過 tcp_retries2 值(默認值 15 次)後,客戶端就會斷開 TCP 鏈接。
    • *

TCP 快速創建鏈接

客戶端在向服務端發起 HTTP GET 請求時,一個完整的交互過程,須要 2.5 個 RTT 的時延。

因爲第三次握手是能夠攜帶數據的,這時若是在第三次握手發起 HTTP GET 請求,須要 2 個 RTT 的時延。

可是在下一次(不是同個 TCP 鏈接的下一次)發起 HTTP GET 請求時,經歷的 RTT 也是同樣,以下圖:

常規 HTTP 請求

在 Linux 3.7 內核版本中,提供了 TCP Fast Open 功能,這個功能能夠減小 TCP 鏈接創建的時延。

常規 HTTP 請求 與 Fast  Open HTTP 請求

  • 在第一次創建鏈接的時候,服務端在第二次握手產生一個 Cookie (已加密)並經過 SYN、ACK 包一塊兒發給客戶端,因而客戶端就會緩存這個Cookie,因此第一次發起 HTTP Get 請求的時候,仍是須要 2 個 RTT 的時延;
  • 在下次請求的時候,客戶端在 SYN 包帶上 Cookie 發給服務端,就提早能夠跳過三次握手的過程,由於 Cookie 中維護了一些信息,服務端能夠從 Cookie 獲取 TCP 相關的信息,這時發起的 HTTP GET 請求就只須要 1 個 RTT 的時延;

注:客戶端在請求並存儲了 Fast Open Cookie 以後,能夠不斷重複 TCP Fast Open 直至服務器認爲 Cookie 無效(一般爲過時)

在 Linux 上如何打開 Fast Open 功能?

能夠經過設置 net.ipv4.tcp_fastopn 內核參數,來打開 Fast Open 功能。

net.ipv4.tcp_fastopn 各個值的意義:

  • 0 關閉
  • 1 做爲客戶端使用 Fast Open 功能
  • 2 做爲服務端使用 Fast Open 功能
  • 3 不管做爲客戶端仍是服務器,均可以使用 Fast Open 功能
TCP Fast Open 抓包分析

在下圖,數據包 7 號,客戶端發起了第二次 TCP 鏈接時,SYN 包會攜帶 Cooike,而且有長度爲 5 的數據。

服務端收到後,校驗 Cooike 合法,因而就回了 SYN、ACK 包,而且確認應答收到了客戶端的數據包,ACK = 5 + 1 = 6

TCP Fast Open 抓包分析

TCP 重複確認和快速重傳

當接收方收到亂序數據包時,會發送重複的 ACK,以使告知發送方要重發該數據包,當發送方收到 3 個重複 ACK 時,就會觸發快速重傳,立該重發丟失數據包。

快速重傳機制

TCP 重複確認和快速重傳的一個案例,用 Wireshark 分析,顯示以下:

  • 數據包 1 指望的下一個數據包 Seq 是 1,可是數據包 2 發送的 Seq 倒是 10945,說明收到的是亂序數據包,因而回了數據包 3 ,仍是一樣的 Seq = 1,Ack = 1,這代表是重複的 ACK;
  • 數據包 4 和 6 依然是亂序的數據包,因而依然回了重複的 ACK;
  • 當對方收到三次重複的 ACK 後,因而就快速重傳了 Seq = 1 、Len = 1368 的數據包 8;
  • 當收到重傳的數據包後,發現 Seq = 1 是指望的數據包,因而就發送了確認報文 ACK;

注意:快速重傳和重複 ACK 標記信息是 Wireshark 的功能,非數據包自己的信息。

以上案例在 TCP 三次握手時協商開啓了選擇性確認 SACK,所以一旦數據包丟失並收到重複 ACK ,即便在丟失數據包以後還成功接收了其餘數據包,也只須要重傳丟失的數據包。若是不啓用 SACK,就必須重傳丟失包以後的每一個數據包。

若是要支持 SACK,必須雙方都要支持。在 Linux 下,能夠經過net.ipv4.tcp_sack 參數打開這個功能(Linux 2.4 後默認打開)。


TCP 流量控制

TCP 爲了防止發送方無腦的發送數據,致使接收方緩衝區被填滿,因此就有了滑動窗口的機制,它可利用接收方的接收窗口來控制發送方要發送的數據量,也就是流量控制。

接收窗口是由接收方指定的值,存儲在 TCP 頭部中,它能夠告訴發送方本身的 TCP 緩衝空間區大小,這個緩衝區是給應用程序讀取數據的空間:

  • 若是應用程序讀取了緩衝區的數據,那麼緩衝空間區的就會把被讀取的數據移除
  • 若是應用程序沒有讀取數據,則數據會一直滯留在緩衝區。

接收窗口的大小,是在 TCP 三次握手中協商好的,後續數據傳輸時,接收方發送確認應答 ACK 報文時,會攜帶當前的接收窗口的大小,以此來告知發送方。

假設接收方接收到數據後,應用層能很快的從緩衝區裏讀取數據,那麼窗口大小會一直保持不變,過程以下:

理想狀態下的窗口變化

可是現實中服務器會出現繁忙的狀況,當應用程序讀取速度慢,那麼緩存空間會慢慢被佔滿,因而爲了保證發送方發送的數據不會超過緩衝區大小,則服務器會調整窗口大小的值,接着經過 ACK 報文通知給對方,告知如今的接收窗口大小,從而控制發送方發送的數據大小。

服務端繁忙狀態下的窗口變化

零窗口通知與窗口探測

假設接收方處理數據的速度跟不上接收數據的速度,緩存就會被佔滿,從而致使接收窗口爲 0,當發送方接收到零窗口通知時,就會中止發送數據。

以下圖,能夠接收方的窗口大小在不斷的收縮至 0:

窗口大小在收縮

接着,發送方會定時發送窗口大小探測報文,以便及時知道接收方窗口大小的變化。

如下圖 Wireshark 分析圖做爲例子說明:

零窗口 與 窗口探測

  • 發送方發送了數據包 1 給接收方,接收方收到後,因爲緩衝區被佔滿,回了個零窗口通知;
  • 發送方收到零窗口通知後,就再也不發送數據了,直到過了 3.4 秒後,發送了一個 TCP Keep-Alive 報文,也就是窗口大小探測報文;
  • 當接收方收到窗口探測報文後,就立馬回一個窗口通知,可是窗口大小仍是 0;
  • 發送方發現窗口仍是 0,因而繼續等待了 6.8(翻倍) 秒後,又發送了窗口探測報文,接收方依然仍是回了窗口爲 0 的通知;
  • 發送方發現窗口仍是 0,因而繼續等待了 13.5(翻倍) 秒後,又發送了窗口探測報文,接收方依然仍是回了窗口爲 0 的通知;

能夠發現,這些窗口探測報文以 3.4s、6.5s、13.5s 的間隔出現,說明超時時間會翻倍遞增。

這鏈接暫停了 25s,想象一下你在打王者的時候,25s 的延遲你還能上王者嗎?

發送窗口的分析

在 Wireshark 看到的 Windows size 也就是 " win = ",這個值表示發送窗口嗎?

這不是發送窗口,而是在向對方聲明本身的接收窗口。

你可能會好奇,抓包文件裏有「Window size scaling factor」,它實際上是算出實際窗口大小的乘法因子,「Windos size value」實際上並非真實的窗口大小,真實窗口大小的計算公式以下:

「Windos size value」 * 「Window size scaling factor」 = 「Caculated window size 」

對應的下圖案例,也就是 32 * 2048 = 65536。

其實是 Caculated window size 的值是 Wireshark 工具幫咱們算好的,Window size scaling factor 和 Windos size value 的值是在 TCP 頭部中,其中 Window size scaling factor 是在三次握手過程當中肯定的,若是你抓包的數據沒有 TCP 三次握手,那可能就沒法算出真實的窗口大小的值,以下圖:

如何在包裏看出發送窗口的大小?

很遺憾,沒有簡單的辦法,發送窗口雖然是由接收窗口決定,可是它又能夠被網絡因素影響,也就是擁塞窗口,實際上發送窗口是值是 min(擁塞窗口,接收窗口)。

發送窗口和 MSS 有什麼關係?

發送窗口決定了一口氣能發多少字節,而 MSS 決定了這些字節要分多少包才能發完。

舉個例子,若是發送窗口爲 16000 字節的狀況下,若是 MSS 是 1000 字節,那就須要發送 1600/1000 = 16 個包。

發送方在一個窗口發出 n 個包,是否是須要 n 個 ACK 確認報文?

不必定,由於 TCP 有累計確認機制,因此當收到多個數據包時,只須要應答最後一個數據包的 ACK 報文就能夠了。


TCP 延遲確認與 Nagle 算法

當咱們 TCP 報文的承載的數據很是小的時候,例如幾個字節,那麼整個網絡的效率是很低的,由於每一個 TCP 報文中都有會 20 個字節的 TCP 頭部,也會有 20 個字節的 IP 頭部,而數據只有幾個字節,因此在整個報文中有效數據佔有的比重就會很是低。

這就好像快遞員開着大貨車送一個小包裹同樣浪費。

那麼就出現了常見的兩種策略,來減小小報文的傳輸,分別是:

  • Nagle 算法
  • 延遲確認
Nagle 算法是如何避免大量 TCP 小數據報文的傳輸?

Nagle 算法作了一些策略來避免過多的小數據報文發送,這可提升傳輸效率。

Nagle 算法的策略:

  • 沒有已發送未確認報文時,馬上發送數據。
  • 存在未確認報文時,直到「沒有已發送未確認報文」或「數據長度達到 MSS 大小」時,再發送數據。

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

禁用 Nagle 算法 與 啓用 Nagle 算法

上圖右側啓用了 Nagle 算法,它的發送數據的過程:

  • 一開始因爲沒有已發送未確認的報文,因此就馬上發了 H 字符;
  • 接着,在還沒收到對 H 字符的確認報文時,發送方就一直在囤積數據,直到收到了確認報文後,此時就沒有已發送未確認的報文,因而就把囤積後的 ELL 字符一塊兒發給了接收方;
  • 待收到對 ELL 字符的確認報文後,因而把最後一個 O 字符發送出去

能夠看出,Nagle 算法必定會有一個小報文,也就是在最開始的時候。

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

能夠在 Socket 設置 TCP_NODELAY 選項來關閉這個算法(關閉 Nagle 算法沒有全局參數,須要根據每一個應用本身的特色來關閉)。

關閉 Nagle 算法

那延遲確認又是什麼?

事實上當沒有攜帶數據的 ACK,它的網絡效率也是很低的,由於它也有 40 個字節的 IP 頭 和 TCP 頭,但沒有攜帶數據。

爲了解決 ACK 傳輸效率低問題,因此就衍生出了 TCP 延遲確認

TCP 延遲確認的策略:

  • 當有響應數據要發送時,ACK 會隨着響應數據一塊兒馬上發送給對方
  • 當沒有響應數據要發送時,ACK 將會延遲一段時間,以等待是否有相應數據能夠一塊兒發送
  • 若是在延遲等待發送 ACK 期間,對方的第二個數據報文又到達了,這時就會馬上發送 ACK

TCP 延遲確認

延遲等待的時間是在 Linux 內核中的定義的,以下圖:

關鍵就須要 HZ 這個數值大小,HZ 是跟系統的時鐘頻率有關,每一個操做系統都不同,在個人 Linux 系統中 HZ 大小是 1000,以下圖:

知道了 HZ 的大小,那麼就能夠算出:

  • 最大延遲確認時間是 200 ms (1000/5)
  • 最短延遲確認時間是 40 ms (1000/25)

TCP 延遲確承認以在 Socket 設置 TCP_QUICKACK 選項來關閉這個算法。

關閉 TCP 延遲確認

延遲確認 和 Nagle 算法混合使用時,會產生新的問題

當 TCP 延遲確認 和 Nagle 算法混合使用時,會致使時耗增加,以下圖:

TCP 延遲確認 和 Nagle 算法混合使用

發送方使用了 Nagle 算法,接收方使用了 TCP 延遲確認會發生以下的過程:

  • 發送方先發出一個小報文,接收方收到後,因爲延遲確認機制,本身又沒有要發送的數據,只能乾等着發送方的下一個報文到達;
  • 而發送方因爲 Nagle 算法機制,在未收到第一個報文的確認前,是否會發送後續的數據;
  • 因此接收方只能等待最大時間 200 ms 後,纔回 ACK 報文,發送方收到第一個報文的確認報文後,也才能夠發送後續的數據。

很明顯,這兩個同時使用會形成額外的時延,這就會使得網絡"很慢"的感受。

要解決這個問題,只有兩個辦法:

  • 要麼發送方關閉 Nagle 算法
  • 要麼接收方關閉 TCP 延遲確認

image.png

相關文章
相關標籤/搜索