長文是對TCP IP的剖析歸類總結,就本身的經驗再次回顧IP協議而寫的概括性筆記,助力初學者掌握。文有不妥之處,請查看原文並留言告知,謝謝!html
若是對網絡工程基礎不牢,建議通讀《細說OSI七層協議模型及OSI參考模型中的數據封裝過程?》linux
下面就是TCP/IP(Transmission Control Protoco/Internet Protocol )協議頭部的格式,是理解其它內容的基礎,就關鍵字段作一些說明ios
TCP Flags:TCP首部中有6個標誌比特,它們中的多個可同時被設置爲1,主要是用於操控TCP的狀態機的,依次爲URG,ACK,PSH,RST,FIN。每一個標誌位的意思以下:nginx
The segment is a request to synchronize sequence numbers and establish a connection. The sequence number field contains the sender's initial sequence number.
該標誌僅在三次握手創建TCP鏈接時有效。它提示TCP鏈接的服務端檢查序列編號,該序列編號爲TCP鏈接初始端(通常是客戶端)的初始序列編號。在這裏,能夠把TCP序列編號看做是一個範圍從0到4,294,967,295的32位計數器。經過TCP鏈接交換的數據中每個字節都通過序列編號。在TCP報頭中的序列編號欄包括了TCP分段中第一個字節的序列編號。git
在鏈接創建時用來同步序號。當SYN=1而ACK=0時,代表這是一個鏈接請求報文。對方若贊成創建鏈接,則應在響應報文中使SYN=1和ACK=1. 所以, SYN置1就表示這是一個鏈接請求或鏈接接受報文。github
The segment carries an acknowledgement and the value of the acknowledgement number field is valid and contains the next sequence number that is expected from the receiver.
面試
大多數狀況下該標誌位是置位的。TCP報頭內的確認編號欄內包含的確認編號(w+1,Figure-1)爲下一個預期的序列編號,同時提示遠端系統已經成功接收全部數據。算法
TCP協議規定,只有ACK=1時有效,也規定鏈接創建後全部發送的報文的ACK必須爲1chrome
網絡上有不少錯誤說法,好比:ACK是可能與SYN,FIN等同時使用的,好比SYN和ACK可能同時爲1,它表示的就是創建鏈接以後的響應,若是隻是單個的一個SYN,它表示的只是創建鏈接。TCP的幾回握手就是經過這樣的ACK表現出來的。其實:ACK&SYN是標誌位,shell
The sender wants to close the connection
用來釋放一個鏈接。
當 FIN = 1 時,代表此報文段的發送方的數據已經發送完畢,並要求釋放鏈接。
Segment is urgent and the urgent pointer field carries valid information.
當URG=1,代表緊急指針字段有效。告訴系統此報文段中有緊急數據
The data in this segment should be immediately pushed to the application layer on arrival.
PSH爲1的狀況,通常只出如今 DATA內容不爲0的包中,也就是說PSH=1表示有真正的TCP數據包內容被傳遞。
There was some problem and the sender wants to abort the connection.
當RST=1,代表TCP鏈接中出現嚴重差錯,必須釋放鏈接,而後再從新創建鏈接
Set by an ECN-Capable sender when it reduces its congestion window (due to a retransmit timeout, a fast retransmit or in response to an ECN notification.
During the three-way handshake it indicates that sender is capable of performing explicit congestion notification. Normally it means that a packet with the IP Congestion Experienced flag set was received during normal transmission. See RFC 3168 for more information.
TCP的鏈接創建和鏈接關閉,都是經過請求-響應的模式完成的。咱們來看下圖,應該基本可以理解TCP握手揮手過程
三次握手的目的是:爲了防止已失效的鏈接請求報文段忽然又傳送到了服務端,於是產生錯誤。推薦閱讀《TCP的三次握手與四次揮手(詳解+動圖》
固然,若是那邊同時打開,就有多是四次握手
在此推薦閱讀《面試題·TCP 爲何要三次握手,四次揮手?》
TCP 鏈接的雙方會經過三次握手肯定 TCP 鏈接的初始序列號、窗口大小以及最大數據段,這樣通訊雙方就能利用鏈接中的初始序列號保證雙方數據段的不重不漏、經過窗口大小控制流量並使用最大數據段避免 IP 協議對數據包的分片。
2014 年提出的 TCP 快啓(TCP Fast Open,TFO)卻能夠在某些場景下經過一次通訊創建 TCP 鏈接。目前TFO被植入了Linux 2.6.34內核,所以RHEL7/CentOS7是支持的,但默認沒有開啓,須要手動開啓:echo 3 > /proc/sys/net/ipv4/tcp_fastopen。
TCP 快啓策略使用存儲在客戶端的 TFO Cookie 與服務端快速創建鏈接。
TCP 鏈接的客戶端向服務端發送 SYN 消息時會攜帶快啓選項,服務端會生成一個 Cookie 並將其發送至客戶端,客戶端會緩存該 Cookie,當其與服務端從新創建鏈接時,它會使用存儲的 Cookie 直接創建 TCP 鏈接,服務端驗證 Cookie 後會向客戶端發送 SYN 和 ACK 並開始傳輸數據,這也就能減小通訊的次數。
TFO是GOOGLE發佈的。目前chrome已經支持TFO,老版的默認關閉。
因此,這裏也很少討論,本人只是對純展現內容開啓TFO。
單個TCP包每次打包1448字節的數據進行發送(以太網Ethernet最大的數據幀是1518字節,以太網幀的幀頭14字節和幀尾CRC校驗4字節(共佔18字節),剩下承載上層協議的地方也就是Data域最大就只剩1500字節. 這個值咱們就把它稱之爲MTU(Maximum Transmission Unit))。
那麼一次性發送大量數據,就必須分紅多個包。好比,一個 10MB 的文件,須要發送7100多個包。
發送的時候,TCP 協議爲每一個包編號(sequence number,簡稱 SEQ),以便接收的一方按照順序還原。萬一發生丟包,也能夠知道丟失的是哪個包。
第一個包的編號是一個隨機數—初始化序列號(縮寫爲ISN:Inital Sequence Number)
爲了便於理解,這裏就把它稱爲1號包。假定這個包的負載長度是100字節,那麼能夠推算出下一個包的編號應該是101。這就是說,每一個數據包均可以獲得兩個編號:自身的編號,以及下一個包的編號。接收方由此知道,應該按照什麼順序將它們還原成原始文件。
若是初始化序列號能夠固定,咱們來看看會出現什麼問題?
假設ISN固定是1,Client和Server創建好一條TCP鏈接後,Client連續給Server發了10個包,這10個包不知怎麼被鏈路上的路由器緩存了(路由器會毫無先兆地緩存或者丟棄任何的數據包),這個時候碰巧Client掛掉了,而後Client用一樣的端口號從新連上Server,Client又連續給Server發了幾個包,假設這個時候Client的序列號變成了5。接着,以前被路由器緩存的10個數據包所有被路由到Server端了,Server給Client回覆確認號10,這個時候,Client整個都很差了,這是什麼狀況?個人序列號纔到5,你怎麼給個人確認號是10了,整個都亂了。
RFC793中,建議ISN和一個假的時鐘綁在一塊兒,這個時鐘會在每4微秒對ISN作加一操做,直到超過2^32,又從0開始,這須要4小時纔會產生ISN的迴繞問題,這幾乎能夠保證每一個新鏈接的ISN不會和舊的鏈接的ISN產生衝突。這種遞增方式的ISN,很容易讓攻擊者猜想到TCP鏈接的ISN,如今的實現大可能是在一個基準值的基礎上進行隨機的。
注:這些內容引用自《從 TCP 三次握手提及:淺析TCP協議中的疑難雜症 》,推薦查看。
Client發送SYN包給Server後掛了,Server回給Client的SYN-ACK一直沒收到Client的ACK確認,這個時候這個鏈接既沒創建起來,也不能算失敗。這就須要一個超時時間讓Server將這個鏈接斷開,不然這個鏈接就會一直佔用Server的SYN鏈接隊列中的一個位置,大量這樣的鏈接就會將Server的SYN鏈接隊列耗盡,讓正常的鏈接沒法獲得處理。
目前,Linux下默認會進行5次重發SYN-ACK包,重試的間隔時間從1s開始,下次的重試間隔時間是前一次的雙倍,5次的重試時間間隔爲1s, 2s, 4s, 8s, 16s,總共31s,第5次發出後還要等32s都知道第5次也超時了,因此,總共須要 1s + 2s + 4s+ 8s+ 16s + 32s = 63s,TCP纔會把斷開這個鏈接。因爲,SYN超時須要63秒,那麼就給攻擊者一個攻擊服務器的機會,攻擊者在短期內發送大量的SYN包給Server(俗稱 SYN flood 攻擊),用於耗盡Server的SYN隊列。對於應對SYN 過多的問題,linux提供了幾個TCP參數:tcp_syncookies、tcp_synack_retries、tcp_max_syn_backlog、tcp_abort_on_overflow 來調整應對。
SYN 攻擊指的是,攻擊客戶端在短期內僞造大量不存在的IP地址,向服務器不斷地發送SYN包,服務器回覆確認包,並等待客戶的確認。因爲源地址是不存在的,服務器須要不斷的重發直至超時,這些僞造的SYN包將長時間佔用未鏈接隊列,正常的SYN請求被丟棄,致使目標系統運行緩慢,嚴重者會引發網絡堵塞甚至系統癱瘓。
SYN 攻擊是一種典型的 DoS(Denial of Service)/DDoS(:Distributed Denial of Service) 攻擊。
檢測 SYN 攻擊很是的方便,當你在服務器上看到大量的半鏈接狀態時,特別是源IP地址是隨機的,基本上能夠判定這是一次SYN攻擊。在 Linux/Unix 上能夠使用系統自帶的 netstats 命令來檢測 SYN 攻擊。
SYN攻擊不能徹底被阻止,除非將TCP協議從新設計。咱們所作的是儘量的減輕SYN攻擊的危害,常見的防護 SYN 攻擊的方法有以下幾種:
TCP還設有一個保活計時器,顯然,客戶端若是出現故障,服務器不能一直等下去,白白浪費資源。服務器每收到一次客戶端的請求後都會從新復位這個計時器,時間一般是設置爲2小時,若兩小時尚未收到客戶端的任何數據,服務器就會發送一個探測報文段,之後每隔75分鐘發送一次。若一連發送10個探測報文仍然沒反應,服務器就認爲客戶端出了故障,接着就關閉鏈接。
現來看下TCP各類狀態含義解析(節選改編自《TCP、UDP 的區別,三次握手、四次揮手》
由上面的」TCP協議狀態機 「圖能夠看出
可是若是Peer在FIN_WAIT1狀態下_首先收到對端Peer的FIN包的話_,那麼該Peer在確認已經收到了對端Peer所有的Data數據包後,_就響應一個ACK給對端Peer_,而後本身進入CLOSEING狀態,Peer在CLOSEING狀態下_收到本身的FIN包的ACK包的話_,那麼就進入TIME WAIT 狀態。因而
TCP的Peer兩端同時發起FIN包進行斷開鏈接,那麼兩端Peer可能出現徹底同樣的狀態轉移 FIN_WAIT1——>CLOSEING——->TIME_WAIT,也就會Client和Server最後同時進入TIME_WAIT狀態
要說明TIME_WAIT的問題,須要解答如下幾個問題:
相信你們都知道,TCP主動關閉鏈接的那一方會最後進入TIME_WAIT。
那麼怎麼界定主動關閉方呢?
是否主動關閉是由FIN包的前後決定的,就是在本身沒收到對端Peer的FIN包以前本身發出了FIN包,那麼本身就是主動關閉鏈接的那一方。對於TCP 的 Peer 兩端同時斷開鏈接 描述的狀況,那麼Peer兩邊都是主動關閉的一方,兩邊都會進入TIME_WAIT。爲何是主動關閉的一方進行TIME_WAIT呢,被動關閉的進入TIME_WAIT能夠不呢?
咱們來看看TCP四次揮手能夠簡單分爲下面三個過程
若是主動關閉方不進入TIME_WAIT,那麼主動關閉方在發送完ACK就走了的話:若是最後發送的ACK在路由過程當中丟掉了,最後沒能到被動關閉方,這個時候被動關閉方 沒收到本身FIN的ACK就不能關閉鏈接,接着被動關閉方 會_超時重發FIN包_,可是這個時候已經沒有對端會給該FIN回ACK,被動關閉方就沒法正常關閉鏈接了,因此主動關閉方須要進入TIME_WAIT 以便可以重發丟掉的被動關閉方FIN的ACK。
TIME_WAIT主要是用來解決如下幾個問題:
TIME_WAIT帶來的問題注意是源於:一個鏈接進入TIME_WAIT狀態後須要等待2*MSL(通常是1到4分鐘)那麼長的時間才能斷開鏈接釋放鏈接佔用的資源,會形成如下問題
( 因爲上面兩個問題,做爲客戶端須要連本機的一個服務的時候,首選UNIX域套接字而不是TCP )
TIME_WAIT很使人頭疼,不少問題是由TIME_WAIT形成的,可是TIME_WAIT又不是多餘的不能簡單將TIME_WAIT去掉,那麼怎麼來解決或緩解TIME_WAIT問題呢?能夠進行TIME_WAIT的快速回收和重用來緩解TIME_WAIT的問題。
TCP在什麼時候發送ACK的時候有以下規定:
這樣作有兩個目的。
按照TCP協議,確認機制是累積的。也就是確認號X的確認指示的是全部X以前但不包括X的數據已經收到了。確認號(ACK)自己就是不含數據的分段,所以大量的確認號消耗了大量的帶寬,雖然大多數狀況下,ACK仍是能夠和數據一塊兒捎帶傳輸的,可是若是沒有捎帶傳輸,那麼就只能單獨回來一個ACK,若是這樣的分段太多,網絡的利用率就會降低。爲緩解這個問題,RFC建議了一種延遲的ACK,也就是說,ACK在收到數據後並不立刻回覆,而是延遲一段能夠接受的時間。延遲一段時間的目的是看能不能和接收方要發給發送方的數據一塊兒回去,由於TCP協議頭中老是包含確認號的,若是能的話,就將數據一塊兒捎帶回去,這樣網絡利用率就提升了。延遲ACK就算沒有數據捎帶,那麼若是收到了按序的兩個包,那麼只要對第二包作確認便可,這樣也能省去一個ACK消耗。因爲TCP協議不對ACK進行ACK的,RFC建議最多等待2個包的積累確認,這樣可以及時通知對端Peer,我這邊的接收狀況。Linux實現中,有延遲ACK(Delay Ack)和快速ACK,並根據當前的包的收發狀況來在這兩種ACK中切換:在收到數據包的時候須要發送ACK,進行快速ACK;不然進行延遲ACK(在沒法使用快速確認的條件下也是)。
通常狀況下,ACK並不會對網絡性能有太大的影響,延遲ACK能減小發送的分段從而節省了帶寬,而快速ACK能及時通知發送方丟包,避免滑動窗口停等,提高吞吐率。
關於ACK分段,有個細節須要說明一下:
ACK的確認號,是確認按序收到的最後一個字節序,對於亂序到來的TCP分段,接收端會回覆相同的ACK分段,只確認按序到達的最後一個TCP分段。TCP鏈接的延遲確認時間通常初始化爲最小值40ms,隨後根據鏈接的重傳超時時間(RTO)、上次收到數據包與本次接收數據包的時間間隔等參數進行不斷調整。
推薦查看《TCP-IP詳解:Delay ACK》
前面說過,每個數據包都帶有下一個數據包的編號。若是下一個數據包沒有收到,那麼 ACK 的編號就不會發生變化
若是發送方發現收到三個連續的重複 ACK,或者超時了尚未收到任何 ACK,就會確認丟包,從而再次發送這個包。
TCP交互過程當中,若是發送的包一直沒收到ACK確認,是要一直等下去嗎?
顯然不能一直等(若是發送的包在路由過程當中丟失了,對端都沒收到又如何給你發送確認呢?),這樣協議將不可用,既然不能一直等下去,那麼該等多久呢?等太長時間的話,數據包都丟了好久了才重發,沒有效率,性能差;等過短時間的話,可能ACK還在路上快到了,這時候卻重傳了,形成浪費,同時過多的重傳會形成網絡擁塞,進一步加重數據的丟失。也是,咱們不能去猜想一個重傳超時時間,應該是經過一個算法去計算,而且這個超時時間應該是隨着網絡的情況在變化的。爲了使咱們的重傳機制更高效,若是咱們可以比較準確知道在當前網絡情況下,一個數據包從發出去到回來的時間RTT(Round Trip Time),那麼根據這個RTT(咱們就能夠方便設置RTO(Retransmission TimeOut)了。
如何計算設置這個RTO?
RFC793中定義了一個經典算法——加權移動平均(Exponential weighted moving average),算法以下:
針對上面算法問題,有衆多大神改進,難以長篇累牘,推薦閱讀《TCP 的那些事兒》、《TCP中RTT的測量和RTO的計算》
經過上面咱們能夠知道,TCP的重傳是由超時觸發的,這會引起一個重傳選擇問題,假設TCP發送端連續發了一、二、三、四、五、六、七、八、九、10共10包,其中四、六、8這3個包全丟失了,因爲TCP的ACK是確認最後連續收到序號,這樣發送端只能收到3號包的ACK,這樣在TIME_OUT的時候,發送端就面臨下面兩個重傳選擇:
上面的問題是因爲單純以時間驅動來進行重傳的,都必須等待一個超時時間,不能快速對當前網絡情況作出響應,若是加入以數據驅動呢?
TCP引入了一種叫Fast Retransmit(快速重傳 )的算法,就是在連續收到3次相同確認號的ACK,那麼就進行重傳。這個算法基於這麼一個假設,連續收到3個相同的ACK,那麼說明當前的網絡情況變好了,能夠重傳丟失的包了。
快速重傳解決了timeout的問題,可是沒解決重傳一個仍是重傳多個的問題。出現難以決定是否重傳多個包問題的根源在於,發送端不知道那些非連續序號的包已經到達接收端了,可是接收端是知道的,若是接收端告訴一下發送端不就能夠解決這個問題嗎?因而,RFC2018提出了 SACK(Selective Acknowledgment)——選擇確認機制,SACK是TCP的擴展選項
一個SACK的例子以下圖,紅框說明:接收端收到了0-5500,8000-8500,7000-7500,6000-6500的數據了,這樣發送端就能夠選擇重傳丟失的5500-6000,6500-7000,7500-8000的包。
SACK依靠接收端的接收狀況反饋,解決了重傳風暴問題,這樣夠了嗎?接收端能不能反饋更多的信息呢?顯然是能夠的,因而,RFC2883對對SACK進行了擴展,提出了D-SACK,也就是利用第一塊SACK數據中描述重複接收的不連續數據塊的序列號參數,其餘SACK數據則描述其餘正常接收到的不連續數據。這樣發送方利用第一塊SACK,能夠發現數據段被網絡複製、錯誤重傳、ACK丟失引發的重傳、重傳超時等異常的網絡情況,使得發送端能更好調整本身的重傳策略。
D-SACK,有幾個優勢:
ACK攜帶兩個信息。
TCP的標準窗口最大爲2^16-1=65535個字節
TCP的選項字段中還包含了一個TCP窗口擴大因子,option-kind爲3,option-length爲3個字節,option-data取值範圍0-14
窗口擴大因子用來擴大TCP窗口,可把原來16bit的窗口,擴大爲31bit。這個窗口是接收端告訴發送端本身還有多少緩衝區能夠接收數據。因而發送端就能夠根據這個接收端的處理能力來發送數據,而不會致使接收端處理不過來。也就是:
發送端是根據接收端通知的窗口大小來調整本身的發送速率的,以達到端到端的流量控制——Sliding Window(滑動窗口)。
TCP協議裏窗口機制有2種:一種是固定的窗口大小;一種是滑動的窗口。
這個窗口大小就是咱們一次傳輸幾個數據。對全部數據幀按順序賦予編號,發送方在發送過程當中始終保持着一個發送窗口,只有落在發送窗口內的幀才容許被髮送;同時接收方也維持着一個接收窗口,只有落在接收窗口內的幀才容許接收。這樣經過調整發送方窗口和接收方窗口的大小能夠實現流量控制。
下面一張圖來分析一下固定窗口大小有什麼問題
假設窗口的大小是1,也是就每次只能發送一個數據只有接受方對這個數據進行確認了之後才能發送第2個數據。咱們能夠看到發送方每發送一個數據接受方就要給發送方一個ACK對這個數據進行確認。只有接受到了這個確認數據之後發送方纔能傳輸下個數據。 這樣咱們考慮一下若是說窗口太小,那麼當傳輸比較大的數據的時候須要不停的對數據進行確認,這個時候就會形成很大的延遲。若是說窗口的大小定義的過大。咱們假設發送方一次發送100個數據。可是接收方只能處理50個數據。這樣每次都會只對這50個數據進行確認。發送方下一次仍是發送100個數據,可是接受方仍是隻能處理50個數據。這樣就避免了沒必要要的數據來擁塞咱們的鏈路。因此咱們就引入了滑動窗口機制,窗口的大小並非固定的而是根據咱們之間的鏈路的帶寬的大小,這個時候鏈路是否擁護塞。接受方是否能處理這麼多數據了。
咱們看看滑動窗口是如何工做的
首先是第一次發送數據這個時候的窗口大小是根據鏈路帶寬的大小來決定的。咱們假設這個時候窗口的大小是3。這個時候接受方收到數據之後會對數據進行確認告訴發送方我下次但願手到的是數據是多少。這裏咱們看到接收方發送的ACK=3(這是發送方發送序列2的回答確認,下一次接收方指望接收到的是3序列信號)。這個時候發送方收到這個數據之後就知道我第一次發送的3個數據對方只收到了2個。就知道第3個數據對方沒有收到。下次在發送的時候就從第3個數據開始發。這個時候窗口大小就變成了2 。
這個時候發送方發送2個數據。
看到接收方發送的ACK是5就表示他下一次但願收到的數據是5,發送方就知道我剛纔發送的2個數據對方收了這個時候開始發送第5個數據。
這就是滑動窗口的工做機制,當鏈路變好了或者變差了這個窗口還會發生變話,並非第一次協商好了之後就永遠不變了。
滑動窗口協議的基本原理就是在任意時刻,發送方都維持了一個連續的容許發送的幀的序號,稱爲發送窗口;同時,接收方也維持了一個連續的容許接收的幀的序號,稱爲接收窗口。發送窗口和接收窗口的序號的上下界不必定要同樣,甚至大小也能夠不一樣。不一樣的滑動窗口協議窗口大小通常不一樣。
窗口有3種動做:展開(右邊向右),合攏(左邊向右),收縮(右邊向左)這三種動做受接收端的控制。
合攏:表示已經收到相應字節的確認了
展開:表示容許緩存發送更多的字節
收縮(很是不但願出現的,某些實現是禁止的):表示原本能夠發送的,如今不能發送;可是若是收縮的是那些已經發出的,就會有問題;爲了不,收端會等待到緩存中有更多緩存空間時才進行通訊。
比特滑動窗口協議
當發送窗口和接收窗口的大小固定爲1時,滑動窗口協議退化爲停等協議(stop-and-wait)。該協議規定發送方每發送一幀後就要停下來,等待接收方已正確接收的確認(acknowledgement)返回後才能繼續發送下一幀。因爲接收方須要判斷接收到的幀是新發的幀仍是從新發送的幀,所以發送方要爲每個幀加一個序號。因爲停等協議規定只有一幀徹底發送成功後才能發送新的幀,於是只用一比特來編號就夠了。其發送方和接收方運行的流程圖如圖所示。
後退n協議
因爲停等協議要爲每個幀進行確認後才繼續發送下一幀,大大下降了信道利用率,所以又提出了後退n協議。後退n協議中,發送方在發完一個數據幀後,不停下來等待應答幀,而是連續發送若干個數據幀,即便在連續發送過程當中收到了接收方發來的應答幀,也能夠繼續發送。且發送方在每發送完一個數據幀時都要設置超時定時器。只要在所設置的超時時間內仍收到確認幀,就要重發相應的數據幀。如:當發送方發送了N個幀後,若發現該N幀的前一個幀在計時器超時後仍未返回其確認信息,則該幀被判爲出錯或丟失,此時發送方就不得不從新發送出錯幀及其後的N幀。
從這裏不難看出,後退n協議一方面因連續發送數據幀而提升了效率,但另外一方面,在重傳時又必須把原來已正確傳送過的數據幀進行重傳(僅因這些數據幀以前有一個數據幀出了錯),這種作法又使傳送效率下降。因而可知,若傳輸信道的傳輸質量不好於是誤碼率較大時,連續測協議不必定優於中止等待協議。此協議中的發送窗口的大小爲k,接收窗口還是1。
選擇重傳協議
在後退n協議中,接收方若發現錯誤幀就再也不接收後續的幀,即便是正確到達的幀,這顯然是一種浪費。另外一種效率更高的策略是當接收方發現某幀出錯後,其後繼續送來的正確的幀雖然不能當即遞交給接收方的高層,但接收方仍可收下來,存放在一個緩衝區中,同時要求發送方從新傳送出錯的那一幀。一旦收到從新傳來的幀後,就能夠原已存於緩衝區中的其他幀一併按正確的順序遞交高層。這種方法稱爲選擇重發(SELECTICE REPEAT),其工做過程如圖所示。顯然,選擇重發減小了浪費,但要求接收方有足夠大的緩衝區空間。
推薦閱讀《計算機網絡 TCP 滑動窗口協議 詳解》
所謂流量控制,主要是接收方傳遞信息給發送方,使其不要發送數據太快,是一種端到端的控制。主要的方式就是返回的ACK中會包含本身的接收窗口的大小,而且利用大小來控制發送方的數據發送。
上圖中,咱們能夠看到:
因而:
下面咱們來看一下發送方的滑動窗口示意圖:
發送端是怎麼作到比較方便知道本身哪些包能夠發,哪些包不能發呢?
一個簡明的方案就是按照接收方的窗口通告,發送方維護一個同樣大小的發送窗口就能夠了。在窗口內的能夠發,窗口外的不能夠發,窗口在發送序列上不斷後移,這就是TCP中的滑動窗口。以下圖所示,對於TCP發送端其發送緩存內的數據均可以分爲4類
[1]-已經發送並獲得接收端ACK的;
[2]-已經發送但還未收到接收端ACK的;
[3]-未發送但容許發送的(接收方還有空間);
[4]-未發送且不容許發送(接收方沒空間了)。
其中,[2]和[3]兩部分合起來稱之爲發送窗口。
下面兩圖演示的窗口的滑動狀況,收到36的ACK後,窗口向後滑動5個byte。
若是接收端通知一個零窗口給發送端,這個時候發送端還能不能發送數據呢?若是不發數據,那一直等接收端口通知一個非0窗口嗎,若是接收端一直不通知呢?
下圖,展現了一個發送端是怎麼受接收端控制的。由上圖咱們知道,當接收端通知一個zero窗口的時候,發送端的發送窗口也變成了0,也就是發送端不能發數據了。若是發送端一直等待,直到接收端通知一個非零窗口在發數據的話,這彷佛太受限於接收端,若是接收端一直不通知新的窗口呢?顯然發送端不能幹等,起碼有一個主動探測的機制。爲解決0窗口的問題,TCP使用了ZWP(Zero Window Probe)。
發送端在窗口變成0後,會發ZWP的包給接收方,來探測目前接收端的窗口大小,讓接收方來ack他的Window尺寸。通常這個值會設置成3次,每次大約30-60秒(不一樣的實現可能會不同)。若是3次事後仍是0的話,有的TCP實現就會發RST掉這個鏈接。
注意:只要有等待的地方均可能出現DDoS攻擊。攻擊者能夠在和Server創建好鏈接後,就向Server通告一個0窗口,而後Server端就只能等待進行ZWP,因而攻擊者會併發大量的這樣的請求,把Server端的資源耗盡。
若是接收端處理能力很慢,這樣接收端的窗口很快被填滿,而後接收處理完幾個字節,騰出幾個字節的窗口後,通知發送端,這個時候發送端立刻就發送幾個字節給接收端嗎?發送的話會不會太浪費了,就像一艘萬噸油輪只裝上幾斤的油就開去目的地同樣。咱們的TCP+IP頭有40個字節,爲了幾個字節,要達上這麼大的開銷,這太不經濟了。
對於發送端產生數據的能力很弱也同樣,若是發送端慢吞吞產生幾個字節的數據要發送,這個時候該不應當即發送呢?仍是累積多點在發送?
本質就是一個避免發送大量小包的問題。形成這個問題緣由有二:
在接收端解決這個問題,David D Clark’s 方案,若是收到的數據致使window size小於某個值,就ACK一個0窗口,這就阻止發送端在發數據過來。等到接收端處理了一些數據後windows size 大於等於了MSS,或者buffer有一半爲空,就能夠通告一個非0窗口。
是在發送端解決這個問題,有個著名的Nagle’s algorithm。Nagle 算法的規則
規則[4]指出TCP鏈接上最多隻能有一個未被確認的小數據包。從規則[4]能夠看出Nagle算法並不由止發送小的數據包(超時時間內),而是避免發送大量小的數據包。因爲Nagle算法是依賴ACK的,若是ACK很快的話,也會出現一直髮小包的狀況,形成網絡利用率低。TCP_CORK選項則是禁止發送小的數據包(超時時間內),設置該選項後,TCP會盡力把小數據包拼接成一個大的數據包(一個 MTU)再發送出去,固然也不會一直等,發生了超時(通常爲 200ms ),也當即發送。Nagle 算法和CP_CORK 選項提升了網絡的利用率,可是增長是延時。從規則[3]能夠看出,設置TCP_NODELAY 選項,就是徹底禁用Nagle 算法了。
這裏要說一個小插曲,Nagle算法和延遲確認(Delayed Acknoledgement)一塊兒,當出現( write-write-read)的時候會引起一個40ms的延時問題,這個問題在HTTP svr中體現的比較明顯。場景以下:
客戶端在請求下載HTTP svr中的一個小文件,通常狀況下,HTTP svr都是先發送HTTP響應頭部,而後在發送HTTP響應BODY(特別是比較多的實如今發送文件的實施採用的是sendfile系統調用,這就出現write-write-read模式了)。當發送頭部的時候,因爲頭部較小,因而造成一個小的TCP包發送到客戶端,這個時候開始發送body,因爲body也較小,這樣仍是造成一個小的TCP數據包,根據Nagle算法,HTTP svr已經發送一個小的數據包了,在收到第一個小包的ACK後或等待200ms超時後才能在發小包,HTTP svr不能發送這個body小TCP包;
客戶端收到http響應頭後,因爲這是一個小的TCP包,因而客戶端開啓延遲確認,客戶端在等待Svr的第二個包來在一塊兒確認或等待一個超時(通常是40ms)在發送ACK包;這樣就出現了你等我、然而我也在等你的死鎖狀態,因而出現最多的狀況是客戶端等待一個40ms的超時,而後發送ACK給HTTP svr,HTTP svr收到ACK包後在發送body部分。你們在測HTTP svr的時候就要留意這個問題了。
推薦閱讀《TCP/IP之TCP協議:流量控制(滑動窗口協議)》
因爲TCP看不到網絡的情況,那麼擁塞控制是必須的而且須要採用試探性的方式來控制擁塞,因而擁塞控制要完成兩個任務:[1]公平性;[2]擁塞事後的恢復。
重介紹一下Reno算法(RFC5681),其包含4個部分:
[1]慢熱啓動算法 – Slow Start
[2]擁塞避免算法 – Congestion Avoidance;
[3]快速重傳 - Fast Retransimit;
[4]快速恢復算法 – Fast Recovery。
咱們怎麼知道,對方線路的理想速率是多少呢?答案就是慢慢試。
開始的時候,發送得較慢,而後根據丟包的狀況,調整速率:若是不丟包,就加快發送速度;若是丟包,就下降發送速度。慢啓動的算法以下(cwnd全稱Congestion Window):
根據RFC5681,若是MSS > 2190 bytes,則N = 2;若是MSS < 1095 bytes,則N = 4;若是2190 bytes >= MSS >= 1095 bytes,則N = 3;一篇Google的論文《An Argument for Increasing TCP’s Initial Congestion Window》建議把cwnd 初始化成了 10個MSS。Linux 3.0後採用了這篇論文的建議(Linux 內核裏面設定了(常量TCP_INIT_CWND),剛開始通訊的時候,發送方一次性發送10個數據包,即"發送窗口"的大小爲10。而後停下來,等待接收方的確認,再繼續發送)
慢啓動的時候說過,cwnd是指數快速增加的,可是增加是有個門限ssthresh(通常來講大多數的實現ssthresh的值是65535字節)的,到達門限後進入擁塞避免階段。在進入擁塞避免階段後,cwnd值變化算法以下:
TCP是看不到網絡的總體情況的,那麼TCP認爲網絡擁塞的主要依據是它重傳了報文段。前面咱們說過TCP的重傳分兩種狀況:
在快速重傳的時候,通常網絡只是輕微擁堵,在進入擁塞避免後,cwnd恢復的比較慢。針對這個,「快速恢復」算法被添加進來,當收到3個冗餘ACK時,TCP最後的[3]步驟進入的不是擁塞避免階段,而是快速恢復階段。
快速恢復的思想是「數據包守恆」原則,即帶寬不變的狀況下,在網絡同一時刻能容納數據包數量是恆定的。當「老」數據包離開了網絡後,就能向網絡中發送一個「新」的數據包。既然已經收到了3個冗餘ACK,說明有三個數據分段已經到達了接收端,既然三個分段已經離開了網絡,那麼就是說能夠在發送3個分段了。因而只要發送方收到一個冗餘的ACK,因而cwnd加1個MSS。快速恢復步驟以下(在進入快速恢復前,cwnd 和 sshthresh已被更新爲:sshthresh = cwnd /2,cwnd = sshthresh):
細心的同窗可能會發現快速恢復有個比較明顯的缺陷就是:它依賴於3個冗餘ACK,並假定不少狀況下,3個冗餘的ACK只表明丟失一個包。可是3個冗餘ACK也頗有多是丟失了不少個包,快速恢復只是重傳了一個包,而後其餘丟失的包就只能等待到RTO超時了。超時會致使ssthresh減半,而且退出了Fast Recovery階段,多個超時會致使TCP傳輸速率呈級數降低。出現這個問題的主要緣由是過早退出了Fast Recovery階段。爲解決這個問題,提出了New Reno算法,該算法是在沒有SACK的支持下改進Fast Recovery算法(SACK改變TCP的確認機制,把亂序等信息會所有告訴對方,SACK自己攜帶的信息就能夠使得發送方有足夠的信息來知道須要重傳哪些包,而不須要重傳哪些包),具體改進以下:
咱們能夠看到,擁塞控制在擁塞避免階段,cwnd是加性增長的,在判斷出現擁塞的時候採起的是指數遞減。爲何要這樣作呢?這是出於公平性的原則,擁塞窗口的增長受惠的只是本身,而擁塞窗口減小受益的是你們。這種指數遞減的方式實現了公平性,一旦出現丟包,那麼當即減半退避,能夠給其餘新建的鏈接騰出足夠的帶寬空間,從而保證整個的公平性。
TCP發展到如今,擁塞控制方面的算法不少,請查看《wiki-具體實現算法》,《斐訊面試記錄—TCP滑動窗口及擁塞控制》
總的來講TCP是一個有鏈接的、可靠的、帶流量控制和擁塞控制的端到端的協議。TCP的發送端能發多少數據,由發送端的發送窗口決定(固然發送窗口又被接收端的接收窗口、發送端的擁塞窗口限制)的,那麼一個TCP鏈接的傳輸穩定狀態應該體如今發送端的發送窗口的穩定狀態上,這樣的話,TCP的發送窗口有哪些穩定狀態呢?
TCP的發送窗口穩定狀態主要有上面三種穩定狀態:
【1】接收端擁有大窗口的經典鋸齒狀
大多數狀況下都是處於這樣的穩定狀態,這是由於,通常狀況下機器的處理速度就是比較快,這樣TCP的接收端都是擁有較大的窗口,這時發送端的發送窗口就徹底由其擁塞窗口cwnd決定了;網絡上擁有成千上萬的TCP鏈接,它們在相互爭用網絡帶寬,TCP的流量控制使得它想要獨享整個網絡,而擁塞控制又限制其必要時作出犧牲來體現公平性。因而在傳輸穩定的時候TCP發送端呈現出下面過程的反覆
[1]用慢啓動或者擁塞避免方式不斷增長其擁塞窗口,直到丟包的發生;
[2]而後將發送窗口將降低到1或者降低一半,進入慢啓動或者擁塞避免階段(要看是因爲超時丟包仍是因爲冗餘ACK丟包);過程以下圖:
【2】接收端擁有小窗口的直線狀態
這種狀況下是接收端很是慢速,接收窗口一直很小,這樣發送窗口就徹底有接收窗口決定了。因爲發送窗口小,發送數據少,網絡就不會出現擁塞了,因而發送窗口就一直穩定的等於那個較小的接收窗口,呈直線狀態。
【3】兩個直連網絡端點間的滿載狀態下的直線狀態
這種狀況下,Peer兩端直連,而且只有位於一個TCP鏈接,那麼這個鏈接將獨享網絡帶寬,這裏不存在擁塞問題,在他們處理能力足夠的狀況下,TCP的流量控制使得他們可以跑慢整個網絡帶寬。
經過上面咱們知道,在TCP傳輸穩定的時候,各個TCP鏈接會均分網絡帶寬的。相信你們學生時代常常會發生這樣的場景,本身在看視頻的時候忽然出現視頻卡頓,因而就大叫起來,哪一個開了迅雷,趕忙給我停了。其實簡單的下載加速就是開啓多個TCP鏈接來分段下載就達到加速的效果,假設宿舍的帶寬是1000K/s,一開始兩個在看視頻,每人平均網速是500k/s,這速度看起視頻來那叫一個順溜。忽然其中一個同窗打打開迅雷開着99個TCP鏈接在下載愛情動做片,這個時候平均下來你能分到的帶寬就剩下10k/s,這網速下你的視頻還不卡成幻燈片。在通訊鏈路帶寬固定(假設爲W),多人公用一個網絡帶寬的狀況下,利用TCP協議的擁塞控制的公平性,多開幾個TCP鏈接就能多分到一些帶寬(固然要忽略有些用UDP協議帶來的影響),然而無論怎麼最多也就能把整個帶寬搶到,因而在佔滿整個帶寬的狀況下,下載一個大小爲FS的文件,那麼最快須要的時間是FS/W,難道就沒辦法加速了嗎?
答案是有的,這樣由於網絡是網狀的,一個節點是要和不少幾點互聯的,這就存在多個帶寬爲W的通訊鏈路,若是咱們可以將要下載的文件,一半從A通訊鏈路下載,另一半從B通訊鏈路下載,這樣整個下載時間就減半了爲FS/(2W),這就是p2p加速。
其實《鮮爲人知的網絡編程:淺析TCP協議中的疑難雜症》講的很是細,並且一遍文章根本總結不了(我也只是搬運工而已,由於所知的太少,都不像筆記了
基礎科普類:https://hit-alibaba.github.io/interview/basic/network/HTTP.html
推薦文章:
《TCP 協議簡介》(阮一峯)