手遊服務端框架之後臺管理工具

後臺管理工具在遊戲運營中的作用

手遊功能的更新迭代是非常頻繁的,有些項目甚至每個星期都會進行停服更新。也就是說,對於生產環境的遊戲進程,我們必須有工具能夠對遊戲服務進行維護,例如更新維護,或者對遊戲內部各種資源進行管理。

典型地,完成這種任務的系統被稱爲後臺管理工具。那麼,後臺管理工具怎麼和遊戲進程進行通信呢?

主要有兩種方式。一種是通過socket,從管理工具建立一條socket連接到遊戲進程,這條socket一般使用短連接即可。一種是通過http請求,管理工具發出一條http命令到遊戲進程。

本文選擇的是,後臺管理工具採用http方式與遊戲進程通信。採用這種方式其中一個原因就是,一般後臺管理工具都是web網站服務,很容易發送http請求。

後臺命令與GM命令的區別

後臺命令與gm命令是很想像的,都是預留一些接口來對遊戲進行管理。但兩者還是有幾點不同:

1. 目的性不同。後臺命令是爲了遊戲運營或者遊戲運維而產生的,而GM命令是爲了方便項目人員調試遊戲功能。部分功能甚至會在兩種命令分別實現一次。

2. 通信方式不同。後臺命令一般是由網站後臺程序發出請求的,而GM命令相當於特殊的客戶端請求,走的是客戶端通信協議。

Mina使用http服務

mina對http服務的支持非常到位,有一個獨立的jar包(mina-http)就是爲了解決http服務的。

其實,http服務與socket服務只有消息的編解碼不同,其他都是非常相似的。

所以,實現一個http服務,只需採用mina-http提供的HttpServerCodec編解碼器就可以了。如下:

public class HttpServer {  

	public void start() throws Exception {  
		IoAcceptor acceptor = new NioSocketAcceptor();  
		acceptor.getFilterChain().addLast("codec", new HttpServerCodec());  
		acceptor.setHandler(new HttpServerHandle()); 
		//http端口
		int port = ServerConfig.getInstance().getHttpPort();
		acceptor.bind(new InetSocketAddress(port));  
	}  
}  

後臺管理命令的設計

1. 後臺命令的類型是比較多的,而且會隨着遊戲的更新逐漸擴充。爲了管理所有類型,我們用一個常量類來保存所有命令類型(HttpCommands.java)。如下:

/**
 * 後臺命令類型枚舉
 * @author kingston
 */
public final class HttpCommands {

	/**  停服 */
	public static final int CLOSE_SERVER = 1;
	/**  查看開服時間 */
	public static final int QUERY_SERVER_OPEN_TIME = 2;
	
}


2. 不同命令所需要的參數也是不同的,所以我們需要一個參數類(HttpCommandParams.java),用於表徵後臺命令的參數。該參數由http的請求參數轉換而來。

public class HttpCommandParams {
	/**  命令類型 {@link HttpCommands} */
	private int cmd;
	
	private Map<String, String> params;
	
	public static HttpCommandParams valueOf(int cmd, Map<String, String> params) {
		HttpCommandParams one = new HttpCommandParams();
		one.cmd    = cmd;
		one.params = params;
		return one;
	}

	public int getCmd() {
		return cmd;
	}

	public Map<String, String> getParams() {
		return params;
	}

	public void setParams(Map<String, String> params) {
		this.params = params;
	}
	
	public String getString(String key) {
		return params.get(key);
	}

	public int getInt(String key) {
		if (params.containsKey(key)) {
			return Integer.parseInt(params.get(key));
		}
		return 0;
	}

	@Override
	public String toString() {
		return "HttpCommandParams [cmd=" + cmd + ", params=" + params
						+ "]";
	}
	
}


3.不同後臺命令的執行邏輯是不同的。對後臺命令處理者,我們建立一個抽象類

/**
 * 抽象後臺命令處理者
 * @author kingston
 */
public abstract class HttpCommandHandler {
	
	/**
	 * 處理後臺命令
	 * @param httpParams
	 * @return
	 */
	public abstract HttpCommandResponse action(HttpCommandParams httpParams);

}


4. 其中,HttpCommandResponse表示命令的執行結果

public class HttpCommandResponse {
	/**  執行成功 */
	public static final byte SUCC = 1;
	/**  執行失敗 */
	public static final byte FAILED = 2;
	/** 執行結果狀態碼 */
	private byte code;
	/** 額外消息 */
	private String message;
	
	public static HttpCommandResponse valueOfSucc() {
		HttpCommandResponse response = new HttpCommandResponse();
		response.code = SUCC;
		return response;
	}
	
	public static HttpCommandResponse valueOfFailed() {
		HttpCommandResponse response = new HttpCommandResponse();
		response.code = FAILED;
		return response;
	}

	public byte getCode() {
		return code;
	}

	public void setCode(byte code) {
		this.code = code;
	}

	public String getMessage() {
		return message;
	}

	public void setMessage(String message) {
		this.message = message;
	}

	@Override
	public String toString() {
		return "HttpCommandResponse [code=" + code + ", message="
						+ message + "]";
	}

}


5. 對於具體的後臺命令,例如停服命令(CloseServerCommandHandler.java),只需繼承自HttpCommandHandler即可。同時,命令的參數申明由註解CommandHandler綁定

@CommandHandler(cmd=HttpCommands.CLOSE_SERVER)
public class CloseServerCommandHandler extends HttpCommandHandler {

	@Override
	public HttpCommandResponse action(HttpCommandParams httpParams) {
		return HttpCommandResponse.valueOfSucc();
	}

}


6. 接下來,我們還需要一個工具類(HttpCommandManager.java)來緩存管理命令與對應的處理者之間的映射關係。

public class HttpCommandManager {

	private static volatile HttpCommandManager instance;

	private static Map<Integer, HttpCommandHandler> handlers = new HashMap<>();

	public static HttpCommandManager getInstance() {
		if (instance != null) {
			return instance;
		}
		synchronized (HttpCommandManager.class) {
			if (instance == null) {
				instance = new HttpCommandManager();
				instance.initialize();
			}
			return instance;
		}
	}

	private void initialize() {
		Set<Class<?>> handleClazzs = ClassScanner.getClasses("com.kingston.http", new ClassFilter() {  
			@Override  
			public boolean accept(Class<?> clazz) {  
				return clazz.getAnnotation(CommandHandler.class) != null;  
			}  
		});  

		for (Class<?> clazz: handleClazzs) {  
			try {  
				HttpCommandHandler handler = (HttpCommandHandler) clazz.newInstance(); 
				CommandHandler annotation = handler.getClass().getAnnotation(CommandHandler.class);
				handlers.put(annotation.cmd(), handler);
			}catch(Exception e) {  
				LoggerUtils.error("", e);
			}  
		}  
	}


	/**
	 * 處理後臺命令
	 * @param httpParams
	 * @return
	 */
	public HttpCommandResponse handleCommand(HttpCommandParams httpParams) {
		HttpCommandHandler handler = handlers.get(httpParams.getCmd());
		if (handler != null) {
			return handler.action(httpParams);
		}
		return null;
	}
}


7. 最後,在HttpServer接受http請求的時候,將參數封裝成HttpCommandParams對象,由HttpCommandManager找到對應的Handler,將處理結果封裝成HttpCommandResponse,返回給客戶端。如下:

class HttpServerHandle extends IoHandlerAdapter {  
	
	private static Logger logger = LoggerFactory.getLogger(HttpServer.class);

	@Override  
	public void exceptionCaught(IoSession session, Throwable cause)  
			throws Exception {  
		cause.printStackTrace();  
	}  

	@Override  
	public void messageReceived(IoSession session, Object message)  
			throws Exception {  
		if (message instanceof HttpRequest) {  
			// 請求,解碼器將請求轉換成HttpRequest對象  
			HttpRequest request = (HttpRequest) message;  
			HttpCommandResponse commandResponse = handleCommand(request);
			// 響應HTML  
			String responseHtml = new Gson().toJson(commandResponse);  
			byte[] responseBytes = responseHtml.getBytes("UTF-8");  
			int contentLength = responseBytes.length;  

			// 構造HttpResponse對象,HttpResponse只包含響應的status line和header部分  
			Map<String, String> headers = new HashMap<String, String>();  
			headers.put("Content-Type", "text/html; charset=utf-8");  
			headers.put("Content-Length", Integer.toString(contentLength));  
			HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SUCCESS_OK, headers);  

			// 響應BODY  
			IoBuffer responseIoBuffer = IoBuffer.allocate(contentLength);  
			responseIoBuffer.put(responseBytes);  
			responseIoBuffer.flip();  

			session.write(response); // 響應的status line和header部分  
			session.write(responseIoBuffer); // 響應body部分  
		}  
	}  

	private HttpCommandResponse handleCommand(HttpRequest request) {
		HttpCommandParams httpParams = toHttpParams(request);
		if (httpParams == null) {
			HttpCommandResponse failed = HttpCommandResponse.valueOfFailed();
			failed.setMessage("參數錯誤");
			return failed;
		}
		logger.info("收到http後臺命令,參數爲{}", httpParams);
		HttpCommandResponse commandResponse = HttpCommandManager.getInstance().handleCommand(httpParams);
		if (commandResponse == null) {
			HttpCommandResponse failed = HttpCommandResponse.valueOfFailed();
			failed.setMessage("該後臺命令不存在");
			return failed;
		}
		return commandResponse;
	}

	private HttpCommandParams toHttpParams(HttpRequest httpReq) {
		String cmd = httpReq.getParameter("cmd"); 
		if (StringUtils.isEmpty(cmd)) {
			return null;
		}
		String paramJson = httpReq.getParameter("params"); 
		if (StringUtils.isNotEmpty(paramJson)) {
			try{
				Map<String, String> params = new Gson().fromJson(paramJson, HashMap.class);
				return HttpCommandParams.valueOf(Integer.parseInt(cmd), params);
			}catch(Exception e) {
			}
		}
		return null;
	}

}  


代碼示例

遊戲啓動後,在瀏覽器輸入

http://localhost:8080/?cmd=1&params={name=kingston}

即可看到執行成功的提示


 文章預告:下一篇主要介紹如何使用組合包優化客戶端協議

手遊服務端開源框架系列完整的代碼請移步github ->> jforgame





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章