手游服务端框架之模仿SpringMvc处理玩家请求

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

经典web项目的三层架构

经典web开发项目通常采用三层架构来组织代码。典型的,第一层为表现层,通常使用MVC模式;第二层为业务逻辑层,该层主要是各种service业务操作类;第三层则为数据访问层,通过dao层对数据表进行增删查改操作。

游戏项目的三层架构

类似的,我们的游戏项目也可以采用上面的三层架构。在命名方面,我们部分借鉴了SpringMvc的命名,使用Controller注解对应MVC模式的控制器,使用RequestMapper注解对应的消息处理者(类似于web的http url地址)。网关层收到玩家请求后,将消息分发到对应控制器的指定方法处理者。控制器只用于控制业务流程,具体的业务逻辑将交由业务逻辑层service(游戏项目习惯用Manager来命名)。

使用控制器处理对应业务模块的请求消息

从前面的Message抽象消息的定义可以看出,每一个请求消息包含有一个模块id,一个模块(一个相对独立的游戏功能模块)映射到唯一的控制器;每一个消息包含一个cmd类型,一个cmd类型就代表该功能模块一个子操作。
模块控制器与cmd业务处理method的一对多关系模型如下:


从上面的模型图可以看出,对于给定的模块号结合给定的cmd类型,可以找到唯一的Method与之对应。

好了,开始我们的coding之路吧。

消息控制器与业务流程映射

1. 申明Controller注解,带有该注解的类被标记为消息控制器

package com.kingston.net.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 控制器Controller 
 * 负责处理由MessageDispatcher分发的请求
 */
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {

}
2.申明RequestMapping注解,带有该注解的方法被标记为处理消息的业务映射

package com.kingston.net.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 处理请求地址映射的注解
 * @author kingston
 */
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {

}

3. 定义消息执行单元(CmdExecutor.java),该类负责封装消息的执行者所需要的全部信息

package com.kingston.net.dispatch;  
  
import java.lang.reflect.Method;  
  
/** 
 * 消息执行单元封装 
 * @author kingston 
 * 
 */  
public class CmdExecutor {  
  
    /** 业务处理的工作方法 */  
    private Method method;  
    /** 传递给工作方法的相关参数 */  
    private Class<?>[] params;  
    /** 控制器实例 */  
    private Object handler;  
  
    public static CmdExecutor valueOf(Method method, Class<?>[] params, Object handler) {  
        CmdExecutor executor = new CmdExecutor();  
        executor.method = method;  
        executor.params = params;  
        executor.handler = handler;  
  
        return executor;  
    }  
  
    public Method getMethod() {  
        return method;  
    }  
  
    public Class<?>[] getParams() {  
        return params;  
    }  
  
    public Object getHandler() {  
        return handler;  
    }  
  
}  

4. 定义消息分发器,该分发器负责Controller的初始化,RequestMapper的绑定,消息的接收处理。
package com.kingston.net.dispatch;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import org.apache.mina.core.session.IoSession;
import org.slf4j.Logger;

import com.kingston.logs.LoggerSystem;
import com.kingston.net.Message;
import com.kingston.net.SessionManager;
import com.kingston.net.annotation.Controller;
import com.kingston.net.annotation.Protocol;
import com.kingston.net.annotation.RequestMapping;
import com.kingston.utils.ClassFilter;
import com.kingston.utils.ClassScanner;

/**
 * 消息分发器
 */
public class MessageDispatcher {

	private final Logger logger = LoggerSystem.EXCEPTION.getLogger();

	private volatile static MessageDispatcher instance;

	/** [module_cmd, CmdExecutor] */
	private static final Map<String, CmdExecutor> MODULE_CMD_HANDLERS = new HashMap<>();

	public static MessageDispatcher getInstance() {
		//双重检查锁单例
		if (instance == null) {
			synchronized (MessageDispatcher.class) {
				if (instance == null) {
					instance = new MessageDispatcher();
				}
			}
		}
		return instance;
	}

	private MessageDispatcher() {
		initalize();
	}

	public void initalize() {
		Set<Class<?>> controllers = ClassScanner.getClasses("com.kingston.game", new ClassFilter() {
			@Override
			public boolean accept(Class<?> clazz) {
				return clazz.getAnnotation(Controller.class) != null;
			}
		});

		for (Class<?> controller: controllers) {
			try {
				Object handler = controller.newInstance();
				Method[] methods = controller.getDeclaredMethods();
				for (Method method:methods) {
					RequestMapping mapperAnnotation = method.getAnnotation(RequestMapping.class);
					if (mapperAnnotation != null) {
						MessageMeta meta = getMessageMeta(method);
						if (meta == null) {
							throw new RuntimeException(String.format("controller[%s]方法[%s]缺少RequestMapping注解", 
									controller.getName(), method.getName()));
						}
						short module = meta.module;
						short cmd    = meta.cmd;
						String key = buildKey(meta.module, meta.cmd);
						CmdExecutor cmdExecutor = MODULE_CMD_HANDLERS.get(key);
						if (cmdExecutor != null) {
							throw new RuntimeException(String.format("module[%d] cmd[%d]重复", module, cmd));
						}

						cmdExecutor = CmdExecutor.valueOf(method, method.getParameterTypes(), handler);
						MODULE_CMD_HANDLERS.put(key, cmdExecutor);
					}
				}
			}catch(Exception e) {
				logger.error("", e);
			}
		}
	}

	private MessageMeta getMessageMeta(Method method) {
		for (Class<?> paramClazz: method.getParameterTypes()) {
			if (Message.class.isAssignableFrom(paramClazz)) {
				Protocol protocol = paramClazz.getAnnotation(Protocol.class);
				if (protocol != null) {
					return MessageMeta.valueOf(protocol.module(), protocol.cmd());
				}
			}
		}
		return null;
	}

	/**
	 * 向线程池分发消息
	 * @param session
	 * @param message
	 */
	public void dispatch(IoSession session, Message message) {
		short module = message.getModule();
		short cmd    = message.getCmd();

		CmdExecutor cmdExecutor = MODULE_CMD_HANDLERS.get(buildKey(module, cmd));
		if (cmdExecutor == null) {
			logger.error("请求协议不存在,module=[%d],cmd=[%d]", module, cmd);
			return;
		}

		Object[] params = convertToMethodParams(session, cmdExecutor.getParams(), message);
		Object controller = cmdExecutor.getHandler();
		try {
			//通过反射,
			cmdExecutor.getMethod().invoke(controller, params);
		}catch(Exception e) {
			logger.error("dispatch message failed", e);
		}

	}


	/**
	 * 将各种参数转为被RequestMapper注解的方法的实参
	 * @param session
	 * @param methodParams
	 * @param message
	 * @return
	 */
	private Object[] convertToMethodParams(IoSession session, Class<?>[] methodParams, Message message) {
		Object[] result = new Object[methodParams==null?0:methodParams.length];

		for (int i=0;i<result.length;i++) {
			Class<?> param = methodParams[i];
			if (IoSession.class.isAssignableFrom(param)) {
				result[i] = session;
			}else if (Long.class.isAssignableFrom(param)) {
				result[i] = SessionManager.INSTANCE.getPlayerId(session);
			}else if (long.class.isAssignableFrom(param)) {
				result[i] = SessionManager.INSTANCE.getPlayerId(session);
			}else if (Message.class.isAssignableFrom(param)) {
				result[i] = message;
			}
		}

		return result;
	}

	private String buildKey(short module, short cmd) {
		return module + "_" + cmd;
	}

}

对上文的IoHandler的messageReceived()进行修改,让消息分发器处理消息

@Override 
	public void messageReceived(IoSession session, Object data ) throws Exception 
	{ 
		Message message = (Message)data;
		System.err.println("收到消息-->" + message); 
		//交由消息分发器处理
		MessageDispatcher.getInstance().dispatch(session, message);
		
	} 

至此,整个消息控制器的接收与逻辑映射就完成了。

服务端与客户端程序入口

1.服务端入口,服务端仅需要完成各自模块的初始化,启动mina nioSocket进行监听
package com.kingston;

import com.kingston.logs.LoggerSystem;
import com.kingston.net.MessageFactory;
import com.kingston.net.SocketServer;

public class ServerStarter {

	public static void main(String args[]) {
		//初始化协议池
		MessageFactory.INSTANCE.initMeesagePool();
		//启动socket服务
		try{
			new SocketServer().start();
		}catch(Exception e) {
			LoggerSystem.EXCEPTION.getLogger().error("ServerStarter failed ", e);
		}
	} 

}
2.客户端程序入口,由于编写客户端界面比较麻烦,这里就用一些机器人模拟登录就好了
package com.kingston;

import com.kingston.net.MessageFactory;
import com.kingston.robot.SocketRobot;

public class ClientStarter {

	public static void main(String[] args) {
		//初始化协议池
		MessageFactory.INSTANCE.initMeesagePool();
		
		SocketRobot robot = new SocketRobot("hello");
		robot.buildConnection();
		robot.sendMessage();
	}
	
}
SocketRobot类完成客户端链路的建立及收发消息
package com.kingston.robot;

import java.net.InetSocketAddress;

import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.transport.socket.nio.NioSocketConnector;

import com.kingston.game.login.message.ReqLoginMessage;
import com.kingston.net.codec.MessageCodecFactory;

public class SocketRobot {

	private String name;

	private IoSession session;

	public SocketRobot(String name) {
		this.name = name;
	}

	public void buildConnection() {
		NioSocketConnector connector = new NioSocketConnector();
		connector.getFilterChain().addLast("codec", new ProtocolCodecFilter(MessageCodecFactory.getInstance()));
		connector.setHandler(new ClientHandler());

		System.out.println("开始连接socket服务端"); 
		ConnectFuture future = connector.connect(new InetSocketAddress(9527));
		
		future.awaitUninterruptibly();

		IoSession session = future.getSession();
		this.session = session;

	}

	public void sendMessage() {
		ReqLoginMessage message = new ReqLoginMessage();
		message.setPassword("kingston");
		message.setAccountId(123L);
		this.session.write(message);
	}

	private class ClientHandler extends IoHandlerAdapter {
		public void messageReceived(IoSession session, Object message) {
			System.out.println("收到响应-->" + message); 
		}
	}

}
3.程序运行结果(先启动服务端,再启动客户端)

a.服务端运行截图

b.客户端运行截图


本文主要讲述Mina socket服务端消息在业务上的流向,从中我们也可以看到,消息是在mina的io线程上进行处理的(服务io线程接收消息后直接处理)。如果业务执行非常耗时,就会影响消息的吞吐量。
文章预告:下一篇主要介绍利用独立线程池来异步处理玩家请求。
手游服务端开源框架系列完整的代码请移步github ->>game_server


猜你喜欢

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