手遊服務端框架之消息線程模型

請求消息映射策略的選擇

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



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章