您的位置:首页 > 编程语言 > Java开发

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

2017-09-03 22:45 197 查看

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

手游功能的更新迭代是非常频繁的,有些项目甚至每个星期都会进行停服更新。也就是说,对于生产环境的游戏进程,我们必须有工具能够对游戏服务进行维护,例如更新维护,或者对游戏内部各种资源进行管理。
典型地,完成这种任务的系统被称为后台管理工具。那么,后台管理工具怎么和游戏进程进行通信呢?
主要有两种方式。一种是通过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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息