Java 網路程式設計:Socket 程式設計基礎指南
什麼是 Socket?
Socket(插座)是網路通訊的端點,提供一種機制,使得兩個程式可以在網路上進行資料交換。
在 Java 中,Socket 是一個類別,封裝底層的網路通訊細節,讓開發者能夠專注於應用邏輯的實現。
Socket 在網路通訊中的角色
在網路通訊模型中,Socket 扮演著關鍵的角色:
-
抽象化網路通訊:Socket 將複雜的網路通訊過程抽象化,提供一個簡單的介面供應用程式使用。
-
建立點對點連接:Socket 允許兩個應用程式之間建立直接的連接,實現資料的雙向傳輸。
-
支援多種協定:Java 的 Socket API 支援多種網路協定,最常用的是 TCP(傳輸控制協定)和 UDP(使用者資料包協定)。
-
跨平台兼容:Java 的 Socket 程式設計提供跨平台的一致性,使得開發者可以編寫一次程式碼,在不同的作業系統上運行。
2. Socket 的基本概念
IP 位址和連接埠
- IP 位址:
- IP 位址是用來識別網路上每個裝置的唯一標識符。
- IPv4 使用 32 位元表示(如 192.168.1.1),而 IPv6 使用 128 位元表示。
-
在 Java 中,可以使用
InetAddress
類別來處理 IP 位址。 -
連接埠(Port):
- 連接埠是一個 16 位元的數字,用於識別特定的網路服務或程序。
- 連接埠範圍從 0 到 65535。
- 常見的連接埠:HTTP(80),HTTPS(443),FTP(21)等。
Socket 類別簡介
Java 提供 java.net.Socket
類別來實現 TCP 通訊,以及 java.net.DatagramSocket
類別來實現 UDP 通訊。
- java.net.Socket:
- 用於建立客戶端 TCP 連接。
-
主要方法包括:
connect(SocketAddress endpoint)
:連接到指定的伺服器。getInputStream()
:獲取輸入串流。getOutputStream()
:獲取輸出串流。close()
:關閉 Socket 連接。
-
java.net.ServerSocket:
- 用於建立伺服器端 TCP 監聽。
-
主要方法包括:
accept()
:等待並接受客戶端連接。close()
:關閉伺服器 Socket。
-
java.net.DatagramSocket:
- 用於 UDP 通訊。
- 主要方法包括:
send(DatagramPacket p)
:發送資料包。receive(DatagramPacket p)
:接收資料包。
3. TCP Socket 程式設計
TCP(傳輸控制協定)是一種可靠的、面向連接的協定。在 Java 中,我們使用 java.net.Socket
和 java.net.ServerSocket
類別來實現 TCP 通訊。
建立 TCP 伺服器
建立一個 TCP 伺服器的基本步驟如下:
- 建立
ServerSocket
物件,指定監聽的連接埠。 - 呼叫
accept()
方法等待客戶端連接。 - 當客戶端連接時,獲取輸入和輸出串流。
- 處理客戶端請求。
- 關閉連接。
以下是簡單的 TCP 伺服器範例:
import java.io.*;
import java.net.*;
public class TCPServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(6789);
System.out.println("Server is listening on port 6789");
while (true) {
Socket clientSocket = serverSocket.accept();
System.out.println("Client connected: " + clientSocket.getInetAddress());
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
String inputLine;
while ((inputLine = in.readLine()) != null) {
System.out.println("Received: " + inputLine);
out.println("Server received: " + inputLine);
}
clientSocket.close();
}
} finally {
if (serverSocket != null) serverSocket.close();
}
}
}
建立 TCP 客戶端
建立一個 TCP 客戶端的基本步驟如下:
- 建立
Socket
物件,指定伺服器的 IP 位址和連接埠。 - 獲取輸入和輸出串流。
- 發送請求並接收回應。
- 關閉連接。
以下是簡單的 TCP 客戶端範例:
import java.io.*;
import java.net.*;
public class TCPClient {
public static void main(String[] args) throws IOException {
Socket socket = null;
try {
socket = new Socket("localhost", 6789);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in));
String userInput;
while ((userInput = stdIn.readLine()) != null) {
out.println(userInput);
System.out.println("Server response: " + in.readLine());
}
} finally {
if (socket != null) socket.close();
}
}
}
實作範例
讓我們來看一個完整的 TCP 伺服器和客戶端通訊的範例,實現簡單的回音(Echo)服務,客戶端發送訊息,伺服器將接收到的訊息回傳給客戶端。
伺服器端程式碼:
import java.io.*;
import java.net.*;
public class EchoServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(6789);
System.out.println("Echo Server is listening on port 6789");
while (true) {
Socket clientSocket = serverSocket.accept();
System.out.println("Client connected: " + clientSocket.getInetAddress());
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
String inputLine;
while ((inputLine = in.readLine()) != null) {
System.out.println("Echoing: " + inputLine);
out.println(inputLine);
}
clientSocket.close();
}
} finally {
if (serverSocket != null) serverSocket.close();
}
}
}
客戶端程式碼:
import java.io.*;
import java.net.*;
public class EchoClient {
public static void main(String[] args) throws IOException {
Socket socket = null;
try {
socket = new Socket("localhost", 6789);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in));
String userInput;
System.out.println("Enter messages to echo (type 'exit' to quit):");
while ((userInput = stdIn.readLine()) != null) {
if ("exit".equalsIgnoreCase(userInput)) break;
out.println(userInput);
System.out.println("Echo: " + in.readLine());
}
} finally {
if (socket != null) socket.close();
}
}
}
4. UDP Socket 程式設計
UDP(使用者資料包協定)是一種無連接的傳輸協定,相較於 TCP,更輕量但不保證資料的可靠傳輸。
在 Java 中,我們使用 java.net.DatagramSocket
和 java.net.DatagramPacket
類別來實現 UDP 通訊。
建立 UDP 伺服器
建立一個 UDP 伺服器的基本步驟如下:
- 建立
DatagramSocket
物件,指定監聽的連接埠。 - 建立
DatagramPacket
物件來接收資料。 - 使用
receive()
方法接收資料包。 - 處理接收到的資料。
- 如果需要回應,建立新的
DatagramPacket
並使用send()
方法發送。
以下是簡單的 UDP 伺服器範例:
import java.net.*;
public class UDPServer {
public static void main(String[] args) throws Exception {
DatagramSocket socket = new DatagramSocket(9876);
byte[] receiveData = new byte[1024];
while(true) {
DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
socket.receive(receivePacket);
String sentence = new String(receivePacket.getData(), 0, receivePacket.getLength());
System.out.println("RECEIVED: " + sentence);
InetAddress IPAddress = receivePacket.getAddress();
int port = receivePacket.getPort();
String capitalizedSentence = sentence.toUpperCase();
byte[] sendData = capitalizedSentence.getBytes();
DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, IPAddress, port);
socket.send(sendPacket);
}
}
}
建立 UDP 客戶端
建立一個 UDP 客戶端的基本步驟如下:
- 建立
DatagramSocket
物件。 - 建立
DatagramPacket
物件,包含要發送的資料、目標 IP 和連接埠。 - 使用
send()
方法發送資料包。 - 如果需要接收回應,使用
receive()
方法。
以下是簡單的 UDP 客戶端範例:
import java.io.*;
import java.net.*;
public class UDPClient {
public static void main(String args[]) throws Exception {
BufferedReader inFromUser = new BufferedReader(new InputStreamReader(System.in));
DatagramSocket clientSocket = new DatagramSocket();
InetAddress IPAddress = InetAddress.getByName("localhost");
byte[] sendData = new byte[1024];
byte[] receiveData = new byte[1024];
System.out.println("Enter a message:");
String sentence = inFromUser.readLine();
sendData = sentence.getBytes();
DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, IPAddress, 9876);
clientSocket.send(sendPacket);
DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
clientSocket.receive(receivePacket);
String modifiedSentence = new String(receivePacket.getData(), 0, receivePacket.getLength());
System.out.println("FROM SERVER:" + modifiedSentence);
clientSocket.close();
}
}
實作範例
將實現一個簡單的時間查詢服務,客戶端發送請求,伺服器回傳當前時間。
伺服器端程式碼:
import java.net.*;
import java.text.SimpleDateFormat;
import java.util.Date;
public class TimeServer {
public static void main(String[] args) throws Exception {
DatagramSocket socket = new DatagramSocket(9876);
byte[] receiveData = new byte[1024];
while(true) {
DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
socket.receive(receivePacket);
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String currentTime = formatter.format(new Date());
InetAddress IPAddress = receivePacket.getAddress();
int port = receivePacket.getPort();
byte[] sendData = currentTime.getBytes();
DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, IPAddress, port);
socket.send(sendPacket);
}
}
}
客戶端程式碼:
import java.net.*;
public class TimeClient {
public static void main(String args[]) throws Exception {
DatagramSocket clientSocket = new DatagramSocket();
InetAddress IPAddress = InetAddress.getByName("localhost");
byte[] sendData = new byte[1024];
byte[] receiveData = new byte[1024];
String sentence = "GET_TIME";
sendData = sentence.getBytes();
DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, IPAddress, 9876);
clientSocket.send(sendPacket);
DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
clientSocket.receive(receivePacket);
String time = new String(receivePacket.getData(), 0, receivePacket.getLength());
System.out.println("Current time from server: " + time);
clientSocket.close();
}
}
5. Socket 程式設計的實踐
錯誤處理
- 使用 try-with-resources:自動關閉資源,避免資源洩漏。
- 適當的例外處理:捕捉並處理可能發生的網路相關例外。
- 優雅地處理連接中斷:實現重連機制或適當的錯誤回饋。
資源管理
- 及時關閉 Socket:不再使用時立即關閉 Socket 連接。
- 使用連接池:對於高併發的應用,考慮使用連接池來管理 Socket 連接。
- 設置超時:為 Socket 操作設置適當的超時時間,避免無限等待。
效能考量
- 使用緩衝區:適當使用緩衝區可以提高 I/O 效率。
- 非阻塞 I/O:考慮使用 NIO(New I/O)來實現非阻塞操作,提高並發處理能力。
- 資料序列化:選擇高效的資料序列化方式,如 Protocol Buffers 或 JSON。
安全性
- 加密通訊:使用 SSL/TLS 加密敏感資料的傳輸。
- 驗證連接:實現適當的身份驗證機制。
- 防止 DoS 攻擊:實現連接限制和超時機制。
程式碼範例
以下是結合上述的實踐建議所撰寫的簡單 TCP 伺服器範例:
import java.io.*;
import java.net.*;
import java.util.concurrent.*;
import javax.net.ssl.*;
import java.security.*;
public class BestPracticeTCPServer {
private static final int PORT = 8888;
private static final int TIMEOUT = 30000; // 30 seconds
private static final int MAX_CONNECTIONS = 100;
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(MAX_CONNECTIONS);
try (ServerSocket serverSocket = createSSLServerSocket()) {
serverSocket.setSoTimeout(TIMEOUT);
System.out.println("Server is listening on port " + PORT);
while (true) {
try {
Socket clientSocket = serverSocket.accept();
executor.submit(new ClientHandler(clientSocket));
} catch (SocketTimeoutException e) {
System.out.println("Timeout waiting for connection, continuing...");
}
}
} catch (IOException e) {
System.err.println("Could not listen on port " + PORT);
e.printStackTrace();
} finally {
executor.shutdown();
}
}
private static SSLServerSocket createSSLServerSocket() throws IOException {
try {
// 加載金鑰庫
KeyStore keyStore = KeyStore.getInstance("JKS");
keyStore.load(new FileInputStream("keystore.jks"), "keystorepassword".toCharArray());
// 創建並初始化金鑰管理器
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, "keystorepassword".toCharArray());
// 創建並初始化 SSL 上下文
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), null, null);
// 創建 SSL 伺服器 socket
SSLServerSocketFactory sslServerSocketFactory = sslContext.getServerSocketFactory();
return (SSLServerSocket) sslServerSocketFactory.createServerSocket(PORT);
} catch (Exception e) {
throw new IOException("Failed to create SSL server socket", e);
}
}
private static class ClientHandler implements Runnable {
private final Socket clientSocket;
public ClientHandler(Socket socket) {
this.clientSocket = socket;
}
@Override
public void run() {
try (
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)
) {
String inputLine;
while ((inputLine = in.readLine()) != null) {
System.out.println("Received: " + inputLine);
out.println("Server received: " + inputLine);
if ("bye".equalsIgnoreCase(inputLine)) {
break;
}
}
} catch (IOException e) {
System.err.println("Error handling client: " + e.getMessage());
} finally {
try {
clientSocket.close();
} catch (IOException e) {
System.err.println("Error closing client socket: " + e.getMessage());
}
}
}
}
}