ESFramework 使用技巧 -- 实现离线消息
在ESFramework 开发手册(01) -- 发送和处理信息一文中,我们介绍了如何使用ESPlus.Application.CustomizeInfo命名空间的组件来发送和处理自定义消息。而在实际的项目中,需要实现离线消息的功能是一个常见的需求,也有很多客户来咨询如何做才能实现离线消息,所以,在这里,我们简单介绍一下使用ESFramework/ESPlus实现离线消息的原理与步骤。
一.如何截获离线消息
我们已经知道,一个在线用户给另一个用户发送二进制信息采用的是ESPlus.Application.CustomizeInfo.Passive.ICustomizeOutter接口的Send方法,如:
/// 向在线用户targetUserID发送二进制信息。如果目标用户不在线,则服务端会调用ICustomizeInfoBusinessHandler.OnTransmitFailed方法来通知应用程序。
/// </summary>
/// <param name="targetUserID">接收消息的目标用户ID</param>
/// <param name="informationType">自定义信息类型</param>
/// <param name="info">二进制信息</param>
void Send(string targetUserID, int informationType, byte[] info);
对于类似在线用户发给其他用户的P2P类型的消息,可以通过P2P通道发送,也可以通过服务器中转。当目标用户不在线时,P2P通道肯定是不存在的,所以消息一定是提交到服务器。服务器接收到要转发的P2P消息时,判断目标用户是否在线,如果在线,则直接转发;否则,框架会触发ESPlus.Application.CustomizeInfo.Server.ICustomizeController接口的TransmitFailed事件:
///<summary>
/// 当因为目标用户不在线而导致服务端转发自定义信息失败时,将触发该事件。参数为转发失败的信息。
///</summary>
event CbGeneric<Information> TransmitFailed;
我们只要预定ICustomizeController接口的这个事件就可以监控到所有的离线消息了。
二.离线消息的管理
截获到离线消息后,我们可能需要将其存到数据库(或其它地方),然后,等到目标用户上线的时候,再从数据库中提取属于该用户的离线消息发送给他即可。
首先,我们需要对离线消息做一个封装 -- OfflineMessage:
public class OfflineMessage
{
#region Ctor
public OfflineMessage() { }
public OfflineMessage(string _sourceUserID, string _destUserID, int _informationType, byte[] info)
{
this.sourceUserID = _sourceUserID;
this.destUserID = _destUserID;
this.informationType = _informationType;
this.information = info;
}
#endregion
#region SourceUserID
private string sourceUserID = "";
/// <summary>
/// 发送离线消息的用户ID。
/// </summary>
public string SourceUserID
{
get { return sourceUserID; }
set { sourceUserID = value; }
}
#endregion
#region DestUserID
private string destUserID = "";
/// <summary>
/// 接收离线消息的用户ID。
/// </summary>
public string DestUserID
{
get { return destUserID; }
set { destUserID = value; }
}
#endregion
#region InformationType
private int informationType = 0;
/// <summary>
/// 信息的类型。
/// </summary>
public int InformationType
{
get { return informationType; }
set { informationType = value; }
}
#endregion
#region Information
private byte[] information;
/// <summary>
/// 信息内容
/// </summary>
public byte[] Information
{
get { return information; }
set { information = value; }
}
#endregion
#region Time
private DateTime time = DateTime.Now;
/// <summary>
/// 服务器接收到要转发离线消息的时间。
/// </summary>
public DateTime Time
{
get { return time; }
set { time = value; }
}
#endregion
}
接下来,我们定义IOfflineMessageManager接口,用于管理离线消息:
/// 离线消息管理器。
/// </summary>
public interface IOfflineMessageManager
{
/// <summary>
/// 存储离线消息。
/// </summary>
/// <param name="msg">要存储的离线消息</param>
void Store(OfflineMessage msg);
/// <summary>
/// 提取目标用户的所有离线消息。
/// </summary>
/// <param name="destUserID">接收离线消息用户的ID</param>
/// <returns>属于目标用户的离线消息列表,按时间升序排列</returns>
List<OfflineMessage> Pickup(string destUserID);
}
实现这个接口,我们便可以将离线消息存储到数据库或文本或网络等等,然后等到需要时再次从中提取。
三.存储离线消息
有了IOfflineMessageManager接口,我们便可以处理ICustomizeController接口的TransmitFailed事件了:
public void OnTransmitFailed(Information information)
{
OfflineMessage msg = new OfflineMessage(information.SourceID, information.DestID, information.InformationType, information.Content);
this.offlineMessageManager.Store(msg);
}
我们也许并不需要将所有的离线消息都存储起来,有些不重要的离线消息可以丢弃,而只保存那些我们关心的消息。这只需要在存储消息之前加一个条件判断进行过滤即可。
四.提取并发送离线消息
我们已经知道,可以通过IUserManager的SomeOneConnected事件来得知某个用户上线了,于是,我们可以在该事件处理函数中,提取属于该用户的离线消息并一一发送给他。我们通过类似下面的代码来做到这一点。
{
#region UserManager
private IUserManager userManager;
public IUserManager UserManager
{
set { userManager = value; }
}
#endregion
#region OfflineMessageManager
private IOfflineMessageManager offlineMessageManager;
public IOfflineMessageManager OfflineMessageManager
{
set { offlineMessageManager = value; }
}
#endregion
#region CustomizeController
private ICustomizeController customizeController;
public ICustomizeController CustomizeController
{
set { customizeController = value; }
}
#endregion
public void Initialize()
{
this.userManager.SomeOneConnected += new CbGeneric<ESFramework.Server.UserManagement.UserData>(userManager_SomeOneConnected);
}
void userManager_SomeOneConnected(ESFramework.Server.UserManagement.UserData userData)
{
List<OfflineMessage> list = this.offlineMessageManager.Pickup(userData.UserID);
if (list != null && list.Count > 0)
{
foreach (OfflineMessage msg in list)
{
byte[] bMsg = CompactPropertySerializer.Default.Serialize<OfflineMessage>(msg);
this.customizeController.Send(msg.DestUserID, InfoTypes.OfflineMessage , bMsg);
}
}
}
}
当用户上线时,会将属于他的离线消息按照时间的顺序一一发送给他(demo中,离线消息的类型为InfoTypes.OfflineMessage,您可以自定义它)。当然,你也可以将属于他的所有离线消息打成一个包,一次性发送也可以。如果是这样,你就需要再增加一条自定义的信息类型和相关的协议类了。
要注意的是,在服务端触发SomeOneConnected事件时,虽然对应的客户端已经完成了登录,但是,其可能还未完成全部的初始化动作,此时,客户端可能就无法正确地处理收到的离线消息。如何解决这种问题了?可以换种稳妥的方案:服务端并不预定SomeOneConnected事件,而客户端了,在完成初始化之后,向服务端发送一个请求消息(InfoTypes.GetOfflineMessage),服务端在接收到这个请求消息后,才将离线消息发送给它。
五.在客户端处理离线消息
根据上面第三点的描述,我们知道,当客户端上线时,客户端会接收到服务端发过来的属于当前客户端的离线消息。所以,客户端在实现的ICustomizeHandler接口的HandleInformation方法需要增加对离线消息的处理:
public void HandleInformation(string sourceUserID, int informationType, byte[] info) { if (informationType == InfoTypes.OfflineMessage) { OfflineMessage msg = CompactPropertySerializer.Default.Deserialize<OfflineMessage>(info, 0); this.HandleInformation(msg.SourceUserID, msg.InformationType, msg.Information); return; }
............................
}
Demo处理中,采用了一种偷懒的方式,将离线消息解析后,直接再次递归调用了HandleInformation方法 -- 就好像是现在刚收到了来自其它客户端的消息一样,忽略了原始的发送时间(OfflineMessage的Time属性)。如果不能忽略原始的发送时间,您可以采取更合适的处理方式。
六.小结
从上面可以看出,基于ESFramework/ESPlus实现离线消息策略是相当简单的,最主要的焦点有两个:第一是可以通过处理ICustomizeController接口的TransmitFailed事件来截获到所有的离线消息;第二是通过IUserManager的SomeOneConnected事件就能知道用户上线的时刻。
有的朋友可能会问离线文件又该怎么实现了?实际上也是同样的原理,只不过要多用到ESPlus.Application.FileTransfering命名空间下的一些类来完成文件的收发功能,这个以后我们再介绍。
本文只是实现离线消息的一个简单示例,在实际的应用中,可能需要做更多的工作来满足项目的具体需要,这里就不再一一赘述了。
Q Q:168757008