手游服务端框架之消息线程模型
请求消息映射策略的选择
在上一篇文章中,我们看到,消息是直接在mina的io线程中处理的。这样做有一个非常严重的缺陷,如果业务处理比较耗时,那么io线程接受消息的速度就会下降,严重影响io的吞吐量。
典型的,我们应该另起线程池,专门用于异步地处理玩家的请求消息。
在我之前的一篇文章(游戏服务端线程模型——无锁处理玩家请求),谈到可以通过某种映射,将玩家的请求分发到特定的线程进行处理,这样可以避免同一个玩家的请求需要进行线程同步。
在那篇文章,我们采用的映射策略是——将玩家的角色id与工作线程总数进行求模映射,这种模型其实是一种简单的策略。在极端的情况下,会造成非常多的玩家请求在同一条线程上(登录的玩家id不具有负载均衡性)。
采用什么映射策略,跟游戏的类型定位的联系非常之大。
举个例子,如果游戏的类型是一款MMORPG(大型多人在线游戏),场景地图非常大,游戏的战斗发生在服务端,pvp同步策略采用状态同步,这样的战斗方案为了减少锁竞争,往往要求同一张地图的所有玩家请求在一条线程上。特别的,由于战斗发生在服务端,怪物的行为,场景定时任务的执行,也保证在同一条线程上。所以,这类游戏的请求消息映射策略往往跟地图id挂钩。
另外一些游戏类型,比如休闲游戏,或者虽然是rpg游戏,但战斗发生在客户端(服务端只做检验),映射策略跟场景没关系,只需保证负载均衡即可。
本文采用的映射策略是第二种,因为战斗发生在服务端的设计难度非常大 =。=
为了达到负载均衡,我们可以在客户端链路的创建时,为该Session创建一个自增长的索引号。这样每一个新的玩家就是轮询地映射到下一条工作线程。
在IoHandler类的sessionCreated(IoSession session)方法,我们增加这样的逻辑
@Override public void sessionCreated(IoSession session) { //显示客户端的ip和端口 System.out.println(session.getRemoteAddress().toString()); session.setAttributeIfAbsent(SessionProperties.DISTRIBUTE_KEY, SessionManager.INSTANCE.getNextDistributeKey()); }
其中SessionManager.getNextDistributeKey()是一个原子变量的自增长器。
异步消息任务模型的定义
1. 定义可分发的任务接口 IDistributeTask.java
/** * 可分发的任务接口 * @author kingston */ public interface IDistributeTask { /** * 分发的工作线程索引 * @return */ int distributeKey(); /** * 获取名字 * @return */ String getName(); /** * 执行业务 */ void action(); }
2. AbstractDistributeTask抽象类是IDistributeTask接口的一个骨架实现,实现部分抽象方法
import com.kingston.net.dispatch.IDistributeTask; public abstract class AbstractDistributeTask implements IDistributeTask{ /** 消息分发器的索引 */ protected int distributeKey; /** 业务开始执行的毫秒数 */ private long startMillis; /** 业务结束执行的毫秒数 */ private long endMillis; public String getName() { return this.getClass().getSimpleName(); } public int distributeKey() { return distributeKey; } public long getStartMillis() { return startMillis; } public void markStartMillis() { this.startMillis = System.currentTimeMillis(); } public long getEndMillis() { return endMillis; } public void markEndMillis() { this.endMillis = System.currentTimeMillis(); } }
3. 消息任务实体(MessageTask.java),用于封装业务执行的相关参数,继承自AbstractDistributeTask类
import java.lang.reflect.Method; import com.kingston.net.Message; public class MessageTask extends AbstractDistributeTask { private long playerId; /** 消息实体 */ private Message message; /** 消息处理器 */ private Object handler; private Method method; /** 处理器方法的参数 */ private Object[] params; public static MessageTask valueOf(int distributeKey, Object handler, Method method, Object[] params) { MessageTask msgTask = new MessageTask(); msgTask.distributeKey = distributeKey; msgTask.handler = handler; msgTask.method = method; msgTask.params = params; return msgTask; } @Override public void action() { try{ method.invoke(handler, params); }catch(Exception e){ } } public long getPlayerId() { return playerId; } public Message getMessage() { return message; } public Object getHandler() { return handler; } public Method getMethod() { return method; } public Object[] getParams() { return params; } @Override public String toString() { return this.getName() + "[" + handler.getClass().getName() + "@" + method.getName() + "]"; } }
消息的生产者消费者模型
import java.util.ArrayList; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import edu.emory.mathcs.backport.java.util.concurrent.atomic.AtomicBoolean; /** * 消息任务处理器 * @author kingston */ public enum TaskHandlerContext { /** 单例 */ INSTANCE; private final int CORE_SIZE = Runtime.getRuntime().availableProcessors(); /** 工作者线程池 */ private final List<TaskWorker> workerPool = new ArrayList<>(); private final AtomicBoolean run = new AtomicBoolean(true); public void initialize() { for (int i=0; i<CORE_SIZE+1; i++) { TaskWorker worker = new TaskWorker(i); workerPool.add(worker); new Thread(worker).start(); } } /** * 接受消息 * @param task */ public void acceptTask(MessageTask task) { if (task == null) { throw new NullPointerException("task is null"); } int distributeKey = task.distributeKey() % workerPool.size(); workerPool.get(distributeKey).addTask(task); } /** * 关闭消息入口 */ public void shutDown() { run.set(false); } private class TaskWorker implements Runnable { /** 工作者唯一号 */ private int workerIndex; /** 生产者队列 */ private BlockingQueue<AbstractDistributeTask> taskQueue = new LinkedBlockingQueue<>(); TaskWorker(int index) { this.workerIndex = index; } public void addTask(AbstractDistributeTask task) { this.taskQueue.add(task); } @Override public void run() { //死循环读消息 while(run.get()) { try { AbstractDistributeTask task = taskQueue.take(); task.markStartMillis(); task.action(); task.markEndMillis(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
有了消费者线程,那么对应的消息生产者入口在哪里呢??
try { //通过反射, cmdExecutor.getMethod().invoke(controller, params); }catch(Exception e) { }
采用生产者模型后,我们只需要改成
int distributeKey = (int)session.getAttribute(SessionProperties.DISTRIBUTE_KEY); TaskHandlerContext.INSTANCE.acceptTask( MessageTask.valueOf(distributeKey, controller, cmdExecutor.getMethod(), params));
浙公网安备 33010602011771号