本文介紹ESFramework 開發手冊(00) -- 概述一文中提到的四大武器的第二個:基礎功能與狀態改變通知。
在解決了發送信息和處理信息之後,還有一些基礎功能是很多分佈式通信系統都需要用到的,比如,查詢某個用戶是否在線、獲取在線的好友列表、當好友上下線時得到通知,等等。ESPlus.Application.Basic命名空間下的組件,爲我們解決了這些基礎問題。
1.客戶端
客戶端通過調用ESPlus.Application.Basic.Passive.IBasicOutter接口對應的方法以及預定其相關的事件,就可以完成基礎功能或得到相關狀態改變通知。
我們可以從ESPlus.Rapid.IRapidPassiveEngine暴露的BasicOutter屬性來獲取IBasicOutter引用。
public interface IBasicOutter :IOutter
{
/// <summary>
/// 當好友上線時,觸發此事件。參數爲好友的UserID
/// </summary>
event CbGeneric<string> FriendConnected;
/// <summary>
/// 當好友下線時,觸發此事件。參數爲好友的UserID
/// </summary>
event CbGeneric<string> FriendOffline;
/// <summary>
/// 當組友上線時,觸發此事件。參數爲組友的UserID
/// </summary>
event CbGeneric<string> GroupmateConnected;
/// <summary>
/// 當組友上線時,觸發此事件。參數爲組友的UserID
/// </summary>
event CbGeneric<string> GroupmateOffline;
/// <summary>
/// 當自己被同名用戶擠掉線時,觸發此事件。此時,客戶端引擎已被Dispose。
/// </summary>
event CbGeneric BeingPushedOut;
/// <summary>
/// 當自己心跳超時掉線時,觸發此事件。此時,客戶端引擎已被Dispose。
/// </summary>
event CbGeneric TimeoutOffline;
/// <summary>
/// 當自己被服務端踢出掉線時,觸發此事件。此時,客戶端引擎已被Dispose。
/// </summary>
event CbGeneric BeingKickedOut;
/// <summary>
/// 當RelogonMode爲IgnoreNew,再次使用同名用戶登錄時,觸發此事件。此時,客戶端引擎已被Dispose。
/// </summary>
event CbGeneric HadLogon;
/// <summary>
/// 客戶端登陸驗證。IRapidPassiveEngine會在初始化時,自動調用該方法來驗證用戶賬號密碼。
/// </summary>
/// <param name="systemToken">系統標誌。用於驗證客戶端是否與服務端屬於同一系統。</param>
/// <param name="password">登陸密碼</param>
LogonResponse Logon(string systemToken, string password);
/// <summary>
/// 獲取自己的IPE。
/// </summary>
/// <returns>通常是經過NAT之後的IPE</returns>
IPEndPoint GetMyIPE();
/// <summary>
/// 獲取當前AS上的所有在線的用戶列表。
/// </summary>
List<string> GetAllOnlineUsers();
/// <summary>
/// 獲取所有在線的好友列表。
/// </summary>
List<string> GetAllOnlineFriends();
/// <summary>
/// 獲取好友列表。
/// </summary>
List<string> GetFriends();
/// <summary>
/// 獲取在線的組友列表。
/// </summary>
/// <returns>在線組好友的UserID列表</returns>
List<string> GetAllOnlineGroupmates();
/// <summary>
/// 查詢用戶是否在線。
/// </summary>
bool IsUserOnline(string userID);
/// <summary>
/// ping服務器。在應用層模擬ping,比普通的ICMP的ping大一些(如8-10ms)。
/// </summary>
/// <returns>ping耗時,單位毫秒</returns>
int Ping();
/// <summary>
/// ping其他在線用戶。在應用層模擬ping,比普通的ICMP的ping大一些(如8-10ms)。
/// 如果目標用戶不在線,將拋出Timeout異常。
/// </summary>
/// <param name="targetUserID">要Ping的目標用戶ID</param>
/// <returns>ping耗時,單位毫秒</returns>
int Ping(string targetUserID);
/// <summary>
/// 命令服務端將目標用戶踢出。如果目標用戶不在當前AS上,則直接返回。
/// </summary>
/// <param name="targetUserID">要踢出的用戶ID</param>
void KickOut(string targetUserID);
/// <summary>
/// 向服務器發送心跳消息。被框架ESPlus.Application.Basic.Passive.HeartBeater使用。
/// </summary>
void SendHeartBeatMessage() ;
}
狀態改變事件通知
首先,我們看看IBasicOutter暴露的幾個事件,這幾個事件都是當自己或好友的狀態發生改變時觸發的。
- FriendConnected、FriendOffline 、GroupmateConnected、GroupmateOffline 四個事件用於在好友與組友上下線時,通知當前的客戶端用戶的。所謂組友Groupmate,就是同屬一個組(比如QQ羣)的成員。至於服務端是如何知道一個用戶有哪些好友和有哪些組友了,請參見好友與組。
- BeingPushedOut、TimeoutOffline、BeingKickedOut、HadLogon 四個事件用於在自己的狀態變化時得到通知的。BeingPushedOut和HadLogon事件是否能觸發、以及在什麼條件下觸發,取決於服務端設置的重登陸模式的策略。關於重登陸模式的更多內容可以參見重登陸模式。
基礎API
接下來,我們看看IBasicOutter的幾個方法。
- Logon方法用於在登錄時驗證用戶密碼。該方法會在客戶端Rapid引擎初始化時被引擎自動調用,所以,在使用Rapid引擎時,我們通常不需要手動調用它。如果有的系統需要驗證除了密碼之外更多的信息,那麼可以通過systemToken參數進行傳遞這些額外信息。Logon方法返回類型爲LogonResponse,其屬性LogonResult是一個枚舉,表示了登錄結果。如果LogonResult爲Succeed,表示登錄成功;如果LogonResult爲HadLoggedOn,表示該賬號已經在其它地方登錄;如果LogonResult爲Failed,則表示驗證賬號密碼沒有通過,沒有通過的原因由LogonResponse的FailureCause屬性指明。
- GetAllOnlineFriends、GetAllOnlineGroupmates用於獲取所有在線的好友和組友,如果我們的系統需要支持好友或組友,那麼,通常我們會在客戶端Rapid引擎初始化成功後,調用這兩個方法以初始化在線好友列表和在線組友列表,並且結合預定前面的FriendConnected、FriendOffline 、GroupmateConnected、GroupmateOffline 這四個事件,那麼,在客戶端實例運行的整個生命期內,就可以實時地知道每個好友和組友的在線狀態了。
- Ping方法,用於獲取當前客戶端到服務端或到另一個在線客戶端的消息來回的耗時,由於其是在應用層來模擬類似ICMP的ping,所以這個方法返回的值通常比ICMP的ping大一些。儘管如此,在一些應用中,該Ping的結果還是有一些參考價值的。
- 有時,我們需要命令服務器將一些惡意的用戶從服務端踢出(斷開其連接),那麼就可以調用KickOut方法,被踢出的客戶端將會觸發上述的BeingKickedOut事件。
- SendHeartBeatMessage方法用於向服務器發送心跳消息。如果我們使用的是Rapid引擎,那麼框架會自動發送心跳消息,所以,我們通常不需要手動調用該方法。關於心跳消息的更多內容可以參見心跳機制。
TCP連接狀態
Basic空間提供了一部分基礎功能,還有另一部分很重要的基礎功能需要涉及到客戶端的TCP引擎(因爲客戶端Rapid引擎內部使用的正是基於二進制的TCP引擎),我們在這裏也一併介紹一下。客戶端如何知道自己與服務器的TCP連接的狀態變化了?ESFramework.Engine.Tcp.Passive.ITcpPassiveEngine 的幾個事件和屬性來獲取這些信息。
我們可以從ESPlus.Rapid.IRapidPassiveEngine暴露的TcpPassiveEngine屬性來獲取ITcpPassiveEngine引用。
public interface ITcpPassiveEngine:IPassiveEngine
{
/// <summary>
/// 當客戶端與服務器的TCP連接斷開時,將觸發此事件。
/// </summary>
event CbGeneric ConnectionInterrupted;
/// <summary>
/// 自動重連開始時,觸發此事件。
/// </summary>
event CbGeneric ConnectionRebuildStart;
/// <summary>
/// 自動重連成功後,觸發此事件。
/// </summary>
event CbGeneric ConnectionRebuildSucceed;
/// <summary>
/// 自動重連超過最大重試次數時,表明重連失敗,將觸發此事件。
/// </summary>
event CbGeneric ConnectionRebuildFailure;
/// <summary>
/// Sock5代理服務器信息。如果不需要代理,則設置爲null。
/// </summary>
Sock5ProxyInfo Sock5ProxyInfo { get; set; }
/// <summary>
/// 當前是否處於連接狀態。
/// </summary>
bool Connected { get; }
/// <summary>
/// 當與服務器斷開連接時,是否自動重連。
/// </summary>
bool AutoReconnect { get; set; }
}
註釋已經很好的說明了每個事件和屬性的用途,這裏就不贅述了。
值得一提的是,ConnectionRebuildSucceed事件,當網絡恢復,TCP重連成功時,將會觸發此事件。但對使用Rapid引擎的開發人員來說,這並不是全部。Rapid客戶端引擎(ESPlus.Rapid.IRapidPassiveEngine),會在ConnectionRebuildSucceed事件觸發時,自動登錄服務器重新驗證用戶賬號和密碼,並觸發RelogonCompleted事件。RelogonCompleted事件的參數爲LogonResult,表明了重新登錄驗證的結果。而且,如果驗證失敗,與服務器的連接將會再次斷開,且後續不會再自動重連。所以,當開發人員在進行二次開發時,要依據IRapidPassiveEngine的RelogonCompleted事件來作爲重連成功/失敗的依據。
2.服務端
基礎控制
Basic的服務端就相當簡單了。首先,我們可以通過ESPlus.Application.Basic.Server.IBasicController的KickOut方法來在服務端進行踢人操作。
我們可以從ESPlus.Rapid.IRapidServerEngine暴露的BasicController屬性來獲取IBasicController引用。
登錄驗證
剛剛我們提到客戶端可以調用IBasicOutter的Logon方法進行登陸驗證,那麼這個驗證服務端是在哪裏做的了?服務端正是通過ESPlus.Application.Basic.Server.IBasicHandler的VerifyUser方法來驗證用戶賬號密碼的。
public interface IBasicHandler :IBusinessHandler
{
/// <summary>
/// 客戶端登陸驗證。
/// </summary>
/// <param name="userID">登陸用戶賬號</param>
/// <param name="systemToken">系統標誌。用於驗證客戶端是否與服務端屬於同一系統。</param>
/// <param name="password">登陸密碼</param>
///<param name="failureCause">如果登錄失敗,該out參數指明失敗的原因</param>
/// <returns>如果密碼和系統標誌都正確則返回true;否則返回false。</returns>
bool VerifyUser(string systemToken, string userID, string password ,out string failureCause);
}
請注意,如果賬號密碼驗證不通過,可以通過failureCause參數返回不通過的原因。failureCause的值將被傳遞並賦值給Logon方法返回的LogonResponse的FailureCause屬性。
同上一章講到的ICustomizeHandler一樣,我們要在系統中根據項目的具體需求來實現IBasicHandler接口並將其注入到框架中。
用戶管理器
服務端如何知道用戶上下線、以及每個在線用戶的狀態了?
只要通過ESFramework.Server.UserManagement.IUserManager的相關事件和方法就能得到這些信息。我們可以從ESPlus.Rapid.IRapidServerEngine暴露的UserManager屬性來獲取IUserManager引用。IUserManager接口定義如下:
public interface IUserManager
{
/// <summary>
/// 當前在線用戶的數量。
/// </summary>
int UserCount { get; }
/// <summary>
/// 目標用戶是否在線。
/// </summary>
bool IsUserOnLine(string userID);
/// <summary>
/// 獲取目標在線用戶的基礎信息。
/// </summary>
/// <param name="userID">目標用戶的ID</param>
/// <returns>如果目標用戶不在線,則返回null</returns>
UserData GetUserData(string userID);
/// <summary>
/// 獲取在線用戶的ID列表。
/// </summary>
List<string> GetOnlineUserList();
/// <summary>
/// 從目標用戶集合中挑出在線用戶的ID列表。
/// </summary>
List<string> SelectOnlineUserFrom(IEnumerable<string> users);
/// <summary>
/// 重登陸模式。
/// </summary>
RelogonMode RelogonMode { get; set; }
/// <summary>
/// 獲取當目標在線用戶的信息。
/// </summary>
/// <param name="userID">目標用戶的ID</param>
/// <returns>如果目標用戶不在線,則返回null</returns>
RichUserData GetRichUserData(string userID);
/// <summary>
/// 如果用戶不在線,返回null
/// </summary>
UserAddress GetUserAddress(string userID);
/// <summary>
/// 客戶端連接被關閉時,將觸發此事件。不要遠程預定該事件。
/// </summary>
event CbGeneric<UserData ,DisconnectedType> SomeOneDisconnected;
/// <summary>
/// 當接收到新連接上的第一個消息時,將觸發此事件。不要遠程預定該事件。
/// </summary>
event CbGeneric<UserData> SomeOneConnected;
/// <summary>
/// 如果RelogonMode爲ReplaceOld,並且當從另外一個新連接上收到一個同名ID用戶的消息時將觸發此事件。
/// 注意,只有在該事件處理完畢後,纔會真正關閉舊的連接並使用新的地址取代舊的地址。可以在該事件的處理函數中,將相關情況通知給舊連接的客戶端。
/// </summary>
event CbGeneric<UserData> SomeOneBeingPushedOut;
/// <summary>
/// 如果RelogonMode爲IgnoreNew,並且當從一個新連接上收到一個同名ID用戶的消息時將觸發此事件。
/// 注意,只有在該事件處理完畢後,纔會關閉新連接。可以在該事件的處理函數中,將相關情況通知給客戶端。
/// </summary>
event CbGeneric<string, UserAddress> NewConnectionIgnored;
/// <summary>
/// 用戶心跳超時。
/// 只有在該事件處理完畢後,才關閉對應的連接,並將其從用戶列表中刪除。可以在該事件的處理函數中,將相關情況通知給客戶端。
/// </summary>
event CbGeneric<UserData> SomeOneTimeOuted;
/// <summary>
/// 當在線用戶數發生變化時,觸發此事件。
/// </summary>
event CbGeneric<int> UserCountChanged;
}
RelogonMode屬性用於設置重登錄模式,關於重登陸模式的更多內容可以參見重登陸模式。
3.UserID的長度
在ESFramework 4.0 進階(01)-- 消息一文中我們介紹了ESPlus提供了默認的消息頭實現,而Rapid引擎使用的就是ESPlus提供的基於二進制的消息頭StreamMessageHeader,這個消息頭的默認長度是36字節,允許的UserID最大長度爲11字節。但是,如果你的系統中需要用到的UserID長度超過了11字節,該怎麼辦了?我們可以通過調用StreamMessageHeader的SetMaxLengthOfUserID靜態方法來設定ESFramework允許的UserID的最大長度:
/// <summary>
/// 設置UserID(包括GroupID)的最大長度(不能超過255)。注意,客戶端與服務端要統一設置。
/// </summary>
public static void SetMaxLengthOfUserID(byte maxLen);
注意,我們必須在Rapid引擎的Initialize方法執行之前調用SetMaxLengthOfUserID方法。而且,客戶端和服務端必須採用相同的設置,否則,就一定會導致服務端和客戶端通信出現異常。如果你的客戶端是使用的Silverlight,那麼使用ESFramework.SL時也是如此。- 服務端和桌面客戶端請調用ESPlus.Core.StreamMessageHeader的SetMaxLengthOfUserID方法進行設置。
- Silverlight的客戶端請調用ESFramework.SL.StreamMessageHeader的SetMaxLengthOfUserID方法進行設置。
在ESFramework內部,組ID(即上面提到的groupID)也採用與UserID相同的規則。
還要提醒的是,在能滿足項目需求的情況下,儘可能使UserID的最大長度短一點,這樣可以使得消息頭更加短小,從而避免浪費本不需要的帶寬。尤其在高性能、巨大併發的應用中,這點就更關鍵了。
4.消息的最大長度
Rapid引擎內部默認設置的消息的最大長度爲1M(1024*1024),並且這個長度還包含了上述消息頭的長度。如果您的應用需要發的單個信息的長度超過了1M,就會被ESFramework認爲是惡意的消息,ESFramework會丟棄該消息並關閉對應的連接。
我們建議:在能同樣滿足項目的需求下,應該儘可能地使傳送的消息小,這樣不僅可以節省帶寬,而且還有助於提升併發的性能。如果應用中確實需要信息的長度超過1M,那麼可以通過Rapid引擎暴露的內部核心引擎接口來設置所允許的最大消息長度。
- 服務端:RapidServerEngine暴露的TcpServerEngine內核引擎,可以設置其MaxMessageSize屬性的值。
- 客戶端:RapidPassiveEngine暴露的TcpPassiveEngine內核引擎,也可以設置其MaxMessageSize屬性的值。
最後,大家可以查看最簡單的那個demo的源碼,並運行demo,來了解上述的狀態改變通知及其它基礎功能。謝謝。
關於ESFramework的任何問題,歡迎聯繫我們: