手游服务端框架之消息线程模型

原创 2017年07月12日 23:17:48

请求消息映射策略的选择

在上一篇文章中,我们看到,消息是直接在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

package com.kingston.net.dispatch;

/**
 * 可分发的任务接口
 * @author kingston
 */
public interface IDistributeTask {
	
	/**
	 * 分发的工作线程索引
	 * @return
	 */
	int distributeKey();
	
	/**
	 * 获取名字
	 * @return
	 */
	String getName();
	
	/**
	 * 执行业务
	 */
	void action();
	
	
}

2. AbstractDistributeTask抽象类是IDistributeTask接口的一个骨架实现,实现部分抽象方法

package com.kingston.net.context;

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类。
package com.kingston.net.context;

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() + "]";
	}
	
}

消息的生产者消费者模型

生产者消费者模型是处理异步逻辑非常强大的工具。为了达到消息分发的目的,我们在服务启动的时候,初始化N条工作线程,每条工作线程都有一个阻塞队列,用于保存未处理的消息列表。工作线程(消费者)的run()方法是一个死循环,不停地弹出队首消息,然后执行业务逻辑
package com.kingston.net.context;

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();
				}
			}
		}
	}
}

在TaskHandlerContext类的acceptTask(MessageTask task)方法里,我们通过task的distributeKey()方法,找到工作线程组的指定线程,加到对应的生产者队列。
有了消费者线程,那么对应的消息生产者入口在哪里呢??
回顾上一篇文章我们的消息分发器(MessageDispatcher)的dispatch(IoSession session, Message message)方法,逻辑是这样的
        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));

至此,我们的消息线程模型就完成了。

文章预告:下一篇主要介绍描述游戏规则的策划配置库与保存玩家数据的用户库的设计。
手游服务端开源框架系列完整的代码请移步github ->>game_server



版权声明:本文为博主原创文章,未经博主允许不得转载。

游戏服务端线程模型——无锁处理玩家请求

采用线程池组技巧,实现游戏服务端为每一个角色指派固定线程,从而达到无锁处理玩家个人请求。...

redis 的惊群处理和分布式锁的应用例子

在并发量比较高的情况下redis有很多应用场景,提升查询效率,缓解底层DBio ,下面列举两个平时开发中应用过的两个例子,欢迎各位一起讨论改进。  1 . redis 惊群处理 1.1 方案的由来 ...

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

经典web开发项目通常采用三层架构来组织代码。典型的,第一层为表现层,通常使用MVC模式;第二层为业务逻辑层,该层主要是各种service业务操作类;第三层则为数据访问层。类似的,我们的游戏项目也可以...

手游服务端框架之配置与玩家数据库设计

一款网络游戏的设计,至少需要策划数据库和用户数据库两种数据库。本文主要介绍这两种数据库的设计及使用,同时,介绍如何通过ORM框架来完成玩家数据的持久化。...

手游服务端框架之使用Guava构建缓存系统

缓存,在项目中的应用非常之广泛。诸如这样的场景,某些对象计算或者获取的代码比较昂贵,并且在程序里你不止一次要用到这些对象,那么,你就应该使用缓存。缓存,在项目中的应用非常之广泛。诸如这样的场景,某些对...

python手游服务端搭建

最近在研究网游了,由于无意中在群里看到了9秒社区发布的源代码链接,

服务端开发笔记二:基于pomelo的分布式手游架构

经过了一个周的时间,新项目的基本结构已经大体确定,拿来给大家分享。 一 基础概念 在说明手游服务器架构之前,一下几点须知。 1 我们的服务器集群分为前端服务器和后端服务器。 前端服务器:负责接收前...

高效率完成一次接入80个手游渠道SDK——游戏接入SDK服务端篇

.1 概要     通常,游戏开发商并不会只在一个渠道上线他们的游戏,接入越多的渠道,代表着可能获取越多的用户,但同时也代表着越多的接入SDK工作量、工期和费用。一款游戏要有足够的用户,甚至需要接入3...

手游服务端开发基础概念扫盲篇

从事手游服务端开发也快3年了,整理了一份资料,介绍在开发过程中碰到的概念以及自己的理解,希望能够帮到即将从事该职业的朋友。...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:手游服务端框架之消息线程模型
举报原因:
原因补充:

(最多只允许输入30个字)