請求消息映射策略的選擇
在上一篇文章中,我們看到,消息是直接在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() + "]";
}
}
消息的生產者消費者模型
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();
}
}
}
}
}
有了消費者線程,那麼對應的消息生產者入口在哪裏呢??
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));
至此,我們的消息線程模型就完成了。