後臺管理工具在遊戲運營中的作用
手遊功能的更新迭代是非常頻繁的,有些項目甚至每個星期都會進行停服更新。也就是說,對於生產環境的遊戲進程,我們必須有工具能夠對遊戲服務進行維護,例如更新維護,或者對遊戲內部各種資源進行管理。
典型地,完成這種任務的系統被稱爲後臺管理工具。那麼,後臺管理工具怎麼和遊戲進程進行通信呢?
主要有兩種方式。一種是通過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¶ms={name=kingston}
即可看到執行成功的提示
文章預告:下一篇主要介紹如何使用組合包優化客戶端協議
手遊服務端開源框架系列完整的代碼請移步github ->> jforgame