在C/S架構中,通常是使用 UserID 作為唯一標志來標記每一個用戶的,也就是說,對於一個指定的UserID,只能有一個客戶端在線。
如果我們開發的系統要支持同帳號多設備同時登錄的場景,即需要像微信一樣,在PC端登錄的同時,也可以使用同一個帳號登錄移動端(iOS或Android),那么,如何才能做到了?
解決方案的原理是比較簡單的:既然C/S系統要求UserID作為用戶標記必須是唯一的,那么我們就引入一個稱為“LoginID”的概念,對於同一個用戶,在不同類型的設備上就使用不同的LoginID,但是這些LoginID都指向同一個真正的UserID。
一. LoginID 與 UserID
1. 不需要支持同帳號多設備同時登錄的簡單場景
在之前不支持同帳號多設備同時登錄的場景中(簡稱“單設備登錄”場景),登錄用的帳號就是真正的UserID,也就是說底層框架中各個API(各個方法以及事件)的參數涉及到的用戶帳號都是真正的UserID。比如,一個帳號abc001,該帳號是存在於數據庫的用戶表中的;使用abc001登錄到服務器,在整個的運作過程中,服務端正是使用abc001來標記對應的客戶端實例。在該場景中,不會存在多個運行的客戶端實例都對應帳號abc001的情況。如果有個客戶端已經使用abc001登錄,然后再用該帳號在其它地方登錄,默認的機制是會把之前登錄的那個客戶端擠掉線。
2. 需要支持同帳號多設備同時登錄的復雜場景
如果現在我們要支持同帳號多設備同時登錄的場景(簡稱“多端登錄”場景),那么,服務端在整個的運作過程中,就不能使用abc001來標記對應的客戶端實例了,因為存在多個客戶端實例都對應同一個abc001帳號的情況。於是,我們使用LoginID來區分這種情況下不同的客戶端實例。
常用的方法是,在真正的UserID前加上兩個字符的前綴以構成LoginID。 比如,對於abc001這個帳號,在使用iOS設備登錄時,我們選擇使用前綴“1#”,這樣iOS設備使用的LoginID就是1#abc001;同理,Android設備就使用2#abc001。
該兩個字符的前綴的含義是這樣的:
(1)第二個字符“#”,是一個標志(token),表示該ID是一個LoginID。
(2)第一個字符,表示設備的類型。比如“0”表示.NET設備(PC),“1”表示iOS設備,“2”表示Android設備,等等。
當使用LoginID后,服務端在整個的運作過程中就不再是使用真正的UserID來標記客戶端實例了,而是使用LoginID -- 也就是說,框架中各個API(各個方法以及事件)的參數涉及到的用戶帳號都是LoginID了。
二. MultiDeviceHelper 類
我寫了一個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 也增加了多端登錄的功能,可下載測試。)