ESFramework 开发手册(02) -- 基础功能与状态通知

      本文介绍ESFramework 开发手册(00) -- 概述一文中提到的四大武器的第二个:基础功能与状态改变通知。

      在解决了发送信息和处理信息之后,还有一些基础功能是很多分布式通信系统都需要用到的,比如,查询某个用户是否在线、获取在线用户列表、自己掉线时得到通知,等等。ESPlus.Application.Basic 命名空间下的组件,为我们解决了这些基础问题。

 

 1.客户端

      客户端通过调用ESPlus.Application.Basic.Passive.IBasicOutter接口对应的方法以及预定其相关的事件,就可以完成基础功能或得到相关状态改变通知。

      我们可以从ESPlus.Rapid.IRapidPassiveEngine暴露的BasicOutter属性来获取IBasicOutter引用。 

    public interface IBasicOutter
    {       
        /// <summary>
        ///  当自己被同名用户挤掉线时,触发此事件。此时,客户端引擎已被Dispose。
         /// </summary>
        event CbGeneric BeingPushedOut;
       
        /// <summary>
        ///  当自己被服务端踢出掉线时,触发此事件。此时,客户端引擎已被Dispose。
         /// </summary>
        event CbGeneric BeingKickedOut;   
       
        /// <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上的所有在线的用户列表。【该方法仅仅用于demo和测试】
         /// </summary>      
        List<string> GetAllOnlineUsers();       
                
        /// <summary>
        /// 查询用户是否在线。
         /// </summary>       
        bool IsUserOnline(string userID);

        /// <summary>
        /// ping服务器。在应用层模拟ping,比普通的ICMP的ping大一些(如8-10ms)。
         /// </summary>
        /// <returns>ping耗时,单位毫秒</returns>
        int Ping();

        /// <summary>
        /// ping其他在线用户(通过服务器中转)。
         /// 如果目标用户不在线,将抛出Timeout异常。
         /// </summary>
        /// <param name="targetUserID">要Ping的目标用户ID</param>
        /// <returns>ping耗时,单位毫秒</returns>
        int PingByServer(string targetUserID);

        /// <summary>
        /// ping其他在线用户(通过P2P通道)。
         /// 如果P2P通道不存在,则。如果目标用户不在线,将抛出Timeout异常。
         /// </summary>
        /// <param name="targetUserID">要Ping的目标用户ID</param>
        /// <returns>ping耗时,单位毫秒</returns>
        int PingByP2PChannel(string targetUserID);

        /// <summary>
        /// 命令服务端将目标用户踢出。如果目标用户不在当前AS上,则直接返回。
         /// </summary>
        /// <param name="targetUserID">要踢出的用户ID</param>
        void KickOut(string targetUserID);

        /// <summary>
        /// 向服务器发送心跳消息。被框架ESPlus.Application.Basic.Passive.HeartBeater使用。
         /// </summary>
        void SendHeartBeatMessage() ;             
    }

状态改变事件通知

      首先,我们看看IBasicOutter暴露的两个事件: 

  • BeingKickedOut 当自己被踢出时将触发该事件。
  • BeingPushedOut 发生于当服务端将重登陆模式设置为ReplaceOld时,并且同名用户的成功登录,将会把老的在线用户挤掉而导致其下线。关于重登陆模式的更多内容可以参见重登陆模式

基础API

      接下来,我们简单看看IBasicOutter的几个方法。 

  • Logon方法用于在登录时验证用户密码。该方法会在客户端Rapid引擎初始化时被引擎自动调用,所以,在使用Rapid引擎时,我们通常不需要手动调用它。如果有的系统需要验证除了密码之外更多的信息,那么可以通过systemToken参数进行传递这些额外信息。Logon方法返回类型为LogonResponse,其属性LogonResult是一个枚举,表示了登录结果。如果LogonResult为Succeed,表示登录成功;如果LogonResult为HadLoggedOn,表示该账号已经在其它地方登录;如果LogonResult为Failed,则表示验证账号密码没有通过,没有通过的原因由LogonResponse的FailureCause属性指明。
  • GetAllOnlineUsers用于获取所有在线用户,通常该方法仅仅用于demo,因为在正式的系统中,在线用户数可能是非常巨大的,这将导致GetAllOnlineUsers的返回消息非常大,甚至可能超过框架的最大消息尺寸的限制。
  • Ping系列方法,用于获取当前客户端到服务端或到另一个在线客户端的消息来回的耗时,由于其是在应用层来模拟类似ICMP的ping,所以这个方法返回的值通常比ICMP的ping大一些。尽管如此,在一些应用中,该Ping的结果还是有一些参考价值的。 
  • 有时,我们需要命令服务器将一些恶意的用户从服务端踢出(断开其连接),那么就可以调用KickOut方法,被踢出的客户端将会触发上述的BeingKickedOut事件。        
  • SendHeartBeatMessage方法用于向服务器发送心跳消息。如果我们使用的是Rapid引擎,那么框架会自动发送心跳消息,所以,我们通常不需要手动调用该方法。关于心跳消息的更多内容可以参见心跳机制 

TCP连接状态

      Basic空间提供了一部分基础功能,还有另一部分很重要的基础功能需要涉及到客户端的Rapid引擎,我们在这里也一并介绍一下。客户端如何知道自己与服务器的TCP连接的状态及其变化了?ESPlus.Rapid.IRapidPassiveEngine 的几个事件和属性来获取这些信息。 

        /// <summary>
        /// 当客户端与服务器的TCP连接断开时,将触发此事件。
         /// </summary>
        event CbGeneric ConnectionInterrupted;

        /// <summary>
        /// 自动重连开始时,触发此事件。如果重连成功则将重新登录,并触发RelogonCompleted事件。
         /// </summary>
        event CbGeneric ConnectionRebuildStart;         

        /// <summary>
        /// 当断线重连成功时,会自动登录服务器验证用户账号密码,并触发此事件。如果验证失败,则与服务器的连接将会断开,且后续不会再自动重连。事件参数表明了登录验证的结果。
         /// </summary>
        event CbGeneric<LogonResponse> RelogonCompleted;

        /// <summary>
        /// 当前引擎所连接的服务器的地址。
         /// </summary>
        AgileIPE ServerAddress { get;  }

        /// <summary>
        /// 当前是否处于连接状态。
         /// </summary>
        bool Connected { get; }

        /// <summary>
        /// 与服务器之间的通道是否处于繁忙状态?
         /// </summary>
        bool ChannelIsBusy { get; }

      注释已经很好的说明了每个事件和属性的用途,这里就不赘述了。

      值得一提的是,RelogonCompleted事件。当网络恢复TCP重连成功时,将自动登录服务器并重新验证用户账号和密码,然后触发RelogonCompleted事件。RelogonCompleted事件的参数为LogonResult,表明了重新登录验证的结果。而且,如果验证失败,与服务器的连接将会再次断开,且后续不会再自动重连。所以,当开发人员在进行二次开发时,一定要注意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        
    {
     /// <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字节,该怎么办了?我们可以通过调用GlobalUtil静态类的SetMaxLengthOfUserID静态方法来设定ESFramework允许的UserID的最大长度:  

        /// <summary>
        /// 设置UserID(包括GroupID)的最大长度(不能超过255)。必须在Rapid引擎初始化之前设置才有效。注意,客户端与服务端要统一设置。
         /// </summary>       
        public static void SetMaxLengthOfUserID(byte maxLen);

   注意,我们必须在Rapid引擎的Initialize方法执行之前调用SetMaxLengthOfUserID方法。而且,客户端和服务端必须采用相同的设置,否则,就一定会导致服务端和客户端通信出现异常。如果你的客户端是使用的Silverlight,那么使用ESFramework.SL时也是如此。

  • 服务端和桌面客户端请调用ESPlus.GlobalUtil 的 SetMaxLengthOfUserID方法进行设置。
  • Silverlight的客户端请调用ESFramework.SL.GlobalUtil 的 SetMaxLengthOfUserID方法进行设置。

      在ESFramework内部,组(Group)ID也采用与UserID相同的规则。
  还要提醒的是,在能满足项目需求的情况下,尽可能使UserID的最大长度短一点,这样可以使得消息头更加短小,从而避免浪费本不需要的带宽。尤其在高性能、巨大并发的应用中,这点就更关键了。

 4.消息的最大长度

    Rapid引擎内部默认设置的消息的最大长度为100K(100*1024),并且这个长度还包含了上述消息头的长度。如果您的应用需要发送的单个信息的长度超过了100k,就会被ESFramework认为是恶意的消息,ESFramework会丢弃该消息并关闭对应的连接。

      我们建议:在能同样满足项目的需求下,应该尽可能地使传送的消息小,这样不仅可以节省带宽,而且还有助于提升并发的性能。如果应用中确实有信息的长度超过最大限制,那么还可以通过ICustomizeOutter的SendBlob方法来将其作为大数据块进行发送。我们可以通过调用GlobalUtil静态类的SetMaxLengthOfMessage静态方法来设定ESFramework允许的最大消息长度。 

        /// <summary>
        /// 设置消息的最大长度,初始值为100k。必须在Rapid引擎初始化之前设置才有效。注意,客户端与服务端要统一设置。
         /// </summary>  
        public static void SetMaxLengthOfMessage(int maxLen);

   

      最后,大家可以查看最简单的那个demo的源码,并运行demo,来了解上述的状态改变通知及其它基础功能。谢谢。

 

阅读 更多ESFramework开发手册系列文章

----------------------------------------------------------------------------------------------------------------------------------------------- 

 下载免费版本的ESFramework 以及 demo源码 

 

关于ESFramework的任何问题,欢迎联系我们:

电话:027-87638960

Q Q:372841921

 

posted on 2011-10-09 14:28  傲瑞中国  阅读(1745)  评论(0编辑  收藏  举报

导航