咱們知道TCP經過三次握手創建可靠鏈接,經過四次揮手斷開鏈接,TCP鏈接是比較昂貴的資源。爲何TCP須要經過三次握手才能創建可靠的鏈接?兩次不行麼?斷開鏈接爲何須要四次?TCP鏈接昂貴在哪裏?
客戶端:「喂,聽獲得嗎?」
服務端:「我能聽到,你能聽到我嗎?」
客戶端:「恩,能聽到。」
爲何須要三次握手,對客戶端而言,再收到服務端的ACK
後,能肯定我發的消息服務端能收到,服務端發的消息我也能收到了,那爲何還要第三次握手?這要從服務端考慮,服務端在接收到SYN
後只能肯定本身能收到客戶端發來的消息,若是沒有第三次握手,服務端是不肯定對方是否能接收到本身這邊發送的消息的,這種不肯定勢必影響到了信道的可靠性。既然三次就已經確保了信道的可靠性,若是在加一次確定就增長了網絡消耗從而影響了創建鏈接的效率。html
客戶端:「不說了,掛了吧。」
服務端:「OK!」
服務端:「你要注意身體啊!」
服務端:「拜拜!」
客戶端:「拜拜!」
斷開鏈接是釋放資源的過程,仍是從客戶端和服務端兩我的的角度去分析揮手過程。web
首先創建鏈接是爲了可靠的數據交付,如今鏈接創建已經有一段時間了,客戶端說數據已經發完了,已經沒什麼要發送了,因而告訴操做系統,嘿,老兄,我數據已經發完了,你能夠把個人發送資源釋放啦,因而操做系統鎖住了發送資源(好比發送隊列)準備釋放,並標記了TCP
鏈接狀態爲FIN_WAIT_1
,因爲數據發送是雙方的事情,客戶端這邊的發送資源已經釋放,客戶端有義務告知服務端這邊的數據已經發送完畢,因此操做系統會發送一條FIN
消息到服務端,告知服務端能夠釋放接收資源了,爲了保證服務端確實收到了FIN
消息並釋放了接收資源,服務端也須要返回一條ACK
消息給客戶端,若是客戶端沒收到ACK
消息,則重試剛剛的FIN
消息。客戶端一旦收到ACK
消息,則說明服務端已經釋放了接收資源,操做系統將TCP
鏈接狀態改成FIN_WAIT_2
。到這裏TCP
鏈接已經關閉一半。緩存
上面的過程只是結束了客戶端的數據發送,釋放了發送數據須要的資源,可是客戶端依然能夠接收從服務端發來的數據,服務端只是結束了數據接收並釋放相關資源,依然能夠放數據,由於服務端處理完接收的數據後要反饋結果給客戶端。等結果反饋完後,沒有數據要處理了,服務端也要結束髮送過程,一樣也得告知客戶端讓其釋放接收數據所須要的資源。服務端重複上面的過程。但不一樣的是,客戶端接收到FIN
消息並返回ACK
消息後須要等一段時間,這是因爲擔憂服務端沒有收到ACK
又重發了FIN
消息。等過了一段時間後並無收到重發的消息,客戶端就會釋放全部資源(這裏就無論服務端到底有沒有收到ACK
了,若是一直管下去就是個死循環)。服務端也是同樣,重試屢次之後也就釋放了全部資源(這裏不清楚究竟是不是釋放了資源,也有可能有其餘機制)。安全
從上分析,安全可靠的斷開鏈接至少須要四次,再多一次的意義不大。服務器
上面分析可知,三次握手和四次揮手無疑會形成巨大的網絡資源和CPU資源的消耗(若是鏈接沒有被複用,這就是一種浪費),另外,客戶端和服務端分別要維護各自的發送和接收緩存,各自在操做系統裏面的變量(好比文件描述符,操做系統維護的一套數據結構),在阻塞式的網絡模型中,服務端還要開啓線程來處理這條鏈接。因此說
TCP
鏈接是比較昂貴的資源,須要鏈接池這種技術來提升它的複用性。
以上都是在理想的狀況下發生的,理想狀態下,一個TCP
鏈接能夠被長期保持。可是現實老是很骨感,在保持TCP
鏈接的過程當中極可能出現各類意外的狀況,好比網絡故障,客戶端崩潰或者異常重啓,在這種狀況下,若是服務端沒有及時清理這些鏈接,服務端將發生鏈接泄露,直至服務端資源耗盡拒絕提供服務(connection refused exception
)。所以在實際應用中,服務器端須要採起相應的方法來探測TCP
鏈接是否已經斷連。探測的原理就是心跳機制,能夠是應用層面的心跳,也能夠是第三方的心跳,可是絕大部分類Unix
系統均在TCP
中提供了相應的心跳檢測功能(雖然並非TCP
規範中的一部分)。
當客戶端程序因未知緣由崩潰或異常退出後,操做系統會給服務端發送一條RST
消息,阻塞模型下,服務端內核沒法主動通知應用層出錯,只有應用層主動調用read()
或者write()
這樣的IO
系統調用時,內核纔會利用出錯來通知應用層對端RST
(Linux
系統報Connection reset by peer
)。非阻塞模型下,服務端select
或者epoll
會返回sockfd
可讀,應用層對其進行讀取時,read()
會報錯RST
。網絡
哪些狀況下,會收到來自對端的RST
消息呢。數據結構
connect
一個不存在的端口,客戶端會收到一條RST
,報錯Connection refused
;send
數據時會收到來自對端的RST
。close(sockfd)
時,直接丟棄接收緩衝區未讀取的數據,並給對方發一個RST
。這個是由SO_LINGER
選項來控制的;TCP socket
在任何狀態下,只要收到RST
包,便可釋放鏈接資源。socket
若是客戶端斷電或網絡異常,而且鏈接通道內沒有任何數據交互,服務端是感知不到客戶端掉線的,此時須要藉助心跳機制來感知這種情況,通常的作法是,服務端往對端發送一個心跳包並啓動一個超時定時器,若是能正確收到對端的迴應,說明在線,若是超時,能夠進行一系列操做,好比重試、關閉鏈接等等。tcp
借鑑一下大神的 文章不少人都知道
TCP
並不會去主動檢測鏈接的丟失,這意味着,若是雙方不產生交互,那麼若是網絡斷了或者有一方機器崩潰,另一方將永遠不知道鏈接已經不可用了。檢測鏈接是否丟失的方法大體有兩種:keepalive
和heart-beat
。分佈式
Keepalive
是不少的TCP實現提供的一種機制,它容許鏈接在空閒的時候雙方會發送一些特殊的數據段,並經過響應與否來判斷鏈接是否還存活着(所謂keep~~alive
)。我曾經寫過一篇關於keepalive的blog ,但後來我也發現,其實keepalive
在實際的應用中並不常見。爲什麼如此?這得歸結於keepalive
設計的初衷。Keepalive
適用於清除死亡時間比較長的鏈接。
好比這樣的場景:一個用戶建立tcp
鏈接訪問了一個web
服務器,當用戶完成他執行的操做後,很粗暴的直接撥了網線。這種狀況下,這個tcp
鏈接已經斷開了,可是web
服務器並不知道,它會依然守護着這個鏈接。若是web server
設置了keepalive
,那麼它就可以在用戶斷開網線的大概幾個小時之後,確認這個鏈接已經中斷,而後丟棄此鏈接,回收資源。
採用keepalive
,它會先要求此鏈接必定時間沒有活動(通常是幾個小時),而後發出數據段,通過屢次嘗試後(每次嘗試之間也有時間間隔),若是仍沒有響應,則判斷鏈接中斷。可想而知,整個週期須要很長的時間。
因此,如前面的場景那樣,須要一種方法可以清除和回收那些在系統不知情的狀況下死去了好久的鏈接,keepalive
是很是好的選擇。
可是,在大部分狀況下,特別是分佈式環境中,咱們須要的是一個可以快速或者實時監控鏈接狀態的機制,這裏,heart-beat
纔是更加合適的方案。Heart-beat
(心跳),按個人理解,它的原理和keepalive
很是相似,都是發送一個信號給對方,若是屢次發送都沒有響應的話,則判斷鏈接中斷。它們的不一樣點在於,keepalive
是tcp
實現中內建的機制,是在建立tcp
鏈接時經過設置參數啓動keepalive
機制;而heart-beat
則須要在tcp
之上的應用層實現。一個簡單的heart-beat
實現通常測試鏈接是否中斷採用的時間間隔都比較短,能夠很快的決定鏈接是否中斷。而且,因爲是在應用層實現,由於能夠自行決定當判斷鏈接中斷後應該採起的行爲,而keepalive
在判斷鏈接失敗後只會將鏈接丟棄。
關於heart-beat
,一個很是有趣的問題是,應該在傳輸真正數據的鏈接中發送心跳
信號,仍是能夠專門建立一個發送「心跳」信號的鏈接。好比說,A
,B
兩臺機器之間經過鏈接m
來傳輸數據,如今爲了可以檢測A
,B
之間的鏈接狀態,咱們是應該在鏈接m
中傳輸心跳
信號,仍是建立新的鏈接n
來專門傳輸心跳
呢?我我的認爲二者皆可。若是擔憂的是端到端的鏈接狀態,那麼就直接在該條鏈接中實現心跳
。但不少時候,關注的是網絡情況和兩臺主機間的鏈接狀態,這種狀況下, 建立專門的心跳
鏈接也何嘗不可。
客戶端正常關閉鏈接:
//發送FIN消息,說明客戶端已經沒有數據發送,服務端read時會返回-1或者null socket.shutdownOutput(); //默認的SO_LINGER參數,客戶端發送FIN消息,服務端read時會返回-1或者null socket.close(); //設置了當即關閉,客戶端發送RST消息,服務端`read`時會報`connection rest by peer`。 socket.close();
read
時會報connection rest by peer
。RST
消息,調用read
時會報connection rest by peer
。