本系列採用客戶端 Applet (棋盤渲染/走法判斷/用戶操作) + 伺服端 Java Application (狀態機/訊息路由/房間管理) 架構。通訊協定為自訂二進位封包 (Header + Body),基於 TCP 長連線,採用雙緩衝繪圖消除閃爍。本文聚焦伺服端核心類別:ChessServer、ChessUser、TcpDataReceiver/Sender、HttpInputStream。
| 欄位 | 位移 | 長度 | 說明 |
|---|---|---|---|
| CmdType | 0 | 1 byte | 指令類型 (0:進桌, 1:離桌, 2:走子, 3:求和, 4:認輸, 5:悔棋, 6:用戶列表, 7:新用戶, 8:用戶退出, 9:聊天, 10:登入回應) |
| DataLen | 1-2 | 2 bytes (short) | Body 長度 (大端序) |
| Reserved | 3-4 | 2 bytes | 保留/擴充 (如桌號、顏色) |
| UserID | 5-8 | 4 bytes (int) | 發送者 ID (大端序) |
| Body | 12+ | DataLen bytes | 指令參數 (如走棋座標、聊天文字、用戶資料) |
關鍵設計:固定 12 bytes Header 確保 HttpInputStream.readBytes() 能精確讀取完整封包,解決 TCP 黏包/半包問題。
Hashtable userlist (全館用戶)、ChessTable[20] tb (20 張桌子)、ServerSocket ss。sendToRoom():廣播給大廳所有用戶 (登入/退出/用戶列表)。sendToTable():廣播給特定桌子用戶 (走棋/聊天/悔棋/求和/認輸)。addTableUser(tableID, userID, color) 分配紅/黑/觀戰座位,回傳實際狀態碼;removeTableUser() 清理。onUserPackageArrived(header, len, data) 根據 CmdType 分派,同步鎖保護共享資料結構。Socket、TcpDataReceiver (讀取執行緒)、TcpDataSender (寫入工具)。TcpDataListener,將收到的封包轉交 ChessServer.hwnd.onUserPackageArrived()。username, userID, tableID, state (0紅走/1黑走/2觀紅/3觀黑)。getBytes() 序列化用戶資訊 (8 bytes 名稱 + 4 bytes ID + 2 bytes 狀態) 供大廳列表廣播。public void run() {
while (isrunning) {
HttpInputStream.readBytes(is, 12, header); // 讀固定 Header
int len = Convert.byteToShort(header[1], header[2]);
if (len > 0) HttpInputStream.readBytes(is, len, data); // 讀 Body
notifyListeners(header, len, len > 0 ? data : null);
}
}
Thread,持續阻塞讀取,解碼固定長度 Header + 可變 Body。Vector,收到完整封包回呼 onTcpDataArrived()。IOException 視為連線斷開,上層負責清理。sendData(header, body, len):依序寫入 Header、Body,flush() 確保發送。sendLoginBack(userID):發送 CmdType=10 登入成功回應 (含分配的 UserID)。sendUserListData(userListBytes):發送 CmdType=6 大廳用戶列表。關鍵靜態方法 readBytes(InputStream, int length, byte[] dest):
public static void readBytes(InputStream in, int length, byte[] data) throws IOException {
int n = 0;
while (n < length) {
int read = in.read(data, n, length - n);
if (read == -1) throw new IOException("Connection closed prematurely");
n += read;
}
}
確保讀滿指定位元組數才返回,解決 InputStream.read() 可能不滿緩衝區就返回的特性。
intToBytes(int)、byteToInt(b1,b2,b3,b4) (大端序/Network Byte Order)。shortToBytes(short)、byteToShort(b1,b2)。ChessLogin.run() 迴圈 ss.accept() 接收新 Socket。chkLogin() 讀取 12 bytes Header + Body,解析用戶名 (Body 即用戶名位元組)。LoginEvent(userID, username, socket) 通知 ChessServer.onLoginEvent()。ChessUser,初始化收發器,發送登入回應 (含 UserID)、大廳用戶列表,廣播「新用戶進入」訊息。| 亮點 | 說明 |
|---|---|
| 固定長度 Header | 簡化解析邏輯,配合 HttpInputStream 徹底解決黏包 |
| 職責分離 | Server/User/Receiver/Sender/Protocol 各司其職,易於單元測試 |
| 觀察者模式解耦 | Receiver 不知業務邏輯,純粹負責解碼分發 |
| 雙緩衝繪圖 (Client) | 離屏渲染消除閃爍,提升棋盤操作流暢度 |
Selector 或虛擬執行緒 (Java 21+)。本象棋室伺服端展示了經典 Java TCP 伺服器架構:ServerSocket.accept() + 每連線專屬讀取執行緒 + 自訂二進位協定 (固定 Header + 可變 Body) + 觀察者模式分發 + 共享狀態同步鎖保護。雖技術棧屬上一代 (BIO、Applet、Swing),但其協定設計、模組解耦、狀態機管理思想在現代 Netty/gRPC 微服務架構中依然通用。建議以此為基礎,遷移至 Netty + Protobuf + WebSocket 實現瀏覽器原生跨平台對弈。