在C/S架構中,一般是使用 UserID 做爲惟一標誌來標記每個用戶的,也就是說,對於一個指定的UserID,只能有一個客戶端在線。數據庫
若是咱們開發的系統要支持同賬號多設備同時登陸的場景,即須要像微信同樣,在PC端登陸的同時,也可使用同一個賬號登陸移動端(iOS或Android),那麼,如何才能作到了?api
解決方案的原理是比較簡單的:既然C/S系統要求UserID做爲用戶標記必須是惟一的,那麼咱們就引入一個稱爲「LoginID」的概念,對於同一個用戶,在不一樣類型的設備上就使用不一樣的LoginID,可是這些LoginID都指向同一個真正的UserID。服務器
在以前不支持同賬號多設備同時登陸的場景中(簡稱「單設備登陸」場景),登陸用的賬號就是真正的UserID,也就是說底層框架中各個API(各個方法以及事件)的參數涉及到的用戶賬號都是真正的UserID。好比,一個賬號abc001,該賬號是存在於數據庫的用戶表中的;使用abc001登陸到服務器,在整個的運做過程當中,服務端正是使用abc001來標記對應的客戶端實例。在該場景中,不會存在多個運行的客戶端實例都對應賬號abc001的狀況。若是有個客戶端已經使用abc001登陸,而後再用該賬號在其它地方登陸,默認的機制是會把以前登陸的那個客戶端擠掉線。微信
若是如今咱們要支持同賬號多設備同時登陸的場景(簡稱「多端登陸」場景),那麼,服務端在整個的運做過程當中,就不能使用abc001來標記對應的客戶端實例了,由於存在多個客戶端實例都對應同一個abc001賬號的狀況。因而,咱們使用LoginID來區分這種狀況下不一樣的客戶端實例。架構
經常使用的方法是,在真正的UserID前加上兩個字符的前綴以構成LoginID。 好比,對於abc001這個賬號,在使用iOS設備登陸時,咱們選擇使用前綴「1#」,這樣iOS設備使用的LoginID就是1#abc001;同理,Android設備就使用2#abc001。app
該兩個字符的前綴的含義是這樣的:框架
(1)第二個字符「#」,是一個標誌(token),表示該ID是一個LoginID。ide
(2)第一個字符,表示設備的類型。好比「0」表示.NET設備(PC),「1」表示iOS設備,「2」表示Android設備,等等。函數
當使用LoginID後,服務端在整個的運做過程當中就再也不是使用真正的UserID來標記客戶端實例了,而是使用LoginID -- 也就是說,框架中各個API(各個方法以及事件)的參數涉及到的用戶賬號都是LoginID了。 測試
我寫了一個MultiDeviceHelper類,用於爲多設備同時登陸提供支持。特別是,提供了與LoginID的構造和解析相關的API。
在MultiDeviceHelper的靜態構造函數中,規定了每種設備的前綴,以下所示:
static MultiDeviceHelper() { #region 若是在當前的應用中,不存在某種類型的設備,則註釋掉下面對應的語句便可。 MultiDeviceHelper.LoginIDPrefixMapping.Add(ClientType.IOS, "1#"); MultiDeviceHelper.LoginIDPrefixMapping.Add(ClientType.Android, "2#"); MultiDeviceHelper.LoginIDPrefixMapping.Add(ClientType.DotNet, "3#"); #endregion }
而後, MultiDeviceHelper提供了多個靜態方法以完成真正UserID、設備類型與LoginID之間的轉換:
客戶端在登陸時會調用IRapidPassiveEngine的Initialize方法:
LogonResponse Initialize(string userID, string logonPassword, string serverIP, int serverPort, ICustomizeHandler customizeHandler);
該方法的第一個參數就須要傳入LoginID,好比 1#abc001。
在服務端會回調IBasicHandler接口的VerifyUser方法來進行賬號密碼驗證:
bool VerifyUser(string systemToken, string userID, string password, out string failureCause);
此時要注意的是,VerifyUser方法傳入的userID參數其實是LoginID,即 1#abc001。咱們須要經過調用MultiDeviceHelper的ParseLoginID方法來獲取真正的UserID,該方法會返回 abc001,而且out參數指明設備類型爲iOS。
服務端是經過回調ICustomizeHandler接口的HandleInformation方法來處理接收到的消息的:
void HandleInformation(string sourceUserID, int informationType, byte[] info);
同上面同樣,此處的sourceUserID參數實際上也是LoginID,因此,也須要調用MultiDeviceHelper的ParseLoginID方法來將其轉換成真正的UserID。
同理,在多設備登陸場景中,框架中各個API(各個方法以及事件)的userID參數實際上都是LoginID,在處理時都須要作相似的處理,這裏就不一一列舉了。
在解決了多設備同時登陸的問題後,還有一個常見的需求:相似QQ的PC和手機端同時在線時,別人給我發一條消息,手機端和PC端都能接收到。這樣的功能是怎麼實現的了?
在單設備登陸場景中,咱們一般是在客戶端調用ICustomizeOutter接口的下列Send方法來發送聊天消息的:
void Send(string targetUserID, int informationType, byte[] info);
該方法的第一個參數是接收者的UserID,表示直接將聊天消息發送給對方(多是通過服務器中轉,或者是經P2P通道直接傳送)。
可是,在多設備登陸場景中,不能再直接發送了,而是必需要通過服務器中轉,經過調用下面的Send方法:
void Send(int informationType, byte[] info);
該Send方法將消息直接發送給服務端,在info參數中包含要消息接收者的UserID。服務端在處理該消息時,須要從info中將接收者UserID解析出來,而後,調用MultiDeviceHelper 的 GetLoginIDList 方法來獲取各個設備類型對應的LoginID,而後,服務端在把該消息發送給每個LoginID。如此,手機端和PC端就都能收到這條聊天消息了。
(注:最新版本的 ESFramework.MSide.dll 已經內置了對多端同時登陸的支持,也就是說,本文所闡述的原理已經在ESFramework框架中進行了實現。另外,OrayTalk 也增長了多端登陸的功能,可下載測試。)