TCP三次握手和四次揮手與Java Socket
簡介
想要理解 TCP 的三次握手和四次揮手和 Java Socket,首先需要掌握 TCP 的報頭結構(傳送門)。如下圖所示:
00~31 表示 32 個位元位,即 32 個二進位制位。
- 序列號 Seq:當前資料段的第一個位元組的序列號。
- 確認編號 Ack:期望接收到下一個資料段的序列號。
- 緊急指標 Urgent Pointer:指向緊急資料序列中最後一個位元組的序列號。
標識 | 描述 |
---|---|
URG | 緊急指標標識位。 |
ACK | 確認編號標識位。 |
PSH | 提示接收端應用程式立即從TCP緩衝區把資料取走 |
RST | 傳送方要求重新建立連線,復位 |
SYN | 請求建立連線。 |
FIN | 希望斷開連線。 |
- URG 設定為 1,緊急指標需要被優先處理
- ACK 設定為 1,表示 確認編號 Ack 有效
- SYN 設定為 1,表示 序列號 Seq 設定為隨機初始值
何為套接字
在 TCP 術語中,一個 IP 地址和一個埠號的組合有時被稱為 端點 (endpoint)或者 套接字 (socket)。
每個 TCP 連線由一對套接字或者端點(四元組,客戶端IP,客戶端埠,服務端IP,服務端埠)唯一地標識。
摘自《TCP/IP 詳解卷 1》——12章第3節;另外 RFC0793 有英文原文描述
簡單來說,套接字 = IP地址 + 埠號。
實戰
啟動 wireshark
啟動 Java 服務端
public class EchoServer { public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(8080); System.out.println("伺服器等待連線..."); Socket clientSocket = serverSocket.accept(); System.out.println("服務端正在接收資訊..."); InputStream inStream = clientSocket.getInputStream(); OutputStream outStream = clientSocket.getOutputStream(); Scanner in = new Scanner(inStream); PrintWriter out = new PrintWriter(outStream, true /*autoFlush*/); out.println("Hello! Enter BYE to exit"); System.out.println("服務端正在讀取資訊..."); boolean done = false; while (!done && in.hasNextLine()) { String line = in.nextLine(); // 回聲 String echo = "Echo:" + line; out.println(echo); if (line.trim().equals("BYE")) done = true; } System.out.println("伺服器關閉連線..."); inStream.close(); serverSocket.close(); } }
首先在 IDEA 中啟動該 Java 服務端:
如上圖所示,服務端程式碼阻塞在了 serverSocket.accept()
等待客戶端的連線
啟動 Windows Telnet 客戶端
Win+R
開啟執行,輸入cmd
開啟命令列提示符- 輸入
telnet 127.0.0.1 8080
- 連線成功,接收到服務端發來的訊息 Hello! Enter BYE to exit
- 同時按下
Ctrl
+]
,進入 Microsoft Telnet Client
- 輸入 close, 按下回車,主動關閉客戶端連線
實驗結果
關於實驗結果的疑問
-
問題一:為什麼發出去的
Hello! Enter BYE to exit
是 24 個位元組,但是卻顯示長度為 26 呢?怎麼多了兩個位元組?
答:查詢 ASCii 碼錶,得知0d
表示 CR 回車,0a
表示 LF 換行/新行。out.println
在原來的基礎上追加了回車和換行字元\r\n
。 -
問題二:除了客戶機第一個發起連線請求的 SYN 報文外,其他每個報文都有 ACK 置位?
RFC0793 明確規定,除了第一個握手報文SYN除外,其它所有報文必須將ACK = 1。
追問:
TCP作為一個可靠傳輸協議,其可靠性就是依賴於收到對方的資料,傳送ACK給對方,這樣對方就可以釋放快取的資料,因為對方確信資料已經被接收到了。但TCP報文是在IP網路上傳輸,丟包是家常便飯,接收方要抓住一切的機會,把訊息告訴傳送方。最方便的方式就是,任何我方傳送的TCP報文,都要捎帶著ACK狀態位。 -
問題三:我們發現,帶資料(長度 Len > 0)的包都將 PSH 置位?
該標誌表示傳送端快取為空。也就是說,當 PSH 置位的資料包傳送完成以後,傳送端沒有其他資料包需要傳送。
三次握手
三段握手,發生在建立連線的階段。
- CLOSED:無連線狀態
- LISTEN:等待任意遠端 TCP 和對應埠發來的連線請求
- SYN-SENT:在發出連線請求後,等待匹配的連線請求
- SYN-RECEIVED:已經收到和發出連線請求,正在等待連線請求的確認
- ESTABLISHED:已建立連線,可以傳送資料給使用者。TCP 連線的資料傳輸階段的正常狀態。
四次揮手
- FIN-WAIT-1:等待來自遠端主機的連線終止請求或先前傳送的連線終止請求的確認。
- FIN-WAIT-2:等待來自遠端主機的連線終止請求。
- CLOSE-WAIT:等待來自遠端主機的連線終止請求。
- LAST-ACK:等待先前傳送給遠端主機的連線終止請求的確認(其中包括其連線終止請求的確認)。
- TIME-WAIT:等待足夠的時間以確保遠端主機收到其連線終止請求的確認。
FIN-WAIT-2 半關閉狀態
已經接收到先前傳送給遠端主機的連線終止請求的確認,等待來自遠端主機的連線終止請求,當前客戶機就進入了 FIN-WAIT-2 階段。
這是在關閉連線時,客戶端和伺服器兩次揮手之後的狀態,是著名的半關閉的狀態了,在這個狀態下,應用程式還有接受資料的能力,但是已經無法傳送資料,但是也有一種可能是,客戶端一直處於 FIN-WAIT-2 狀態,而伺服器則一直處於 CLOSE-WAIT 狀態,而直到應用層來決定關閉這個狀態。
Java 半關閉客戶端
public class HalfCloseClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket();
socket.connect(new InetSocketAddress(InetAddress.getLocalHost(), 8080));
PrintWriter out = new PrintWriter(socket.getOutputStream());
Scanner in = new Scanner(socket.getInputStream());
out.print("nice to meet you");
out.flush();
socket.shutdownOutput();
while (in.hasNextLine()) {
String line = in.nextLine();
System.out.println(line);
}
socket.close();
}
}
結果
半關閉 (half-close) 提供了這樣一種能力:套接字連線的一端可以終止其輸出,同時仍就可以接收來自另一端的資料。
如上圖所示,客戶端執行 socket.shutdownOutput()
之後,客戶端進入了 FIN-WAIT-2 階段,但是此時客戶端仍然可以接收服務端傳輸的資料,並且還可以傳送確認。