網路中國象棋室包含伺服端應用程式與客戶端 Applet。伺服端負責用戶認證、大廳/桌子狀態管理、走棋/聊天訊息路由;客戶端負責棋盤渲染(雙緩衝)、走法合法性判斷、用戶交互。本文聚焦登入子系統:ChessLogin、LoginEvent、LoginListener,展示事件驅動架構如何解耦網路 I/O 與業務邏輯。
public class LoginEvent {
public final int userID; // 伺服器分配的唯一 ID
public final String username; // 客戶端傳送的用戶名
public final Socket socket; // 已建立連線的 Socket
public LoginEvent(int userID, String username, Socket socket) {
this.userID = userID;
this.username = username;
this.socket = socket;
}
}
採用 final 欄位確保事件發布後不可竄改,執行緒安全。
public interface LoginListener {
void onLoginEvent(LoginEvent evt);
}
業務邏輯層 (ChessServer) 實作此介面,處理用戶進入後的初始化流程。
public class ChessLogin implements Runnable {
private final ServerSocket ss;
private final Vector listeners = new Vector<>();
private int guestId = 0; // 簡易 ID 分配器
private volatile boolean running = true;
public ChessLogin(ChessServer server) {
this.ss = server.ss;
addLoginListener(server); // Server 註冊為監聽器
new Thread(this).start();
}
public void run() {
while (running) {
try {
Socket socket = ss.accept(); // 阻塞等待連線
String username = chkLogin(socket.getInputStream());
if (username != null) {
notifyListener(guestId++, username, socket);
} else {
socket.close(); // 登入失敗關閉連線
}
} catch (IOException e) {
if (running) e.printStackTrace(); // 正常關閉時會拋 SocketException
}
}
}
private String chkLogin(InputStream is) throws IOException {
// 1. 讀取固定 12 bytes Header
byte[] header = new byte[12];
HttpInputStream.readBytes(is, 12, header);
int cmdType = header[0];
int totalSize = Convert.byteToShort(header[1], header[2]);
if (totalSize == 0) return null;
// 2. 讀取 Body (用戶名位元組)
byte[] data = new byte[totalSize];
HttpInputStream.readBytes(is, totalSize, data);
return parseLoginData(data); // 預設直接轉字串
}
private String parseLoginData(byte[] data) {
return new String(data, StandardCharsets.UTF_8); // 建議明確指定編碼
}
public void addLoginListener(LoginListener l) { listeners.add(l); }
private void notifyListener(int userId, String username, Socket socket) {
LoginEvent evt = new LoginEvent(userId, username, socket);
for (LoginListener l : listeners) {
try { l.onLoginEvent(evt); } catch (Exception e) { e.printStackTrace(); }
}
}
public void shutdown() { running = false; try { ss.close(); } catch (IOException ignore) {} }
}
| 模式 | 角色 | 優勢 |
|---|---|---|
| 觀察者模式 | ChessLogin (Subject) → LoginListener (Observer) | 網路層與業務層完全解耦,可動態增減處理器 (如日誌、審計、機器人) |
| 單一職責 | ChessLogin 只負責「接收連線→讀取登入封包→發布事件」 | 邏輯清晰,易於單元測試 (Mock InputStream 驗證解析) |
| 不可變事件 | LoginEvent 所有欄位 final | 多執行緒環境下無需同步,避免競態條件 |
| 模板方法 (隱性) | chkLogin() 定義讀取流程,parseLoginData() 可覆寫支援加密/Token 驗證 | 擴充性強,不修改核心迴圈即可支援新認證機制 |
同前文,HttpInputStream.readBytes() 保證讀滿指定位元組;Convert.byteToShort/Int() 處理大端序網路位元組序轉換。
guestId++ 非執行緒安全,長期運行會溢位。建議用 AtomicInteger 或 UUID。socket.setSoTimeout(5000) 防止惡意連線佔用資源不發送資料。chkLogin 讀取加密 Token 或挑戰回應 (SCRAM/BCrypt)。SSLServerSocketFactory 建立 SSLServerSocket,保護用戶名/密碼傳輸。shutdown() 關閉 ServerSocket 觸發 accept() 拋出異常跳出迴圈,再逐個關閉已連線 Socket。ByteArrayInputStream 驗證 chkLogin() 解析正確性。ChessLogin,用 Socket 客戶端發送正確/錯誤封包,驗證 LoginListener 回呼與連線狀態。登入子系統展示了事件驅動架構在網路伺服器中的經典應用:I/O 執行緒專注通訊、業務執行緒專注邏輯、事件對象傳遞上下文。這種分離讓 ChessServer 無需知道 Socket 讀取細節,只需關注「用戶進入了,做什麼事」。下一篇將深入 ChessServer 核心狀態機:大廳/桌子管理、走棋協定、廣播/單播路由、雙緩衝客戶端渲染。