Socket 編程看似複雜,掌握三步核心邏輯即可入門到精通。本文遵循「理解本質 → 多客戶並發 → 即時廣播」學習路徑,附帶完整可運行代碼,最終構建一個支援暱稱、在線列表、即時廣播的聊天室服務端。
Socket (客戶端) / ServerSocket (服務端) 位於 java.netaccept():阻塞等待連接,返回客戶端 SocketgetInputStream():獲取輸入流 (對端發送的數據)getOutputStream():獲取輸出流 (發送給對端的數據)IOException
// Server
public class SimpleServer {
public static void main(String[] args) throws IOException {
try (ServerSocket ss = new ServerSocket(10000)) {
while (true) {
try (Socket socket = ss.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8), true)) {
String line = in.readLine();
out.println("You input: " + line);
}
}
}
}
}
// Client
public class SimpleClient {
public static void main(String[] args) throws IOException {
try (Socket socket = new Socket("localhost", 10000);
PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
BufferedReader keyboard = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8))) {
String line = keyboard.readLine();
out.println(line);
System.out.println("Server: " + in.readLine());
}
}
}
設計原理:主線程 accept() 循環接受連接,每個連接交給獨立線程處理,主線程立即回去接受下一個。
public class ConcurrentServer extends ServerSocket {
public ConcurrentServer() throws IOException { super(10000); }
public static void main(String[] args) throws IOException {
new ConcurrentServer().runServer();
}
void runServer() throws IOException {
try {
while (true) {
Socket socket = accept();
new ClientHandler(socket).start(); // 關鍵:啟動新線程
}
} finally { close(); }
}
static class ClientHandler extends Thread {
private final Socket client;
private BufferedReader in;
private PrintWriter out;
ClientHandler(Socket s) throws IOException {
this.client = s;
this.in = new BufferedReader(new InputStreamReader(client.getInputStream(), StandardCharsets.UTF_8));
this.out = new PrintWriter(new OutputStreamWriter(client.getOutputStream(), StandardCharsets.UTF_8), true);
out.println("--- Welcome ---");
}
@Override public void run() {
try {
String line = in.readLine();
while (line != null && !"bye".equalsIgnoreCase(line)) {
String msg = processMessage(line); // 業務處理
out.println(msg);
line = in.readLine();
}
out.println("--- See you, bye! ---");
} catch (IOException e) {
// log
} finally {
try { client.close(); } catch (IOException ignored) {}
}
}
String processMessage(String line) { return "Echo: " + line; }
}
}
核心數據結構:
List onlineUsers :在線用戶暱稱List handlers :所有客戶端處理器引用 (用於廣播)Queue messageQueue :待廣播消息隊列BroadcastThread:專門消費隊列並推送給所有客戶端public class ChatServer extends ServerSocket {
private static final List ONLINE_USERS = new CopyOnWriteArrayList<>();
private static final List HANDLERS = new CopyOnWriteArrayList<>();
private static final Queue MSG_QUEUE = new ConcurrentLinkedQueue<>();
private static final AtomicInteger COUNTER = new AtomicInteger();
public ChatServer() throws IOException { super(10000); }
public static void main(String[] args) throws IOException {
new ChatServer().runServer();
}
void runServer() throws IOException {
new BroadcastThread().start(); // 啟動廣播線程
try {
while (true) {
Socket socket = accept();
new ClientHandler(socket).start();
}
} finally { close(); }
}
// 廣播線程:消費隊列,推送給所有客戶端
static class BroadcastThread extends Thread {
@Override public void run() {
while (true) {
String msg = MSG_QUEUE.poll();
if (msg != null) {
for (ClientHandler h : HANDLERS) {
h.sendMessage(msg);
}
} else {
try { Thread.sleep(10); } catch (InterruptedException ignored) {}
}
}
}
}
static class ClientHandler extends Thread {
private final Socket client;
private BufferedReader in;
private PrintWriter out;
private String username;
ClientHandler(Socket s) throws IOException {
this.client = s;
this.in = new BufferedReader(new InputStreamReader(client.getInputStream(), StandardCharsets.UTF_8));
this.out = new PrintWriter(new OutputStreamWriter(client.getOutputStream(), StandardCharsets.UTF_8), true);
out.println("--- Welcome to Chatroom ---");
out.println("Input your nickname:");
}
void sendMessage(String msg) { out.println(msg); }
@Override public void run() {
try {
String line = in.readLine();
int flag = 0;
while (line != null && !"bye".equalsIgnoreCase(line)) {
if ("l".equals(line)) { // 列表命令
out.println(listOnlineUsers());
} else if (flag++ == 0) { // 首條消息作為暱稱
username = line;
ONLINE_USERS.add(username);
HANDLERS.add(this);
broadcast("[" + username + " come on in]");
out.println(listOnlineUsers());
} else {
broadcast("<" + username + ">" + line);
}
line = in.readLine();
}
} catch (IOException e) {
// log
} finally {
cleanup();
}
}
void broadcast(String msg) { MSG_QUEUE.offer(msg); }
String listOnlineUsers() {
StringBuilder sb = new StringBuilder("-+- Online List -+-\r\n");
for (String u : ONLINE_USERS) sb.append("[").append(u).append("]\r\n");
sb.append("-+---------------------+-");
return sb.toString();
}
void cleanup() {
if (username != null) {
ONLINE_USERS.remove(username);
HANDLERS.remove(this);
broadcast("[" + username + " left]");
}
try { client.close(); } catch (IOException ignored) {}
}
}
}
| 設計 | 作用 | 技術點 |
|---|---|---|
| CopyOnWriteArrayList | 線程安全遍歷,無需加鎖 | 廣播時遍歷 HANDLERS,寫入較少讀取極多場景最優 |
| ConcurrentLinkedQueue | 無鎖隊列,生產者(ClientHandler) / 消費者(BroadcastThread) 分離 | 避免客戶端線程阻塞於發送,解耦接收與廣播 |
| 專用廣播線程 | 統一發送邏輯,減少鎖競爭 | 單線程序列化發送,簡化併發控制 |
| 暱稱註冊機制 | 首條消息作暱稱,後續消息帶標識 | 簡單協議設計,無需複雜登錄流程 |
| 清理機制 | 斷開時移除列表、廣播下線通知 | finally 塊保證執行 |
public class ChatClient {
public static void main(String[] args) throws IOException {
try (Socket socket = new Socket("localhost", 10000);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8), true);
BufferedReader keyboard = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8))) {
String line;
while ((line = in.readLine()) != null) {
System.out.println(line); // 顯示服務端消息
if (line.contains("nickname")) {
System.out.print("> ");
String nick = keyboard.readLine();
out.println(nick);
} else {
System.out.print("> ");
String input = keyboard.readLine();
if (input != null) out.println(input);
if ("bye".equalsIgnoreCase(input)) break;
}
}
}
}
}
三步精通 Socket:
此聊天室雖簡單,但包含了服務端架構的靈魂:接收、處理、路由、廣播、狀態管理、生命週期。理解此模型,再學 Netty、gRPC、WebSocket 皆為變體應用。