很開心,上半年釋出的spring boot 2中,預設的web 容器是netty ,這說明“反應式” 容器已經是大勢所趨,無論是go 語言的協從執行緒,還是java 基於reactor 執行緒模型,都是基於事件程式設計實現高併發的例項。這周開始我會講關於NiO的一切,底層原理是什麼,應用架構有哪些,如何利用其優勢構建高效能伺服器,歡迎關注。
在介紹NIO之前有必要了解下TCP協議,因為目前多數應用都是給予應用層進行操作,導致隱藏了大量的網路細節,知道這些細節以及原理對我們的問題排查很有益處。
一、TCP 特性
TCP 是一種面向連線的協議,它給使用者程式提供可靠的全雙工的位元組流。確保資料包的可靠,有序,以及支援流量控制。關於TCP 為何要做這些,我們從以下幾個方面入手:
IP網路層為何不保證資料包的可靠性
TCP協議如何保證包可達、有序
TCP協議如何支援流量控制
TCP幾種狀態以及應用
二、IP網路層為何不保證資料包的可靠性
我們先看下OSI的網路分層,在以下分層中,TCP 位於傳輸層,它保證的是協議的可靠性和連續性。具體的收發報是有底層的鏈路層以及物理層所決定的,所以TCP 所做的工作,也是基於底層的優化和改進。
客戶端與伺服器之間的通訊使用應用協議,傳輸層的通訊採用TCP協議,而TCP 協議又採用了更低的一層IP協議,IP則使用某種形式的資料鏈路層通訊。
我們知道網路的中的資料,最終通過多個路由器連線傳送的。最底層的乙太網協議規定了電子訊號如何組成資料包,解決了區域網的點對點通訊問題,但無法解決多個區域網的的互通問題。
而網路層採用的IP協議,就是定義了一套自己的地址規則,主要解決定址和路由的功能,根據對方的IP地址,尋找最佳路徑傳輸資訊。區域網通過路由器連線,路由器基於IP協議,指導資料包向某個路由藉口轉發。但IP協議不保證包一定到達以及完整性,特別是網路擁堵的時候,會丟棄一些資料包,保證資料的傳送效率。
而保證資料包的完整、有序以及可靠,這就是TCP 協議要來做的事情了。
三、TCP 協議
1、TCP 包組成
很多網路有一個最大傳送單元,它是鏈路層中的網路對資料幀的一個限制,以乙太網為例,MTU為1500個位元組。一個IP資料包在乙太網中 傳輸,如果它的長度大於該MTU值,就要進行分片傳輸,使得每片資料包的長度小於MTU。
另外一個資料包還包含頭資訊,除了自己的Tcp包頭,還有IP 頭資訊和乙太網頭資訊。IP 資料包在乙太網資料包的負載裡面,最少需要20位元組,所以 IP 資料包的負載最多為1480位元組。
那麼tcp的一個包大小是多少吶?
我們需要機遇MSS這個值來確定,MSS是TCP裡的一個概念(首部的選項欄位中)。MSS是TCP資料包每次能夠傳輸的最大資料分段,TCP報文段的長度大於MSS時,要進行分段傳輸。 如果不設定,則MSS的預設值就為536個位元組 。也就是說一個tcp包的在500位元組左右。
2、如何保證可靠性
上述也說了,底層的路由轉發包,並不保證包的可靠性以及有序性。
首先為了保證包的完整性,TCP 會基於MSS 為大於 MSS的包進行分包處理,預設MSS大小為563byte,其大小小於MUT,以防止在網路層被分片處理。
其次增加SEQ和ACK,同時採用超時重發的機制來保證包的可靠性。
1)SEQ
為了保證有序性,TCP 為每個包編配一個Sequence number ,簡稱 SEQ 。以便接收的一方按照順序還原。萬一發生丟包,也可以知道丟失的是哪一個包。一般第一個包的編號是一個隨機數,也可以從1開始。
2)ACK
那麼有編號了,如何確保包一定到達?
基於ACK 進行確認。對於接收方來說,每次接受一個包必須返回ack資訊,傳送端從而確認這個包已經傳送到。另外,接收方要對每一條報文做校驗。如果校驗發現出錯,則不傳送確認報文,從而觸發傳送方超時重傳。
ACK 包含以下資訊:
期待要收到下一個資料包的編號 next SEQ
接收方的接收視窗的剩餘容量
我們採用wiershark抓包一個oschina的包看下三次握手的資料。
我的本機ip:192.168.1.103
oschinaIp:116.211.174.177
三次握手過程:
1.me->osChina:syn=1 seq=x ack=0
2.osChina->me:syn=1 seq=y ack=x+1
3.me->osChina:seq=x+1 ack=y+1複製程式碼
1、me->osChina:syn=1 seq=0 ack=0
2、osChina->me:syn=1 seq=0 ack=0+1
3、me->osChina:seq=0+1 ack=0+1
對比一下三次握手的過程。
3)超時重傳
我們知道網路極其不穩定,資料包即便增加了SEQ和ACK,能夠保證其有序性,但依然保證丟包或者超時的問題。如果傳送端傳送資料,或者接收端回覆ACK的訊息在網路中丟失或者超時怎麼處理?
RTO ,超時重傳時間。要知道包是否出現超時,需要有一個評估方式,而RTT是對一個給定連線的往返時間的測量。由於網路流量的變化,這個時間會相應地發生改變,TCP需要跟蹤這些變化並動態調整超時時間RTO。
傳送方如果一定時間內沒收到報文的ACK,就認為該報文丟失在網路中了,自動重發該報文。這種機制稱之為超時重傳。
在這期間,如果接收端的訊息,由於丟失,接收端沒有收到ack 訊息,傳送端會向接收端重發這個包。如果因為超時原因,傳送端在超時定時器之後收到了這個包的ack 資訊,而且傳送端已經重複傳送了這個訊息,此時傳送端不會處理,直接丟棄該ack 。而接收端接收到了之後會再次回覆ack 資訊。
四、流量控制
上述中我們知道了TCP協議可以保證資料的可靠性,但是也得兼顧效率。兼顧效率的話需要考慮以下三個方面:
支援批量發包
能夠基於網路的狀況,支援擁堵控制
能夠了解接收端的狀況,防止接收端處理不過來
基於以上三個需求,做了以下處理。
1、滑動視窗
如果TCP 中的包,都需要傳送一個確認一個的話,效率太低了,單次傳送和確認一個包,雖然保證了可靠性,但無法保證其效率。此時需要一個批量傳送和確認的方式,這就是滑動視窗所做的事情。
傳送滑動視窗:
傳送視窗從左向右移動在這個傳送視窗之前的資料必然是已經傳送而且得到接收方確認的資料落在傳送視窗之內的資料是傳送方可以傳送的資料在傳送視窗之後的資料是不能傳送的資料。
如果發生超時或者丟失現象。那麼有兩種解決方案:
1、回退N,丟失的包號之後所有包都重發2、選擇重傳ARQ,只發丟失的,避免重複的(效率高,防止傳送重複的)
滑動視窗還有一個作用是讓傳送端知道接收端的處理狀況。假設TCP接收方的快取已經滿了,無法處理更多的,而傳送方並不知道,每次會給對方告知當前滑動視窗的大小值 ,此時傳送端就不會再傳送資料了。
接收方接收到資料同樣馬上傳送確認,但是同時對傳送方宣佈視窗大小為0。這樣傳送方就暫時不會傳送資料。
報文到達時不馬上傳送確認,直到快取有足夠的空間。這樣就可以避免傳送方滑動視窗。但是這也存在一個問題,接收方延遲傳送確認的時間不應該超過超時時間,如果過長會導致傳送方誤以為資料丟失重新傳送資料。
2、擁堵控制
我們知道網路狀況有好友壞,好的時候,可以多發些包,壞的時候,如果發包速率不變的話,除了會加重網路負擔以外,還會造成包的過多丟失,除非更多的超時重發,這無疑識降低了通訊效率。
基於此,TCP通訊雙方維護一個叫做擁塞視窗(cwnd,congesion window)的值,這個值取決於網路中的擁塞率,傳送方的傳送視窗的值就等於擁塞視窗的大小。只要網路中沒有出現擁塞,擁塞視窗的值就可以增大一些,這樣傳送方可以傳送到網路中的資料就多一些。反之,擁塞視窗的值就減小,從而避免加劇網路的擁塞率。
TCP目前擁塞控制主要有以下4種演算法:
慢啟動
擁塞避免
快速重傳
快恢復
具體的演算法實現方式就不再介紹了,大概實現的功能就是,基於當前的網路狀況,找到一個合適的傳送速率,防止給網路造成過大的負擔。比如說慢啟動,就是開始的時候,傳送得較慢,然後根據丟包的情況,調整速率:如果不丟包,就加快傳送速度;如果丟包,就降低傳送速度。
五、TCP 狀態
瞭解TCP的都知道,TCP 建立連線的時候,有三次握手,斷開連結的時候又四次握手互動。那麼其中的狀態是有哪些?
上面的圖看著是不是太亂記不住,我們看看下面這張梳理一下,看看具體應用狀態。
從上面可以看到,連線建立成功的時候,其狀態是ESTABLISHED 的。當接受端的狀態為SYN—RECV的時候,表示接受端,已經回覆第二次握手資訊了,等待傳送端再次確認。如果網路中遭受到大量的SYN 攻擊,會存在大量的SYN_RECV 狀態。此時可以定位這些問題IP ,通過防火牆過濾就能解決大量的假連線問題。
六、消失的連線——TIME_WAIT
在網路中,某一端主動關閉而沒有通過四次握手關閉,此時tcp已經建立的通道是否還在,多久會關閉?此時的TCP 狀態為TIME_WAIT ,可以想象,現實中經常出現這種狀況,多數的關閉連線都是主動關閉而非通過協商通訊關閉。那麼此時關閉,若果再重連還能重連上之前的tcp 通道麼,還是需要重現建立。
任何TCP實現必須為MSL選擇一個值,預設是2分鐘或者30秒,TIME_WAIT預設是2倍的MSL,持續時間在1-4分鐘之間。MSL是IP資料包能在網路中存活的最長時間。
TIME_WAIT 存在的兩個理由: 1、可靠的實現TCP全雙工連線的終止 2、允許老的重複分節在網路中消失
TCP必須防止某個連線的老的重複分組在該連線已經終止後再現,從而被誤解成屬於同一連線的化身,有time_wait 足夠長,是2倍的MSL的,那麼足夠讓某個方向上的分組最多存活MSL秒就被丟棄。
從TIME_WAIT狀態到CLOSED狀態,有一個超時設定,這個超時設定是 2*MSL(RFC793定義了MSL為2分鐘,Linux設定成了30s),如此超過了這個時間,當前的tcp通道就會被定義為關閉。
更多架構知識,歡迎關注我的公眾號,大碼候(cool_wier)