ESFramework 开发手册(02) -- 登录验证、在线用户管理、基础功能及状态通知
本文介绍ESFramework 开发手册(00) -- 概述一文中提到的四大武器的第二个:在线用户管理、基础功能及状态通知。
在解决了发送信息和处理信息之后,还有一些基础功能是很多分布式通信系统都需要用到的,比如,查询某个用户是否在线、获取在线用户列表、自己掉线时得到通知,等等。ESPlus.Application.Basic 命名空间下的组件,为我们解决了这些基础问题。
一. 客户端
客户端通过调用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> /// 获取自己的IPE。 /// </summary> /// <returns>通常是经过NAT之后的IPE</returns> IPEndPoint GetMyIPE(); /// <summary> /// 获取当前AS上的所有在线的用户列表。【该方法仅仅用于demo和测试】 /// </summary> List<string> GetAllOnlineUsers(); /// <summary> /// 查询用户是否在线。 /// </summary> bool IsUserOnline(string userID); /// <summary> /// 当我的其它设备上线时,触发此事件。 /// </summary> event CbGeneric<ClientType> MyDeviceOnline; /// <summary> /// 当我的其它设备下线时,触发此事件。 /// </summary> event CbGeneric<ClientType> MyDeviceOffline; /// <summary> /// 获取自己的在线设备。 /// </summary> List<ClientType> GetMyOnlineDevice();
/// <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() ; }
1. 状态改变事件通知
首先,我们看看IBasicOutter暴露的两个事件:
(1)BeingKickedOut 当自己被踢出时将触发该事件。
(2)BeingPushedOut 发生于当服务端将重登陆模式设置为ReplaceOld时,并且同名用户的成功登录,将会把老的在线用户挤掉而导致其下线。
关于重登陆模式的更多内容可以参见重登陆模式。
2. 基础API
接下来,我们简单看看IBasicOutter的几个方法。
(1)GetAllOnlineUsers用于获取所有在线用户,通常该方法仅仅用于demo,因为在正式的系统中,在线用户数可能是非常巨大的,这将导致GetAllOnlineUsers的返回消息非常大,甚至可能超过框架的最大消息尺寸的限制。
(2)Ping系列方法,用于获取当前客户端到服务端或到另一个在线客户端的消息来回的耗时,由于其是在应用层来模拟类似ICMP的ping,所以这个方法返回的值通常比ICMP的ping大一些。尽管如此,在一些应用中,该Ping的结果还是有一些参考价值的。
(3)有时,我们需要命令服务器将一些恶意的用户从服务端踢出(断开其连接),那么可以调用KickOut方法,被踢出的客户端将会触发上述的BeingKickedOut事件。
(4)SendHeartBeatMessage方法用于向服务器发送心跳消息。如果我们使用的是Rapid引擎,那么框架会自动发送心跳消息,所以,我们通常不需要手动调用该方法。关于心跳消息的更多内容可以参见心跳机制。
3. 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; }
注释已经很好的说明了每个事件和属性的用途,这里就不赘述了。
4. 建立TCP连接与登录
在ESFramework体系中,与服务端建立TCP连接,并进行帐号登录是在IRapidPassiveEngine的Initialize方法中完成的:
/// <summary> /// 完成客户端引擎的初始化,与服务器建立TCP连接,连接成功后立即验证用户密码。如果连接失败,则抛出异常。 /// </summary> /// <param name="userID">当前登录的用户ID,由数字和字母组成,最大长度为11</param> /// <param name="logonPassword">用户登陆密码。</param> /// <param name="serverIP">服务器的IP地址。</param> /// <param name="serverPort">服务器的端口。</param> /// <param name="customizeHandler">自定义处理器,用于处理服务器或其它用户发送过来的消息</param> LogonResponse Initialize(string userID, string logonPassword, string serverIP, int serverPort, ICustomizeHandler customizeHandler);
值得一提的是,RelogonCompleted事件。当网络恢复TCP重连成功时,将自动登录服务器并重新验证用户账号和密码,然后触发RelogonCompleted事件。RelogonCompleted事件的参数为LogonResult,表明了重新登录验证的结果。而且,如果验证失败,与服务器的连接将会再次断开,且后续不会再自动重连。所以,当开发人员在进行二次开发时,一定要注意RelogonCompleted事件的参数的值来作为重连成功/失败的依据。
二.服务端
1. 基础控制
Basic的服务端就相当简单了。首先,我们可以通过ESPlus.Application.Basic.Server.IBasicController的KickOut方法来在服务端进行踢人操作。
我们可以从ESPlus.Rapid.IRapidServerEngine暴露的BasicController属性来获取IBasicController引用。
2. 登录验证
客户端通过调用 IRapidPassiveEngine 的 Initialize 方法进行登录,那么服务端是在哪里验证登录的账号密码的了?服务端是通过ESPlus.Application.Basic.Server.IBasicHandler的VerifyUser方法来验证用户账号密码的。
{
/// <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接口并将其注入到框架中。
3. 在线用户管理器
服务端如何知道用户上下线、以及每个在线用户的状态了?
只要通过ESFramework.Server.UserManagement.IUserManager的相关事件和方法就能得到这些信息。我们可以从ESPlus.Rapid.IRapidServerEngine暴露的UserManager属性来获取IUserManager引用。IUserManager接口定义如下:
public interface IUserManager { ///<summary> /// 当前在线用户的数量。 ///</summary> int UserCount { get; } ///<summary> /// 在线用户设备显示器。 ///</summary> ILoginDeviceDisplayer LoginDeviceDisplayer { get; set; }
///<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> /// 获取所有在线用户信息。 ///</summary> List<UserData> GetAllUserData(); ///<summary> /// 获取所有登录设备。 ///</summary> List<LoginDeviceData> GetAllLoginDevice();
///<summary> /// 当某用户的第一个设备登录成功时,触发此事件。 ///</summary> event CbGeneric<string> UserConnected; ///<summary> /// 当某用户的最后一个设备下线时,触发此事件。 ///</summary> event CbGeneric<string> UserDisconnected; ///<summary> /// 当从另外一个新连接上收到一个同名LoginID用户登录成功的消息时,触发此事件。 /// 注意,只有在该事件处理完毕后,才会真正关闭旧的连接并使用新的地址取代旧的地址。可以在该事件的处理函数中,将相关情况通知给旧连接的客户端。 ///</summary> event CbGeneric<LoginDeviceData> ClientDeviceBeingPushedOut; ///<summary> /// 当客户端设备登录成功时,触发此事件。不要远程预定该事件。 ///</summary> event CbGeneric<LoginDeviceData> ClientDeviceConnected; ///<summary> /// 客户端设备断开下线时,触发此事件。不要远程预定该事件。 ///</summary> event CbGeneric<LoginDeviceData> ClientDeviceDisconnected; ///<summary> /// 当在线用户数发生变化时,触发此事件。 ///</summary> event CbGeneric<int> UserCountChanged; }
三.UserID的长度
ESFramework的Rapid引擎使用的消息头的默认长度是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时也是如此。
(1)服务端和桌面客户端请调用ESPlus.GlobalUtil 的 SetMaxLengthOfUserID方法进行设置。
(2)Silverlight的客户端请调用ESFramework.SL.GlobalUtil 的 SetMaxLengthOfUserID方法进行设置。
(3)在ESFramework内部,组(Group)ID也采用与UserID相同的规则。
(4)在满足项目需求的情况下,尽可能使UserID的最大长度短一点,这样可以使得消息头更加短小,从而避免浪费本不需要的带宽。
四.消息的最大长度
Rapid引擎内部默认设置的消息的最大长度为1M(1024*1024),并且这个长度还包含了上述消息头的长度。如果您的应用需要发送的单个信息的长度超过了1M,就会被ESFramework认为是恶意的消息,ESFramework会丢弃该消息并关闭对应的连接。
我们建议:在能同样满足项目的需求下,应该尽可能地使传送的消息小,这样不仅可以节省带宽,而且还有助于提升并发的性能。比如,将消息最大长度设置为1M,这样,ESF会为每个TCP连接分配1M的缓冲区,如此一来,1500个TCP连接将用到多于1.5G的内存,如果是32为进程,这就可能导致抛出 OutOfMemoryException(内存溢出)异常。
如果应用中确实有信息的长度超过最大限制,那么还可以通过ICustomizeOutter的SendBlob方法来将其作为大数据块进行发送。我们可以通过调用GlobalUtil静态类的SetMaxLengthOfMessage静态方法来设定ESFramework允许的最大消息长度。
/// <summary> /// 设置消息的最大长度,初始值为1M。必须在Rapid引擎初始化之前设置才有效。注意,客户端与服务端要统一设置。 /// </summary> public static void SetMaxLengthOfMessage(int maxLen);
下一篇:ESFramework 开发手册(03) -- 文件(夹)传送
上一篇:ESFramework 开发手册(01) -- 发送和处理信息
-----------------------------------------------------------------------------------------------------------------------------------------------
Q Q:168757008