很開心,上半年釋出的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)