天天看點

TCP協定解析說明:1.網絡協定設計2.TCP和IP協定3.TCP協定4.端到端意義上的TCP協定效率5.IP網絡之上的TCP6.其它

       轉載自https://blog.csdn.net/dog250/article/details/6612496,感謝整理和分享。

說明:

1).本文以TCP的發展曆程解析容易引起混淆,誤會的方方面面

2).本文不會貼大量的源碼,大多數是以文字形式描述,我相信文字看起來是要比代碼更輕松的

3).針對對象:對TCP已經有了全面了解的人。因為本文不會解析TCP頭裡面的每一個字段或者3次握手的細節,也不會解釋慢啟動和快速重傳的定義

4).除了《TCP/IP詳解》(卷一,卷二)以及《Unix網絡程式設計》以及Linux源代碼之外,學習網絡更好的資源是RFC 

5).本文給出一個提綱,如果想了解細節,請直接查閱RFC

6).翻來覆去,終于找到了這篇備忘,本文基于這篇備忘文檔修改。

1.網絡協定設計

ISO提出了OSI分層網絡模型,這種分層模型是理論上的,TCP/IP最終實作了一個分層的協定模型,每一個層次對應一組網絡協定完成一組特定的功能,該組網絡協定被其下的層次複用和解複用。這就是分層模型的本質,最終所有的邏輯被編碼到線纜或者電磁波。

     分層模型是很好了解的,然而對于每一層的協定設計卻不是那麼容易。TCP/IP的漂亮之處在于:協定越往上層越複雜。我們把網絡定義為互相連接配接在一起的裝置,網絡的本質作用還是“端到端”的通信,然而希望互相通信的裝置并不一定要“直接”連接配接在一起,是以必然需要一些中間的裝置負責轉發資料,是以就把連接配接這些中間裝置的線纜上跑的協定定義為鍊路層協定,實際上所謂鍊路其實就是始發與一個裝置,通過一根線,終止于另一個裝置。我們把一條鍊路稱為“一跳”。是以一個端到端的網絡包含了“很多跳”。

2.TCP和IP協定

終止于IP協定,我們已經可以完成一個端到端的通信,為何還需要TCP協定?這是一個問題,了解了這個問題,我們就能了解TCP協定為何成了現在這個樣子,為何如此“複雜”,為何又如此簡單。

     正如其名字所展示的那樣,TCP的作用是傳輸控制,也就是控制端到端的傳輸,那為何這種控制不在IP協定中實作的。答案很簡單,那就是這會增加IP協定的複雜性,而IP協定需要的就是簡單。這是什麼原因造成的呢?

     首先我們認識一下為何IP協定是沙漏的細腰部分。它的下層是繁多的鍊路層協定,這些鍊路提供了互相截然不同且相差很遠的語義,為了互聯這些異構的網絡,我們需要一個網絡層協定起碼要提供一些适配的功能,另外它必然不能提供太多的“保證性服務”,因為上層的保證性依賴下層的限制性更強的保證性,你永遠無法在一個100M吞吐量的鍊路之上實作的IP協定保證1000M的吞吐量...

     IP協定設計為分組轉發協定,每一跳都要經過一個中間節點,路由的設計是TCP/IP網絡的另一大創舉,這樣,IP協定就無需方向性,路由資訊和協定本身不再強關聯,它們僅僅通過IP位址來關聯,是以,IP協定更加簡單。路由器作為中間節點也不能太複雜,這涉及到成本問題,是以路由器隻負責選路以及轉發資料包。

     是以傳輸控制協定必然需要在端點實作。在我們詳談TCP協定之前,首先要看一下它不能做什麼,由于IP協定不提供保證,TCP也不能提供依賴于IP下層鍊路的這種保證,比如帶寬,比如時延,這些都是鍊路層決定的,既然IP協定無法修補,TCP也不能,然而它卻能修正始于IP層的一些“不可保證性質”,這些性質包括IP層的不可靠,IP層的不按順序,IP層的無方向/無連接配接。

     将該小節總結一下,TCP/IP模型從下往上,功能增加,需要實作的裝置減少,然而裝置的複雜性卻在增加,這樣保證了成本的最小化,至于性能或者因素,靠軟體來調節吧,TCP協定就是這樣的軟體,實際上最開始的時候,TCP并不考慮性能,效率,公平性,正是考慮了這些,TCP協定才複雜了起來。

3.TCP協定

這是一個純軟體協定,為何将其設計上兩個端點,參見上一小節,本節詳述TCP協定,中間也穿插一些簡短的論述。

3.1.TCP協定

确切的說,TCP協定有兩重身份,作為網絡協定,它彌補了IP協定盡力而為服務的不足,實作了有連接配接,可靠傳輸,封包按序到達。作為一個主機軟體,它和UDP以及左右的傳輸層協定隔離了主機服務和網絡,它們可以被看做是一個多路複用/解複用器,将諸多的主機程序資料複用/解複用到IP層。可以看出,不管從哪個角度,TCP都作為一個接口存在,作為網絡協定,它和對端的TCP接口,實作TCP的控制邏輯,作為多路複用/解複用器,它和下層IP協定接口,實作協定棧的功能,而這正是分層網絡協定模型的基本定義(兩類接口,一類和下層接口,另一類和對等層接口)。

     我們習慣于将TCP作為協定棧的最頂端,而不把應用層協定當成協定棧的一部分,這部分是因為應用層被TCP/UDP解複用了之後,呈現出了一種太複雜的局面,應用層協定用一種不同截然不同的方式被解釋,應用層協定習慣于用類似ASN.1标準來封裝,這正展現了TCP協定作為多路複用/解複用器的重要性,由于直接和應用接口,它可以很容易直接被應用控制,實作不同的傳輸控制政策,這也是TCP被設計到離應用不太遠的地方的原因之一。

     總之,TCP要點有四,一曰有連接配接,二曰可靠傳輸,三曰資料按照到達,四曰端到端流量控制。注意,TCP被設計時隻保證這四點,此時它雖然也有些問題,然而很簡單,然而更大的問題很快呈現出來,使之不得不考慮和IP網絡相關的東西,比如公平性,效率,是以增加了擁塞控制,這樣TCP就成了現在這個樣子。

3.2.有連接配接,可靠傳輸,資料按序到達的TCP

IP協定是沒有方向的,資料報傳輸能到達對端全靠路由,是以它是一跳一跳地到達對端的,隻要有一跳沒有到達對端的路由,那麼資料傳輸将失敗,其實路由也是網際網路的核心之一,實際上IP層提供的核心基本功能有兩點,第一點是位址管理,第二點就是路由選路。TCP利用了IP路由這個簡單的功能,是以TCP不必考慮選路,這又一個它被設計成端到端協定的原因。

     既然IP已經能盡力讓單獨的資料報到達對端,那麼TCP就可以在這種盡力而為的網絡上實作其它的更加嚴格的控制功能。TCP給無連接配接的IP網絡通信增加了連接配接性,确認了已經發送出去的資料的狀态,并且保證了資料的順序。

3.2.1.有連接配接

這是TCP的基本,因為後續的傳輸的可靠性以及資料順序性都依賴于一條連接配接,這是最簡單的實作方式,是以TCP被設計成一種基于流的協定,既然TCP需要事先建立連接配接,之後傳輸多少資料就無所謂了,隻要是同一連接配接的資料能識别出來即可。

疑難雜症1:3次握手和4次揮手

TCP使用3次握手建立一條連接配接,該握手初始化了傳輸可靠性以及資料順序性必要的資訊,這些資訊包括兩個方向的初始序列号,确認号由初始序列号生成,使用3次握手是因為3次握手已經準備好了傳輸可靠性以及資料順序性所必要的資訊,該握手的第3次實際上并不是需要單獨傳輸的,完全可以和資料一起傳輸。

     TCP使用4次揮手拆除一條連接配接,為何需要4次呢?因為TCP是一個全雙工協定,必須單獨拆除每一條信道。注意,4次揮手和3次握手的意義是不同的,很多人都會問為何建立連接配接是3次握手,而拆除連接配接是4次揮手。3次握手的目的很簡單,就是配置設定資源,初始化序列号,這時還不涉及資料傳輸,3次就足夠做到這個了,而4次揮手的目的是終止資料傳輸,并回收資源,此時兩個端點兩個方向的序列号已經沒有了任何關系,必須等待兩方向都沒有資料傳輸時才能拆除虛鍊路,不像初始化時那麼簡單,發現SYN标志就初始化一個序列号并确認SYN的序列号。是以必須單獨分别在一個方向上終止該方向的資料傳輸。

疑難雜症2:TIME_WAIT狀态

為何要有這個狀态,原因很簡單,那就是每次建立連接配接的時候序列号都是随機産生的,并且這個序列号是32位的,會回繞。現在我來解釋這和TIME_WAIT有什麼關系。

     任何的TCP分段都要在盡力而為的IP網絡上傳輸,中間的路由器可能會随意的緩存任何的IP資料報,它并不管這個IP資料報上被承載的是什麼資料,然而根據經驗和網際網路的大小,一個IP資料報最多存活MSL(這是根據地球表面積,電磁波在各種媒體中的傳輸速率以及IP協定的TTL等綜合推算出來的,如果在火星上,這個MSL會大得多...)。

     現在我們考慮終止連接配接時的被動方發送了一個FIN,然後主動方回複了一個ACK,然而這個ACK可能會丢失,這會造成被動方重發FIN,這個FIN可能會在網際網路上存活MSL。

     如果沒有TIME_WAIT的話,假設連接配接1已經斷開,然而其被動方最後重發的那個FIN(或者FIN之前發送的任何TCP分段)還在網絡上,然而連接配接2重用了連接配接1的所有的5元素(源IP,目的IP,TCP,源端口,目的端口),剛剛将建立好連接配接,連接配接1遲到的FIN到達了,這個FIN将以比較低但是确實可能的機率終止掉連接配接2.

     為何說是機率比較低呢?這涉及到一個比對問題,遲到的FIN分段的序列号必須落在連接配接2的一方的期望序列号範圍之内。雖然這種巧合很少發生,但确實會發生,畢竟初始序列号是随機産生了。是以終止連接配接的主動方必須在接受了被動方且回複了ACK之後等待2*MSL時間才能進入CLOSE狀态,之是以乘以2是因為這是保守的算法,最壞情況下,針對被動方的ACK在以最長路線(經曆一個MSL)經過網際網路馬上到達被動方時丢失。

     為了應對這個問題,RFC793對初始序列号的生成有個建議,那就是設定一個基準,在這個基準之上搞随機,這個基準就是時間,我們知道時間是單調遞增的。然而這仍然有問題,那就是回繞問題,如果發生回繞,那麼新的序列号将會落到一個很低的值。是以最好的辦法就是避開“重疊”,其含義就是基準之上的随機要設定一個範圍。

     要知道,很多人很不喜歡看到伺服器上出現大量的TIME_WAIT狀态的連接配接,是以他們将TIME_WAIT的值設定的很低,這雖然在大多數情況下可行,然而确實也是一種冒險行為。最好的方式就是,不要重用一個連接配接。

疑難雜症3:重用一個連接配接和重用一個套接字

這是根本不同的,單獨重用一個套接字一般不會有任何問題,因為TCP是基于連接配接的。比如在伺服器端出現了一個TIME_WAIT連接配接,那麼該連接配接辨別了一個五元素,隻要用戶端不使用相同的源端口,連接配接伺服器是沒有問題的,因為遲到的FIN永遠不會到達這個連接配接。記住,一個五元素辨別了一個連接配接,而不是一個套接字(當然,對于BSD套接字而言,服務端的accept套接字确實辨別了一個連接配接)。

3.2.2.傳輸可靠性

基本上傳輸可靠性是靠确認号實作的,也就是說,每發送一個分段,接下來接收端必然要發送一個确認,發送端收到确認後才可以發送下一個位元組。這個原則最簡單不過了,教科書上的“停止-等待”協定就是這個原則的位元組版本,隻是TCP使用了滑動視窗機制使得每次不一定發送一個位元組,但是這是後話,本節僅僅談一下确認的逾時機制。

     怎麼知道資料到達對端呢?那就是對端發送一個确認,但是如果一直收不到對端的确認,發送端等多久呢?如果一直等下去,那麼将無法發現資料的丢失,協定将不可用,如果等待時間過短,可能确認還在路上,是以等待時間是個問題,另外如何去管理這個逾時時間也是一個問題。

疑難雜症4:逾時時間的計算

絕對不能随意去揣測逾時的時間,而應該給出一個精确的算法去計算。毫無疑問,一個TCP分段的回複到達的時間就是一個資料報往返的時間,是以标準定義了一個新的名詞RTT,代表一個TCP分段的往返時間。然而我們知道,IP網絡是盡力而為的,并且路由是動态的,且路由器會毫無先兆的緩存或者丢棄任何的資料報,是以這個RTT是需要動态測量的,也就是說起碼每隔一段時間就要測量一次,如果每次都一樣,萬事大吉,然而世界并非如你所願,是以我們需要找到的恰恰的一個“平均值”,而不是一個準确值。

     這個平均值如果僅僅直接通過計算多次測量值取算術平均,那是不恰當的,因為對于資料傳輸延時,我們必須考慮的路徑延遲的瞬間抖動,否則如果兩次測量值分别為2和98,那麼逾時值将是50,這個值對于2而言,太大了,結果造成了資料的延遲過大(本該重傳的等待了好久才重傳),然而對于98而言,太小了,結果造成了過度重傳(路途遙遠,本該很慢,結果大量重傳已經正确确認但是遲到的TCP分段)。

     是以,除了考慮每兩次測量值的偏差之外,其變化率也應該考慮在内,如果變化率過大,則通過以變化率為自變量的函數為主計算RTT(如果陡然增大,則取值為比較大的正數,如果陡然減小,則取值為比較小的負數,然後和平均值權重求和),反之如果變化率很小,則取測量平均值。這是不言而喻的,這個算法至今仍然工作的很好。

疑難雜症5:逾時計時器的管理-每連接配接單一計時器

很顯然,對每一個TCP分段都生成一個計時器是最直接的方式,每個計時器在RTT時間後到期,如果沒有收到确認,則重傳。然而這隻是理論上的合理,對于大多數作業系統而言,這将帶來巨大的記憶體開銷和排程開銷,是以采取每一個TCP連接配接單一計時器的設計則成了一個預設的選擇。可是單一的計時器怎麼管理如此多的發出去的TCP分段呢?又該如何來設計單一的計時器呢。

     設計單一計時器有兩個原則:1.每一個封包在長期收不到确認都必須可以逾時;2.這個長期收不到中長期不能和測量的RTT相隔太遠。是以RFC2988定義一套很簡單的原則:

a.發送TCP分段時,如果還沒有重傳定時器開啟,那麼開啟它。

b.發送TCP分段時,如果已經有重傳定時器開啟,不再開啟它。

c.收到一個非備援ACK時,如果有資料在傳輸中,重新開啟重傳定時器。

d.收到一個非備援ACK時,如果沒有資料在傳輸中,則關閉重傳定時器。

我們看看這4條規則是如何做到以上兩點的,根據a和c(在c中,注意到ACK是非備援的),任何TCP分段隻要不被确認,逾時定時器總會逾時的。然而為何需要c呢?隻有規則a存在的話,也可以做到原則1。實際上确實是這樣的,但是為了不會出現過早重傳,才添加了規則c,如果沒有規則c,那麼萬一在重傳定時器到期前,發送了一些資料,這樣在定時器到期後,除了很早發送的資料能收到ACK外,其它稍晚些發送的資料的ACK都将不會到來,是以這些資料都将被重傳。有了規則c之後,隻要有分段ACK到來,則重置重傳定時器,這很合理,是以大多數正常情況下,從資料的發出到ACK的到來這段時間以及計算得到的RTT以及重傳定時器逾時的時間這三者相差并不大,一個ACK到來後重置定時器可以保護後發的資料不被過早重傳。

     這裡面還有一些細節需要說明。一個ACK到來了,說明後續的ACK很可能會依次到來,也就是說丢失的可能性并不大,另外,即使真的有後發的TCP分段丢失現象發生,也會在最多2倍定時器逾時時間的範圍内被重傳(假設該封包是第一個封包發出啟動定時器之後馬上發出的,丢失了,第一個封包的ACK到來後又重新開機了定時器,又經過了一個逾時時間才會被重傳)。雖然這裡還沒有涉及擁塞控制,但是可見網絡擁塞會引起丢包,丢包會引起重傳,過度重傳反過來加重網絡擁塞,設定規則c的結果可以緩解過多的重傳,畢竟将啟動定時器之後發送的資料的重傳逾時時間拉長了最多一倍左右。最多一倍左右的逾時偏差做到了原則2,即“這個長期收不到中長期不能和測量的RTT相隔太遠”。

     還有一點,如果是一個發送序列的最後一個分段丢失了,後面就不會收到備援ACK,這樣就隻能等到逾時了,并且逾時時間幾乎是肯定會比定時器逾時時間更長。如果這個分段是在發送序列的靠後的時間發送的且和前面的發送時間相隔時間較遠,則其逾時時間不會很大,反之就會比較大。

疑難雜症6:何時測量RTT

目前很多TCP實作了時間戳,這樣就友善多了,發送端再也不需要儲存發送分段的時間了,隻需要将其放入協定頭的時間戳字段,然後接收端将其回顯在ACK即可,然後發送端收到ACK後,取出時間戳,和目前時間做算術差,即可完成一次RTT的測量。

3.2.3.資料順序性

基本上傳輸可靠性是靠序列号實作的。

疑難雜症7:确認号和逾時重傳

确認号是一個很詭異的東西,因為TCP的發送端對于發送出去的一個資料序列,它隻要收到一個确認号就認為确認号前面的資料都被收到了,即使前面的某個确認号丢失了,也就是說,發送端隻認最後一個确認号。這是合理的,因為确認号是接收端發出的,接收端隻确認按序到達的最後一個TCP分段。

     另外,發送端重發了一個TCP封包并且接收到該TCP分段的确認号,并不能說明這個重發的封包被接收了,也可能是資料早就被接收了,隻是由于其ACK丢失或者其ACK延遲到達導緻了逾時。值得說明的是,接收端會丢棄任何重複的資料,即使丢棄了重複的資料,其ACK還是會照發不誤的。

     标準的早期TCP實作為,隻要一個TCP分段丢失,即使後面的TCP分段都被完整收到,發送端還是會重傳從丢失分段開始的所有封包,這就會導緻一個問題,那就是重傳風暴,一個分段丢失,引起大量的重傳。這種風暴實則不必要的,因為大多數的TCP實作中,接收端已經緩存了亂序的分段,這些被重傳的丢失分段之後的分段到達接收端之後,很大的可能性是被丢棄。關于這一點在擁塞控制被引入之後還會提及(問題先述為快:本來封包丢失導緻逾時就說明網絡很可能已然擁塞,重傳風暴隻能加重其擁塞程度)。

疑難雜症8:亂序資料緩存以及選擇确認

TCP是保證資料順序的,但是并不意味着它總是會丢棄亂序的TCP分段,具體會不會丢棄是和具體實作相關的,RFC建議如果記憶體允許,還是要緩存這些亂序到來的分段,然後實作一種機制等到可以拼接成一個按序序列的時候将緩存的分段拼接,這就類似于IP協定中的分片一樣,但是由于IP資料報是不确認的,是以IP協定的實作必須緩存收到的任何分片而不能将其丢棄,因為丢棄了一個IP分片,它就再也不會到來了。     

     現在,TCP實作了一種稱為選擇确認的方式,接收端會顯式告訴發送端需要重傳哪些分段而不需要重傳哪些分段。這無疑避免了重傳風暴。

疑難雜症9:TCP序列号的回繞的問題

TCP的序列号回繞會引起很多的問題,比如序列号為s的分段發出之後,m秒後,序列号比s小的序列号為j的分段發出,隻不過此時的j比上一個s多了一圈,這就是回繞問題,那麼如果這後一個分段到達接收端,這就會引發徹底亂序-本來j該在s後面,結果反而到達前面了,這種亂序是TCP協定檢查不出來的。我們仔細想一下,這種情況确實會發生,資料分段并不是一個位元組一個位元組發送出去的,如果存在一個速率為1Gbps的網絡,TCP發送端1秒會發送125MB的資料,32位的序列号空間能傳輸2的32次方個位元組,也就是說32秒左右就會發生回繞,我們知道這個值遠小于MSL值,是以會發生的。

     有個細節可能會引起誤會,那就是TCP的視窗大小空間是序列号空間的一半,這樣恰好在滿載情況下,資料能填滿發送視窗和接收視窗,序列号空間正好夠用。然而事實上,TCP的初始序列号并不是從0開始的,而是随機産生的(當然要輔助一些更精妙的算法),是以如果初始序列号比較接近2的32次方,那麼很快就會回繞。

     當然,如今可以用時間戳選項來輔助作為序列号的一個識别的部分,接收端遇到回繞的情況,需要比較時間戳,我們知道,時間戳是單調遞增的,雖然也會回繞,然而回繞時間卻要長很多。這隻是一種政策,在此不詳談。還有一個很現實的問題,理論上序列号會回繞,但是實際上,有多少TCP的端點主機直接架設在1G的網絡線纜兩端并且接收方和發送方的視窗還能恰好被同時填滿。另外,就算發生了回繞,也不是一件特别的事情,回繞在計算機裡面太常見了,隻需要能識别出來即可解決,對于TCP的序列号而言,在高速網絡(點對點網絡或者以太網)的兩端,資料發生亂序的可能性很小,是以當收到一個序列号突然變為0或者終止序列号小于起始序列号的情況後,很容易辨識出來,隻需要和前一個确認的分段比較即可,如果在一個經過路由器的網絡兩端,會引發IP資料報的順序重排,對于TCP而言,雖然還會發生回繞,也會慢得多,且考慮到擁塞視窗(目前還沒有引入)一般不會太大,視窗也很難被填滿到65536。

3.2.4.端到端的流量控制

端到端的流量控制使用滑動視窗來實作。滑動視窗的原理非常簡單,基本就是一個生産者/消費者模型

疑難雜症10:流量控制的真實意義

很多人以為流量控制會很有效的協調兩端的流量比對,确實是這樣,但是如果你考慮到網絡的使用率問題,TCP的流量控制機制就不那麼完美了,造成這種局面的原因在于,滑動視窗隻是限制了最大發送的資料,卻沒有限制最小發送的資料,結果導緻一些很小的資料被封裝成TCP分段,封包協定頭所占的比例過于大,造成網絡使用率下降,這就引出了接下來的内容,那就是端到端意義的TCP協定效率。

~~~~~~~~~~~~~~~~~~~~

承上啟下

終于到了闡述問題的時候了,以上的TCP協定實作的非常簡單,這也是TCP的标準實作,然而很快我們就會發現各種各樣的問題。這些問題導緻了标準化協會對TCP協定進行了大量的修補,這些修補雜糅在一起讓人們有些雲裡霧裡,不知所措。本文檔就旨在分離這些雜亂的情況,實際上,根據RFC,這些雜亂的情況都是可以找到其單獨的發展軌迹的。

~~~~~~~~~~~~~~~~~~~~

4.端到端意義上的TCP協定效率

4.1.三個問題以及解決

問題1描述:接收端處理慢,導緻接收視窗被填滿

這明顯是速率不比對引發的問題,然而即使速率不比對,隻要滑動視窗能協調好它們的速率就好,要快都快,要慢都慢,事實上滑動視窗在這一點上做的很好。但是如果我們不得不從效率上來考慮問題的話,事實就不那麼樂觀了。考慮此時接收視窗已然被填滿,慢速的應用程式慢騰騰的讀取了一個位元組,空出一個位置,然後通告給TCP的發送端,發送端得知空出一個位置,馬上發出一個位元組,又将接收端填滿,然後接收應用程式又一次慢騰騰...這就是糊塗視窗綜合症,一個大多數人都很熟悉的詞。這個問題極大的浪費了網絡帶寬,降低了網絡使用率。好比從大同拉100噸煤到北京需要一輛車,拉1Kg煤到北京也需要一輛車(超級誇張的一個例子,請不要相信),但是一輛車開到北京的開銷是一定的...

問題1解決:視窗通告

對于問題1,很顯然問題出在接收端,我們沒有辦法限制發送端不發送小分段,但是卻可以限制接收端通告小視窗,這是合理的,這并不影響應用程式,此時經典的延遲/吞吐量反比律将不再适用,因為接收視窗是滿的,其空出一半空間表示還有一半空間有資料沒有被應用讀取,和其空出一個位元組的空間的效果是一樣的,是以可以限制接收端當視窗為0時,直接通告給發送端以阻止其繼續發送資料,隻有當其接收視窗再次達到MSS的一半大小的時候才通告一個不為0的視窗,此前對于所有的發送端的視窗probe分段(用于探測接收端視窗大小的probe分段,由TCP标準規定),全部通告視窗為0,這樣發送端在收到視窗不為0的通告,那麼肯定是一個比較大的視窗,是以發送端可以一次性發出一個很大的TCP分段,包含大量資料,也即拉了好幾十噸的煤到北京,而不是隻拉了幾公斤。

     即,限制視窗通告時機,解決糊塗視窗綜合症

問題2描述:發送端持續發送小包,導緻視窗閑置

這明顯是發送端引起的問題,此時接收端的視窗開得很大,然而發送端卻不積累資料,還是一味的發送小塊資料分段。隻要發送了任和的分段,接收端都要無條件接收并且确認,這完全符合TCP規範,是以必然要限制發送端不發送這樣的小分段。

問題2解決:Nagle算法

Nagel算法很簡單,标準的Nagle算法為:

IF 資料的大小和視窗的大小都超過了MSS

    Then 發送資料分段

ELSE

    IF 還有發出的TCP分段的确認沒有到來

        Then 積累資料到發送隊列的末尾的TCP分段

    ELSE

        發送資料分段

    EndIF

EndIF

可是後來,這個算法變了,變得更加靈活了,其中的:

    IF 還有發出的TCP分段的确認沒有到來

變成了

    IF 還有發出的不足MSS大小的TCP分段的确認沒有到來

這樣如果發出了一個MSS大小的分段還沒有被确認,後面也是可以随時發送一個小分段的,這個改進降低了算法對延遲時間的影響。這個算法展現了一種自适應的政策,越是确認的快,越是發送的快,雖然Nagle算法看起來在積累資料增加吞吐量的同時也加大的時延,可事實上,如果對于類似互動式的應用,時延并不會增加,因為這類應用回複資料也是很快的,比如Telnet之類的服務必然需要回顯字元,是以能和對端進行自适應協調。

     注意,Nagle算法是預設開啟的,但是卻可以關閉。如果在開啟的情況下,那麼它就嚴格按照上述的算法來執行。

問題3.确認号(ACK)本身就是不含資料的分段,是以大量的确認号消耗了大量的帶寬

這是TCP為了確定可靠性傳輸的規範,然而大多數情況下,ACK還是可以和資料一起捎帶傳輸的。如果沒有捎帶傳輸,那麼就隻能單獨回來一個ACK,如果這樣的分段太多,網絡的使用率就會下降。從大同用火車拉到北京100噸煤,為了确認煤已收到,北京需要派一輛同樣的火車空載開到大同去複命,因為沒有别的交通工具,隻有火車。如果這位複命者剛開着一列火車走,又從大同來了一車煤,這拉煤的哥們兒又要開一列空車去複命了。

問題3的解決:

RFC建議了一種延遲的ACK,也就是說,ACK在收到資料後并不馬上回複,而是延遲一段可以接受的時間,延遲一段時間的目的是看能不能和接收方要發給發送方的資料一起回去,因為TCP協定頭中總是包含确認号的,如果能的話,就将ACK一起捎帶回去,這樣網絡使用率就提高了。往大同複命的确認者不必開一輛空載火車回大同了,此時北京正好有一批貨物要送往大同,這位複命者搭着這批貨的火車傳回大同。

     如果等了一段可以接受的時間,還是沒有資料要發往發送端,此時就需要單獨發送一個ACK了,然而即使如此,這個延遲的ACK雖然沒有等到可以被捎帶的資料分段,也可能等到了後續到來的TCP分段,這樣它們就可以取最大者一起傳回了,要知道,TCP的确認号是收到的按序封包的最後一個位元組的後一個位元組。最後,RFC建議,延遲的ACK最多等待兩個分段的積累确認。

4.2.分析三個問題之間的關聯

三個問題導緻的結果是相同的,但是要知道它們的原因本質上是不同的,問題1幾乎總是出現在接收端視窗滿的情況下,而問題2幾乎總是發生在視窗閑置的情況下,問題3看起來是最無聊的,然而由于TCP的要求,必須要有确認号,而且一個确認号就需要一個TCP分段,這個分段不含資料,無疑是很小的。

     三個問題都導緻了網絡使用率的降低。雖然兩個問題導緻了同樣的結果,但是必須認識到它們是不同的問題,很自然的将這些問題的解決方案彙總在一起,形成一個全局的解決方案,這就是如今的作業系統中的解決方案。

4.3.問題的雜糅情況

疑難雜症11:糊塗視窗解決方案和Nagle算法

糊塗視窗綜合症患者希望發送端積累TCP分段,而Nagle算法确實保證了一定的TCP分段在發送端的積累,另外在延遲ACK的延遲的那一會時間,發送端會利用這段時間積累資料。然而這卻是三個不同的問題。Nagle算法可以緩解糊塗視窗綜合症,卻不是治本的良藥。

疑難雜症12:Nagle算法和延遲ACK

延遲ACK會延長ACK到達發送端的時間,由于标準Nagle算法隻允許一個未被确認的TCP分段,那無疑在接收端,這個延遲的ACK是毫無希望等待後續資料到來最終進行積累确認的,如果沒有資料可以捎帶這個ACK,那麼這個ACK隻有在延遲确認定時器逾時的時候才會發出,這樣在等待這個ACK的過程中,發送端又積累了一些資料,是以延遲ACK實際上是在增加延遲的代價下加強了Nagle算法。在延遲ACK加Nagle算法的情況下,接收端隻有不斷有資料要發回,才能同時既保證了發送端的分段積累,又保證了延遲不增加,同時還沒有或者很少有空載的ACK。

     要知道,延遲ACK和Nagle是兩個問題的解決方案。

疑難雜症13:到底何時可以發送資料

到底何時才能發送資料呢?如果單從Nagle算法上看,很簡單,然而事實證明,情況還要更複雜些。如果發送端已經排列了3個TCP分段,分段1,分段2,分段3依次被排入,三個分段都是小分段(不符合Nagle算法中立即發送的标準),此時已經有一個分段被發出了,且其确認還沒有到來,請問此時能發送分段1和2嗎?如果按照Nagle算法,是不能發送的,但實際上它們是可以發送的,因為這兩個分段已經沒有任何機會再積累新的資料了,新的資料肯定都積累在分段3上了。問題在于,分段還沒有積累到一定大小時,怎麼還可以産生新的分段?這是可能的,但這是另一個問題,在此不談。

     Linux的TCP實作在這個問題上表現的更加靈活,它是這麼判斷能否發送的(在開啟了Nagle的情況下): 

IF (沒有超過擁塞視窗大小的資料分段未确認 || 資料分段中包含FIN ) &&

    資料分段沒有超越視窗邊界

    Then 

    IF 分段在中間(上述例子中的分段1和2) ||

           分段是緊急模式            ||

       通過上述的Nagle算法(改進後的Nagle算法)

        Then 發送分段

    EndIF 

EndIF

     曾經我也改過Nagle算法,确切的說不是修改Nagle算法,而是修改了“到底何時能發送資料”的政策,以往都是發送端判斷能否發送資料的,可是如果此時有延遲ACK在等待被捎帶,而待發送的資料又由于積累不夠或者其它原因不能發送,是以兩邊都在等,這其實在某些情況下不是很好。我所做的改進中對待何時能發送資料又增加了一種情況,這就是“ACK拉”的情況,一旦有延遲ACK等待發送,判斷一下有沒有資料也在等待發送,如果有的話,看看資料是否大到了一定程度,在此,我選擇的是MSS的一半:

IF (沒有超過擁塞視窗大小的資料分段未确認 || 資料分段中包含FIN ) &&

     資料分段沒有超越視窗邊界                      

    Then 

    IF 分段在中間(上述例子中的分段1和2) ||

           分段是緊急模式            ||

       通過上述的Nagle算法(改進後的Nagle算法)

        Then 發送分段

    EndIF 

ELSE IF 有延遲ACK等待傳輸                &&

    發送隊列中有待發送的TCP分段       &&

    發送隊列的頭分段大小大于MSS的一半

        Then 發送隊列頭分段且捎帶延遲ACK 

EndIF

另外,發送隊列頭分段的大小是可以在統計意義上動态計算的,也不一定非要是MSS大小的一半。我們發現,這種算法對于互動式網路應用是自适應的,你打字越快,特定時間内積累的分段就越長,對端回複的越快(可以捎帶ACK),本端發送的也就越快(以Echo舉例會更好了解)。  

疑難雜症14:《TCP/IP詳解(卷一)》中Nagle算法的例子解讀

這個問題在網上搜了很多的答案,有的說RFC的建議,有的說别的。可是實際上這就是一個典型的“競态問題”:

首先伺服器發了兩個分段:

資料段12:ack 14

資料段13:ack 14,54:56

然後用戶端發了兩個分段:

資料段14:ack 54,14:17

資料段15:ack 56,17:18

可以看到資料段14本來應該确認56的,但是确認的卻是54。也就是說,資料段已經移出隊列将要發送但還未發送的時候,資料段13才到來,軟中斷處理程式搶占了資料段14的發送程序,要知道此時隻是把資料段14移出了隊列,還沒有更新任何的狀态資訊,比如“發出但未被确認的分段數量”,此時軟中斷處理程式順利接收了分段13,然後更新視窗資訊,并且檢檢視有沒有資料要發送,由于分段14已經移出隊列,下一個接受發送檢查的就是分段15了,由于狀态資訊還沒有更新,是以分段15順利通過發送檢測,發送完成。

     可以看Linux的源代碼了解相關資訊,tcp_write_xmit這個函數在兩個地方會被調用,一個是TCP的發送程序中,另一個就是軟中斷的接收進行中,兩者在調用中的競态就會引起《詳解》中的那種情況。注意,這種不加鎖的發送方式是合理的,也是最高效的,是以TCP的處理語義會做出判斷,丢棄一切不該接收或者重複接收的分段的。

~~~~~~~~~~~~~~~~~~~~

承上啟下

又到了該承上啟下,到此為止,我們叙述的TCP還都是簡單的TCP,就算是簡單的TCP,也存在上述的諸多問題,就更别提繼續增加TCP的複雜性了。到此為止,我們的TCP都是端到端意義上的,然而實際上TCP要跑在IP網絡之上的,而IP網絡的問題是很多的,是一個很擁堵網絡。不幸的是,TCP的有些關于确認和可靠性的機制還會加重IP網絡的擁堵。

~~~~~~~~~~~~~~~~~~~~

5.IP網絡之上的TCP

5.1.端到端的TCP協定和IP協定之間的沖突

端到端的TCP隻能看到兩個節點,那就是自己和對方,它們是看不到任何中間的路徑的。可是IP網絡卻是一跳一跳的,它們的沖突之處在于TCP的端到端流量控制必然會導緻網絡擁堵。因為每條TCP連接配接的一端隻知道它對端還有多少空間用于接收資料,它們并不管到達對端的路徑上是否還有這麼大的容量,事實上所有連接配接的這些空間加在一起将瞬間超過IP網絡的容量,是以TCP也不可能按照滑動視窗流量控制機制很理想的運作。

     勢必需要一種擁塞控制機制,反應路徑的擁塞情況。

疑難雜症15:擁塞控制的本質

由于TCP是端到端協定,是以兩端之間的控制範疇屬于流量控制,IP網絡的擁塞會導緻TCP分段的丢失,由于TCP看不到中間的路由器,是以這種丢失隻會發生中間路由器,當然兩個端點的網卡或者IP層丢掉資料分段也是TCP看不到的。是以擁塞控制必然作用于IP鍊路。事實上我們可以得知,隻有在以下情況下擁塞控制才會起作用:

a.兩個或兩個以上的連接配接(其中一個一定要是TCP,另一個可以是任意連接配接)經過同一個路由器或者同一個鍊路時;

b.隻有一個TCP連接配接,然而它經過了一個路由器時。

其它情況下是不會擁塞的。因為一個TCP總是希望獨享整條網絡通路,而這對于多個連接配接而言是不可能的,必須保證TCP的公平性,這樣這種擁塞控制機制才合理。本質上,擁塞的原因就是大家都想獨享全部帶寬資源,結果導緻擁塞,這也是合理的,畢竟TCP看不到網絡的狀态,同時這也決定了TCP的擁塞控制必須采用試探性的方式,最終到達一個足以引起其“反應”的“刺激點”。

     擁塞控制需要完成以下兩個任務:1.公平性;2.擁塞之後退出擁塞狀态。

疑難雜症16:影響擁塞的因素

我們必須認識到擁塞控制是一個整體的機制,它不偏向于任何TCP連接配接,是以這個機制内在的就包含了公平性。那麼影響擁塞的因素都有什麼呢?具有諷刺意味的是,起初TCP并沒有擁塞控制機制,正是TCP的逾時重傳風暴(一個分段丢失造成後續的已經發送的分段均被重傳,而這些重傳大多數是不必要的)加重了網絡的擁塞。是以重傳必然不能過頻,必須把重傳定時器的逾時時間設定的稍微長一些,而這一點在單一重傳定時器的設計中得到了加強。除此TCP自身的因素之外,其它所有的擁塞都可以靠擁塞控制機制來自動完成。

     另外,不要把路由器想成一種線速轉發裝置,再好的路由器隻要接入網絡,總是會拉低網絡的總帶寬,是以即使隻有一個TCP連接配接,由于TCP的發送方總是以發送鍊路的帶寬發送分段,這些分段在經過路由器的時候排隊和處理總是會有時延,是以最終肯定會丢包的。

     最後,丢包的延後性也會加重擁塞。假設一個TCP連接配接經過了N個路由器,前N-1個路由器都能順利轉發TCP分段,但是最後一個路由器丢失了一個分段,這就導緻了這些丢失的分段浪費了前面路由器的大量帶寬。

5.2.擁塞控制的政策

在介紹擁塞控制之前,首先介紹一下擁塞視窗,它實際上表示的也是“可以發送多少資料”,然而這個和接收端通告的接收視窗意義是不一樣的,後者是流量控制用的視窗,而前者是擁塞控制用的視窗,展現了網絡擁塞程度。

     擁塞控制整體上分為兩類,一類是試探性的擁塞探測,另一類則是擁塞避免(注意,不是正常意義上的擁塞避免)。

5.2.1.試探性的擁塞探測分為兩類,之一是慢啟動,之二是擁塞視窗加性擴大(也就是熟知的擁塞避免,然而這種方式是避免不了擁塞的)。

5.2.2.擁塞避免方式擁塞控制旨在還沒有發生擁塞的時候就先提醒發送端,網絡擁塞了,這樣發送端就要麼可以進入快速重傳/快速恢複或者顯式的減小擁塞視窗,這樣就避免網絡擁塞的一沓糊塗之後出現逾時,進而進入慢啟動階段。

5.2.3.快速重傳和快速恢複。所謂快速重傳/快速恢複是針對慢啟動的,我們知道慢啟動要從1個MSS開始增加擁塞視窗,而快速重傳/快速恢複則是一旦收到3個備援ACK,不必進入慢啟動,而是将擁塞視窗縮小為目前閥值的一半加上3,然後如果繼續收到備援ACK,則将擁塞視窗加1個MSS,直到收到一個新的資料ACK,将視窗設定成正常的閥值,開始加性增加的階段。

     當進入快速重傳時,為何要将擁塞視窗縮小為目前閥值的一半加上3呢?加上3是基于資料包守恒來說的,既然已經收到了3個備援ACK,說明有三個資料分段已經到達了接收端,既然三個分段已經離開了網絡,那麼就是說可以在發送3個分段了,隻要再收到一個備援ACK,這也說明1個分段已經離開了網絡,是以就将擁塞視窗加1個MSS。直到收到新的ACK,說明直到收到第三個備援ACK時期發送的TCP分段都已經到達對端了,此時進入正常階段開始加性增加擁塞視窗。

疑難雜症17:逾時重傳和收到3個備援ACK後重傳

這兩種重傳的意義是不同的,逾時重傳一般是因為網絡出現了嚴重擁塞(沒有一個分段到達,如果有的話,肯定會有ACK的,若是正常ACK,則重置重傳定時器,若是備援ACK,則可能是個别封包丢失或者被重排序,若連續3個備援ACK,則很有可能是個别分段丢失),此時需要更加嚴厲的縮小擁塞視窗,是以此時進入慢啟動階段。而收到3個備援ACK後說明确實有中間的分段丢失,然而後面的分段确實到達了接收端,這因為這樣才會發送備援ACK,這一般是路由器故障或者輕度擁塞或者其它不太嚴重的原因引起的,是以此時擁塞視窗縮小的幅度就不能太大,此時進入快速重傳/快速恢複階段。

疑難雜症18:為何收到3個備援ACK後才重傳

這是一種權衡的結構,收到兩個或者一個備援ACK也可以重傳,但是這樣的話可能或造成不必要的重傳,因為兩個資料分段發生亂序的可能性不大,超過三個分段發生亂序的可能性才大,換句話說,如果僅僅收到一個亂序的分段,那很可能被中間路由器重排了,那麼另一個分段很可能馬上就到,然而如果連續收到了3個分段都沒能彌補那個缺漏,那很可能是它丢失了,需要重傳。是以3個備援ACK是一種權衡,在減少不必要重傳和确實能檢測出單個分段丢失之間所作的權衡。

     注意,備援ACK是不能捎帶的。

疑難雜症19:乘性減和加性增的深層含義

為什麼是乘性減而加性增呢?擁塞視窗的增加受惠的隻是自己,而擁塞視窗減少受益的大家,可是自己卻受到了傷害。哪一點更重要呢?我們知道TCP的擁塞控制中内置了公平性,恰恰就是這種乘性減實作了公平性。擁塞視窗的1個MSS的改變影響一個TCP發送者,為了使得自己擁塞視窗的減少影響更多的TCP發送者-讓更多的發送者受益,那麼采取了乘性減的政策。

     當然,BIC算法提高了加性增的效率,不再一個一個MSS的加,而是一次加比較多的MSS,采取二分查找的方式逐漸找到不丢包的點,然後加性增。

疑難雜症20:TCP連接配接的傳輸穩定狀态是什麼

首先,先說一下發送端的發送視窗怎麼确定,它取的是擁塞視窗和接收端通告視窗的最小值。然後,我們提出三種發送視窗的穩定狀态:

a.IP網際網路絡上接收端擁有大視窗的經典鋸齒狀

b.IP網際網路絡上接收端擁有小視窗的直線狀态

c.直連網絡端點間的滿載狀态下的直線狀态

其中a是大多數的狀态,因為一般而言,TCP連接配接都是建立在網際網路上的,而且是大量的,比如Web浏覽,電子郵件,網絡遊戲,Ftp下載下傳等等。TCP發送端用慢啟動或者擁塞避免方式不斷增加其擁塞視窗,直到丢包的發生,然後進入慢啟動或者擁塞避免階段(要看是由于逾時丢包還是由于備援ACK丢包),此時發送視窗将下降到1或者下降一半,這種情況下,一般接收端的接收視窗是比較大的,畢竟IP網絡并不是什麼很快速的網絡,一般的機器處理速度都很快。

     但是如果接收端特别破,處理速度很慢,就會導緻其通告一個很小的視窗,這樣的話,即使擁塞視窗再大,發送端也還是以通告的接收視窗為發送視窗,這樣就不會發生擁塞。最後,如果唯一的TCP連接配接運作在一個直連的兩台主機上,那麼它将獨享網絡帶寬,這樣該TCP的資料流在最好的情況下将填滿網絡管道(我們把網絡管道定義為帶寬和延時的乘積),其實在這種情況下是不存在擁塞的,就像你一個人獨自徘徊在飄雨黃昏的街頭一樣...

5.2.4.主動的擁塞避免

前面我們描述的擁塞控制方式都是試探性的檢測,然後擁塞視窗被動的進行乘性減,這樣在接收端視窗很大的情況下(一般都是這樣,網絡擁堵,分段就不會輕易到達接收端,導緻接收端的視窗大量空置)就可能出現鋸齒形狀的“時間-視窗”圖,類似在一個擁堵的北京X環上開車,發送機發動,車開動,停止,等待,發動機發動,車開動...聽聲音也能聽出來。

     雖然TCP看不到下面的IP網絡,然而它還是可以通過檢測RTT的變化以及擁塞視窗的變化推算出IP網絡的擁堵情況的。就比方說北京東四環一家快遞公司要持續送快遞到西四環,當發件人發現貨到時間越來越慢的時候,他會意識到“下班高峰期快到了”...

     可以通過持續觀測RTT的方式來主動調整擁塞視窗的大小而不是一味的加性增。然而還有更猛的算法,那就是計算兩個內插補點的乘積:

(目前擁塞視窗-上一次擁塞視窗)x(目前的RTT-上一次的RTT)

如果結果是正數,則擁塞視窗減少1/8,若結果是負數或者0,則視窗增加一個MSS。注意,這回不再是乘性減了,可以看出,減的幅度比乘性減幅度小,這是因為這種擁塞控制是主動的,而不是之前的那種被動的試探方式。在試探方式中,乘性減以一種懲罰的方式實作了公平性,而在這裡的主動方式中,當意識到要擁塞的時候,TCP發送者主動的減少了擁塞視窗,為了對這種自首行為進行鼓勵,采用了小幅減少擁塞視窗的方式。需要注意的是,在擁塞視窗減小的過程中,乘積的前一個內插補點是負數,如果後一個內插補點也是負數,那麼結果就是繼續縮減視窗,直到擁塞緩解或者視窗減少到了一定程度,使得後一個內插補點成了正數或者0,這種情況下,其實後一個內插補點隻能變為0。

疑難雜症21:路由器和TCP的互動

雖然有了5.2.4節介紹的主動的擁塞檢測,那麼路由器能不能做點什麼幫助檢測擁塞呢?這種對路由器的擴充是必要的,要知道,每天有無數的TCP要通過路由器,雖然路由器不管TCP協定的任何事(當然排除連接配接跟蹤之類的,這裡所說的是标準的IP路由器),但是它卻能以一種很簡單的方式告訴TCP的兩端IP網絡發生了擁堵,這種方式就是當路由器檢測到自己發生輕微擁堵的時候随機的丢包,随機丢包而不是連續丢包對于TCP而言是有重大意義的,随機丢包會使TCP發現丢棄了個别的分段而後續的分段仍然會到達接收端,這樣TCP發送端就會接收到3個備援ACK,然後進入快速重傳/快速恢複而不是慢啟動。

     這就是路由器能幫TCP做的事。

6.其它

疑難雜症22:如何學習TCP

很多人發帖問TCP相關的内容,接下來稀裡嘩啦的就是讓看《TCP/IP詳解》和《Unix網絡程式設計》裡面的特定章節,我覺得這種回答很不負責任。因為我并不認為這兩本書有多大的幫助,寫得确實很不錯,然而可以看出Richard Stevens是一個實用主義者,他喜歡用執行個體來解釋一切,《詳解》通篇都是用tcpdump的輸出來講述的,這種方式隻是适合于已經對TCP很了解的人,然而大多數的人是看不明白的。

     如果想從設計的角度來說,這兩本書都很爛。我覺得應該先看點入門的,比如Wiki之類的,然後看RFC文檔,793,896,1122等),這樣你就明白TCP為何這麼設計了,而這些你永遠都不能在Richard Stevens的書中得到。最後,如果你想,那麼就看一點Richard Stevens的書,最重要的還是寫點代碼或者敲點指令,然後抓包自己去分析。

疑難雜症23:Linux,Windows和網絡程式設計

我覺得在Linux上寫點TCP的代碼是很不錯的,如果有BSD那就更好了。不推薦用Winsock學習TCP。雖然微軟聲稱自己的API都是為了讓事情更簡單,但實際上事情卻更複雜了,如果你用Winsock學習,你就要花大量的時候去掌握一些和網絡程式設計無關但是windows平台上卻少不了的東西

6.1.總結

TCP協定是一個端到端的協定,雖然話說它是一個帶流量控制,擁塞控制的協定,然而正是因為這些所謂的控制才導緻了TCP變得複雜。同時這些特性是互相雜糅的,流量控制帶來了很多問題,解決這些問題的方案最終又帶來了新的問題,這些問題在解決的時候都隻考慮了端到端的意義,但實際上TCP需要盡力而為的IP提供的網絡,是以擁塞成了最終的結症,擁塞控制算法的改進也成了一個單獨的領域。

     在學習TCP的過程中,切忌一鍋粥一盤棋的方式,一定要厘清楚每一個算法到底是解決什麼問題的,每一個問題和其他問題到底有什麼關聯,這些問題的解決方案之間有什麼關聯,另外TCP的發展曆史也最好了解一下,這些都搞明白了,TCP協定就徹底被你掌控了。接下來你就可以學習Socket API了,然後高效的TCP程式出自你手!