手遊服務端框架之使用事件驅動模型解決業務高耦合

事件驅動的作用與目標

假設這樣的業務需求:遊戲服務器希望在玩家升級時觸發多種效果。例如玩家升級後,各種屬性都會提高,開啓新的系統玩法,學習新的技能……入門程序員寫出來的代碼可能是這樣——

private void handleRoleUpgrade(Object role){  
        if(meetUpgradeCondition(role)){//滿足升級條件  
            RoleManager.getInstance().upgradeAttribution(role);//屬性提升  
            SkillManager.getInstance().learnNewSkill(role);//學會新技能  
            //其他一堆業務  
        }  
}  
可以看出,玩家升級後,所有跟升級掛鉤的業務都要集中在一起,依次被處理。這樣寫出來的代碼耦合度非常高。一旦有新的業務加入,這裏就要繼續插代碼。

爲了達到解耦的效果,我們引入了事件驅動模型。

當玩家觸發了升級這個動作,我們完全可以把“升級”這個動作包裝成一個事件,任何對這個事件感興趣的“觀察者”就可以捕捉並執行相應的邏輯。

我們希望我們的事件驅動模型能夠滿足以下幾個要求:

1. 當觸發某個動作時,將動作包裝成事件並進行分發,所有與之相關的監聽器自動感應事件的發生;

2. 同一個事件可以被多個監聽器響應;

3. 一個監聽器可以同時監聽多個事件;

4. 事件可以選擇同步執行,也可以選擇異步執行。

事件驅動的代碼實現

下面開始我們的編碼邏輯

1. 首先,我們定義”事件“這個抽象概念(GameEvent)。注意,事件有一個基類方法標識是否同步執行。

/**
 * 監聽器監聽的事件抽象類
 */
public abstract class GameEvent {
	
	/** 創建時間 */
	private long createTime;
	/** 事件類型 */
	private final EventType eventType;
	
	public GameEvent(EventType evtType) {
		this.createTime = System.currentTimeMillis();
		this.eventType  = evtType;
	}
	
	public long getCreateTime() {
		return this.createTime;
	}
	
	public EventType getEventType() {
		return this.eventType;
	}
	
	/**
	 * 是否在消息主線程同步執行
	 * @return
	 */
	public boolean isSynchronized() {
		return true;
	}

}
2. 爲了區分各種事件,我們定義一個表示事件類型的枚舉器(EventType.java)

public enum EventType {
	
	/** 升級事件 */
	LEVEL_UP;

}
3. 在遊戲業務裏,很多事件都是綁定角色的,所以定義一個跟玩家關係密切的玩家事件(PlayerEvent.java)
/**
 * 玩家事件抽象類
 */
public abstract class PlayerEvent extends GameEvent {

	/** 玩家id */
	private final long playerId;
	
	public PlayerEvent(EventType evtType, long playerId) {
		super(evtType);
		this.playerId = playerId;
	}
	
	public long getPlayerId() {
		return this.playerId;
	}
}
4.定義一個註解(Listener.java),標識”監聽器“

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Listener {

}
5. 定義事件分發器(EventDispatcher),該分發器擁有以下作用:

綁定事件與事件監聽者;

分發事件,若爲同步事件,則在當前的業務主線程執行,若爲異步事件,則放到獨立線池異步執行。

public class EventDispatcher {

	private static EventDispatcher instance = new EventDispatcher();

	private EventDispatcher() {
		new NameableThreadFactory("event-dispatch").newThread(new EventWorker()).start();
	};  

	public static EventDispatcher getInstance() {
		return instance;
	}

	/** 事件類型與事件監聽器列表的映射關係 */
	private final Map<EventType, Set<Object>> observers = new HashMap<>(); 
	/** 異步執行的事件隊列 */
	private LinkedBlockingQueue<GameEvent> eventQueue = new LinkedBlockingQueue<>();

	/**
	 * 註冊事件監聽器
	 * @param evtType
	 * @param listener
	 */
	public void registerEvent(EventType evtType, Object listener) {  
		Set<Object> listeners = observers.get(evtType);  
		if(listeners == null){  
			listeners = new CopyOnWriteArraySet<>();  
			observers.put(evtType, listeners);  
		}  
		listeners.add(listener);  
	}  

	/**
	 * 分發事件
	 * @param event
	 */
	public void fireEvent(GameEvent event) {  
		if(event == null){  
			throw new NullPointerException("event cannot be null");  
		}  
		//如果事件是同步的,那麼就在消息主線程執行邏輯
		if (event.isSynchronized()) {
			triggerEvent(event);
		} else {
		//否則,就丟到事件線程異步執行
			eventQueue.add(event);
		}
		
	}  
	
	private void triggerEvent(GameEvent event) {
		EventType evtType = event.getEventType();  
		Set<Object> listeners = observers.get(evtType);  
		if(listeners != null){  
			listeners.forEach(listener->{
				try{  
					ListenerManager.INSTANCE.fireEvent(listener, event);
				}catch(Exception e){  
					LoggerUtils.error("triggerEvent failed", e);;  //防止其中一個listener報異常而中斷其他邏輯  
				}  
			});
		}  
	}
	
	private class EventWorker implements Runnable {
		@Override
		public void run() {
			while(true) {
				try {
					GameEvent event = eventQueue.take();
					triggerEvent(event);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}

}
6.分發器只是綁定了事件與其監聽器,並沒有說明,這個事件由其監聽器的哪個方法監聽。爲了達到方法級別的綁定,我們引入另一個註解(EventHandler.java)。
/**
 * 事件處理者
 * @author kingston
 */
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EventHandler {
	
	/** 綁定的事件類型列表 */
	public EventType[] value();
	
}

也就是說,監聽器由Listener註解標識,該監聽器要監聽多個事件,那麼就在每一個事件的執行者方法加一個EventHandler註解。如果看過之前的文章,那篇關於使用Controller, RequestMapper來自動映射玩家消息與業務執行者。就會發現,Listerner與ListenerHandler所發揮的作用,跟之前是完全一樣的。

7. 由於一個監聽器可以同時監聽多個事件,爲了精確找到具體的業務執行者,我們必須將監聽器與事件類型作爲聯合主鍵,緩存這樣一個映射關係。key=listener_eventType, value=listenerMethod。所以,我們又加入一個工具類
(ListenerManager.java)

public enum ListenerManager {

	INSTANCE;

	private Map<String, Method> map = new HashMap<>();

	private final String SCAN_PATH = "com.kingston.game";

	public void initalize() {
        Set<Class<?>> listeners = ClassScanner.getClasses(SCAN_PATH, new ClassFilter() {
            @Override
            public boolean accept(Class<?> clazz) {
                return clazz.getAnnotation(Listener.class) != null;
            }
        });

        for (Class<?> listener: listeners) {
            try {
                Object handler = listener.newInstance();
                Method[] methods = listener.getDeclaredMethods();
                for (Method method:methods) {
                	EventHandler mapperAnnotation = method.getAnnotation(EventHandler.class);
                    if (mapperAnnotation != null) {
                    	EventType[] eventTypes = mapperAnnotation.value();
                    	 for(EventType eventType: eventTypes) {
                    		 EventDispatcher.getInstance().registerEvent(eventType, handler);
                    		 map.put(getKey(handler, eventType), method);
                         }
                    }
                }
            }catch(Exception e) {
                LoggerUtils.error("", e);
            }
        }
    }

	/**
	 * 分發給具體監聽器執行
	 * @param handler
	 * @param event
	 */
	public void fireEvent(Object handler,GameEvent event) {
		try {
			Method method = map.get(getKey(handler, event.getEventType()));
			method.invoke(handler, event);
		} catch (Exception e) {
			LoggerUtils.error("", e);
		}

	}

	private String getKey(Object handler, EventType eventType) {
		return handler.getClass().getName() + "-" + eventType.toString();
	}
}
至此,事件驅動器的全部代碼就完成了。

事件驅動模擬的測試案例

我們直接將引子的場景作爲測試吧,當玩家升級時,就會觸發學習一個新技能。
1.申明事件類型,EventType.LEVEL_UP
2.申明事件監聽器(Listener)與執行事件的方法(ListenerHandler)
@Listener
public class SkillListener {
	
	@EventHandler(value=EventType.LEVEL_UP)
	public void onPlayerLevelup(EventPlayerLevelUp levelUpEvent) {
		System.err.println(getClass().getSimpleName()+"捕捉到事件"+levelUpEvent);
	}

}
3. 測試代碼,服務啓動時,直接拋出一個升級事件
		//啓動socket服務
		try{
			new SocketServer().start();
		}catch(Exception e) {
			LoggerUtils.error("ServerStarter failed ", e);
		}

		Player player = PlayerManager.getInstance().get(10000L);
		EventDispatcher.getInstance().fireEvent(new EventPlayerLevelUp(EventType.LEVEL_UP,
				player.getId(), 2));
4. SkillListener直接捕捉到升級事件,輸出如下
SkillListener捕捉到事件EventPlayerLevelUp [upLevel=2, playerId=2815129724291645440, EventType=LEVEL_UP]

到這裏,關於事件驅動模型的實現就介紹完畢了。



文章預告:下一篇主要介紹如何使用http服務構建後臺管理系統
手遊服務端開源框架系列完整的代碼請移步github ->>game_server





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