《TCP/IP協(xié)議 詳解》思考總結(jié) · TCP上篇

前言

開始這篇文章之前,我非常的緊張,因為要寫好這個TCP協(xié)議說實話并不簡單。作為TCP/IP協(xié)議簇最為核心的部分,《TCP/IP協(xié)議 卷一》花了整整八章的篇幅去介紹它。如何在保證正確的前提之下,合理有序的寫出一些有意義的內(nèi)容,這是一個很大的挑戰(zhàn)。

整個看書學(xué)習(xí)的過程,實際也是一種享受。在了解TCP的各項策略時,你可以通過書本了解到前輩設(shè)計時的所思所想。如何在無連接不可靠的IP網(wǎng)絡(luò)上實現(xiàn)一個可靠有連接的傳輸;如何根據(jù)已有的信息去推測診斷當(dāng)前的網(wǎng)絡(luò)環(huán)境;如何充分利用當(dāng)前的資源以最高效的方式傳輸;如何動態(tài)的感知網(wǎng)絡(luò)的波動。相信你在了解之后也一定會和我一樣忍不住拍手稱為。

本文介紹TCP,依然是從三次握手和四次揮手開始;之后介紹了兩種不同情況下TCP的傳輸策略;在文章的結(jié)尾我們簡單說了帶寬時延乘積,這是一個非常重要的概念,理解它之后才會明白擁塞發(fā)生的情形,以及我們是如何把數(shù)據(jù)報的傳遞抽象成流。雖然是熟悉的概念,但文章盡力從一些網(wǎng)上其他文章沒有提到的角度來分析這些問題,希望能夠給你一些新的啟示。

在計算機網(wǎng)絡(luò)的學(xué)習(xí)過程中,概念和參數(shù)并不是最重要的。如何去理解一個協(xié)議,如何從現(xiàn)有的工具里去觀察一個協(xié)議從而分析問題,如何去借助文檔回答問題,這些能力才是我們真正應(yīng)該重視的。

再談三次握手和四次揮手

在其他介紹三次握手的文章里,經(jīng)常會從類似如下的示意圖開始

三次握手流程

圖上的內(nèi)容非常的簡單,但是實際的握手過程遠(yuǎn)比這個復(fù)雜。首先,我們要考慮的就是為什么TCP的建立需要三次握手。這個問題我們在這個系列的文章第一篇就進(jìn)行了討論,當(dāng)時給出的結(jié)論是:

TCP需要在不可靠的信道上進(jìn)行可靠的傳輸,那么必須要在通訊之前就某些問題達(dá)成一致。一條消息如果需要被單方面確認(rèn),需要一次單向握手,那么雙方同時就某個問題達(dá)成一致就需要兩次單向握手。

序號 方向 具體操作
1) A --> B Send A’s SYN
2) A <-- B ACK A’s SYN
3) A <-- B Send B’s SYN
4) A --> B ACK B’s SYN

其中2 , 3兩步可以合并成一條信息,這就是三次握手的由來。

但這里仍然留有幾個模糊不清的點。

1.參考上面的流程圖,整個握手結(jié)束之后實際只有B可以確認(rèn)消息雙方都已經(jīng)得到,而A無法確定最后一條ACK B’s SYN是否送達(dá)。

這是計算機網(wǎng)絡(luò)中一個非常有名的思想實驗:兩軍問題。實際上百度可以得到非常多的資料,但是各類博客抄來抄去,一些好的文章原作者已經(jīng)不可考,所以這里我再簡單做一下闡述。

兩支軍隊(我們暫時稱為A1和A2)預(yù)備從兩邊去攻打低洼的一座城池(暫稱為B)。他們的力量對比是

  1. B < A1 + A1
  2. B > A1
  3. B > A2

A1和A2必須要約定好在同一個時間發(fā)起攻擊,才可以獲勝,單獨行動都會被B消滅。但是A1和A2通信必須要經(jīng)過B的城池,這也就意味著通信兵可能會被截獲。如何設(shè)計一個通信的方案可以使得A1和A2必勝呢?

兩軍問題本質(zhì)上和我們TCP遇到的情況一樣:在不可靠的信道上試圖就某些信息達(dá)成一致。目前的方案是每當(dāng)發(fā)送一份消息,必須要返回一份回執(zhí)來告知發(fā)送方消息已送達(dá)。

引出的一個問題就是發(fā)送回執(zhí)的一方如何確認(rèn)自己的回執(zhí)被送達(dá)了呢?再返回一條回執(zhí)來確認(rèn)自己收到了對方的這條回執(zhí)?所謂子子孫孫無窮盡也,大抵就是如此。這意味著不可靠的信道上最后一條被發(fā)送的消息永遠(yuǎn)都是無法被雙方同時確認(rèn)的。兩軍問題本質(zhì)上是無解的。

還需要強調(diào)的是,可靠性并不會因為握手次數(shù)的增加而提高。三次握手是可靠性和效率兩者平衡妥協(xié)的結(jié)果。最后一次通信的發(fā)送方必須要承擔(dān)行動的風(fēng)險。

除了連接建立時,在一端(我們假設(shè)為客戶端)發(fā)起主動關(guān)閉時,也會遇到同樣的問題。四次揮手的過程如下。

四次揮手流程

發(fā)起主動關(guān)閉的一方在發(fā)送最后一個FIN ACK之后會在TIME_WAIT狀態(tài)停留2MSL的時間。這樣做的目的其一就是為了實現(xiàn)全雙工的TCP連接的可靠終止。

MSL是Maximum Segment Lifetime,指代任何報文段被丟棄前在網(wǎng)絡(luò)內(nèi)的最長時間

因為最后一條FIN包的ACK發(fā)出以后客戶端是無法確認(rèn)對端服務(wù)器一定收到的,如果客戶端發(fā)送完FIN ACK之后認(rèn)為已經(jīng)結(jié)束關(guān)閉了這個連接,但實際FIN ACK又未送達(dá),這時服務(wù)端重新發(fā)送了一個FIN包給客戶端,會得到一條RST的響應(yīng),這會被服務(wù)器解析成一種錯誤,而實際客戶端是正常關(guān)閉的。

為此客戶端必須要維護(hù)狀態(tài)信息2MSL的時間,并在結(jié)束時按照最后一條FIN ACK丟失的情況處理,重新發(fā)送一次FIN ACK。

TIME_WAIT另一個目的是允許之前連接的報文在網(wǎng)絡(luò)中消逝。熟悉 socket編程的朋友應(yīng)該知道可以通過四元組(目的端IP地址和端口,源端IP地址和端口)來確定唯一的一條TCP連接。但是如果關(guān)閉了一個TCP連接之后,在相同四元組之上重新打開一個TCP連接,后一個連接會被認(rèn)為是之前連接的化身。

這里的翻譯比較讓人困惑,在RFC 793中是這樣描述的 : New instances of a connection will be referred to as incarnations of the connection.
后面我們會多次提到化身,指的是同一個四元組上新創(chuàng)建的連接。

存在一種可能是某個連接之前的重復(fù)分組在該連接終止后再現(xiàn),如果此時創(chuàng)建了新的化身,很可能會帶來誤解。

為此TCP拒絕為處于TIME_WAIT的端口創(chuàng)建新的化身。TIME_WAIT的時間是2MSL,這保證了無論是哪個方向上的報文(存活時間MSL),還是另一端的應(yīng)答(發(fā)送的報文最長存活MSL,返回的應(yīng)答最多存活2MSL)都會在TIME_WAIT期間消逝。

實際這個規(guī)則存在例外,我們將在后面遇到這種情況

2. 在三次握手的過程中,雙方嘗試在哪些方法達(dá)成約定?

為了研究這個問題,我們可以打開Wireshark,找一個處于三次握手的TCP連接。下圖是我任意找的一個報文。

wireshark觀察的三次握手
有關(guān)SYN FIN的介紹非常多,本文不再介紹。如果你不太熟悉,沒有必要一一去硬背下來,了解這幾個名稱實際指代的單詞會有助于理解和記憶。

1. SYN = synchronization 同步。正如我們上文介紹所說,TCP連接的建立必須要就某些問題達(dá)成約定,也就是同步信息
2. PSH = push 發(fā)送端通知接收端不要因為等待額外數(shù)據(jù)而讓已送達(dá)的數(shù)據(jù)在緩沖區(qū)滯留。類似flush()
3. FIN = finish 也就是我們所說的四次揮手。結(jié)束的含義
4. ACK = acknowledge 確認(rèn)報文。你可以簡單認(rèn)為是回執(zhí),具體是確認(rèn)哪一部分的數(shù)據(jù),需要結(jié)合sequence number

這是一個非常典型的三次握手,上文所說的握手報文合并也可以在報文 489看出。注意圖中紅框標(biāo)示的信息。

  • 49817 - > 443源端口 -> 目的端口

細(xì)心的朋友應(yīng)該看出443端口是為https服務(wù)指定的,實際也確實如此,下一個未展示的報文就是Client Hello

  • SYN標(biāo)示指明了這是一個發(fā)起連接請求的報文。
  • Seq就是我們馬上將要介紹的重點Sequence Number
  • Win是Window的意思,這一個字段我們會在后續(xù)仔細(xì)介紹
  • Len是length,用以指明TCP數(shù)據(jù)部分的長度。注意這個長度并不包含報文首部,所以在SYN包中Len是0
  • WS是表明發(fā)起端192.168.199.170可以處理Selective Acknowledgements。TSvalTSecr是時間戳選項相關(guān)的內(nèi)容。這三個參數(shù)本文不做介紹,有興趣的朋友可以自行查閱。what is 'WS' 'TSval' and 'SACK_PERM' mean in packet info columns???

我們需要關(guān)心的是Seq字段。

在連接建立的過程中客戶端和服務(wù)器會互相通告自己的ISN,也就是SYN包中我們看到的Seq字段的值。需要注意的是只有在SYN包中Seq字段才是發(fā)送端的ISN。

ISN = Initial Sequence Number

Seq是序號的意思,它可以描述當(dāng)前發(fā)送的數(shù)據(jù)報中的數(shù)據(jù)相對于整體數(shù)據(jù)開始位置的偏移量,單位是字節(jié)。與之類似的是ACK數(shù)據(jù)報中的Ack字段,它是為了通告對端已經(jīng)接收的數(shù)據(jù)相對于整體數(shù)據(jù)開始位置的偏移量(也可以理解為對端期待接收的數(shù)據(jù)相對于整體數(shù)據(jù)開始位置的偏移量)。

下圖描述的是一個最簡單的TCP連接和中斷的過程。

TCP連接和中斷的過程

首先報文段1通告了srv的ISN也就是圖中的1415531521。之后報文段2通告了bsdi的ISN也就是1823083521,但是它多了一條ack,注意它的數(shù)值是1415531522 = 1415531521 + 1!表明bsdi已經(jīng)確認(rèn)收到了srv的SYN包。

SYNFIN包會讓Seq加一,你可以簡單認(rèn)為是一個長度為1的數(shù)據(jù)報。

注意報文段4,srv的Seq被設(shè)置成1415531522。因為對端已經(jīng)ack了SYN包,也就意味著我們發(fā)送的數(shù)據(jù)應(yīng)該從這之后開始。

但是我們需要注意,包括上面我們截圖的三條報文,打開Wireshark你去觀察任一一個SYN包,里面的Seq字段永遠(yuǎn)都是0,而不是我們流程圖里那一長串的數(shù)字。這是因為Wireshark展示的SeqAck字段全部都是相對數(shù)值也就是Seq/Ack - ISN。

Wireshark里的SYN/ACK

我們必須要思考的一個問題就是,ISN是如何選擇的?如果僅僅是為了標(biāo)記收發(fā)數(shù)據(jù)的偏移量,我們完全可以默認(rèn)從0開始計算而不必加上ISN。這似乎更加簡單。

RFC 793中有關(guān)這部分做了討論,它首先提出了一個問題: how does the TCP identify duplicate segments from previous incarnations of the connection? 比如在一個連接(四元組不變)上短時間內(nèi)快速的重復(fù)打開關(guān)閉,或者一個連接因為內(nèi)存不足而斷開繼而復(fù)位。連接的化身很可能會接收到之前連接存留在網(wǎng)絡(luò)內(nèi)的數(shù)據(jù)報。

我們上文介紹的TIME_WAIT設(shè)計的初衷,部分就是為了避免這種混淆。但是這并不保險。為了解決這個問題,TCP選擇初始一個ISN,并在此基礎(chǔ)之上累加,從而讓連接的化身能夠正確分辨數(shù)據(jù)報。

socket編程中,我們可以指定SO_REUSEADDR選項讓處于TIME_WAIT狀態(tài)的端口可用
主機或者TCP模塊的崩潰也會遺失狀態(tài)的記錄。

ISN的生成器實質(zhì)上是一個32比特的計數(shù)器,每隔一定的時間加1(通常是4ms,但不同系統(tǒng)實現(xiàn)不一樣)。選擇這樣的生成方式是為了考慮到一種更極端的情況: even if a TCP crashes and loses all knowledge of the sequence numbers it has been using。ISN的生成器實質(zhì)是和TCP模塊互相獨立的。

ISN的范圍是0 ~ 2 ^ 32 - 1,達(dá)到最大之后ISN會環(huán)回到0開始。在4ms加1這種實現(xiàn)的系統(tǒng)里,大約需要4.55小時ISN環(huán)回一遍。這個時間是遠(yuǎn)遠(yuǎn)大于TIME_WAIT的,所以不必?fù)?dān)心TIME_WAIT期間ISN發(fā)生回繞從而重復(fù)。

上文我們說過TIME_WAIT有一個特例:在源自Berkeley的實現(xiàn)當(dāng)中,如果到達(dá)的ISN大于之前連接的結(jié)束序列號,那么Berkeley的實現(xiàn)是允許當(dāng)前處于TIME_WAIT的端口復(fù)用。簡單來看這么做是沒有問題的,因為FIN包中的Seq一定是當(dāng)前連接最大的Sequence Number。如果新連接的ISN大于這個Seq那么顯而易見,這個SYN包肯定不屬于之前連接的。

但是問題出在ISN的選擇是環(huán)回的!當(dāng)Sequence Number達(dá)到最大也就是2^32 - 1時會環(huán)回到0重新開始。假設(shè)之前連接的通訊過程中Sequence Number發(fā)生了環(huán)回,我們上文的結(jié)論也就不成立了。所以這種特例是存在陷阱的。

通常在一個高速通道上Sequence Number非常容易發(fā)生環(huán)回,造成的問題不僅僅是我們提到TIME_WAIT,中間超時重傳的包也可能會讓對端造成錯誤的理解。

使用窗口擴大選項的TCP連接,最大的窗口接近2 ^ 30!這意味著按照最大窗口發(fā)送,第五個數(shù)據(jù)報Seq就會發(fā)生環(huán)回。舉一個簡單的例子,假設(shè)我們需要傳輸6G的數(shù)據(jù)。

序號 方向 數(shù)據(jù)
1. A —> B Seq 0G : 1G
2. A —> B Seq 1G : 2G
3. A —> B Seq 2G : 3G
4. A —> B Seq 3G : 4G
5. A —> B Seq 0G : 1G
6. A —> B Seq 1G : 2G

假如第二個數(shù)據(jù)報發(fā)生丟失,在發(fā)送第六個數(shù)據(jù)報發(fā)生重傳,那么接收端就會發(fā)生混淆,這個時候單單依靠Seq是沒有辦法判斷數(shù)據(jù)報的先后順序的。為此TCP引入了時間戳選項來解決這個問題,作為32比特的Seq的一個拓展。

需要強調(diào)的有兩點
一是Seq數(shù)值的增長是和數(shù)據(jù)的傳輸速度有關(guān)的,而ISN是根據(jù)定時器線性增長的。二是實際發(fā)生這種情況的條件非常苛刻。因為如果發(fā)生環(huán)回的時間大于MSL,那么我們上文提到的第二個數(shù)據(jù)報在第六個數(shù)據(jù)報發(fā)送時,一定消失在網(wǎng)絡(luò)當(dāng)中了。所以發(fā)生這種情況一般是在高速通道上。在RFC 1185 TCP Extension for High-Speed Paths中做了詳細(xì)的討論。

ISN是三次握手需要協(xié)商約定的一個重要選項。

除此之外SYN包的TCP首部中,選項里最常見的一個字段就是MSS(Maximum Segment Size)。雙方在建立連接的時候會互相通告對方己端能夠接收的最大報文長度,目的是為了避免發(fā)生分片。需要注意的是MSS的值是不包括IP首部和TCP首部的,例如在MTU位1500的外出接口上,通告的MSS應(yīng)該是1460。但是這個選項的局限在于它僅僅只在SYN包出現(xiàn),這也就意味著如果通訊建立的過程當(dāng)中MSS的數(shù)值發(fā)生了變化,對端是無法感知的。另外,MSS僅僅只聲明了自己的約束,如果中間網(wǎng)絡(luò)的MTU小于兩端通告的MSS,那么分片依然是無法避免的。

IPv6是期待以1280打天下的。它要求硬件提供的最小MTU是1280

有關(guān)三次握手的討論,暫時告一段落。為了與之呼應(yīng),我們再來看一看TCP斷開連接的四次揮手。TCP作為全雙工的通信,在連接建立完成之后實際存在了兩條虛擬信道:客戶端 —> 服務(wù)器服務(wù)器 —> 客戶端。因此我們在關(guān)閉的時候也要逐一的拆除。

但和連接建立不同的是,雙方的傳輸任務(wù)無法保證在同一時間結(jié)束。這意味著在某一端發(fā)起關(guān)閉的時候,我們必須要保證,在拆除其中一條信道的同時,不影響另一條信道的通訊。這也就是半關(guān)閉的由來。

在socket編程中,關(guān)閉連接的方式通常是close()函數(shù)。每次調(diào)用close()函數(shù)時會把對應(yīng)的描述符sockfd引用計數(shù)減一,在計數(shù)為0時同時關(guān)閉讀和寫也就是完全關(guān)閉。為了應(yīng)對半關(guān)閉的情況,我們會使用shutdown()函數(shù),指定第二個參數(shù)為SHUT_WR來實現(xiàn)半關(guān)閉


交互數(shù)據(jù)流和成塊數(shù)據(jù)流

在書中的十九和二十章,討論了交互數(shù)據(jù)流和成塊數(shù)據(jù)流兩種情況的傳輸策略。但是書中并未就這兩種數(shù)據(jù)流給出明確的定義。在了解它們各自策略是如何實現(xiàn)之前,明確它們的特點和定義還是十分有必要的。

什么是交互數(shù)據(jù)流和成塊數(shù)據(jù)流

顧名思義,交互數(shù)據(jù)流的特點表現(xiàn)在交互上,這也就是說數(shù)據(jù)流流動的方向是雙向的,本質(zhì)是通信兩端的信息交換。通常情況下客戶端向服務(wù)器發(fā)出一條消息,服務(wù)器除了會返回ACK對消息進(jìn)行確認(rèn)之外,還會針對客戶端的請求反饋相關(guān)的信息。交互數(shù)據(jù)流的每一個報文通常都會比較小

我們采用客戶端-服務(wù)器模型,并且規(guī)定主動發(fā)起的一方為客戶端。之后的例子在沒有特殊說明的情況下默認(rèn)都是這樣約定

在書中有過這樣一段描述

一些有關(guān)TCP通信量的研究發(fā)現(xiàn),如果按照分組數(shù)量計算,約有一半的TCP報文段包含成塊數(shù)據(jù)(如FTP、電子郵件和Usenet新聞),另一半則包含交互數(shù)據(jù)(如Telnet和Rlogin)。如果按字節(jié)計算,則成塊數(shù)據(jù)與交互數(shù)據(jù)比例約為 90%和10%。這是因為成塊數(shù)據(jù)的報文段基本上都是滿長渡的,而交互數(shù)據(jù)則小的多。

成塊數(shù)據(jù)流的特點與交互數(shù)據(jù)流相反,它的側(cè)重在于單向的傳輸,所承擔(dān)的是要把一個較大的數(shù)據(jù)盡快的送抵到對端的任務(wù)。

我們可以做一個簡單的比喻來幫助理解:交互數(shù)據(jù)流類似QQ上兩人的聊天;而成塊數(shù)據(jù)流則是在傳輸文件。

交互數(shù)據(jù)流

交互數(shù)據(jù)流的傳輸策略有兩個重點

1. 經(jīng)受時延的確認(rèn)

通常TCP在接收到數(shù)據(jù)時并不立即發(fā)送ACK,而是推遲發(fā)送等待一段時間,之后如果有相同方向的數(shù)據(jù)需要傳遞,會捎帶ack一起發(fā)送。

屏幕快照 2018-01-11 下午8.49.22.png

絕大多數(shù)的實現(xiàn)里是以200ms作為最大的時延等待。這里需要說明的是,時延并不是以數(shù)據(jù)到達(dá)目的端開始計算的,而是以TCP的實現(xiàn)當(dāng)中一個200ms的定時器為準(zhǔn)。如果有ack需要發(fā)送那么會在定時器下一次的溢出時執(zhí)行。考慮到數(shù)據(jù)到達(dá)的時間是隨機的,那么ack的發(fā)送時機也就不固定,范圍在0 - 200ms。

與之類似的是TCP超時定時器

使用ack捎帶的一個好處在于它提高了TCP的性能,原因在于它提高了有效載荷的比例。

ack捎帶

當(dāng)我們嘗試通過TCP發(fā)送數(shù)據(jù)的時候,無論是1個字節(jié)或是MSS大小的數(shù)據(jù)報,每一份報文都是以固定的格式發(fā)出的(這里忽略各類首部的額外選項)

IP首部 + TCP首部 + 有效載荷

假設(shè)我們需要傳輸一份大小為N的數(shù)據(jù),需要分拆成m個包來完成,那么傳輸?shù)臄?shù)據(jù)總量就是

m * (20 + 20) + N

每一個報文盡可能多的裝載數(shù)據(jù),或者說使用盡可能少的分包來完成數(shù)據(jù)的傳遞,有效載荷占傳輸總量的比例也就越高。

ack捎帶的處理可以壓縮我們需要傳輸?shù)臄?shù)據(jù),節(jié)省了原先ack首部的字節(jié)傳輸;在等待的同時,如果有多份數(shù)據(jù)抵達(dá),那么這些數(shù)據(jù)的確認(rèn)可以合并成一個ack報文,減少了報文的數(shù)量??紤]到交互數(shù)據(jù)流每一個報文數(shù)據(jù)量相對較小,ack報文的壓縮帶來的效率提升會更加明顯。

其次ack捎帶可以有效避免糊涂窗口綜合征。

糊涂窗口綜合征指的是當(dāng)發(fā)送端應(yīng)用進(jìn)程產(chǎn)生數(shù)據(jù)很慢、或者接收端應(yīng)用進(jìn)程處理接收緩沖區(qū)數(shù)據(jù)很慢,或者兩個情況同時存在;使得通信兩端傳輸?shù)膱笪亩魏苄。ㄌ貏e是有效載荷很小)的情況。

極端情況下,有效載荷可能只有1個字節(jié);而傳輸開銷有40字節(jié)(20字節(jié)的IP頭+20字節(jié)的TCP頭)

為了避免這種情況的發(fā)生,我們可以從發(fā)送端和接收端兩邊入手解決。這個問題我們留在講解完滑動窗口之后再討論,現(xiàn)在需要明確的是對于接收端而言,ack捎帶是一個可行的解決方案。

2. Nagle算法

Nagle算法要求在一個TCP連接上最多只能有一個未被確認(rèn)的分組,在該分組被確認(rèn)之前不能夠發(fā)送其他的分組。如果在等待期間有需要發(fā)送的分組。會被收集起來在收到確認(rèn)以后用同一個分組發(fā)出。

該算法的優(yōu)越之處在于是自適應(yīng)的:數(shù)據(jù)被對端確認(rèn)的越快,發(fā)送端數(shù)據(jù)發(fā)送的也就越快;在相對低速的環(huán)境下可以有效減少微小分組的數(shù)據(jù),提高TCP的傳輸效率。

在上文介紹ack捎帶的時候我們提到壓縮報文數(shù)量可以提高有效載荷在總體傳輸數(shù)據(jù)當(dāng)中的比例,一定程度上提高了TCP傳輸?shù)男?。另一點需要強調(diào)的是,TCP提供的傳輸服務(wù)是有序的。考慮到傳輸過程中數(shù)據(jù)報可能會丟失、亂序,接收端必須要對數(shù)據(jù)報進(jìn)行處理。即使是微小分組,也是一個獨立的數(shù)據(jù)報,過多的微小分組很顯然會給接收端帶來相應(yīng)的處理壓力,這不是我們所期望的。

微小分組指的是有效載荷非常小的報文

LAN上的通信相對簡單,一般不會出現(xiàn)擁塞,傳輸速率相對WAN也比較高。我們做的大部分處理更多是要為低速的WAN考慮。這里可以簡單做一個例子說明。

屏幕快照 2018-01-12 下午7.38.22.png

參照上圖可以看出,LAN內(nèi)一個字節(jié)從被發(fā)送到收到確認(rèn)和回顯的平均往返時間約為t = 16ms。如果我們的輸入速度小于60個字節(jié)每秒,那么Nagle算法并不會對我們的傳輸造成任何影響,因為每次我們準(zhǔn)備好下一個輸出字節(jié)的時候,上一個字節(jié)已經(jīng)送抵對端并且收到確認(rèn)和回顯了。

1s = 1000ms。 1000/16 = 62.5 ≈ 60

當(dāng)平均往返時間 t 增加時(比如在WAN內(nèi))情況會發(fā)生變化。很可能我們鍵入新的字節(jié)但是之前數(shù)據(jù)還未被確認(rèn),這時我們鍵入的數(shù)據(jù)都會被收集等待確認(rèn)一起發(fā)送。在某些應(yīng)用程序上可能會感受到卡頓和反饋不及時。

比如X窗口系統(tǒng)服務(wù)器,用以標(biāo)示鼠標(biāo)移動的微小分組必須無時延地發(fā)送,以便提供實時的反饋消息。

Nagle算法在某些情況下甚至可能成為我們網(wǎng)絡(luò)通信的瓶頸。假設(shè)這樣一種情況:客戶端發(fā)送了一條報文給服務(wù)端,之后等待服務(wù)端的確認(rèn);而服務(wù)器在收到報文之后并不立即返回ack而是等待。等待的原因可以是ack捎帶,也可以是認(rèn)為服務(wù)器認(rèn)為客戶端提供的信息不足夠重復(fù)等待期望更多數(shù)據(jù),無論是哪一種情況都勢必陷入一個死鎖的狀態(tài):雙方都在等待對方的消息。通常情況超時才能打破這種僵局,但這顯然是一種無意義的消耗。

socket編程中,可以設(shè)置TCP_NODELAY來關(guān)閉Nagle算法

之前看到這樣一個問題:如果客戶端發(fā)送了一條消息之后,因為某些原因沒有收到確認(rèn)發(fā)生了超時,在這期間如果客戶端收集了新的數(shù)據(jù),超時之后發(fā)送的這個數(shù)據(jù)報應(yīng)該如果發(fā)送?

TCP有一個實際存在的緩沖區(qū),客戶端發(fā)送的數(shù)據(jù)會留有備份,在接收到對應(yīng)ack之后才會移除。如果發(fā)生超時客戶端只需要把緩沖區(qū)的數(shù)據(jù)發(fā)送出去即可(我們假設(shè)所有的數(shù)據(jù)可以放在一個報文里發(fā)出)。

為什么說是一個實際存在的緩沖區(qū)呢。因為udp雖然有緩沖區(qū)這個概念,但是并不存在,所謂緩沖區(qū)的大小只是一個最大udp報文長度的限定。

Nagle算法也可以避免糊涂窗口綜合征。這是從發(fā)送端入手的一種解決方式。

總結(jié)

兩種傳輸策略實質(zhì)是分別從發(fā)送端(Nagle算法)和接收端(ACK捎帶)兩端入手,通過壓縮報文的數(shù)量來提高交互數(shù)據(jù)流整體傳輸?shù)乃俾省?/p>

成塊數(shù)據(jù)流

在上文中我們討論了交互數(shù)據(jù)流的Nagle算法,在低速的WAN上經(jīng)常會造成時延。這對于成塊數(shù)據(jù)流的傳輸是不太能夠接受的。我們必須要考慮到既然是交互,那么每一條消息的發(fā)出除了對端的確認(rèn),額外的反饋消息也是非常重要的,因為這很可能會影響到后續(xù)交互的邏輯。但是在成塊數(shù)據(jù)流上則沒有這個麻煩,在大部分的情況下它的目的非常明確:盡快地將數(shù)據(jù)搬運到對端。出于效率的考慮,TCP使用另一種傳輸策略,允許發(fā)送方在停止并等待確認(rèn)前可以連續(xù)發(fā)送多個分組。

舉一個例子:假設(shè)登錄過程就是一次交互,那么客戶端傳遞了用戶名和密碼之后,必須要等待服務(wù)器的反饋:這對用戶名密碼是否正確。之后客戶端必須根據(jù)判定的結(jié)果來繼續(xù)下一步的操作。

書中二十章的內(nèi)容全部在圍繞一個關(guān)鍵詞展開:窗口。要理解成塊數(shù)據(jù)流的傳輸策略,明確窗口的定義和它設(shè)計的意義,是非常有必要的。

首先我們來看一下數(shù)據(jù)傳遞的過程。

數(shù)據(jù)傳遞流程

數(shù)據(jù)在被送達(dá)對端之后,存儲在TCP的接收緩沖區(qū),這是一個有限的空間。上層的應(yīng)用進(jìn)程會從接收緩沖區(qū)讀取數(shù)據(jù),之后相應(yīng)的數(shù)據(jù)會從TCP的接收緩沖區(qū)移除,用以騰出空間接收新的數(shù)據(jù)。通告窗口(advertise window)就是用以描述自身接收緩沖區(qū)中當(dāng)前可用的空間量的,在通告發(fā)送端之后可以確保它發(fā)送的數(shù)據(jù)不會使接收緩沖區(qū)溢出。

這是TCP提供的一種流量控制。我們必須要思考成塊數(shù)據(jù)流的傳輸策略為什么要引入窗口這個概念?因為TCP無法保證發(fā)送端傳輸?shù)乃俾屎徒邮斩颂幚頂?shù)據(jù)的速率保持一致!

如果發(fā)送端的傳輸速率相對接收端的處理速率較慢,那么每次數(shù)據(jù)報送抵接收端都可以確保緩沖區(qū)有足夠的空間去接收。但是情況反過來,發(fā)送端盡可能快的將數(shù)據(jù)拋出,接收端會因為緩沖區(qū)空間不足而丟棄分組。為了避免這樣一種情況,TCP采用了滑動窗口協(xié)議。

在交互數(shù)據(jù)流中,情況相對簡單很多。因為Nagle只允許網(wǎng)絡(luò)中存在最多一個未被確認(rèn)的分組,一來一回的傳輸策略邏輯比較簡單;而且交互數(shù)據(jù)流報文相對較短,接收端壓力不會太大。

滑動窗口協(xié)議

注意圖中紅框標(biāo)示的報文7和8。在這之前svr主機連續(xù)發(fā)送了三個報文(4 - 6),在第7個報文的ack只確認(rèn)了4 - 5兩個報文的內(nèi)容。我們可以合理推斷,在bsdi主機處理第4個報文時,執(zhí)行了ack捎帶的操作,在這期間bsdi處理完報文 5,之后時延定時器發(fā)生溢出發(fā)出ack確認(rèn)4 - 5。下一個時延定時器溢出bsdi處理完報文 6,發(fā)出報文 8確認(rèn)了報文 6。注意win參數(shù)從3072 = 4096 - 1024!這說明報文 6還留在bsdi的TCP緩沖區(qū)里,可用空間減少了相應(yīng)的大小。

用另一種可視化的方式來展示這個過程

滑動窗口協(xié)議1

綠色部分代表已經(jīng)被確認(rèn)的報文;黃色部分是通告窗口大小,表示接收端緩沖區(qū)可以同時容納報文4 5 6 7 8 9;紅色部分標(biāo)示的是后續(xù)待發(fā)送的數(shù)據(jù),但因為超出了通告窗口大小的限制當(dāng)前不能發(fā)送。

但是這部分有一個需要強調(diào)的點是,發(fā)送端并非是從黃色部分的左側(cè)邊沿開始(圖中的報文 4)選擇報文發(fā)送。因為上圖我們漏掉了一種可能:已經(jīng)發(fā)送但尚未被確認(rèn)的報文。

繼續(xù)以上圖為例,假設(shè)在接收端通告窗口的時候,雖然只確認(rèn)了報文 1- 3,但是發(fā)送端實際已經(jīng)發(fā)送了報文段 4 - 6,那么實際可用的窗口大小實際是報文 7 8 9這個范圍。因為那些未被確認(rèn)的報文(inflight)我們假設(shè)它們尚在路上,會在之后得到確認(rèn)。

實際窗口

通告窗口的大小并不是一成不變的,受各種條件的影響通告窗口兩端的邊界會滑動使得通告窗口縮小或者擴大。這也是為什么我們稱之為滑動窗口協(xié)議的原因。

  1. 通告窗口左邊會隨著報文被接收端確認(rèn)而向右移動,我們稱之為窗口合攏。因為確認(rèn)過的報文不會被取消確認(rèn),所以窗口左邊不可能出現(xiàn)向左移動的情況。

  2. 接收端的應(yīng)用進(jìn)程從TCP緩沖區(qū)讀取數(shù)據(jù)之后,會騰出相應(yīng)的空間來接收新的數(shù)據(jù)。這個時候通告窗口右邊會向右移動,我們稱之為窗口張開。

  3. 通告窗口右邊在極少數(shù)情況下會向左移動,我們稱之為窗口收縮。雖然TCP被要求必須能夠在對端出現(xiàn)這種情況時進(jìn)行處理,但這是極不推薦的一種方式。

滑動窗口

如果通告窗口的左邊沿和右邊沿發(fā)生合攏,那么此時我們稱之為零窗口。發(fā)送端無法繼續(xù)發(fā)送數(shù)據(jù),必須等待接收端處理。

窗口更新

圖中紅框標(biāo)示的報文 14就是我們提到的零窗口。之后接收端重新發(fā)送了一條ack(報文 15),但并沒有確認(rèn)新的數(shù)據(jù),只是更新了win告訴發(fā)送端可以繼續(xù)發(fā)送。這種情況我們稱之為窗口更新。

擁塞窗口

上文所示的例子有一個局限:它們測試在LAN內(nèi),在傳輸?shù)囊婚_始就發(fā)出多個報文段直到接收端通告了窗口并且達(dá)到了窗口的限制。在LAN內(nèi)當(dāng)然沒有問題,因為我們不需要考慮發(fā)送端和接收端之間可能存在的多個路由和鏈路,但是如果情況放在WAN內(nèi)這顯然就不夠穩(wěn)妥了。多個分組的發(fā)出在經(jīng)過一些中間路由的時候可能需要被緩存,發(fā)送端不受限制的發(fā)送很可能會耗盡存儲器的空間。

最理想的情況應(yīng)當(dāng)是發(fā)送端發(fā)送數(shù)據(jù)的速率和接收到確認(rèn)的速率保持一致(更快只會因為接收端或者中間路由無法處理而丟包)。為了探測出未知網(wǎng)絡(luò)環(huán)境下合理的發(fā)送速率,TCP引入了慢啟動算法和擁塞窗口(congestion window)概念。

所謂慢啟動,指的是發(fā)送端首先會發(fā)送一個分組,等待接收端的確認(rèn)。在收到確認(rèn)之后擁塞窗口會從初始的1個分組大小增加到2個。再次收到確認(rèn)之后擁塞窗口會拓展為4個分組大小。以此類推,在出現(xiàn)避讓之前擁塞窗口是以指數(shù)級別增長的。

這里需要強調(diào)的是兩點:

  1. TCP發(fā)送的分組同時受通告窗口和擁塞窗口限制,兩者取較小的一個值。

  2. 雖然擁塞窗口和通告窗口一樣是以字節(jié)為單位,但擁塞窗口通常是分組大小的整倍數(shù)。我們在描述擁塞窗口時,會以單個分組大小作為單位1,這樣方便我們描述它的增長過程。

慢啟動算法實質(zhì)模擬的是一個試探的過程。它在每次發(fā)送數(shù)據(jù)被確認(rèn)之后都會拓展擁塞窗口來試探傳輸速率的極限,指數(shù)增長的方式讓擁塞窗口雖然初始數(shù)值很小,但增長確是爆炸式的,擁塞窗口會很快突破網(wǎng)絡(luò)的極限,導(dǎo)致中間路由丟棄分組。當(dāng)丟包發(fā)生時,發(fā)送方會被通知擁塞窗口開的過大,需要作出修改。

為什么考慮的是中間路由丟棄分組而不是接收方緩沖區(qū)空間不足? 因為發(fā)送的分組的大小同時受通告窗口和擁塞窗口限制。

TCP的流量控制依賴于丟包這個條件,TCP需要根據(jù)是否丟包來決定擴大發(fā)送分組還是減少。但問題在于TCP對丟包的實際情況了解的并不全面,實際TCP是不知道丟包的真正原因的!TCP認(rèn)為丟包就是網(wǎng)絡(luò)傳輸出現(xiàn)了擁堵,所以慢啟動算法里出現(xiàn)丟包會讓擁塞窗口做指數(shù)級的避讓。也許這個假設(shè)在TCP發(fā)明的當(dāng)時是成立的,但現(xiàn)在很多情況比如無限網(wǎng)絡(luò)中這個假設(shè)成了TCP的一個缺陷。例如信號干擾或者亂序誤判都可能讓發(fā)送方認(rèn)為丟包,但這種情況下避讓是完全沒有必要的。

有關(guān)超時和重傳的部分,受限于本文的篇幅會在下一篇展開。這里就不做贅述?,F(xiàn)在針對TCP慢啟動算法的缺陷也提出了解決方案(Google BBR算法),這個內(nèi)容我們會留在后續(xù)介紹TCP改進(jìn)的文章里細(xì)說。

帶寬時延乘積

日常生活里,有時候會聽到朋友抱怨網(wǎng)速太慢

" 這個小水管滴答滴答受不了了"

這確實是一個非常有趣的比喻。我們說TCP是一個無邊界的字節(jié)流傳輸協(xié)議,通信兩端存在一個虛擬的管道,數(shù)據(jù)在里面靜靜的流淌。但是這個虛擬的管道要如何去描述它?理解了這個問題之后相信會對你TCP的學(xué)習(xí)有很大幫助。

假設(shè)我們用一個矩形來描述時間t內(nèi)傳輸?shù)臄?shù)據(jù)S

屏幕快照 2018-01-11 下午7.57.33.png

如果t無限小,那么S就會收縮成一條線,我們可以認(rèn)為這條線的高度就是管道瞬時的傳輸速率,我們稱之為帶寬

屏幕快照 2018-01-11 下午7.58.25.png

之前我們提到過一個概念往返時延RTT(round-trip time) ,用以描述數(shù)據(jù)發(fā)出到被確認(rèn)的時間。那么在帶寬為v的管道上經(jīng)過時間RTT傳輸?shù)臄?shù)據(jù)就是整個管道的容量

屏幕快照 2018-01-11 下午7.59.04.png

我們稱之為帶寬時延乘積,公式如下

帶寬時延乘積(capacity)[bit] = 帶寬(bandwidth)[b/s] x 往返時延RTT(round-trip time) [s]

現(xiàn)在讓我們考慮下圖的這種情況

屏幕快照 2018-01-11 下午8.24.05.png

這種情況非常普遍,局域網(wǎng)內(nèi)的主機將數(shù)據(jù)發(fā)往處于另一個局域網(wǎng)的主機,需要經(jīng)過相對低速的廣域網(wǎng)。我們可以明顯看到瓶頸出現(xiàn)在路由器R1這里:R1左側(cè)的帶寬明顯大于右側(cè),當(dāng)輸入速率大于輸出速率時,就會擁塞。路由器R2則沒有問題,因為它是從低速的WAN內(nèi)向LAN傳輸數(shù)據(jù)。

需要注意的是經(jīng)過路由器R 2的分組,之間的間隔和在WAN內(nèi)保持一致的。如圖所示t1 = t2。雖然我們圖中看到的是WAN內(nèi)帶寬打滿,但是每個分組左側(cè)邊沿所在的位置標(biāo)示的才是該分組實際發(fā)出的時間。兩個標(biāo)示分組的矩形,它們左側(cè)邊沿之間的距離就是發(fā)出時間的間隔。

這可能有點比較難以理解,因為我們將實際數(shù)據(jù)報的傳遞抽象成了流。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1.這篇文章不是本人原創(chuàng)的,只是個人為了對這部分知識做一個整理和系統(tǒng)的輸出而編輯成的,在此鄭重地向本文所引用文章的...
    SOMCENT閱讀 13,390評論 6 174
  • 個人認(rèn)為,Goodboy1881先生的TCP /IP 協(xié)議詳解學(xué)習(xí)博客系列博客是一部非常精彩的學(xué)習(xí)筆記,這雖然只是...
    貳零壹柒_fc10閱讀 5,215評論 0 8
  • 傳輸層-TCP, TCP頭部結(jié)構(gòu) ,TCP序列號和確認(rèn)號詳解 TCP主要解決下面的三個問題 1.數(shù)據(jù)的可靠傳輸...
    抓兔子的貓閱讀 4,638評論 1 46
  • 套接字選項SO_RESUEADDR 即使端口處于2MSL狀態(tài),使用該選項,仍然能夠在該端口建立連接。服務(wù)器常會設(shè)置...
    Myth52125閱讀 1,523評論 0 0
  • 20.1 引言 在第15章我們看到TFTP使用了停止等待協(xié)議。數(shù)據(jù)發(fā)送方在發(fā)送下一個數(shù)據(jù)塊之前需要等待接收對已發(fā)送...
    張芳濤閱讀 941評論 0 2

友情鏈接更多精彩內(nèi)容