標籤:java 伺服器 非同步 遊戲 lambda
前段時間我們遊戲伺服器的開發環境升級到了Java8,這兩天我又把伺服器的執行緒模式重新設計了一下,用上了Lambda運算式。Lambda運算式確實能夠大幅簡化Java代碼,特別是醜陋不堪的匿名內部類,這篇文章主要就是想和大家分享這一點。
執行緒模式
首先簡單介紹一下我們遊戲伺服器的執行緒模式,大致如所示:
Netty線程池只處理訊息的收發,當Netty收到訊息之後,會交給遊戲邏輯線程處理。由於是單線程在處理遊戲邏輯,所以每一個訊息必須很快處理完,也就是說,不能有資料庫等耗時操作,不然邏輯線程很可能會被卡住。為了不卡住邏輯線程,資料庫操作由單獨的線程池來處理。邏輯線程發起資料庫操作後便立即返回繼續處理其他訊息,資料庫線程池處理完畢後,再通知邏輯線程,從而達到了非同步資料庫操作的效果。
GameAction
Netty部分的網路代碼,在收到訊息後,會根據訊息找到對應的Action,然後執行。具體代碼省略,下面是簡化後的GameAction的代碼:
public abstract class GameAction { /** * 在邏輯線程裡處理訊息. * @param gs * @param req 請求訊息 */ public void execute(GameSession gs, Object req) { GameLogicExecutor.execute(() -> { doExecute(gs, req); }); } // 子類實現 public abstract void doExecute(GameSession gs, Object req); }execute()方法裡,使用了Lambda運算式來實現Runnable介面。
GameLogicExecutor
GameLogicExecutor是遊戲邏輯執行線程,代碼如下所示:
import java.util.concurrent.Executor;import java.util.concurrent.Executors;/** * 遊戲邏輯線程. */public class GameLogicExecutor { // todo 限制隊列長度 private static final Executor executor = Executors.newSingleThreadExecutor(); /** * 把遊戲邏輯放到隊列. * @param gameLogic */ public static void execute(Runnable gameLogic) { executor.execute(gameLogic); } }
GetPlayerListAction
下面看一個具體的GameAction實現,這個Action根據使用者ID返回使用者建立的玩家列表,代碼如下:
import java.util.List;public class GetPlayerListAction extends GameAction { @Override public void doExecute(GameSession gs, Object req) { int userId = (Integer) req; PlayerDao.getPlayerList(userId, (List<Player> players) -> { gs.write(players); }); } }假設請求參數是玩家ID,doExecute()方法並沒有等待資料庫操作,而是馬上就返回了。傳遞給DAO的回調對象是個Lambda運算式,在回調方法裡,玩家列表通過GameSession被寫到用戶端(這隻是示範,實際的響應訊息可能是protobuf或JSON)。
PlayerDao
import java.util.ArrayList;import java.util.List;public class PlayerDao { public static void getPlayerList(int userId, DbOpCallback<List<Player>> cb) { DbOpExecutor.execute(() -> { try { List<Player> players = getPlayerList(userId); cb.ok(players); } catch (Exception e) { cb.fail(e); } }); } // 耗時的資料庫操作 private static List<Player> getPlayerList(int userId) { return new ArrayList<>(); } }
getPlayerList()方法接收兩個參數,第一個參數是使用者ID,第二個參數是個callback,當資料庫操作完畢(成功或失敗)時,會通知這個callback。DAO內部使用的可能是JDBC或MyBatis等,總之是耗時的操作,由資料庫線程池執行。
DbOpExecutor
DbOpExecutor的代碼比較簡單,和GameLogicExecutor相似,如下所示:
import java.util.concurrent.Executor;import java.util.concurrent.Executors;/** * 資料庫操作線程池. */public class DbOpExecutor { // todo 根據cpu數等確定線程數 private static final Executor executor = Executors.newFixedThreadPool(4); /** * 把資料庫操作放進隊列. * @param dbOp */ public static void execute(Runnable dbOp) { executor.execute(dbOp); } }
DbOpCallback
最後看一下DbOpCallback代碼:
/** * 資料庫操作回調. * @param <T> */@FunctionalInterfacepublic interface DbOpCallback<T> { /** * 處理資料庫返回結果. * @param result */ void handleResult(T result); /** * 資料庫操作正常結束. * @param result */ default void ok(T result) { // 在遊戲邏輯線程裡處理結果 GameLogicExecutor.execute(() -> { try { handleResult(result); } catch (Exception e) { // todo 處理異常 } }); } /** * 資料庫操作出現異常. * @param e */ default void fail(Exception e) { // todo 處理異常 } }
@FunctionalInterface說明這是一個函數式介面,簡單的說,就是只有一個抽象方法的介面。介面的default方法是Java8的新文法,具體請參考Java8相關方面的資料。ok()方法確保資料庫操作的結果是在邏輯線程裡處理。
結論
上面的代碼,在Java8之前用匿名內部類也是可以寫的,只是相比Lambda運算式,更加冗長醜陋而已。另外要注意,上面的代碼只是簡化後的範例程式碼,並非真實代碼。如果想將上面的代碼應用到自己的項目中,還請注意異常的處理。