標籤:

TCP、WebSocket等網路協議簡單分析

? Young 2016-09-29 11:30

Welcome to My GitHub

背景

目前Web通信使用的是HTTP協議,HTTP協議是基於TCP協議的應用層協議,HTTP協議的工作模式是request/response模式,在一次通信中,必須首先由客戶端向服務端發起TCP連接,在TCP連接建立之後,首先由客戶端發起HTTP request,然後服務端再發起response。

因此,在這種標準HTTP工作模式的約定下,服務端是不被允許在未收到HTTP request的情況下,發送一個HTTP response給客戶端的,在這種約束下,為了能夠不斷的隨時從服務端發送內容到客戶端,就必須保證服務端隨時都有一個待響應的request(注意這與persistent connection不是同一個概念,persistent connection是用來複用HTTP之下的TCP連接)。

如何在現有的HTTP框架內,來獲得這麼一個時刻存在於服務端的待響應請求呢?這就是COMET技術,主要有如下幾種方式:

  • 輪詢(polling)

輪詢的做法就是讓客戶端每隔一段時間就自動送一個HTTP請求給服務端,獲取最新的數據。

  • 長輪詢(long polling)

長輪詢則是讓服務端在接收到客戶端送出HTTP請求後,服務端會等待一段時間,若這段時間裡面服務端數據有更新,它就會把更新的數據傳回給客戶端,如果等待時間到了之後也沒有更新,則會送一個回應給瀏覽器,告知客戶端資料沒有更新。

與輪詢相比區別在於客戶端發起的HTTP請求數量明顯變少,服務端邏輯較複雜(訂閱器模式推送等)。

  • 基於Iframe及htmlfile的流(streaming)

串流是讓服務端在接收到客戶端所送出HTTP請求後,立即產生一個回應客戶端的連線,並且讓這個連線持續一段時間不要中斷,而服務端在這段時間內如果有數據更新,則可以透過這個連線將更新的數據馬上傳送給客戶端。

與長輪詢相比區別在於不用顯式的連續發起HTTP請求了。

  • 服務端發送事件(Server-Sent Event)

服務端發送事件(以下簡稱SSE)是HTML 5規範的一個組成部分,可以實現服務端到客戶端的單向數據通信。通過SSE,客戶端可以自動獲取數據更新,而不用重複發送HTTP請求。一旦連接建立,事件便會自動被推送到客戶端。服務端SSE通過事件流(Event Stream)的格式產生並推送事件。事件流對應的MIME類型為text/event-stream,包含四個欄位:event、data、id和retry。event表示事件類型,data表示消息內容,id用於設置客戶端EventSource對象的last event ID string內部屬性,retry指定了重新連接的時間。

應該可以理解為streaming方式的標準化吧。

  • Flash

基於Flash,首先就目前來說所有應用程序實現socket通信的一般流程都是調用操作系統的Socket庫然後委託操作系統的協議棧來實現的,那麼Flash也應該是這樣;然後Flash以插件的形式嵌入到瀏覽器中,再暴露出相應的介面為JavaScript調用,從而達到實時傳輸的目的。

由上可知傳統Web模式在處理高並發及實時性需求的時候,會遇到難以逾越的瓶頸,我們需要一種高效節能的雙向通信機制來保證數據的實時傳輸,在此背景下基於HTML5規範的、有Web TCP之稱的WebSocket應運而生。

基礎知識查漏補缺

WireShark

Wireshark是一個免費開源的網路數據包分析軟體,能截取網路數據包,並儘可能顯示出最為詳細的網路數據包數據。

因為後邊的實例分析會頻繁的用到這個工具,所以這裡簡單介紹下;

TCP/IP四層模型和OSI七層模型

HTTP協議位於應用層,TCP、UDP協議位於傳輸層,IP協議位於網路層等;

IP協議中還包括ICMP和ARP協議,ICMP用於告知網路包傳輸過程中產生的錯誤以及各種控制消息,ARP用於根據IP地址查詢相應的乙太網MAC地址;

一般來說瀏覽器、郵件等應用程序收發數據時用TCP,DNS查詢等收發較短的控制數據時用UDP。

IP地址(IPv4)

實際的IP地址是一串32位比特的數字,按照8比特(一個位元組)一組分成4組,分別用十進位表示然後再用圓點隔開。

在IP地址的規則中網路號和主機號連起來總共32比特,但是這兩部分的具體結構是不固定的,所以還需要另外的附加信息來表示IP地址的內部結構,這個附加信息被稱為子網掩碼,子網掩碼為1的部分表示網路號,子網掩碼為0的部分表示主機號。

IP地址域名並用的理由

域名更方便人使用,IP地址更方便機器使用(IP地址定長,只有四個位元組,域名最短也要幾十個位元組而且長度不定,計算機處理起來肯定IP地址效率高),然後通過DNS機制相互轉換就能完美兼容人和機器了。

TCP詳解

傳輸控制協議,是一種面向連接的、可靠的、基於位元組流的運輸層通信協議;

  • 面向連接:通信前要建立連接,通信後要拆除連接;
  • 可靠:會對報文狀態進行跟蹤;
  • 位元組流:以位元組為最小單位的流服務。

1. TCP首部數據結構

在計算機科學領域裡邊一般來說具有什麼樣的功能,通常是由其結構決定的(這句話反過來說也是可以的,結構決定功能,或者某種功能的最優實現總有一種最優結構與之對應,總之數據結構很重要哈),所以首先來了解TCP報文的首部數據結構。

發送方埠號

接收方埠號

首部長度

由於TCP首部包含一個長度可變的選項部分,所以需要這麼一個值來指定這個TCP報文段到底有多長。

序列號

發送方告知接收方當前網路包發送的數據起始於所有發送數據的第幾個位元組,主要用來解決網路報文亂序的問題;

客戶端發送建立TCP連接報文時,序列號為當前連接的初始序列號(ISN),ISN是不能固定編碼的,不然會出問題;

建立TCP連接時序列號為0,當這個連接超時,再次發起建立連接時序列號還是為0,但是上一個超時的報文返回了,此時會被認為是再次建立連接成功。

還有一個問題就是如果ISN固定編碼,每個新建連接的序列號都從0開始,那麼通信過程就會很容易被預測,有人會利用這一點來發送攻擊。

RFC793中說,ISN會和一個假的時鐘綁在一起,這個時鐘會在每4微秒對ISN做加一操作,直到超過2^32,又從0開始。這樣一個ISN的周期大約是4.55個小時,那麼只要保證一個TCP報文在網路上的存活時間不超過4.55個小時,就不會出現相同連接ISN也相同的情況。

有個東東需要稍微提及一下,MSL(Maximum Segment Lifetime)譯為報文最大生存時間,只要這個MSL小於4.55個小時就萬事大吉了哈,實際也不會設置為這麼長,在RFC793中MSL被定義為2分鐘,在Linux系統中一般被設置為30s。

確認序列號

確認序列號表示發送方所期望收到下一個報文的序列號,因此確認序列號應當是發送方上一次已成功收到的報文的序列號加報文長度再加一,不過只有當標誌位ACK為1時確認序號欄位才有效,主要用來解決不丟包的問題。

控制位

主要用於操作TCP請求的狀態機;

  • ACK表示數據報文中的確認序列號欄位有效,一般用來表示數據已被接收方收到;
  • PSH表示push操作,所謂push操作就是指在數據到達接收方之後,立即傳送給應用程序,而不是在緩衝區中排隊;
  • SYN表示同步序號,常用來建立連接;和ACK搭配使用,當建立連接時,SYN=1,ACK=0;當建立連接被響應時,SYN=1,ACK=1;另外這個標誌的報文經常被用來進行埠掃描;
  • URG表示緊急指針有效;
  • RST在TCP協議中表示複位,用來異常的關閉連接,在TCP的設計中它是不可或缺的。發送方在發送RST包關閉連接時,不必等緩衝區的包都發出去會直接就丟棄緩存區的包。而接收方收到RST包後,也不必發送ACK包來確認;(可以通過偽造RST數據包實現RST攻擊);
  • FIN發送端完成發送任務。

客戶端A和伺服器B之間建立了TCP連接,此時攻擊者C偽造了一個TCP包發給B,使B異常的斷開了與A之間的TCP連接,這就是RST攻擊,實現這種攻擊的關鍵在於C怎麼偽裝成A,C需要準確的知道序列號以及源埠號等。

滑動窗口(Window)

TCP滑動窗口包括發送窗口和接收窗口,由於TCP協議是全雙工協議,會話的雙方都可以同時接收、發送數據,所以會話的雙方都各自維護一個發送窗口和接收窗口,其中各自接收窗口的大小取決於應用、系統、硬體的限制(TCP傳輸速率不能大於應用程序的處理速率),各自發送窗口的大小則取決於對端通告的接收窗口大小。

滑動窗口主要有兩個作用,一是提供TCP的可靠性(和序列號一起作用),二是提供TCP的流控特性。

對於TCP發送方,任何時候在其發送緩存內的數據都可以分為4類,已經發送並收到對端ACK的、已經發送但還未收到對端ACK的、未發送但準備發送的、未發送且未準備發送的。已經發送但還未收到對端ACK的未發送但準備發送的,這兩部分稱之為發送窗口。

對於TCP的接收方,在某一時刻在它的接收緩存內存在3類數據,已接收、未接收準備接收、未接收並未準備接收(由於ACK直接由TCP協議棧回復,默認無應用延遲,不存在已接收未回復ACK的數據)。其中未接收準備接收稱之為接收窗口。

窗口左邊向右邊移動稱為窗口合攏,這種現象發生在發送窗口發送報文和接收窗口接收報文時;

窗口右邊向右邊移動稱為窗口張開,這種現象發生在應用層讀取已經確認的報文並釋放TCP接收緩存時;

窗口右邊向左邊移動稱為窗口收縮,Host Requirements RFC強烈建議不要使用這種方式,但TCP必須能夠在某一端產生這種情況時進行處理。

我其實也不太清楚為啥,但是根據圖可以看到,窗口右邊如果能收縮,那麼就存在兩種操作了,同一資源多種操作,還能並發,也許是因為較複雜的原因吧......

實例分析,請往下瀏覽TCP協議數據數據傳輸

在TCP協議中還有個窗口,叫擁塞窗口,可以看作是對發送方發送數據作出的限制,用於擁塞控制;TCP真正的發送窗口大小是由滑動窗口和擁塞窗口的最小值決定的。

最長報文大小(MSS)

MSS就是TCP報文每次能夠傳輸的最大報文長度;

不同類型的網路對數據幀的長度都有一個限制(應該出於響應時間的考慮),這個限制被稱為最大傳輸單元(MTU),IP協議則會根據MTU對數據報進行分片處理(分片操作即可以發生在原始主機上,也可以發生在中間路由器上,已經分片的數據有可能會再次進行分片,分片數據只有到達目的地後才進行重新組裝);

TCP協議出於最佳傳輸效能的考慮,在建立連接的時候通常要協商雙方的MSS;

TCP計算MSS的方法是使用網路介面的MTU大小然後減去其它協議頭數據大小,例如:MTU為1500的乙太網在減去20位元組的IPv4頭和20位元組的TCP頭後得到的MSS為1460。

以下為某次TCP連接建立時協商MSS的例子

2. 建立TCP連接(三次握手)

由於客戶端發起HTTP請求之前需要建立TCP連接,那麼我們可以使用Wireshark抓包工具抓取初次訪問公共介面授權頁面(需要注意該頁面由於富途安全策略可能不能通過外網訪問,此時訪問任意HTTP協議頁面也可)時的網路請求詳情查看。

需要注意圖中的SYN、ACK、SEQ等縮寫單詞的意義,SYN表示TCP頭部信息中控制位SYN標識為1,但是ACK和SEQ則分別表示的是確認序列號和序列號。

確認包的ACK = 待確認包的SEQ+1

建立TCP連接時服務端和客戶端同時打開連接請求

正常情況下TCP連接的建立是由一方發起建立連接,另一方響應該請求,但是如果出現,通信雙方同時請求建立連接時,則連接建立過程並不是三次握手過程,而且這種情況的連接也只有一條,並不會建立兩條連接。

同時打開連接時,兩邊幾乎同時發送 SYN,並進入 SYNSENT 狀態,當每一端收到 SYN 時,狀態變為 SYNRCVD,同時雙方都再發 SYN 和 ACK 作為對收到的 SYN 進行確認應答。當雙方都收到 SYN 及相應的 ACK 時,狀態變為 ESTABLISHED。

沒有親自驗證這種情況,也沒有去查閱相關權威資料。

SYN Flood攻擊和TCP連接建立SYN超時

如果服務端接到了客戶端發的SYN報文後回了SYN-ACK報文後客戶端掉線了,服務端沒有接收到客戶端發送回來的ACK,那麼,這個連接處於一個中間狀態,即沒成功,也沒失敗。於是,服務端如果在一定時間內沒有收到的客戶端的確認報文ACK,就會重發SYN-ACK。在Linux下,默認重試次數為5次,重試的間隔時間從1s開始每次都翻倍,5次的重試時間間隔分別為1s, 2s, 4s, 8s, 16s,總共31s,第5次發出後還要等32s都知道第5次也超時了,所以,總共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 2^6 -1 = 63s,TCP才會把斷開這個連接。

一些惡意的人就為此製造了SYN Flood攻擊,給服務端發送一個SYN後就斷開,於是服務端需要默認等待63秒才會斷開連接,這樣攻擊者就可以把服務端的SYN連接隊列耗盡,讓正常的連接請求不能處理。

於是Linux系統中給了一個叫tcpsyncookies的參數來應對這個事,當SYN隊列滿了之後,TCP會通過源地址埠、目標地址埠和時間戳打造出一個特別的序列號發回去,如果是攻擊者則不會有響應(攻擊者就是通過主動斷開連接來攻擊的哈),如果是正常連接,則會把這個序列號發回來,然後服務端就可以通過這個序列號建立連接了(即使你不在SYN隊列中)。請注意千萬別用tcpsyncookies來處理正常的大負載的連接的情況,因為tcp_syncookies採用的是妥協版的TCP協議,並不嚴謹。

3. TCP數據傳輸

通過Wireshark抓包來看,除了Window size value欄位,還有兩個和滑動窗口相關的欄位,分別是Calculated window size和Window size scaling factor,之所以是這樣,是因為隨著帶寬越來越大,頭部16位的窗口大小已經不夠了,為了突破這個限制便有了Window Size Scaling選項(可見要想你設計的東西不斷的與時俱進適當的預留項是很重要的哈),另外Wireshark中的Calculated window size只是一個邏輯值,Wireshark幫我們算出來方便我們理解用的。

TCP三次握手完成之後,客戶端會發起一個HTTP GET請求;

此時標誌位為[PSH、ACK],目標埠為8080,數據報文長度為439,窗口大小位132384。

服務端接收到客戶端HTTP協議的GET請求後,首先會發送一個確認報文;

確認報文的ACK = 待確認報文的SEQ+待確認報文的長度。

服務端發送確認報文後會對客戶端的HTTP協議的GET請求的資源進行響應,發送一些資源報文給客戶端;

統一資源會被分隔為MSS整數份以及餘下部分,該報文標誌為[ACK]序列號為6757,可以理解為發送的數據是從第6757位到8107位(Next sequence number = Sequence number + TCP Segment Len)。

客戶端接收到服務端報文後會對發送確認報文;

該確認報文的標誌位為[ACK],序列號為8107等於對應報文的下一序列號,表示第8107位之前的已經接收到了,期望下一次報文是從第8107位開始,從這裡也可以看出TCP協議是怎麼通過報文處理亂序問題的,另外窗口大小也隨著接收數據的不斷增多變為了1256641。

理論上一個處理緩慢的接收方是可以把發送方的滑動窗口降為0的,此時也就是Zero Window情形了,發送方滑動窗口降為0就不會再發送數據,但是如果等一段時間後接收方有緩存區域可以用了呢,該怎麼通知發送方呢?為了解決這個問題,TCP使用了Zero Window Probe技術,縮寫為ZWP,也就是說,發送方窗口變為0後,會發ZWP報文給接收方,讓接收方不斷的發ACK報文給發送方來更新其窗口大小,一般會重複三次,每次大約間隔30-60秒(不同的實現可能會不一樣),如果三次過後窗口大小還是0,那麼一般實現都會在此時斷開TCP連接。

另外需要注意的是基本上任何等待的地方都可能出現DDos攻擊,Zero Window也不例外,一些攻擊者會在連接建立後,發送GET請求後,就把Window設置為0,然後服務端就只能等待進行ZWP,於是攻擊者會並發大量這樣的請求,把服務端資源耗盡。

客戶端除了發送確認報文之後,還發送了一個被Wireshark標識為[TCP Window Update]的報文;

該報文序列號也為8107;TCP Window Update是TCP通信中的一個狀態,它可以發生的原因有很多,但最終可以歸結於發送方傳輸數據的速度比接收方讀取數據還快,這使得接收方的緩衝區必須釋放一部分空間,所以接收方就給發送方發送該報文,告訴發送方應該以多大的速度發送數據,從而使得數據傳輸與接收恢復正常。

所以服務端在接收到TCP Window Update報文,發送的下一個報文長度只有176,而且標誌位為[ACK PSH],至於為啥長度是176,應該處於降低接收方緩存增長過快的壓力,然後發送的最小報文吧(HTML文檔整除MSS後剛好還剩下176);

服務端資源傳輸完成之後就會發送HTTP Response請求,至於之後的幾次keep alive和RST,就先不展開了(可能涉及保活定時器以及堅持定時器等),因為我本來只想分析下WebSocket是怎麼通過HTTP和TCP來進行全雙工通信的,結果一周過去了,我還掙扎在TCP協議各種機制中...

TCP報文發送機制

應用程序交給協議棧發送的數據長度是由應用程序本身來決定的,不同的應用程序在實現上有所不同,有些應用程序會一次性傳遞所有數據,有些應用程序會逐位元組或者逐行傳遞數據,協議棧並不能控制這一行為;

在這種情況下一收到數據就馬上發送出去,就有可能會發送大量的小包,導致網路效率下降,因此需要在數據累積到一定量時再發送出去,這個累積量的大小就是上邊所說的MSS

但是累積量方式也會有一個問題,如果每次都要等到長度累積到MSS時再發送,可能會因為等待時間太長而造成發送延誤,這種情況下,即便緩衝區中的數據長度沒有達到MSS,也應該果斷髮送出去,為此協議棧內部有一個定時器,當經過一段時間後,就會把網路包發送出去;

上述兩種方式其實時互相矛盾的,具體的發送策略是由協議棧的開發者綜合考慮來決定的,因此不同種類和版本的操作系統在相關操作上是存在差異的;

另外協議棧也給應用程序保留了控制發送時機的餘地,應用程序在傳輸數據給協議棧時可以指定一些選項,比如控制位PSH標識置為1,那麼協議棧就會按照要求直接發送數據。

TCP報文確認機制

TCP不對ACK報文進行確認,除非ACK報文長度不為0,因此客戶端一般會對服務端發送的ACK數據報文進行確認,但是服務端卻沒有對客戶端發送的TCP Window Update報文進行確認,很明顯可以看到TCP Window Update報文長度為0。

有時候會發現客戶端並沒有對服務端每一個數據報文都進行了確認,究其原因則是因為TCP的報文確認機制啟動的一種延遲確認的機制(Delayed ACK),它的作用就是延遲ACK報文的發送,使得協議棧有機會合併多個ACK,提高網路性能,一般實現的延遲時間為40ms。

比如說接收方在接收到1報文後準備發送ACK確認報文,該確認報文長度為0,然後40毫秒內又收到了2報文,則還需要對2報文進行確認,那麼這種延遲確認機制就會把2報文的ACK報文和1報文的ACK報文合併,而接收方在接收到這個合併的報文就意味著1、2報文都已經被接收了。這樣也可以提高系統的效率(這樣就能很好的解釋為什麼有時候客戶端並沒有對服務端的數據報文一一進行確認回復了)。

當接收方應用層處理數據很慢時或者當發送方應用進程產生數據很慢時,就會導致大量的短報文充斥網路,效率很低;這種情況又被稱為糊塗窗口綜合症,解決辦法為Nagle演算法,也不過多展開了,理由同上。

延遲ACK能減少帶寬浪費,但是在協議性能需要優化時,有丟包的情況下,需要考慮啟動快速ACK,因為這樣可以及時通知發送方丟包了,避免Zero Window,提升吞吐率。

選擇性重傳(Selective Acknowledgment)

其實看英文名字,應該叫選擇性確認哈

以上只描述了TCP協議確認連續報文時的機制,但是由於網路環境很複雜,如果接收方收到的數據報文沒有錯誤,只是未按序號,這種現象如何處理呢?

TCP協議會保證數據報按序到達應用層,但是並沒有規定如何處理自身接收到錯序報文的情況,而是由TCP協議的實現者自己去確定,通常有兩種方法進行處理:一是對沒有按序號到達的報文直接丟棄,二是將未按序號到達的數據包先放於緩衝區內,等待它前面 的序號包到達後,再將它交給應用進程。

舉例來說如果發送方連續發送了7個數據報文段,其序號分別是1,101,201,...,701;假設其它數據報文都收到了,但是201這個數據報文沒有收到,那麼按第一種方式301-701的數據報文會被丟棄,接收方只會發送確認1-201數據報文的確認報文,發送方超時重發時需要重發301-701的數據報文;按第二種方式301-701的數據報文會被接收方放入緩衝區,而且會發送確認接收到的所有數據報文的確認報文,那麼發送方就會根據接收方的確認報文重發接收方未接收到的數據報文(這種機制也被稱為選擇性重傳)。

序列號和確認序列號並不能解決返回的確認報文明確指明未接收到報文的位置,所以這時候就需要在TCP頭部的選項指明;一般SACK信息分兩種,一種標識是否支持SACK,是在TCP握手時發送;另一種是具體的SACK信息,這些信息主要是為了告訴發送方,接收方已經接收到並緩存的不連續的報文,通過這些信息發送方就能判斷究竟是哪塊丟失,從而重發相應的報文。

超時重傳

發送端發送一個TCP報文後,會啟動一個重傳定時器,如果在一定時間內沒有收到接收方的確認報文,就會重傳;看起來的超時重傳機制很簡單,但是這裡有個問題,怎麼確定超時時間(RTO)?首先超時時間固定是肯定會有問題的(考慮最長報文大小、發送端和接收接收方的網路環境),網路環境很複雜,如果超時時間固定,那麼時間定長了,判斷一次重傳需要等待很長時間;如果定短了,在網路環境好的情況下就會大量無意義的重傳。一般RTO是經過RTT計算出來的(RTT是發送端發出一個報文到接收到接收方反饋報文的時間),沒有仔細了解這個演算法(包括經典演算法、Karn/Partridge演算法、Jacobson/Karels演算法等,總之網路環境很複雜......),但可以確定RTO肯定要比RTT大。

快速重傳

TCP繼續發展,RTO演算法不斷優化,還是覺得超時重傳機制效率偏低,然後重傳機製得到了進一步加強,出現了快速重傳;快速重傳機制規定,發生丟包時,在重傳定時器被觸發之前,當發送方連續收到3個對丟失數據包的重傳請求時將引起立即重傳。

例如發送方發送1、2、3、4、5連續數據報文,接收方接收到1後發送確認報文,此後接連接收到3、4、5但是沒有接收到2,那麼就會發送三次確認2報文未接收的報文,發送方再接收到三次同一報文就可以在超時重傳之前確認2數據報文接收方未接收到此時會立即重傳2數據報文。

慢啟動

最初的TCP在連接建立成功後會向網路中發送大量的報文,這樣很容易導致網路中路由器緩存空間耗盡,從而發生擁塞,因此新建立的連接不能一開始就大量發送報文,而只能根據網路情況逐步增加每次發送的數據量,以避免上述現象的發生。具體來說,當新建連接時,cwnd初始化為1一個最大報文大小(MSS),發送端開始按照擁塞窗口大小發送數據,每當有一個報文被確認時,cwnd就增加一個MSS大小,這樣cwnd的值就隨著網路往返時間呈指數級增長,事實上慢啟動的速度一點都不慢,只是起點比較低而已。

知道這兩種機制,其實我們在處理一些循環定時邏輯的時候也可以參考哈,比如可以加入反饋、嘗試機制等(前端童鞋如果不怕複雜話,其實可以在輪詢中加入這些機制,伺服器應答很快我們就減少間隔時間,應答很慢就增加間隔時間等,而不像現在統統寫死,100個用戶訪問也是3秒輪詢一次,10000個用戶訪問也是3秒輪詢一次之類的)。

擁塞避免

從慢啟動可以看到,cwnd可以很快的增長上來,從而最大程度利用網路帶寬資源,但是cwnd不能一直這樣無限增長下去,一定需要某個限制。TCP使用了一個叫慢啟動門限(ssthresh)的變數,當cwnd超過該值後,慢啟動過程結束,進入擁塞避免階段。對於大多數TCP實現來說,ssthresh的值是65536(同樣以位元組計算)。擁塞避免的主要思想是加法增大,也就是cwnd的值不再指數級往上升,開始加法增加。此時當窗口中所有的報文段都被確認時,cwnd的大小加1,cwnd的值就隨著RTT開始線性增加,這樣就可以避免增長過快導致網路擁塞,慢慢的增加調整到網路的最佳值。

擁塞發生

慢啟動和擁塞避免都是沒有檢測到擁塞的情況下的行為,那麼當擁塞發生時cwnd又該怎樣去調整呢?

當發生重傳時,TCP協議認為出現擁塞的可能性很大,這時ssthresh降低為cwnd值的一半,cwnd重新設置為1,重新進入慢啟動過程。

快速恢復

當發生快速重傳時,ssthresh設置為cwnd的一半,cwnd再設置為ssthresh的值,重新進入擁塞避免階段。

上述幾個機制或者說是演算法是擁塞控制的主要演算法,這些演算法不是同時誕生的,各自經歷了很多時間的發展,到今天都還在優化中,而且我也不知道怎麼模擬相關環境去仔細了解其機制,所以就先記下來假裝了解一下。

4.斷開TCP連接(四次揮手)

我所接觸到的所有資料都說斷開TCP連接的過程是四次揮手的過程,但是我實際抓包時卻發現客戶端和服務端在斷開TCP連接時只有三次交互(如下圖所示),理論上的四次揮手和實際的三次揮手的差別在於,服務端接收到客戶端發送的關閉TCP請求後,理論上服務端會發兩個確認請求,一次為ACK報文一次為FIN報文,分別表示傳輸未傳輸完成數據以及確認關閉,但實際上只有一個請求,而且該請求控制為ACK和FIN;

我推測應該是當服務端沒有任何未傳輸完成的數據時,會把理論上的兩個請求合併為一個請求(根據延遲確認機制哈)。

客戶端和服務端同時關閉連接

正常情況下,通信一方請求連接關閉,另一方響應連接關閉請求,並且被動關閉連接。但是若出現同時關閉連接請求時,通信雙方均從ESTABLISHED狀態轉換為FINWAIT1狀態,任意一方收到對方發來的FIN報文後,其狀態均由FINWAIT1轉變到CLOSING狀態,並發送最後的ACK報文,當接收到最後的ACK報文後,狀態轉變為TIME_WAIT,在等待2MSL時間後進入到CLOSED狀態,最終釋放整個TCP傳輸連接。

至於為什麼關閉連接時狀態需要先轉變為TIME_WAIT,在等待2MSL時間才後進入CLOSED狀態,是因為2MSL剛好是一個報文一來一回能在網路中生存的最大時間,能保證發送方未發送完成的數據被接收方接收;另外還有足夠的時間讓這個連接不會跟後面的連接混在一起。

5. TCP狀態機

機器與機器的連接就和人和人之間的聯繫類似,網路上的傳輸是沒有連接的,所謂的連接其實只不過在通訊的雙方維持一個「連接的狀態」。

tcpipguide.com/free/t_T

紫色區域為TCP連接建立過程中服務端和客戶端狀態變化情況(左邊為服務端、右邊為客戶端);

  • CLOSED:TCP連接創建之前的默認狀態,表示兩個設備之間沒有連接或者連接還沒有建立或者連接已經被銷毀;
  • LISTEN:服務端正在等待客戶端發送SYN報文,此時還沒有發送自己的SYN報文;
  • SYN-SENT:客戶端已經發送了SYN報文正在等待對應的服務端返回SYN報文;
  • SYN-RECEIVED:客戶端和服務端都接收到對方的SYN報文後,服務端正在等待客戶端對其SYN報文進行確認,完成連接的建立;
  • ESTABLISHED:TCP連接建立完成狀態,此時數據可以自由的從一端傳送到另一端,這種狀態會持續到連接由於某些原因關閉的時候;服務端發送完ACK+SYN並收到來自客戶端的ACK後進入該狀態,客戶端收到來自伺服器的SYN+ACK並發送ACK後也進入該狀態;
  • CLOSE-WAIT:被動關閉方接收到主動關閉方發送的FIN報文,回應對方ACK報文後,進入該狀態;
  • LAST-ACK:被動關閉方發送FIN報文後,等待對方的ACK報文時,進入該狀態;該狀態下接收對方的ACK報文後進入CLOSED狀態;
  • FIN-WAIT-1:主動關閉連接,無論客戶端還是服務端發送FIN關閉連接報文後都會進入該狀態;
  • FIN-WAIT-2:主動關閉方接收到被動關閉方返回的ACK後,會進入該狀態;
  • CLOSING:被動關閉方接收了FIN關閉報文,而且已經對該報文發送了ACK確認報文,但是還沒有接收到對方對自己FIN關閉報文的確認時處於該狀態;
  • TIME-WAIT:表示收到對方的FIN報文並發送了ACK報文,就等2MSL後即可回到CLOSED狀態,如果處於FINWAIT1狀態下收到對方同時帶FIN標誌和ACK標誌的報文時,可以直接進入TIMEWAIT狀態,無需經過FINWAIT_2狀態。

Wireshark常見異常狀態

  • TCP Previous segment not captured TCP前分片未收到
  • TCP Out-Of-Order TCP數據亂序
  • TCP Dup ACK 47#1 重複應答

當收到一個出問題的分片,TCP協議規定接收方應立即產生一個應答。這個相同的ack不會延遲。這個相同應答的意圖是讓對端知道一個分片被收到的時候出現問題,並且告訴它希望得到的序列號。上圖中TCP Dup ACK 47#1 的意思第47位數據出現問題,次數是1次。

  • TCP Keep-Alive TCP保持活動

根據規範,TCP keepalive保活包不應該包含數據且Seq號是將前一個TCP包的Seq號減去1。

如果另一方響應ACK,就認為當前連接是有效的。

如果服務端程序退出,就返回RST應答,撤銷TCP連接;如果服務端程序崩潰,就返回FIN應答;如果服務端沒有任何應答,則客戶端繼續發送探測包,直至超時。

6. 疑問

為啥建立連接需要三次握手?

這個問題的本質就是信道不可靠,但是通信雙方需要就某個問題達成一致,而要解決這個問題,無論你在消息中包含什麼信息,三次通信是理論上的最小值。

為啥建立連接是三次握手,斷開連接卻是四次揮手?

主要是因為當一方發起關閉請求,只是表示發起方不再給另一方傳輸數據了,但是有可能另一方還存在一些數據沒有傳輸給發送端,所以需要在另一方也發起關閉請求前,把待傳輸數據發送過去,所以斷開連接就比建立連接多一次交互了。

WebSocket詳解

以下內容基本翻譯於RFC6455,由於水平有限可能會出現錯誤或者解釋不清的情況,請直接移步到上述鏈接,或者查看國內大牛翻譯的RFC6455。

後續所有實例都是基於SocketIO實現的簡易聊天室。

WebSocket是HTML5一種新的協議,它建立在TCP協議之上,同HTTP一樣通過TCP來傳輸數據,包括兩部分:握手和數據傳輸。

1. 握手

目前WebSocket的握手使用HTTP協議來實現;

客戶端

  • Origin: http://example.com用來聲明請求來源,方便伺服器過濾那些未授權的跨站請求;
  • Upgrade: websocket表示客戶端請求伺服器如果伺服器支持WebSocket協議,請切換到WebSocket協議;

    > Upgrade是HTTP1.1中用於定義協議轉換的屬性。
  • Sec-WebSocket-Protocol: chat, superchat表示客戶端告訴伺服器自己支持哪些子協議;
  • Sec-WebSocket-Version: 13表示客戶端告訴伺服器自己支持WebSocket協議的版本號;
  • Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==表示客戶端傳遞給伺服器的隨機字元串,伺服器會根據某個演算法把這個隨機字元串轉換成另外的字元串重新傳遞給客戶端,用來標識當前WebSocket連接創建成功。

服務端

  • HTTP/1.1 101 Switching Protocols,狀態碼101表示伺服器接收客戶端切換協議的請求,如果狀態碼不是101那麼此次建立WebSocket連接失敗;
  • Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=表示伺服器把處理後的值返回給客戶端,客戶端會去校驗,如果這個值不存在或者沒有通過校驗則此次建立WebSocket連接失敗;
  • Sec-WebSocket-Protocol: chat表示伺服器選擇的子協議。

實例分析

另外需要注意的是SocketIO類庫在建立WebSocket連接時出於兼容性的考慮,會在不支持WebSocket連接的時候用長輪詢替換,所以通過wireshark抓包會發現一些其它非握手的HTTP請求。

2. 數據傳輸

數據幀格式

WebSocket中所有發送的數據使用幀的形式發送,雖然所有幀都用一種格式,但從客戶端發到服務端的數據是被某種演算法處理過的,出於安全方面的考慮。

我發現在沒什麼密碼學的基礎的情況下去了解各種網路協議的安全策略稍顯吃力,因此我決定先跳過,等今後有機會再查漏補缺。

  • FIN標識這是消息的最後一個片段,第一個片段也可能是最後一個片段;
  • RSV1RSV2RSV3這三個屬性都必須是0,除非協商好了非0值的擴展意義;如果屬性不為0但是又不存在協商好的非0擴展意義,那麼數據幀的接收方必須關閉當前連接;
  • Opcode定義了Payload data的解釋,如果一個未知的Opcode被接收到,那麼接收方必須關閉當前連接;

    > + %x0 代表一個繼續幀;

    > + %x1 代表一個文本幀;

    > + %x2 代表一個二進位幀;

    > + %x3-7 保留用於未來的非控制幀;

    > + %x8 代表連接關閉;

    > + %x9 代表ping;

    > + %xA 代表pong;

    > + %xB-F 保留用於未來的控制幀。
  • Mask標識Payload data是否被掩飾了,如果設置為1,那麼數據幀的masking-key屬性會存在一個值,接收方會利用這個值來進行解掩碼操作,所有從客戶端傳輸到伺服器的數據幀的Mask都被設置為1。
  • Payload length表示Payload data的長度,如果值在0-125之間,那麼這個值就是長度;如果值等於126,那麼後續16位所表示的值為實際長度;如果值等於127,那麼後續64位所表示的值為實際長度。

實例分析

建立連接後如果什麼都不操作,客戶端和伺服器總在不停的相互發送數據幀,根據WebSocket協議規範,存在心跳機制,一方可以通過發送ping消息給另一方,另一方收到ping後應該儘可能快的返回pong,但問題在於ping數據幀的Opcode應該是%x9,pong數據幀的Opcode應該是%xA,通過wireshark抓包卻發現,客戶端和伺服器自動發送的數據幀的Opcode都為%x1

通過斷點調試源代碼發現SocketIO處理心跳包時,並沒有設置Opcode的值,進一步代碼無法調試,姑且認為SocketIO委託瀏覽器發ping包時並沒有設置Opcode值,然後瀏覽器在委託操作系統網路棧之前根據內容或者默認處理為%x1了。

另外還要注意的是客戶端發給伺服器的心跳包中Reserved 0x4,有標準可知RSV1RSV2RSV3這三個屬性都必須是0,除非已經協商好了其它意義。

那麼這裡是什麼意思呢?查資料可知,這裡應該是客戶端通知伺服器信息壓縮採用了其它方式,從握手時客戶端傳給伺服器的頭信息中的可以看到。

輸入一段文字後,就可以在wireshark中看到,如下數據包了;

另外還需要注意的是WebSocket標準是允許消息分片機制存在的,主要目的是允許實時發送一個大小未知的消息;其次是為了應用於多路復用。

這裡的分片應該不是指已知一個整體然後分為很多小塊,而是未知大小然後每積攢到一定數量就分為一個小塊。

3. 揮手

WebSocket揮手一般是客戶端或者服務端發送一個帶有一段特殊控制序列數據的控制幀,來開始揮手過程;另一端一旦接收到這樣的幀,再發送關閉幀作為回應,之後就是TCP揮手。


推薦閱讀:

MQTT進階篇
web AR系統-3 效率評估-websocket
一步一步教您用websocket+nodeJS搭建簡易聊天室(4)
對於 Socket 粘包的困惑?

TAG:TCPIP | WebSocket |