如何創建 Java TCP Socket
1. 前言
TCP 的英文全稱是 Transmission Control Protocol,翻譯成中文叫做傳輸控制協議,它是 TCP/IP 協議族中非常重要的一個傳輸層協議。TCP 是一個面向連接的、面向字節流的、可靠的傳輸層協議,有丟包重傳機制、有流控機制、有擁塞控制機制。TCP 保證數據包的順序,并且對重復包進行過濾。相比不可靠傳輸協議 UDP,TCP 完全是相反的。
對于可靠性要求很高的應用場景來說,選擇可靠 TCP 作為傳輸層協議肯定是正確的。例如,著名的 HTTP 協議和 FTP 協議都是采用 TCP 進行傳輸。當然 TCP 為了保證傳輸的可靠性,引入了非常復雜的保障機制,比如:TCP 連接建立時的三次握手和連接關閉時的四次揮手機制,滑動窗口機制,發送流控機制,慢啟動和擁塞避免機制等。當然,操作系統的網絡協議棧已經實現了這些復雜的機制,
本小節主要是介紹通過 Java 語言編寫 TCP 客戶端、服務器程序的方法。
編寫 TCP 客戶端、服務器程序主要分為如下幾個步驟:
- 創建客戶端 Socket,連接到某個服務器監聽的端口,需要指定服務器監聽的 host 和 port。host 可以是 IP 地址,也可以是域名。
- 創建服務端 Socket,綁定到一個固定的服務端口,監聽客戶端的連接請求。
- 客戶端發起連接請求,完成三次握手過程。
- TCP 連接建立成功后,雙方進行數據流交互。
- 數據流交互完成后,關閉連接。
2. 傳統 TCP 客戶端和服務器建立過程
為了更好地理解編寫 TCP 客戶端和服務器程序的步驟,下圖展示了通過 C 語言 Socket API 編寫客戶端和服務器程序的過程。
圖中的矩形方框都是 C 函數,很好的展示了客戶端和服務器 Socket 的建立過程。對于 Java 語言來說,只是應用面向對象的思維對上面的過程進行了抽象,下來我們就探討一下如何編寫 Java 客戶端和服務器程序。
3. Java Socket 類分析
Java 語言抽象了 java.net.Socket 類,表示一個 Socket,既可以用在客戶端,又可以用在服務器端。其實 java.net.Socket 也是一個包裝類,對外抽象了一組公共方法,具體實現是在 java.net.SocketImpl 類中完成的,它允許用戶自定義具體實現。java.net.Socket 類包含的主要功能如下:
- 創建 Socket,具體就是創建一個 java.net.Socket 類的對象。
- 建立 TCP 連接,可以通過 java.net.Socket 類的構造方法完成,也可以調用它的 connect 方法完成。
- 將 Socket 綁定到本地接口 IP 地址或者端口,可以調用 java.net.Socket 類的 bind 方法完成。
提示:
服務器需要做 bind 操作,客戶端一般不需要做 bind 操作。
-
關閉連接,可以調用 java.net.Socket 類的 close 方法完成。
-
接收數據,可以通過 java.net.Socket 類的 getInputStream 方法,返回一個 java.io.InputStream 對象實現數據接收。
-
發送數據,可以通過 java.net.Socket 類的 getOutputStream 方法,返回一個 java.io.OutputStream 對象實現數據發送。
java.net.Socket 類提供了一組重載的構造方法,方便程序員選擇,大體分為四類:
- 可以傳入服務器的 host 和 port 參數
原型如下:
public Socket(String host, int port)
throws UnknownHostException, IOException
public Socket(InetAddress address, int port) throws IOException
對于 host 參數,你可以傳入 IP 地址或者是域名。當然,你可以傳入構造好的 InetAddress 地址結構。
在 java.net.Socket 的構造方法中,首先會構造一個 InetAddress 地址結構,然后進行域名解析,最后調用它的 connect 方法和服務器建立連接。
- 可以傳入綁定的本地地址參數
原型如下:
public Socket(String host, int port, InetAddress localAddr, int localPort) throws IOException
public Socket(InetAddress address, int port, InetAddress localAddr, int localPort) throws IOException
這類構造方法也可以傳入 host 和 port 外,功能和上面類似。另外,還可以傳入 localAddr 和 localPort,會調用 java.net.Socket 類的 bind 方法,綁定在本地的接口地址和端口。
- 無參構造方法
public Socket()
此構造方法,除了構造一個 java.net.Socket 類的對象,并不會去 connect 服務器。你需要調用它的 connect 方法連接服務器。
public void connect(SocketAddress endpoint, int timeout) throws IOException
自己調用 connect 方法,需要構造 SocketAddress 結構,當然你可以設置連接的超時時間,單位是毫秒(milliseconds)。
- 訪問代理服務器
public Socket(Proxy proxy)
當你需要訪問某個代理服務器時,可以調用此構造方法,Socket 會自動去連接代理服務器。
創建一個簡單的 java.net.Socket 客戶端,示例代碼如下:
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
public class TCPClient {
// 服務器監聽的端口號
private static final int PORT = 56002;
private static final int TIMEOUT = 15000;
public static void main(String[] args) {
Socket client = null;
try {
// 在構造方法中傳入 host 和 port
// client = new Socket("192.168.43.49", PORT);
// 調用無參構造方法
client = new Socket();
// 構造服務器地址結構
SocketAddress serverAddr = new InetSocketAddress("192.168.0.101", PORT);
// 連接服務器,超時時間是 15 毫秒
client.connect(serverAddr, TIMEOUT);
System.out.println("Client start:" + client.getLocalSocketAddress().toString());
// 向服務器發送數據
OutputStream out = new BufferedOutputStream(client.getOutputStream());
String req = "Hello Server!\n";
out.write(req.getBytes());
// 不能忘記 flush 方法的調用
out.flush();
System.out.println("Send to server:" + req);
// 接收服務器的數據
BufferedInputStream in = new BufferedInputStream(client.getInputStream());
StringBuilder inMessage = new StringBuilder();
while(true){
int c = in.read();
if (c == -1 || c == '\n')
break;
inMessage.append((char)c);
}
System.out.println("Recv from server:" + inMessage.toString());
} catch (IOException e) {
e.printStackTrace();
} finally {
if (client != null){
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
這里我們創建的是阻塞式的客戶端,有幾點需要注意的地方:
- 通過 OutputStream 的對象向服務器發送完數據后,需要調用 flush 方法。
- BufferedInputStream 的 read 方法會阻塞線程,所以需要設計好消息邊界的識別機制,示例代碼是通過換行符 ‘\n’ 表示一個消息邊界。
- java.net.Socket 的各個方法都拋出了 IOException 異常,需要捕獲。
- 注意調用 close 方法,關閉連接。
4. Java ServerSocket 類分析
Java 語言抽象了 java.net.ServerSocket 類表示服務器監聽 Socket,此類只用在服務器端,通過調用它的 accept 方法來獲取新的連接。accept 方法的返回值是 java.net.Socket 類型,后續服務器和客戶端的數據收發,都是通過 accept 方法返回的 Socket 對象完成。
java.net.ServerSocket 類也提供了一組重載的構造方法,方便程序員選擇。
public ServerSocket(int port) throws BindException, IOException
public ServerSocket(int port, int queueLength) throws BindException, IOException
public ServerSocket(int port, int queueLength, InetAddress bindAddress) throws IOException
public ServerSocket() throws IOException
- port 參數用于傳入服務器監聽的端口號。如果傳入的 port 是 0,系統會隨機選擇一個端口監聽。
- queueLength 參數用于設置連接接收隊列的長度。不傳入此參數,采用系統默認長度。
- bindAddress 參數用于將監聽 Socket 綁定到一個本地接口。如果傳入此參數,服務器會監聽指定的接口地址;如果不指定此參數,默認會監聽通配符 IP 地址,比如 IPv4 是 0.0.0.0。
- 提示:
- 可以通過 netstat 命令查看服務器程序監聽的 IP 地址和端口號。
如果你是通過無參構造方法構造 java.net.ServerSocket 類的對象,需要手動調用它的 bind 方法,綁定監聽端口和接口地址。
創建一個簡單的服務器監聽 Socket,示例代碼如下:
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class TCPServer {
private static final int PORT =56002;
public static void main(String[] args) {
ServerSocket ss = null;
try {
// 創建一個服務器 Socket
ss = new ServerSocket(PORT);
// 監聽新的連接請求
Socket conn = ss.accept();
System.out.println("Accept a new connection:" + conn.getRemoteSocketAddress().toString());
// 讀取客戶端數據
BufferedInputStream in = new BufferedInputStream(conn.getInputStream());
StringBuilder inMessage = new StringBuilder();
while(true){
int c = in.read();
if (c == -1 || c == '\n')
break;
inMessage.append((char)c);
}
System.out.println("Recv from client:" + inMessage.toString());
// 向客戶端發送數據
String rsp = "Hello Client!\n";
BufferedOutputStream out = new BufferedOutputStream(conn.getOutputStream());
out.write(rsp.getBytes());
out.flush();
System.out.println("Send to client:" + rsp);
conn.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (ss != null){
try {
ss.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
System.out.println("Server exit!");
}
}
我們創建的阻塞式服務端,所以 java.net.ServerSocket 的 accept 方法會阻塞線程,直到新連接返回。同樣,在接收客戶端的消息的時候注意消息邊界的處理,最后向客戶端發送響應的時候,需要調用 flush 方法。
5. 小結
用 Java 語言編寫 TCP 客戶端和服務器程序非常方便,你只需要創建一個 java.net.ServerSocket 實例,然后調用它的 accept 方法監聽客戶端的請求;你只需要創建一個 java.net.Socket 實例,可以通過構造方法或者 connect 連接對應的服務器,然后就可以進行數據的收發,最后數據交互完成后,調用 close 方法關閉連接即可。
示例代碼中的服務器功能還不完善,不能持續提供服務,不能同時接收多個客戶端的連接請求,需要在后續的小節逐步完善。