手游服务端框架之后台管理工具

xiaoxiao2021-02-28  63

后台管理工具在游戏运营中的作用

手游版本的更新迭代是非常频繁的,有些项目甚至每个星期都会进行停服更新。也就是说,对于生产环境的游戏进程,我们必须有工具能够对游戏服务进行维护。例如更新维护,或者对游戏内部各种资源进行管理。

后台管理工具的通信方式

典型地,完成这种任务的系统被称为后台管理工具。那么,后台管理工具怎么和游戏进程进行通信呢?

主要有两种方式。一种是通过socket,从管理后台建立一条socket连接到游戏进程,并且使用短连接即可。另一种是通过http请求,管理后台发出一条http命令到游戏进程。

两种通信方式的对比 使用http使用socket基于请求-响应模式“全双工”,双方可以同时主动推送消息使用“短链接”,请求无状态可以是“短链接”,也可以是“长链接”任务执行不能切换线程切换线程后,异步处理任务

本文选择的是,后台管理工具采用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}

网站的后台网站请参考 ->> 管理后台web工程

 

 文章预告:下一篇主要介绍如何使用组合包优化客户端协议

手游服务端开源框架系列完整的代码请移步github ->> jforgame

 

 

 

转载请注明原文地址: https://www.6miu.com/read-96412.html

最新回复(0)