Java Netty遊戲架構-伺服器命令模型實踐
阿新 • • 發佈:2018-12-15
本篇將通過註解和反射來介紹一種遊戲伺服器命令的接收和處理的方式,希望各位喜歡。
在 Netty實戰手冊(三)中,HandlerService有一段程式碼:
cmd.docommand( _ctx , ( ByteBuf ) _obj );
這裡是接收訊息的入口,通過它,我們需要來完成3件事:解析命令,找到實現類,通過反射執行方法。
HandlerService.java:
@Override public void docommand( ChannelHandlerContext _ctx , ByteBuf _request ){ // 1. 約定4個位元組的命令ID int cmd = _request.readInt(); ByteBuf _response = _ctx.alloc().buffer(); try{ // 2. 通過註解找出命令對應的實現類和方法,保證效率的話,可以將命令和實現類存入靜態的Map物件 // CommonContextHolder為自定義Spring的Bean物件管理類,參考jees-jsts // 註解查詢實現類 Collection< Object > action_collection = CommonContextHolder .getApplicationContext().getBeansWithAnnotation( GameCommand.class ).values(); Iterator< Object > action_iterate = action_collection.iterator(); boolean dorequest = false; while( action_iterate.hasNext() ){ Object a = action_iterate.next(); Method[] mths = a.getClass().getMethods(); // 註解查詢實現方法 for ( Method m : mths ) { GameRequest g = AnnotationUtils.findAnnotation( m , GameCommand.class ); if( g != null && cmd == g.value() ){ //3. 通過反射,呼叫命令處理的部分 dorequest = true; ReflectionUtils.invokeMethod( m , a, _request, _response ); break; } } if( dorequest ) break; } // 這裡的處理僅僅是為了保證錯誤的資料不會被傳遞到客戶端 _ctx.writeAndFlush( _response ); }catch (Exception e) { // 出錯後可以通知客戶端,指定的錯誤資訊 } }
這種形式雖然不如直接switch-case寫法效率高,但可維護性大大增強。通過自定義註解GameCommand,我們只需要關注功能邏輯即可。
GameCommand.java
@Target( {ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GameCommand {
public int value() default 0;
}
註解需要定義的地方不多,這裡僅定義一個value用於寫命令,並宣告該註解可用於類和方法。
下面來看下如何寫功能的實現:
GameActionDemo.java
/** * 這裡的@GameCommand註解用於讓Spring容器可以獲取到,並識別為指定的相關類 */ @Component @GameCommand public class GameActionDemo { @Autowired GameCache game; /** * 命令實現 * @GameCommand 用於宣告需要處理哪個命令,注意如果同一個命令被多個方法宣告,按照前面的寫法只會執行一個。 * @Transactional 用於宣告資料事務的範圍 * @param _request 為服務端接收的客戶端資料 * @param _response 為服務端要發往客戶端的資料 */ @GameCommand( IGameRequest.CMD_ACTION_DEMO ) @Transactional public void cmd_action( ByteBuf _request, ByteBuf _response ) { // 接收資料示意 int int_val = _request.readInt(); long long_val = _request.readLong(); // 這裡可以理解為功能處理,可以直接寫這裡,或者另外寫個資料處理的類 game.dosomething( int_val, long_val ); // 寫回資料示意 _response.writeInt( int_val ); _response.writeLong( long_val ); } }
其實整個命令結構不會太複雜,複雜的是多樣的功能需求和資料處理。另外需要額外說明和注意的地方有這麼3點:
1. ChannelHandlerContext物件可以記錄,這樣我們可以有效的識別訊息來源,使用者資料等是否合法,並可以合法的傳遞回去。
2. 對於資料庫處理,儘量集中完成,確保完成後在通知客戶端。當然也可以使用快取的方式處理,定時的更新快取到資料庫這種方式。
3. 重要事項:由於訊息是非同步傳遞的,在資料處理部分注意使用者資料和資料庫的原子性,特別是多使用者資料互動的時候。儲存物件儘量使用ConcurrentMap這類,適當的地方加上synchronized關鍵字。
Github: https://github.com/aiyoyoyo
討論群:8802330