手機網遊服務端需支撐大量客戶端併發連接,本文以《瘋狂的炸彈》實戰項目為例,剖析請求處理線程模型、協議設計、用戶匹配邏輯與同步控制。
客戶端 ←HTTP/TCP→ 服務端
├── Acceptor線程 (ServerSocket.accept)
├── 處理線程池 (每連接一線程/線程池)
├── 邏輯引擎 (遊戲狀態機)
└── 數據存儲 (內存/Redis/MySQL)
public class GameServer {
private final ServerSocket serverSocket;
private final ExecutorService threadPool = Executors.newCachedThreadPool();
public void start() {
while (running) {
Socket client = serverSocket.accept();
threadPool.submit(new ClientHandler(client));
}
}
}
class ClientHandler implements Runnable {
private final Socket socket;
public void run() {
try (InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream()) {
// 讀取請求
byte[] request = readRequest(is);
if (request.length == 0) return;
// 邏輯處理
byte[] response = LogicEngine.process(request, this);
// 發送響應
if (response != null) {
sendResponse(os, response);
}
} catch (IOException e) {
log.error("連接異常", e);
} finally {
closeConnection();
}
}
}
// 客戶端→服務端指令
interface RequestCmd {
byte READY = 0x00; // 就緒/註冊
byte START = 0x01; // 開始匹配
byte MOVE = 0x02; // 移動
byte LAY_BOMB = 0x03; // 放炸彈
byte GET_DATA = 0x04; // 請求遊戲數據
byte QUIT = 0x05; // 退出
}
// 服務端→客戶端響應
interface ResponseCmd {
byte READY_OK = 0x00; // 就緒成功,返回userId
byte CONTINUE_WAIT = 0x01; // 繼續等待匹配
byte INIT_DATA = 0x02; // 初始化數據(房間/地圖/角色)
byte GAME_DATA = 0x03; // 幀同步數據
}
// 請求編碼:[指令1字節][參數長度2字節][參數體]
public byte[] encodeMove(short x, short y, byte direction) {
ByteBuffer buf = ByteBuffer.allocate(1 + 2 + 5);
buf.put(RequestCmd.MOVE);
buf.putShort(5); // 參數長度
buf.putShort(x);
buf.putShort(y);
buf.put(direction);
return buf.array();
}
// 響應解碼
public GameData decodeGameData(byte[] data) {
ByteBuffer buf = ByteBuffer.wrap(data);
byte cmd = buf.get();
if (cmd != ResponseCmd.GAME_DATA) throw new ProtocolException();
GameData gd = new GameData();
gd.roomId = buf.getInt();
gd.players = decodePlayers(buf);
gd.map = decodeMap(buf);
gd.bombs = decodeBombs(buf);
return gd;
}
public class UserManager {
private final AtomicLong idGenerator = new AtomicLong(10000);
private final ConcurrentMap sessions = new ConcurrentHashMap<>();
public long allocateUserId() {
return idGenerator.getAndIncrement(); // 原子操作,無鎖
}
public void registerSession(long userId, UserSession session) {
sessions.put(userId, session);
}
}
public class MatchMaker {
private final Queue waitingQueue = new ConcurrentLinkedQueue<>();
private final Map rooms = new ConcurrentHashMap<>();
public synchronized Room tryMatch(long userId) {
waitingQueue.offer(userId);
if (waitingQueue.size() >= 2) {
Long u1 = waitingQueue.poll();
Long u2 = waitingQueue.poll();
if (u1 != null && u2 != null) {
Room room = new Room(u1, u2);
rooms.put(room.id, room);
return room;
}
}
return null; // 需繼續等待
}
}
// 客戶端每秒請求一次
public byte[] handleGetData(Request req, ClientHandler handler) {
long userId = req.getUserId();
Room room = roomManager.getRoomByUser(userId);
if (room == null) return encodeError("不在房間中");
// 返回當前幀數據
return room.getCurrentFrame().encode();
}
// 服務端定時推送(可選)
@Scheduled(fixedRate = 100) // 10 FPS
public void broadcastFrames() {
for (Room room : rooms.values()) {
Frame frame = room.calculateNextFrame();
room.broadcast(frame.encode());
}
}
| 優化點 | 方案 | 效果 |
|---|---|---|
| 線程模型 | NIO選擇器+線程池/Netty事件循環 | 支撐萬級併發,降低線程開銷 |
| 協議壓縮 | Protobuf/MessagePack替代自定義二進制 | 減少50%+頻寬,解碼加速 |
| 狀態存儲 | 熱數據內存+異步落地Redis | 毫秒級讀寫,支持集群 |
| 幀同步 | 增量更新/死重播/客戶端預測 | 弱網下流暢體驗 |
| 防作弊 | 服務端權威校驗/重放檢測 | 保證公平性 |
從「每連接一線程」起步,逐步演進至NIO/Netty架構,是手遊服務端成長的必經之路。