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

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/littleschemer/article/details/77778219

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

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

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

主要有两种方式。一种是通过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编解码器就可以了。如下:

扫描二维码关注公众号,回复: 3362845 查看本文章
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





猜你喜欢

转载自blog.csdn.net/littleschemer/article/details/77778219