1. 程式人生 > >【Java TCP/IP Socket程式設計】----深入剖析----TCP套接字生命週期

【Java TCP/IP Socket程式設計】----深入剖析----TCP套接字生命週期

目錄

 

簡介

TCP連線

關閉TCP連線

解調多路複用


--------筆記來自於書籍《Java TCP/IP Socket程式設計》

簡介

    新的Socket例項建立後(無論是通過公有的建構函式,或通過呼叫ServerSocket類的accept()方法)立即就能用於傳送和接收資料。也就是說,當Socket例項返回時,它已經連線到一個遠端終端,並通過協議的底層實現完成了TCP訊息或握手資訊的交換,本節主要對TCP的連線和關閉TCP連線具體實現細節,以及解調多路複用進行了描述。

TCP連線

客戶端的連線事件

Socket建構函式的呼叫與客戶端連線建立時所關聯的協議事件之間的關係如下圖所示,其中大箭頭表示導致底層套接字資料結構發生狀態改變的外部事件,客戶端的網際網路地址表示為A.B.C.D,伺服器端的網際網路地址表示為W.X.Y.Z;伺服器的埠號是Q。

    當客戶端以伺服器端的網際網路地址W.X.Y.Z和埠號Q作為引數,呼叫Socket的建構函式時,底層實現將建立一個套接字例項,該例項的初始狀態是關閉狀態(Closed)。如果在呼叫建構函式時客戶端沒有指定本地地址或埠號,底層實現將選擇一個沒有被其他TCP套接字使用的本地埠號(P)。同時還要指定本地的網際網路地址,如果沒有顯式地指定,則將向伺服器傳送資料報文的網路介面地址作為本地地址。底層實現將本地和遠端地址和埠複製到底層套接字的資料結構,並初始化TCP連線建立時的握手訊息。

    TCP的開放握手也稱為3次握手,包含一條從客戶端到伺服器端的連線請求,一條從伺服器端到客戶端的確認訊息,以及另外一條從客戶端到伺服器端的確認訊息

。客戶端一收到伺服器端發來的確認訊息,就立即認為連線已經成功建立。但客戶端的起始訊息或伺服器端的回覆訊息都可能在傳輸過程中丟失。出於這個原因,TCP協議實現將以遞增的時間間隔重複傳送幾次握手訊息,如果TCP客戶端在這一段時間後還沒有收到伺服器的回覆訊息,則發生超時並放棄連線。

伺服器端的事件序列

    伺服器首先建立一個關聯已知埠號(在此為Q)的ServerSocket例項,套接字實現為新的ServerSocket例項建立了一個底層資料結構。並將Q賦給本地埠,將特定的萬用字元地址(*)賦給本地IP地址。此時套接字處於“LISTENING”狀態,表示準備好接受傳入該埠的連線請求。現在伺服器端可以呼叫ServerSocket的accept()方法,該方法將阻塞等待,直到與某個客戶端完成開放握手資訊交換,併成功建立了新的連線。

                     

    當客戶端的連線請求到來時,將為該連線建立一個新的套接字資料結構。新套接字的地址根據到來的分組報文設定:分組報文的目標網際網路地址和埠號(分別是W.X.Y.Z和Q)稱為該套接字的本地網際網路地址和埠號;而分組報文的源地址和埠號(分別為A.B.C.D和P)則稱為該套接字的遠端網際網路地址和埠號。新套接字的本地埠號總是與ServerSocket的埠號是一致的。新套接字的狀態為正在連線(Connectioning)。除了要建立一個新的底層套接字資料結構外,伺服器方的TCP實現還要向客戶端發回一個TCP握手確認訊息。

                   

    在接收到客戶端發來的3次握手的第3條訊息之前,伺服器端TCP並不會認為握手訊息已經完成。第三條握手訊息到來後,新資料結構狀態為“ESTABLISHED”,並將其移動到ServerSocket資料結構關聯的另一個套接字資料結構列表中,該列表代表了能夠通過ServerSocket的accept()方法進行接收的已成功建立的連線。伺服器在呼叫ServerSocket的accept()方法後,只要其關聯的套接字資料結構列表中有新的連線到來,該方法就立即停止阻塞,此時一個新的連線資料結構將從列表中移除,併為其建立一 個Socket例項,作為accept()方法返回值。

                 

    在ServerSocket關聯的列表中的每個資料結構,都代表了一個與另一端客戶端已經完成建立的TCP連線。實際上,客戶端只要接收到開放握手的第2條訊息,就可以立即傳送資料,這個可能比伺服器呼叫accept()方法為其獲取一個Socket例項要早很長時間。

關閉TCP連線

    TCP協議有一個優雅的關閉機制,以保證應用從程式在關閉連線時不必擔心正在傳輸的資料會丟失。這個機制還設計為允許兩個方向的護具相互獨立地終止。關閉機制的工作流程:應用程式通過呼叫連線套接字的close()方法或者shutdownOutput()方法表明資料已經發送完畢。底層的TCP實現首先將留存在SendQ佇列中的資料傳輸出去(要依賴於另一端的RecvQ佇列的剩餘空間),然後向另一端傳送一個關閉TCP連線的握手訊息。該關閉握手訊息可以看作是流終止標誌:它告訴接收端TCP不會再有新的資料傳入RecvQ隊列了。(注意,關閉握手訊息本身並沒有傳遞給接收端應用程式,而是通過read()方法返回-1來指示其在位元組流中的位置。)正在關閉的TCP將等待其關閉握手訊息的確認訊息,該確認訊息表明在連線上傳輸所有的資料已經完全地傳輸到RecvQ中,只要收到了確認訊息,該連線就變成了半關閉狀態,直到連線的另一個方向上收到了對稱的握手訊息後,連線才完全關閉。即連線的兩端都表明它們再沒有資料要傳送了。

    TPC連線的關閉事件序列可能以兩種方式發生:一種方式是先由一個應用程式呼叫close()方法(或shutdownOutput()方法),並在另一端呼叫close()方法之前完成關閉握手訊息,另一種方式是兩端同時呼叫close()方法,它們的關閉握手訊息在網路上交叉傳輸。如下是第一種方式關閉連線時,底層實現中事件序列。

                   

    需要注意的是如果連線處於半關閉狀態時,遠端終端已經離開,那麼本地底層資料結構則將無限期地保持在該狀態。當另一端關閉握手訊息到達後,則發回一條確認訊息並將狀態改變成“Time-Wait”。雖然應用程式中相應Socket例項可能早已訊息,與之關聯的底層資料結構還將在底層實現中存留幾分鐘。

    下圖展示了沒有首先發起關閉的終端上的事件序列,關閉握手訊息到達後,它立即發回一個訊息,並將狀態改變為“Close-Wait”.此時只需要等待應用程式呼叫Socket的close()方法。呼叫該方法後,將發起最終的關閉握手訊息,並釋放底層套接字資料結構。需要注意的一點是close()或者shutdownOutput()方法都沒有等待關閉握手的完成,而是呼叫後立即返回。所以可能還有資料留存在SendQ佇列中,如果連線的任何一端在資料傳輸到RecvQ佇列之前崩潰,資料將丟失,而傳送端應用程式不會知道。解決方案就是最好一種應用程式協議,以使呼叫close()方法的一方在接收到應用程式層的資料已接收保證後,才真正執行關閉操作。

                                     

    關閉TCP連線的最後微妙之處在於對Time—Wait狀態的需要。TCP規範要求在終止連線時,兩端的關閉握手都完成後,至少要有一個套接字在Time—Wait狀態保持一段時間。這個要求的提出是由於訊息在網路中傳輸時可能延遲。如果在連線兩端都完成了關閉握手後,它們都移除了其底層資料結構,而此時在同樣一對套接字地址之間又建立了新的連線,那麼前一個連線在網路上傳輸時延遲的訊息就可能在新建立的連線後到達。由於包含了相同的源地址和目的地址,舊訊息就會被錯誤地認為是屬於新連線的,其包含的資料就可能被錯誤地分配到應用程式中。雖然這種情況很少發生,TCP還是使用了包括Time—Write狀態在內的多種機制對其進行防範。

     Time—Wait狀態最重要的作用是:只要底層套接字資料結構還存在,就不允許在相同的本地埠上關聯其他套接字,尤其試圖使用該埠建立新的Socket例項時,將丟擲IOException異常

解調多路複用

    伺服器端存在這樣一個問題:同一個機器上的不同套接字可以有相同的本地地址和埠號。如:在只有一個IP地址的機器上,每個通過ServerSocket的accept()方法接收到的新Socket例項都將使用與ServerSocket相同的本地埠號。顯然要確定傳入的分組報文應該分配到那個套接字(即解調多路複用)不僅僅是檢視分組報文的目的地址和埠。方對於TCP和UDP來說,將傳入的分組報文匹配到某個套接字過程是一樣的。可以歸納為以下幾點:

    1)套接字資料結構中的本地埠號必須與傳入的分組報文的目的埠號相匹配。

    2)在套接字資料結構中,任何包含了統配符(*)的欄位可以匹配分組報文中相應欄位的任何值。

    3)如果有一個以上的套接字資料結構與傳入分組報文地址的4個欄位匹配,那麼誰使用的統配符少,誰就獲得該分組報文。