ESFramework 开发手册(05) -- 好友与组
(注意:“好友与组”功能已过期,被“联系人”功能取代。 )
本文介绍ESFramework 开发手册(00) -- 概述一文中提到的ESPlus的两个可选功能:好友关系与组关系。
大部分分布式通信系统中,除了客户端与服务器进行通信外,都还会涉及到客户端之间相互通信、以及需要将客户端进行分组的功能,或者是类似这方面的需求。ESFramework对这一常见的任务内置了强大的支持,包括从客户端到服务端、一直到ESPlatform群集。在设计时,我们就考虑到了如何对常见的好友通信与组广播通信进行最大的支持,以期让ESFramework的使用者非常容易的就能够使用这些功能。
在ESFramework中,好友与组成员并不仅仅是指用户(某人),而是指任何一个运行的客户端实例。只要两个客户端实例之间需要频繁相互通信,那么它们就可以建立好友关系(friend)。如果需要在某些特定的客户端实例间进行广播通信,那么这些实例就可以被划分到同一个组,成为组友(groupmate)。如果是使用ESFramework开发类似IM的系统,那么这两种关系就更明显,它们就类似QQ的好友与群。
一. 非强制性依赖
ESPlus定义了好友管理和组管理的接口(IFriendsManager与IGroupManager),如果你的应用需要好友或组方面的功能,那么只要实现对应的接口,并通过IRapidServerEngine对应的属性注入到框架中即可。如果不需要好友与组方面的功能,那么可以完全忽略它们的存在。ESFramework 并不会强制性地要求你的应用必须要实现一个与自己的项目需求没有任何关系的接口(比如IFriendManager与IGroupManager)。我们将选择的权利交到了你的手中,你可以根据项目的具体需求,决定要实现哪些接口,并注入到ESFramework框架中。
二. 好友关系
1.服务端
ESPlus.Application.Friends 命名空间用于实现好友关系的功能,在服务端,定义了ESPlus.Application.Friends.Server.IFriendsManager接口:
public interface IFriendsManager { /// <summary> /// 获取好友列表。 /// </summary> /// <param name="ownerID">主人的UserID</param> /// <param name="tag">自定义标签,比如当好友有分类时,可以传分类名称。如果传入null,表示获取所有好友。</param> /// <returns>好友列表</returns> List<string> GetFriendsList(string ownerID ,string tag); }
GetFriendList 方法用于获取某个用户的所有好友的UserID列表。同很多常见的返回集合的方法设计规则一样,该方法不允许返回null,如果目标用户没有任何好友,那么请返回元素个数为0的List。
如果要使用好友关系,请实现IFriendsManager接口,并在IRapidServerEngnie初始化之前,注入到其FriendsManager属性。
当服务端引擎初始化成功后,还可以通过其暴露的FriendsController属性来对好友关系功能进行某些设置:
public interface IFriendsController { /// <summary> /// 当用户上线/掉线时,是否通知其好友。默认值true。 /// </summary> bool FriendNotifyEnabled { get; set; } /// <summary> /// 好友通知是否使用单独的线程。默认值为false。 /// 如果要开启好友通知线程,必须先开启好友通知(即必须先将FriendNotifyEnabled设置为true)。 /// </summary> bool UseFriendNotifyThread { get; set; } }
UseFriendNotifyThread用于控制是否开启了好友通知线程。如果为true,表示所有的好友通知消息都集中在一个单独的线程中发送。我们建议,在一个支持好友关系且需要承载大量用户同时在线的系统中,如果开启了好友通知功能,请务必将UseFriendNotifyThread也设置为true。
2.客户端
在客户端,可以通过IRapidPassiveEngine暴露出的FriendsOutter属性,来使用好友关系功能。
public interface IFriendsOutter { /// <summary> /// 当好友上线时,触发此事件。参数为好友的UserID /// </summary> event CbGeneric<string> FriendConnected; /// <summary> /// 当好友下线时,触发此事件。参数为好友的UserID /// </summary> event CbGeneric<string> FriendOffline; /// <summary> /// 获取所有在线的好友列表。 /// </summary> List<string> GetAllOnlineFriends(); /// <summary> /// 获取好友列表。 /// </summary> List<string> GetFriends(string tag); }
(1)用户上/下线时,通知其好友。当用户上线或下线时,框架会触发IFriendsOutter接口的FriendConnected或FriendOffline事件以通知所有的在线好友。
(2)客户端可以通过IFriendsOutter接口的GetFriends方法和GetAllOnlineFriends方法来获取所有好友以及所有在线的好友列表。
有了这两组特性的支持,每个运行的客户端实例,在其运行的整个生命周期中,都可以清楚地知道每个好友的在线状态。在具体项目中,我们可以这么做,当某个客户端登陆成功后,就获取所有好友列表和所有的在线好友列表,并预定IFriendsOutter的FriendConnected和FriendOffline事件,然后在运行的过程中,当FriendConnected和FriendOffline事件触发时,就修改对应好友的状态。这样就保证我们的客户端可以实时地知道每个好友是否在线。
ESPlus 内置了IFriendManager接口的两个实现,一个就是上面提到的占位符EmptyFriendsManager,还有一个是DefaultFriendsManager。
(1)EmptyFriendsManager 假设所有的用户都不是好友关系 -- 即其GetFriends方法始终返回一个元素个数为0的列表。
(2)DefaultFriendsManager 则假设所有的在线用户都是好友 -- 即其GetFriends方法始终返回所有在线用户列表(将自己ownerID排除在外)。
DefaultFriendsManager一般用于demo或者测试程序中,使得demo或测试程序的实现更简单。
三. 组关系
1.服务端
ESPlus.Application.Group 命名空间用于实现组关系的功能,在服务端,定义了ESPlus.Application.Friends.Server.IGroupManager接口:
public interface IGroupManager { /// <summary> /// 获取用户的所有组友。 /// </summary> List<string> GetGroupmates(string userID); /// <summary> /// 获取目标组的所有成员。 /// </summary> List<string> GetGroupMembers(string groupID); }
接口中的两个方法都返回一个列表,所以也遵循相同的方法设计规则:如果没有任何满足条件的结果,请返回元素个数为0的List。
(1)所谓组友(groupmate),即同属某个组的成员之间的关系。由于一个用户可以加入到多个组,该用户的组友就是所有这些组的成员的并集。
注意,GetGroupmates方法返回的列表中不能包含重复的UserID。
GetGroupmates方法在框架内的主要用途:当某个用户上下线时,框架可以自动通知该用户的所有组友他的状态变化事件。
(2)GetGroupMembers 用于获取一个组的所有成员列表。比如,在实现该接口时,我们可以从DB中加载目标组及组成员,然后返回成员列表。
GetGroupMembers方法在框架内的主要用途:在发送组内广播消息功能时,框架只有通过GetGroupMembers才知道要将消息转发给哪些用户。
如果要使用组关系,请实现IGroupManager接口,并在IRapidServerEngnie初始化之前,注入到其GroupManager属性。
当服务端引擎初始化成功后,还可以通过其暴露的GroupController属性来对组关系功能进行某些设置:
public interface IGroupController { /// <summary> /// 当服务端接收到要转发的广播消息(包括大数据块信息)时,触发此事件。参数:broadcasterID - groupID - broadcastType - broadcastContent /// </summary> event CbGeneric<string, string, int, byte[]> BroadcastReceived; /// <summary> /// 加入/退出组、上下线等事件是否通知组友。默认值为true。 /// 注意,如果关闭该选项,客户端则不能保持实时的组的状态,将导致广播信息发送出现错误(IGroupOutter.Broadcast方法)。 /// </summary> bool GroupNotifyEnabled { get; set; } /// <summary> /// 组友上下线通知是否使用单独的线程。默认值为false。 /// 如果要开启组友通知线程,必须先开启组友通知(即必须先将GroupNotifyEnabled设置为true)。 /// </summary> bool UseGroupNotifyThread { get; set; } /// <summary> /// 在组内广播信息。 /// </summary> void Broadcast(string groupID, int broadcastType, byte[] broadcastContent, ActionTypeOnChannelIsBusy action); }
(1)UseGroupNotifyThread的用途与前面的IFriendsController的UseFriendNotifyThread属性一样。
(2)BroadcastReceived事件可用于在服务端监控所有转发的广播信息。
(3)Broadcast方法使得我们可以在服务端直接向某个组广播信息。
2.客户端
在客户端,可以通过IRapidPassiveEngine暴露出的GroupOutter属性,来使用组关系功能。
public interface IGroupOutter { /// <summary> /// 当组成员上线时,触发该事件。参数:MemberID /// </summary> event CbGeneric<string> GroupmateConnected; /// <summary> /// 当组成员下线时,触发该事件。参数:MemberID /// </summary> event CbGeneric<string> GroupmateOffline; /// <summary> /// 当接收到某个组内的广播消息(包括大数据块信息)时,触发此事件。参数:broadcasterID - groupID - broadcastType - broadcastContent。 /// 如果broadcasterID为null,表示是服务端发送的广播。 /// </summary> event CbGeneric<string, string, int, byte[]> BroadcastReceived; /// <summary> /// 获取组的成员。 /// </summary> Groupmates GetGroupMembers(string groupID); /// <summary> /// 在组内广播信息。 /// </summary> /// <param name="groupID">接收广播信息的组ID</param> /// <param name="broadcastType">广播信息的类型</param> /// <param name="broadcastContent">信息的内容</param> /// <param name="action">当通道繁忙时采取的操作。</param> void Broadcast(string groupID, int broadcastType, byte[] broadcastContent, ActionTypeOnChannelIsBusy action); }
(1)GetGroupMembers 会返回某个组的所有成员,并将在线成员与不在线成员区分开来。
(2)当用户上线或下线时,框架会回调IGroupOutter 接口的GroupmateConnected或GroupmateOffline事件以通知所有的在线组友。
(3)可以通过Broadcast方法向任何一个组发送广播,目标组的每个在线成员都将会通过IGroupOutter 的BroadcastReceived事件来获得广播内容。
(4)同IFriendManager一样,有了(1)(2)两个特性的支持,每个运行的客户端实例,在其运行的整个生命周期中,就可以清楚地知道每个组友的在线状态了。
四. 请注意性能
如果具体的项目中需要频繁地用到好友与组等特性,那么在实现IFriendManager接口和IGroupManager接口时,要特别注意性能问题。
1. 实现时使用缓存
因为IFriendManager接口和IGroupManager接口的方法会被框架频繁调用,所以,必须想办法提高IFriendManager和IGroupManager接口的实现的性能。
如果IFriendManager和IGroupManager接口的方法被调用时,每次都需要从外部介质(比如DB、文件等)重新加载好友关系与组关系,那么毫无疑问将严重地降低应用程序的性能。通常的解决方案是,将好友关系以及组关系缓存在内存中,以避免重复从外部介质加载。
至于究竟采用何种策略来提升IFriendManager和IGroupManager的性能,需要根据你的项目具体情况而作妥当设计。特别是在高性能的分布式通信系统中,这一点是万万不可忽视的。
2. 精简好友/组友列表
IFriendManager和IGroupManager返回的好友列表和组友列表,请尽可能的精简,不要包含垃圾数据。
特别是当服务端引擎的FriendNotifyEnabled和GroupNotifyEnabled属性(将在后面介绍)设置为true时,用户的状态变化会自动通知其所有好友和组友。如果用户状态改变频繁,而其好友和组友数量又很巨大时,这种开销是非常大的。必要时,可以考虑将FriendNotifyEnabled或GroupNotifyEnabled设置为false以关闭状态改变自动通知。
五. 最佳实践
1. 上下线通知避免重复
因在实际的应用中,一个人的好友和组友通常有很大部分的交集,这样在通知好友/组友上下线时,就难免会出现重复的情况。我们推荐的解决方案是这样的:
(1)关闭好友上下线通知: rapidServerEngine.FriendsController.FriendNotifyEnabled = false;
(2)开启组友上下线通知: rapidServerEngine.GroupController.GroupNotifyEnabled = true;
(3)实现IGroupManager的GetGroupmates方法时,让其返回“组友+好友”的并集(并剔除重复值)。
2. 当在线用户数较大时
采用上面1中描述的方案后,如果在线人数较大,则需开启UseGroupNotifyThread:
rapidServerEngine.GroupController.UseGroupNotifyThread = true;
下一篇:ESFramework 开发手册(06) -- Rapid通信引擎
上一篇:ESFramework 开发手册(04) -- 可靠的P2P
-----------------------------------------------------------------------------------------------------------------------------------------------
Q Q:168757008