套接字早在 James Gosling 設計 Java 語言之前,已是網絡通信的核心機制。Java 的優勢在於將底層作業系統的複雜細節封裝起來,讓開發者能高效、跨平台地使用 Socket。本文將帶你從零開始,理解客戶端/服務器模型、網絡協議,並動手實現一個支持上千並發的微型文件服務器。
想像一下在餐廳用餐:你(客戶端)查看菜單並點了一份披薩;侍者(通信通道)將訂單傳遞給廚房(服務端);廚房處理請求並製作披薩;最後侍者將成品端上桌。你無需知道廚房如何運作、食材從哪來,只需享用結果。
這就是經典的 Client/Server (C/S) 架構:
C/S 模型的核心價值在於 「數據表示與處理存儲分離」。例如航空訂票系統,前端錄入數據,後端統一存儲、校驗,保證數據一致性。Web 更是典型:服務端管理共享資源(HTML 頁面),客戶端(瀏覽器)跨平台訪問,無需適配每種作業系統。Java 「一次編寫,到處運行」的特性,完美契合這一模型。
人與人交談有禮儀(不搶話、語言一致),計算機通信亦需規則——網絡協議。它定義了數據打包、傳輸速度、錯誤處理、還原數據的標準。常見協議包括 TCP/IP、UDP、AppleTalk、NetBEUI。
Java 提供豐富的網絡類庫(java.net),支持兩種通信工具:
| 工具 | 協議 | 特點 | 適用場景 |
|---|---|---|---|
| DatagramPacket (數據報) | UDP | 無連接、不可靠、無序、速度快、開銷小 | 少量數據、局域網、實時性要求高但允許丟包(如視頻流、廣播) |
| Socket (流套接字) | TCP/IP | 面向連接、可靠、有序、字節流、雙向通信 | 文件傳輸、郵件、Web、數據庫連接等核心業務 |
Socket 的優勢:屏蔽了客戶端平台差異。只要遵循 TCP/IP,無論是 Windows、Linux、Mac 還是嵌入式設備,皆可互通。Java 通過 Socket(客戶端)和 ServerSocket(服務端)兩大核心類,將複雜的協議棧封裝為簡潔的「流」操作(InputStream/OutputStream),極大降低開發門檻。
Bruce Eckel 在《Java 編程思想》中將 Socket 比作「兩台機器連接的終端」。每台機器一個 Socket,中間像插著一根虛擬電纜。物理硬件細節完全隱藏,程序員只需操作這條邏輯通道發送字節流。
更直觀的比喻:電話聽筒。雙方撥通電話(建立連接),通過專用線路講話聽話(雙工通信),掛斷(關閉連接)前線路一直佔用。TCP 保證了通話內容「不丟字、不亂序」。
TCP 連接需要兩個座標:
192.168.0.110):定位網絡中的唯一設備。域名(如 www.example.com)通過 DNS 解析為 IP,方便人類記憶。URL https://www.jinnuo.com 隱含了協議、域名、默認端口 80、默認文檔 index.html。
下面通過一個完整案例,演示如何使用 Socket 連接服務端(端口 1001),請求讀取 C 盤文件內容並打印。
RemoteFileClientimport java.io.*;
import java.net.*;
public class RemoteFileClient {
private BufferedReader socketReader; // 讀取服務端響應
private PrintWriter socketWriter; // 發送請求給服務端
private String hostIp;
private int hostPort;
// 構造器:注入目標主機信息
public RemoteFileClient(String hostIp, int hostPort) {
this.hostIp = hostIp;
this.hostPort = hostPort;
}
// 建立連接並初始化流
public void setUpConnection() {
try {
Socket client = new Socket(hostIp, hostPort); // 核心:創建 TCP 連接
// 將字節流包裝為字符流,支持按行讀寫
socketReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
socketWriter = new PrintWriter(client.getOutputStream());
} catch (UnknownHostException e) {
System.err.println("未知主機: " + hostIp + ":" + hostPort);
} catch (IOException e) {
System.err.println("連接異常: " + e.getMessage());
}
}
// 請求文件內容
public String getFile(String fileNameToGet) {
StringBuffer fileLines = new StringBuffer();
try {
socketWriter.println(fileNameToGet); // 發送文件路徑
socketWriter.flush(); // 強制發送,不關閉連接
String line;
while ((line = socketReader.readLine()) != null) { // 循環讀取響應行
fileLines.append(line).append("\n");
}
} catch (IOException e) {
System.err.println("讀取文件失敗: " + fileNameToGet);
}
return fileLines.toString();
}
// 斷開連接,釋放資源
public void tearDownConnection() {
try {
if (socketWriter != null) socketWriter.close();
if (socketReader != null) socketReader.close();
} catch (IOException e) {
System.err.println("關閉連接異常: " + e.getMessage());
}
}
// 測試入口
public static void main(String[] args) {
RemoteFileClient client = new RemoteFileClient("127.0.0.1", 1001);
client.setUpConnection();
String content = client.getFile("RemoteFileServer.java");
System.out.println(content);
client.tearDownConnection();
}
}
Socket.getInputStream() → InputStreamReader(字節轉字符) → BufferedReader(緩衝按行讀取)。寫入同理用 PrintWriter 包裝 OutputStream。flush() 僅推送緩衝區數據,連接保持;close() 會關閉底層 Socket,斷開連接。請求-響應模式中,客戶端發送請求後必須 flush(),否則數據滯留緩衝區,服務端收不到。readLine() 在無數據時會阻塞線程,直到有數據到達或流關閉(返回 null)。這是傳統 BIO 模型的特徵,簡單但高併發下線程開銷大(下篇將介紹 NIO 改進)。UnknownHostException(IP 無效/DNS 失敗)和 IOException(網絡中斷、流錯誤),避免程序崩潰。本文奠定了 Socket 開發的理論基礎與入門實戰:理解 C/S 模型、TCP/UDP 選型、IP/端口寻址、Java 核心 API(Socket/ServerSocket)、流包裝模式、阻塞 I/O 工作流程。掌握這些,你已能編寫基本的網絡客戶端。
下篇預告:我們將實現服務端 RemoteFileServer,深入講解 ServerSocket.accept() 監聽機制、多線程處理併發請求、以及如何優化支持上千並發連接。敬請期待!