【Java TCP/IP Socket】深入剖析socket——TCP套接字的生命周期
建立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()方法的一方在接收到了應用程序的數據已接收保證后,才真正執行關閉操作。例如,在《TCP Socket通信中由read返回值造成的的死鎖問題》這篇博客的分析示例中,客戶端程序確認其接收到的字節數與其發送的字節數相等后,它就能夠知道此時在連接的兩個方向上都沒有數據在傳輸,因此可以安全地關閉連接。
關閉TCP連接的最后微妙之處在于對Time—Wait狀態的需要。TCP規范要求在終止連接時,兩端的關閉握手都完成后,至少要有一個套接字在Time—Wait狀態保持一段時間。這個要求的提出是由于消息在網絡中傳輸時可能延遲。如果在連接兩端都完成了關閉握手后,它們都移除了其底層數據結構,而此時在同樣一對套接字地址之間又建立了新的連接,那么前一個連接在網絡上傳輸時延遲的消息就可能在新建立的連接后到達。由于包含了相同的源地址和目的地址,舊消息就會被錯誤地認為是屬于新連接的,其包含的數據就可能被錯誤地分配到應用程序中。雖然這種情況很少發生,TCP還是使用了包括Time—Write狀態在內的多種機制對其進行防范。
Time—Wait狀態最重要的作用是:只要底層套接字數據結構還存在,就不允許在相同的本地端口上關聯其他套接字,尤其試圖使用該端口創建新的Socket實例時,將拋出IOException異常。
來自: http://www.importnew.com/20245.html