在线用户管理--ESFramework 4.0 进阶(05)
无论我们采用何种通信框架来构建我们的分布式系统,在服务端进行用户管理都是非常重要的一个环节。然而用户管理是否应该隶属于通信框架了?这个并不一定,通常来说,用户管理是与具体应用紧密相关的,应该是由应用解决的部分,因为不同的应用程序对用户管理的需求是不尽相同的。但是,如果我们对大多数应用中的用户管理任务进行分析,我们发现它们都会关注一些最基础的用户管理需求(如用户状态监控)。如果能在通信框架中内置一种简洁的、灵活的、可扩展的用户管理组件,定会为大多数应用程序提供非常多的方便。
ESFramework抽象了最基本的用户管理任务,定义了统一的用户管理的基础接口,并提供了默认的实现,它们能与前面介绍的网络通信引擎无缝地集成。对于许多不是特别复杂的分布式通信应用,ESFramework内置的用户管理功能几乎就可以满足要求。另外,ESFramework和ESPlus以及ESPlatform的许多扩展特性和功能也需要基于用户管理组件的抽象接口来展开。
一.用户管理器接口IUserManager
用户管理器是用在服务端的,所以IUserManager接口的定义位于ESFramework.Server.UserManagement命名空间,IUserManager不仅可以用于基于TCP的服务端、也可以用于基于UDP的服务端。
我们首先看看其类图:
IUserManager继承自ICoreUserManager接口,ICoreUserManager是更基础的用户管理接口,其主要被用于ESPlatform平台,否则,其中定义的内容可以完全合并到IUserManager接口中。下面我把这两个接口的定义源码贴出来,里面较详细的注释说明可以让我少敲些字。
{
/// <summary>
/// 在线用户的数量。
/// </summary>
int UserCount { get; }
/// <summary>
/// 获取目标在线用户的基础信息。
/// </summary>
/// <param name="userID">目标用户的ID</param>
/// <returns>如果目标用户不在线,则返回null</returns>
UserData GetUserData(string userID);
/// <summary>
/// 获取所有在线用户的ID列表。
/// </summary>
List<string> GetOnlineUserList();
/// <summary>
/// 如果是基于tcp引擎,则当tcp连接上接收或发送数据抛出异常时,将关闭连接,并触发此事件。
/// </summary>
event CbGeneric<UserData> SomeOneDisconnected;
/// <summary>
/// 如果是基于tcp引擎,当接收到新连接上的第一个消息时,将触发此事件。
/// </summary>
event CbGeneric<UserData> SomeOneConnected;
}
{
/// <summary>
/// 用户管理器依赖该属性显示所有在线用户的状态信息。
/// </summary>
IUserDisplayer UserDisplayer { set; }
/// <summary>
/// 在线用户的心跳检测器。
/// </summary>
IHeartBeatChecker HeartBeatChecker { set; }
/// <summary>
/// 重登陆模式。
/// </summary>
RelogonMode RelogonMode { get; set; }
/// <summary>
/// 初始化用户管理器。
/// </summary>
void Initialize();
/// <summary>
/// 清除所有的在线用户。
/// </summary>
void Clear();
/// <summary>
/// 激活心跳。
/// </summary>
void ActivateUser(string userID);
/// <summary>
/// 目标用户是否在线。
/// </summary>
bool IsUserOnLine(string userID);
/// <summary>
/// 从在线列表中移除目标用户(依据用户的地址)。
/// </summary>
void UnregisterUser(IUserAddress address);
/// <summary>
/// 从在线列表中移除目标用户。
/// </summary>
void UnregisterUser(string userID);
/// <summary>
/// 根据在线用户的地址获取用户ID。
/// </summary>
string GetUserID(IUserAddress address);
/// <summary>
/// GetTimeLogon 获取目标在线用户的登录时间。
/// </summary>
DateTime GetTimeLogon(string userID);
/// <summary>
/// 用于统计发送给用户的消息。如果用户还未注册,则忽略。
/// </summary>
void AfterSentMessageToUser(string userID, IUserAddress address, IMessage msg);
/// <summary>
/// 如果用户不在线,返回null
/// </summary>
IUserAddress GetUserAddress(string userID);
/// <summary>
/// 当用户已经注册过,如果RelogonMode为IgnoreNew,则不会更改其address,而是直接返回;如果RelogonMode为ReplaceOld,则使用新的连接取代旧的连接。
/// </summary>
void RegisterUser(string userID, IUserAddress address);
/// <summary>
/// 旧连接被挤掉。如果RelogonMode为ReplaceOld,并且当从另外一个新连接上收到一个同名ID用户的消息时将触发此事件。
event CbGeneric<UserData> SomeOneBeingPushedOut;
/// <summary>
/// 新连接被忽略。如果RelogonMode为IgnoreNew,并且当从一个新连接上收到一个同名ID用户的消息时将触发此事件。
/// </summary>
event CbGeneric<string ,IUserAddress> NewConnectionIgnored;
/// <summary>
/// 用户超时。只有在该事件处理完毕后,才将其从用户列表中删除。
/// 但是,用户对应的TCP连接可能并没有被释放 -- 所以,在该事件处理函数中最好主动关闭TCP连接(将触发SomeOneDisconnected事件)。
/// </summary>
event CbGeneric<UserData> SomeOneTimeOuted;
/// <summary>
/// 当在线用户数发生变化时,触发此事件。
/// </summary>
event CbGeneric<int> UserCountChanged;
}
(1)默认情况下,IUserManager会从客户端发过来的第一条消息中取出消息头的UserID属性的值,并将其与对应的用户地址IUserAddress对应起来,并触发SomeOneConnected事件。有一点我们是要特别注意,那就是同一个客户端每次给服务器发消息时,要保证每个消息的Header中UserID是一致的。而用户管理器只会认定第一个消息的Header中的UserID的值。
(2)在用户管理器中,UserAddress与UserID是一一对应的,一个UserAddress实例只能对应一个UserID,同样的,一个UserID最多存在一个UserAddress对象。我们可以通过GetUserID方法通过UserAddress获取对应的UserID,也可以通过GetUserData方法根据UserID得到对应的UserAddress对象。UserAddress对象中包括了客户端与服务器进行通信的IP地址和Port信息。
(3)如果从不同的连接上接收到相同UserID的信息,IUserManager采取的策略取决于RelogonMode属性的设置,其有可能触发SomeOneBeingPushedOut事件(RelogonMode为ReplaceOld)、也有可能触发NewConnectionIgnored事件(RelogonMode为IgnoreNew)。关于重登陆方面的更多信息,可以参见ESFramework 4.0 快速上手 -- 重登陆模式。
(4)在处理SomeOneBeingPushedOut事件时要注意:只有在该事件处理完毕后,IUserManager才会真正使用新的地址取代旧的地址。所以,我们必须在该事件的处理函数中,关闭旧的连接(TCP)或Session(UDP)以释放资源(将触发SomeOneDisconnected事件)。但是在关闭之前,可以将相关情况通知给旧连接/Session的客户端。
(5)在处理NewConnectionIgnored事件时也要注意:我们必须在该事件的处理函数中,关闭新的连接或Session,同样的,在关闭之前,可以将相关情况通知给该连接/Session的客户端。
(6)当通信引擎检测到底层连接断开时(针对TCP),IUserManager会触发SomeOneDisconnected事件。
(7)IUserManager借助于IHeartBeatChecker对在线用于进行心跳检测,如果用户因为心跳超时,则会触发SomeOneTimeOuted事件。关于ESFramework中的TCP掉线与心跳机制的更多信息,可以参见ESFramework 4.0 快速上手 -- 玩的就是“心跳”。
二.用户状态跟踪及显示
(1)当用户上线时,IUserManager会记录其上线时间和地址UserAddress。
(2)当服务端每次向客户端发送数据之后,IUserManager会通过AfterSentMessageToUser方法来进行记录,以此可以统计每个用户请求服务的次数和下载的数据量。
(3)我们可以通过IUserManager接口的GetUserData方法获取在线用户的实时状态。GetUserData返回UserData对象,从该对象的类图可以看出其包含了哪些状态信息:
(4)IUserManager接口还可以注入一个IUserDisplayer属性,通过IUserDisplayer可以在UI上显示每个用户的实时状态。
{
/// <summary>
/// 清除所有。通常是通信引擎停止时被调用。
/// </summary>
void ClearAll() ;
/// <summary>
/// 当用户状态更新时被调用。
/// </summary>
/// <param name="userID">用户ID</param>
/// <param name="justServiceType">刚刚发送给用户的消息类型</param>
/// <param name="totalDataLen">该用户下载的总的数据量</param>
/// <param name="userAddress">用户的地址</param>
/// <param name="totalReqCount">总的请求次数</param>
void SetOrUpdateUserItem(string userID, int justServiceType, long totalDataLen, string userAddress, long totalReqCount);
/// <summary>
/// 移除用户。通常在用户下线时被调用。
/// </summary>
void RemoveUser(string userID ,string cause);
}
IUserManager借助于IUserDisplayer来展现每个在线用户的状态,我们可以实现该接口,以我们想要的方式显示这些状态数据。
如果没有特别需求,可以直接使用ESPlus提供的ESPlus.Widgets.UserDisplayer控件,它显示的效果如下所示:
一定要提醒读者的是,如果服务器处理的是巨大并发量的任务,就不要使用任何形式的UserDisplayer控件,甚至,服务端最好都不要有UI,而是以一个service的形式运行。因为在这种巨大并发量的系统中,在线用户数量巨大(数万计),而且状态更新频繁,会导致大量的CPU时间浪费在UI的更新上。经验之谈,谨记之。
三.与通信引擎集成
ESFramework通过ESFramework.Server.UserManagement.UserManagerBridge将用户管理器IUserManager与服务端引擎IServerEngine桥接起来:
在UserManagerBridge的Initialize方法中,它会将IServerEngine的相关事件(如MessageReceived事件、MessageSent事件,对于TCP服务端引擎还有SomeOneDisconnect事件)传递给IUserManager的对应方法去处理,这样用户管理器就被驱动起来了。
最后,我们实例化一个用户管理器并就将其与服务端引擎集成起来作为示范:
IHeartBeatChecker heartBeatChecker = new ESBasic.Threading.Application.HeartBeatChecker(1, 30);//心跳超时设为30秒
heartBeatChecker.Initialize();
IUserManager userManager = new UserManager();
userManager.HeartBeatChecker = heartBeatChecker;
userManager.RelogonMode = RelogonMode.ReplaceOld;//设置重登陆模式
userManager.Initialize();
UserManagerBridge userManagerBridge = new UserManagerBridge();
userManagerBridge.UserManager = userManager;
userManagerBridge.ServerEngine = streamTcpEngine;
userManagerBridge.Initialize();
关于如何实例化一个TCP服务端引擎,可以参见ESFramework 4.0 进阶(04)-- 驱动力:通信引擎(下)一文中的代码示例。