Socket深入剖析

建立TCP連接

     新的Socket實例創建後,就立即能用於發送和接收數據。也就是說,當Socket實例返回時,它已經連接到了一個遠程終端,並通過協議的底層實現完成了TCP消息或握手信息的交換。

     客戶端連接的建立

     Socket構造函數的調用與客戶端連接建立時所關聯的協議事件之間的關係下圖所示:


     

     當客戶端以服務器端的互聯網地址W.X.Y.Z和端口號Q作爲參數,調用Socket的構造函數時,底層實現將創建一個套接字實例,該實例的初始狀態是關閉的。TCP開放握手也稱爲3次握手,這通常包括3條消息:一條從客戶端到服務端的連接請求,一條從服務端到客戶端的確認消息,以及另一條從客戶端到服務端的確認消息。對客戶端而言,一旦它收到了服務端發來的確認消息,就立即認爲連接已經建立。通常這個過程發生的很快,但連接請求消息或服務端的回覆消息都有可能在傳輸過程中丟失,因此TCP協議實現將以遞增的時間間隔重複發送幾次握手消息。如果TCP客戶端在一段時間後還沒有收到服務端的回覆消息,則發生超時並放棄連接。如果服務端並沒有接收連接,則服務端的TCP將發送一條拒絕消息而不是確認消息。


     服務端連接的建立

     當客戶端的事件序列則有所不同。服務端首先創建一個ServerSocket實例,並將其與已知端口相關聯(在此爲Q),套接字實現爲新的ServerSocket實例創建一個底層數據結構,並就Q賦給本地端口,並將特定的通配符(*)賦給本地IP地址(服務器可能有多個IP地址,不過通常不會指定該參數),如下圖所示:


     現在服務端可以調用ServerSocket的accept()方法,來將阻塞等待客戶端連接請求的到來。當客戶端的連接請求到來時,將爲連接創建一個新的套接字數據結構。該套接字的地址根據到來的分組報文設置:分組報文的目標互聯網地址和端口號成爲該套接字的本地互聯網地址和端口號;而分組報文的源地址和端口號則成爲改套接字的遠程互聯網地址和端口號。注意,新套接字的本地端口號總是與ServerSocket的端口號一致。除了要創建一個新的底層套接字數據結構外,服務端的TCP實現還要向客戶端發送一個TCP握手確認消息。如下圖所示:


     但是,對於服務端來說,在接收到客戶端發來的第3條消息之前,服務端TCP並不會認爲握手消息已經完成。一旦收到客戶端發來的第3條消息,則表示連接已建立,此時一個新的數據結構將從服務端所關聯的列表中移除,併爲創建一個Socket實例,作爲accept()方法的返回值。如下圖所示:


    這裏有非常重要的一點需要注意,在ServerSocket關聯的列表中的每個數據結構,都代表了一個與另一端的客戶端已經完成建立的TCP連接。實際上,客戶只要收到了開放握手的第2條消息,就可以立即發送數據——這可能比服務端調用accept()方法爲其獲取一個Socket實例要早很長時間。


   關閉TCP連接

     

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

     TCP連接的關閉事件序列可能以兩種方式發生:一種方式是先由一個應用程序調用close()方法或shutdownOutput方法,並在另一端調用close()方法之前完成其關閉握手消息;另一種方式是兩端同時調用close()方法,他們的關閉握手消息在網絡上交叉傳輸。下圖展示了以第一種方式關閉連接時,發起關閉的一端底層實現中的事件序列:


     注意,如果連接處於半關閉狀態時,遠程終端已經離開,那麼本地底層數據結構則無限期地保持在該狀態。當另一端的關閉握手消息到達後,則發回一條確認消息並將狀態改爲“Time—Wait”。雖然應用程序中相應的Socket實例可能早已消失,與之關聯的底層數據結構還將在底層實現中繼續存留幾分鐘。

    對於沒有首先發起關閉的一端,關閉握手消息達到後,它立即發回一個確認消息,並將連接狀態改爲“Close—Wait”。此時,只需要等待應用程序調用Socket的close()方法。調用該方法後,將發起最終的關閉消息 ,並釋放底層套接字數據結構。 下圖展示了沒有首先發起關閉的一端底層實現中的事件序列:


     

     注意這樣一個事實:close()方法和shutdownOutput()方法都沒有等待關閉握手的完成,而是調用後立即返回,這樣,當應用程序調用close()方法或shutdownOutput()方法併成功關閉連接時,有可能還有數據留在SendQ隊列中。如果連接的任何一端在數據傳輸到RecvQ隊列之前崩潰,數據將丟失,而發送端應用程序卻不會知道。

     最好的解決方案是設計一種應用程序協議,以使首先調用close()方法的一方在接收到了應用程序的數據已接收保證後,才真正執行關閉操作。例如,在http://blog.csdn.net/ns_code/article/details/14642873這篇博客的分析示例中,客戶端程序確認其接收到的字節數與其發送的字節數相等後,它就能夠知道此時在連接的兩個方向上都沒有數據在傳輸,因此可以安全地關閉連接。


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

     Time—Wait狀態最重要的作用是:只要底層套接字數據結構還存在,就不允許在相同的本地端口上關聯其他套接字,尤其試圖使用該端口創建新的Socket實例時,將拋出IOException異常。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章