ServerSocket 是 TCP 服務端的聽眾,負責綁定端口、監聽連接請求、通過 accept() 產生客戶端專屬 Socket。本文演進展示:單線程 → 多線程 → 線程池 → 生產級優雅關閉,附帶完整可運行代碼。
| 構造器 | 參數 | 用途 |
|---|---|---|
ServerSocket(int port) | 端口號 | 綁定所有網卡 (0.0.0.0) |
ServerSocket(int port, int backlog) | 端口 + 等待隊列長度 | 高併發調大 backlog (默認 50,建議 1024+) |
ServerSocket(int port, int backlog, InetAddress bindAddr) | 端口 + 隊列 + 綁定地址 | 多網卡指定綁定 (如只綁內網 IP) |
public class ServerV1_SingleThread {
public static void main(String[] args) throws IOException {
try (ServerSocket server = new ServerSocket(10000)) {
System.out.println("Server V1 啟動,端口 10000");
while (true) {
try (Socket client = server.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream(), StandardCharsets.UTF_8));
PrintWriter out = new PrintWriter(new OutputStreamWriter(client.getOutputStream(), StandardCharsets.UTF_8), true)) {
System.out.println("客戶端連接: " + client.getInetAddress());
String cmd;
while ((cmd = in.readLine()) != null) {
cmd = cmd.toUpperCase();
if (cmd.startsWith("BYE")) break;
if (cmd.startsWith("DATE") || cmd.startsWith("TIME")) out.println(new Date());
else if (cmd.startsWith("DOM")) out.println(LocalDate.now().getDayOfMonth());
else if (cmd.startsWith("DOW")) out.println(DayOfWeek.from(LocalDate.now()));
else if (cmd.startsWith("DOY")) out.println(LocalDate.now().getDayOfYear());
else if (cmd.startsWith("PAUSE")) { Thread.sleep(3000); out.println("Paused 3s"); }
else out.println("Unknown: " + cmd);
}
} catch (IOException | InterruptedException e) {
System.err.println("客戶端異常: " + e.getMessage());
}
}
}
}
}
缺陷:同一時間只能服務一個客戶端,其他連接排隊阻塞於 accept()。
public class ServerV2_ThreadPerClient {
public static void main(String[] args) throws IOException {
try (ServerSocket server = new ServerSocket(10000)) {
System.out.println("Server V2 啟動 (每連接一線程)");
while (true) {
Socket client = server.accept();
new Thread(new ClientHandler(client), "Client-" + client.getPort()).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)) {
System.out.println("處理客戶端: " + clientInfo);
String cmd;
while ((cmd = in.readLine()) != null) {
cmd = cmd.toUpperCase();
if (cmd.startsWith("BYE")) break;
// ... 業務邏輯同 V1
}
} catch (IOException e) {
System.err.println("客戶端 " + clientInfo + " 異常: " + e.getMessage());
} finally {
try { socket.close(); } catch (IOException ignored) {}
}
}
}
}
缺陷:連接數大時線程爆炸,OOM、上下文切換開銷巨大。
public class ServerV3_ThreadPool {
private static final ExecutorService POOL = Executors.newFixedThreadPool(100);
private static final AtomicBoolean RUNNING = new AtomicBoolean(true);
public static void main(String[] args) throws IOException {
// 優雅關閉鉤子
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
RUNNING.set(false);
POOL.shutdown();
try { POOL.awaitTermination(30, TimeUnit.SECONDS); } catch (InterruptedException ignored) {}
}));
try (ServerSocket server = new ServerSocket(10000)) {
server.setSoTimeout(1000); // accept 超時,定期檢查 RUNNING
System.out.println("Server V3 啟動 (線程池 100)");
while (RUNNING.get()) {
try {
Socket client = server.accept();
POOL.submit(new ClientHandler(client));
} catch (SocketTimeoutException ignored) {
// 定期檢查 RUNNING 標誌
} catch (IOException e) {
if (RUNNING.get()) System.err.println("Accept 異常: " + e.getMessage());
}
}
} finally {
POOL.shutdownNow();
}
}
// ClientHandler 同 V2
}
| 要點 | 做法 | 理由 |
|---|---|---|
| Backlog 設置 | new ServerSocket(port, 1024) | 高併發下避免連接請求被拒 (TCP SYN 隊列滿) |
| 線程池隔離 | 固定大小 + 有界隊列 (ThreadPoolExecutor) | 防止線程無限增長,拒絕策略保護系統 |
| accept 超時 | server.setSoTimeout(1000) | 讓主循環可檢查關閉標誌,實現優雅關閉 |
| 關閉鉤子 | Runtime.addShutdownHook | JVM 關閉時 (Ctrl+C, kill, 容器停止) 觸發優雅停機 |
| 編碼/緩衝/自動刷新 | BufferedReader/PrintWriter(UTF-8, true) | 文本協議標配,避免亂碼/粘包/延遲 |
| 異常隔離 | ClientHandler 內部 try-catch | 單客戶端異常不影響主線程/其他客戶端 |
| 資源釋放 | try-with-resources / finally close | 防止文件描述符洩漏 |
// Netty 服務端啟動 (事件驅動、非阻塞、零拷貝)
ServerBootstrap b = new ServerBootstrap();
b.group(new NioEventLoopGroup(1), new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childOption(ChannelOption.TCP_NODELAY, true)
.childHandler(new ChannelInitializer() {
@Override protected void initChannel(SocketChannel ch) {
ch.pipeline()
.addLast(new LoggingHandler(LogLevel.INFO))
.addLast(new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()))
.addLast(new StringDecoder(StandardCharsets.UTF_8))
.addLast(new StringEncoder(StandardCharsets.UTF_8))
.addLast(new BusinessHandler()); // 你的業務處理器
}
});
b.bind(10000).sync().channel().closeFuture().sync();
ServerSocket 服務端演進路徑:單線程 → 每連接一線程 → 固定線程池 + 優雅關閉 → Netty 事件驅動。掌握 accept() 循環、線程池隔離、setSoTimeout 配合關閉標誌、ShutdownHook 優雅停機,即可構建健壯 TCP 服務。高併發/長連接場景,直接上 Netty。