網絡編程的本質是解決兩個核心問題:**主機定位**(IP 層)與 **可靠數據傳輸**(傳輸層)。Java 在 java.net 包中封裝了操作系統 Socket 接口,提供了跨平台的網絡編程能力。本文系統梳理 TCP/IP 協議分層、Socket 生命週期、阻塞/非阻塞模型、以及生產級開發要點。
| TCP/IP 層 | 對應 OSI 層 | 核心協議/功能 | Java API 映射 |
|---|---|---|---|
| 應用層 | 應用/表示/會話 | HTTP、FTP、SMTP、DNS、WebSocket | HttpClient、JNDI、JavaMail |
| 傳輸層 | 傳輸層 | TCP(可靠流)、UDP(不可靠數據報) | Socket / ServerSocket、DatagramSocket |
| 網絡層 | 網絡層 | IP、ICMP、ARP、路由選擇 | InetAddress、NetworkInterface |
| 鏈路層 | 數據鏈路/物理層 | 以太網、WiFi、MAC 地址、驅動程序 | JNI/本地庫(通常不直接操作) |
Java Socket 編程主要聚焦於 傳輸層,屏蔽了網絡層路由與鏈路層物理傳輸細節。
| 特性 | TCP (Transmission Control Protocol) | UDP (User Datagram Protocol) |
|---|---|---|
| 連接模式 | 面向連接(三次握手建立,四次揮手斷開) | 無連接(發即達,無會話狀態) |
| 可靠性 | 可靠:序列號、確認應答、重傳、流控、擁塞控制 | 不可靠:無序、丟包、重複、無流控 |
| 數據邊界 | 無邊界(字節流,需應用層分幀) | 有邊界(保護消息邊界,單次收發一個數據報) |
| 傳輸效率 | 較低(頭部 20-60B,確認機制開銷) | 高(頭部僅 8B,無握手確認) |
| 適用場景 | 文件傳輸、Web 瀏覽、郵件、數據庫連接、即時通訊核心通道 | 視頻流、語音通話、DNS 查詢、廣播/組播、遊戲狀態同步、IoT 心跳 |
| Java 類 | Socket、ServerSocket | DatagramSocket、DatagramPacket |
| 最大傳輸單元 | 無理論上限(流式),受窗口大小限制 | 單數據報 ≤ 64KB(含 IP/UDP 頭,實際荷載 ~65.5KB) |
public class TcpServer {
public static void main(String[] args) throws IOException {
int port = 8080;
try (ServerSocket serverSocket = new ServerSocket(port)) {
// 可選:設置 SO_REUSEADDR 避免 TIME_WAIT 導致重啟失敗
serverSocket.setReuseAddress(true);
// 可選:設置接收緩衝區
serverSocket.setReceiveBufferSize(64 * 1024);
System.out.println("Server listening on port " + port);
while (true) {
// 阻塞等待客戶端連接
Socket clientSocket = serverSocket.accept();
// 生產環境應提交線程池異步處理,避免主線程阻塞
new Thread(new ClientHandler(clientSocket)).start();
}
}
}
static class ClientHandler implements Runnable {
private final Socket socket;
ClientHandler(Socket socket) { this.socket = socket; }
@Override
public void run() {
String clientInfo = socket.getInetAddress() + ":" + socket.getPort();
try (BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8), true)) {
// 設置讀取超時,防止客戶端掛死佔用線程
socket.setSoTimeout(30_000);
String line;
while ((line = in.readLine()) != null) {
System.out.println("[" + clientInfo + "] " + line);
// 回聲響應
out.println("ECHO: " + line);
}
} catch (SocketTimeoutException e) {
System.out.println("[" + clientInfo + "] Read timeout");
} catch (IOException e) {
System.err.println("[" + clientInfo + "] Error: " + e.getMessage());
} finally {
closeQuietly(socket);
}
}
}
}
public class TcpClient {
public static void main(String[] args) throws IOException {
String host = "127.0.0.1";
int port = 8080;
try (Socket socket = new Socket()) {
// 連接超時(含 DNS 解析 + TCP 三次握手)
socket.connect(new InetSocketAddress(host, port), 5000);
// 設置讀寫超時
socket.setSoTimeout(10_000);
// 開啟 TCP_NODELAY 禁用 Nagle 算法,適合低延遲交互
socket.setTcpNoDelay(true);
// 發送緩衝區
socket.setSendBufferSize(64 * 1024);
try (BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8), true);
BufferedReader console = new BufferedReader(new InputStreamReader(System.in))) {
String input;
while ((input = console.readLine()) != null) {
out.println(input);
String response = in.readLine();
if (response == null) break; // 服務端關閉
System.out.println("Server: " + response);
}
}
}
}
}
| 方法 | 對應選項 | 建議值/說明 |
|---|---|---|
setSoTimeout(ms) | SO_TIMEOUT | 讀操作阻塞最大時間,防止死等,**服務端/客戶端必設** |
setTcpNoDelay(true) | TCP_NODELAY | 禁用 Nagle 算法,減少小包延遲,適合即時通訊/游戲 |
setKeepAlive(true) | SO_KEEPALIVE | 啟用 TCP 保活機制(默認 2h),檢測死連接,長連接必開 |
setSendBufferSize(bytes) | SO_SNDBUF | 發送緩衝區,大文件傳輸建議 128KB-256KB |
setReceiveBufferSize(bytes) | SO_RCVBUF | 接收緩衝區,同發送緩衝區 |
setReuseAddress(true) | SO_REUSEADDR | 服務端重啟時復用 END_WAIT 端口 |
setSoLinger(true, sec) | SO_LINGER | 關閉時強制發送剩餘數據,0 表示發送 RST 立即斷開 |
setTrafficClass(0x10) | IP_TOS | 設置 DSCP 優先級(如 0x10 低延遲),需網絡設備支持 |
// 服務端
public class UdpServer {
public static void main(String[] args) throws IOException {
try (DatagramSocket socket = new DatagramSocket(9999)) {
socket.setSoTimeout(5000);
byte[] buffer = new byte[65535]; // 最大 UDP 包
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
while (true) {
try {
socket.receive(packet);
String msg = new String(packet.getData(), 0, packet.getLength(), StandardCharsets.UTF_8);
System.out.println("From " + packet.getAddress() + ":" + packet.getPort() + " - " + msg);
// 回複
String reply = "ACK: " + msg;
byte[] replyBytes = reply.getBytes(StandardCharsets.UTF_8);
DatagramPacket replyPacket = new DatagramPacket(replyBytes, replyBytes.length,
packet.getAddress(), packet.getPort());
socket.send(replyPacket);
} catch (SocketTimeoutException ignored) {}
}
}
}
}
// 客戶端
public class UdpClient {
public static void main(String[] args) throws IOException {
try (DatagramSocket socket = new DatagramSocket()) {
socket.setSoTimeout(3000);
InetAddress serverAddr = InetAddress.getByName("127.0.0.1");
int port = 9999;
String msg = "Hello UDP";
byte[] data = msg.getBytes(StandardCharsets.UTF_8);
DatagramPacket packet = new DatagramPacket(data, data.length, serverAddr, port);
socket.send(packet);
byte[] buffer = new byte[65535];
DatagramPacket reply = new DatagramPacket(buffer, buffer.length);
socket.receive(reply);
System.out.println("Server reply: " + new String(reply.getData(), 0, reply.getLength(), StandardCharsets.UTF_8));
}
}
}
TCP 是字節流,無消息邊界。接收端可能讀到:半個包、一個半包、多個包粘在一起。必須在應用層定義協議幀:
| 方案 | 原理 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|---|
| 固定長度 | 每條消息固定 N 字節 | 解析極簡 | 浪費帶寬,不支持變長數據 | 指令控制、固定結構傳感器數據 |
| 分隔符 | 特殊字符結尾(\n、\r\n、\0) | 人類可讀,調試方便 | 數據中不能出現分隔符,需轉義 | 文本協議、HTTP Headers、Telnet |
| 長度前綴 | 頭部 2/4 字節標明 Body 長度 | 高效、支持二進制、無轉義 | 需處理大端/小端、頭部不完整讀取 | **通用首選**、Netty/Protobuf/gRPC 默認 |
| TLV (Type-Length-Value) | 類型+長度+值 結構化 | 擴展性強、自描述 | 解析稍複雜 | 複雜協議、ISUP、Diameter |
public class LengthFieldDecoder {
private static final int HEADER_SIZE = 4; // int32 大端
private final ByteBuffer buffer = ByteBuffer.allocate(8192);
public List<byte[]> decode(ByteBuffer in) {
buffer.put(in);
buffer.flip();
List<byte[]> messages = new ArrayList<>();
while (buffer.remaining() >= HEADER_SIZE) {
buffer.mark(); // 標記位置,以便長度不足時回退
int length = buffer.getInt(); // 大端讀取長度
if (length < 0 || length > 1024 * 1024) throw new IllegalArgumentException("Invalid length: " + length);
if (buffer.remaining() < length) {
buffer.reset(); // 回退,等待更多数据
break;
}
byte[] msg = new byte[length];
buffer.get(msg);
messages.add(msg);
}
buffer.compact(); // 未處理數據移至開頭
return messages;
}
}
BIO (Blocking I/O) → 一連接一線程 → 連接數 < 1K,簡單業務
│
▼
NIO (New I/O) → Selector + 非阻塞 Channel → 連接數 1K-10K,自研協議
│
▼
AIO (Async I/O / NIO.2) → CompletionHandler 回調 → Windows IOCP 優勢場景
│
▼
Netty / Mina / gRPC → 事件驅動、零拷貝、內存池、編解碼器 → 生產環境標準選擇
核心建議:除非教學/極簡工具,**嚴禁在生產環境手寫 NIO 選擇器循環**。請直接使用 Netty 4.1+,它解決了:
SSLSocketFactory / Netty SslHandler,強制雙向認證(mTLS)用於服務間通訊。Java Socket 編程看似門檻低,但要構建高性能、高可靠、安全的網絡應用,需要深入理解 TCP/IP 協議細節、作業系統內核參數、JVM 運行時特性以及現代網絡框架的設計哲學。建議以 Netty 為基石,專注於業務協議設計與架構治理,而非重複造輪子。