架構設計
- ServletContextListener:應用啟動時啟動 Socket 服務端線程,關閉時優雅停止。
- ServerSocket 接收線程:持續
accept() 客戶端連接,每連接啟動一個處理線程。
- 業務處理線程:讀取輸入流、解析協議、執行邏輯、寫回響應,支持長連接心跳。
核心代碼實現
1. 啟動監聽器
@WebListener
public class SocketServerListener implements ServletContextListener {
private SocketServerThread serverThread;
@Override
public void contextInitialized(ServletContextEvent sce) {
serverThread = new SocketServerThread(8888); // 端口配置化
serverThread.start();
sce.getServletContext().log("Socket 服務已啟動在端口 8888");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
if (serverThread != null) {
serverThread.shutdown();
sce.getServletContext().log("Socket 服務已停止");
}
}
}
2. ServerSocket 接收線程
public class SocketServerThread extends Thread {
private final int port;
private ServerSocket serverSocket;
private volatile boolean running = true;
private final ExecutorService workerPool = Executors.newCachedThreadPool();
public SocketServerThread(int port) { this.port = port; }
@Override
public void run() {
try {
serverSocket = new ServerSocket(port);
serverSocket.setSoTimeout(1000); // 定期檢查 running 標誌
while (running) {
try {
Socket client = serverSocket.accept();
client.setSoTimeout(30000); // 讀超時
workerPool.submit(new ClientHandler(client));
} catch (SocketTimeoutException ignored) { /* 定期檢查 running */ }
}
} catch (IOException e) {
if (running) log.error("ServerSocket 異常", e);
} finally {
shutdownPool();
}
}
public void shutdown() {
running = false;
try { if (serverSocket != null) serverSocket.close(); } catch (IOException ignored) {}
}
private void shutdownPool() {
workerPool.shutdown();
try { if (!workerPool.awaitTermination(5, TimeUnit.SECONDS)) workerPool.shutdownNow(); }
catch (InterruptedException e) { workerPool.shutdownNow(); }
}
}
3. 客戶端處理器 (長連接 + 協議解析)
public class ClientHandler implements Runnable {
private final Socket socket;
private final BufferedReader in;
private final BufferedWriter out;
private volatile boolean connected = true;
public ClientHandler(Socket socket) throws IOException {
this.socket = socket;
this.in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
this.out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8));
}
@Override
public void run() {
try {
String line;
while (connected && (line = in.readLine()) != null) {
if (line.trim().isEmpty()) continue;
// 簡單協議: CMD,param1,param2,...\n
String[] parts = line.split(",", 2);
String cmd = parts[0].trim();
String payload = parts.length > 1 ? parts[1].trim() : "";
String response = processCommand(cmd, payload);
sendResponse(response);
}
} catch (IOException e) {
log.warn("客戶端連接異常: {}", e.getMessage());
} finally {
closeResources();
}
}
private String processCommand(String cmd, String payload) {
switch (cmd) {
case "BEAT": return "RES,OK,HEARTBEAT\n"; // 心跳
case "DATA": return handleData(payload); // 業務數據
case "QUIT": connected = false; return "RES,OK,BYE\n";
default: return "ERR,UNKNOWN_CMD\n";
}
}
private String handleData(String payload) {
// 實際業務邏輯:解析 payload、調用 Service、構建響應
return "RES,OK,RECEIVED:" + payload + "\n";
}
private void sendResponse(String resp) {
try { out.write(resp); out.flush(); } catch (IOException e) { connected = false; }
}
private void closeResources() {
try { in.close(); } catch (IOException ignored) {}
try { out.close(); } catch (IOException ignored) {}
try { socket.close(); } catch (IOException ignored) {}
}
}
4. 客戶端測試代碼
public class SocketClientTest {
public static void main(String[] args) throws Exception {
try (Socket socket = new Socket("localhost", 8888);
PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8"), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"))) {
// 發送心跳
out.println("BEAT");
System.out.println("Server: " + in.readLine());
// 發送數據
out.println("DATA,溫度:25.6,濕度:60%");
System.out.println("Server: " + in.readLine());
// 退出
out.println("QUIT");
System.out.println("Server: " + in.readLine());
}
}
}
生產環境關鍵優化
| 問題 | 解決方案 |
| 線程池耗盡 | 使用有界隊列 ThreadPoolExecutor、拒絕策略、監控指標 |
| 半包/粘包 | 定義明確協議分隔符(\n)、長度前綴、或使用 Netty 內建解碼器 |
| 連接洩漏 | finally 塊確保關閉、定期清理空閒連接、啟用 TCP KeepAlive |
| 編碼亂碼 | 統一 UTF-8、顯式指定 Charset、避免平台默認編碼 |
| 優雅關閉 | ShutdownHook 關閉 ServerSocket、中斷工作線程、等待任務完成 |
技術選型建議
- 簡單場景:上述原生實現足夠。
- 高併發/長連接/複雜協議:直接用 Netty,事件驅動、零拷貝、內建心跳/解碼/編解碼器,避免重複造輪子。
- WebSocket 需求:Spring WebSocket / Jetty / Tomcat 原生 WebSocket 支持,而非自建 TCP。
- 上一篇文章: Java EE 架構演進與併發設計隨想:從腳手架到核心原理
- 下一篇文章: FreeMarker 入門:最簡單模板引擎示例詳解
Copyright ©
程式碼之家 All Rights Reserved