继上篇《GGTalk 开源即时通讯系统源码剖析之:虚拟数据库》详细介绍了 GGTalk 内置的虚拟的数据库,无需部署真实数据库便能体验GGTalk的全部功能,虚拟数据库将极大地简化服务端的部署过程,能使服务端立即运行起来。接下来我们将进入GGTalk的客户端,此篇将介绍GGTalk 客户端全局缓存及本地存储。

GGTalk V8.0 对需要频繁请求服务器的数据做了客户端全局缓存处理,大大减少了向服务器的请求次数,降低了服务器的压力,而且,这也使得客户端的运行速度更快、用户操作体验更流畅。

这篇文章将会详细的介绍GGTalk客户端的全局缓存以及客户端的本地持久化存储。还没有GGTalk源码的朋友,可以到 GGTalk源码下载中心 下载。

一. GGTalk 客户端缓存设计

1. ClientGlobalCache类

ClientGlobalCache 类是GGTalk客户端全局缓存的核心实现,其代码位置如下图所示:

然后来到这个类的定义:



这个类的核心作用是在内存中保存用户和群组的数据。首先这个类接受两个泛型参数,分别为TUserTGroup,并且限定TUser为引用类型,并且需要实现TalkBase.IUser接口,还要具有一个无参数的公共构造函数;限定TGroup需要实现TalkBase.IGroup接口,且要求具有一个无参数的公共构造函数。除此之外,这个类继承自BaseGlobalCache<TUser, TGroup>类(后面将详细介绍)。

在 ClientGlobalCache 类的实现里,首先我们可以看到三个私有字段的定义,其作用如下:

  • rapidPassiveEngine;:rapid客户端引擎,用于与rapid服务端引擎之间进行通信。
  • talkBaseHelper;:工具方法调用器,由TalkBase类库约定方法的定义。
  • talkBaseInfoTypes;:客户端与服务端进行通信的消息的类型。

紧接着,我们来到ClientGlobalCache类构造函数的实现:

public ClientGlobalCache(IRapidPassiveEngine engine, ITalkBaseHelper<TGroup> helper, TalkBaseInfoTypes infoTypes, string persistenceFilePath, IAgileLogger logger) {
this.rapidPassiveEngine = engine;
this.talkBaseHelper = helper;
this.talkBaseInfoTypes = infoTypes;
this.Initialize(this.rapidPassiveEngine.CurrentUserID, persistenceFilePath, helper, logger);
}

构造函数接受五个参数,其分别是:

  • engine:rapid客户端引擎。
  • helper:工具方法调用器。
  • infoTypes:消息类型。
  • persistenceFilePath:数据缓存文件的目录。
  • logger:日志记录器。

在构造函数方法体内,分别对ClientGlobalCache类内部定义的三个私有字段进行了赋值,并且还调用Initialize方法,这个方法的作用是什么呢?想要了解这个方法我们得先去了解ClientGlobalCache类的父类BaseGlobalCache,因为 Initialize 方法定义在其父类里面。

2. BaseGlobalCache类

我们找到 BaseGlobalCache 类的源码,接着查看关于这个类的部分实现:

public abstract class BaseGlobalCache<TUser, TGroup>
where TUser : TalkBase.IUser
where TGroup : TalkBase.IGroup
{
//...
private ObjectManager<string, TUser> userManager = new ObjectManager<string, TUser>(); //缓存用户资料
private ObjectManager<string, TGroup> groupManager = new ObjectManager<string, TGroup>();
private UserLocalPersistence<TUser, TGroup> originUserLocalPersistence;
//...
}

首先我们能够看到两个字段,userManagergroupManager,它们作用分别是用来缓存用户和群组的数据,它们的类型都是ObjectManager(看到这里如果你了解GGTalk服务端虚拟数据库设计和GGTalk服务端全局缓存的话,你会发现他们都用到了这个类型),这里就不再赘述了。

二. GGTalk 客户端本地持久化存储

接下来我们再来看 BaseGlobalCache 的 originUserLocalPersistence字段,这个字段的作用是将用户和群组的数据缓存到本地文件。它的类型是UserLocalPersistence<TUser, TGroup>,来到定义:



在这个类的内部定义了四个属性,分别为FriendListGroupListQuickAnswerListRecentList,其代表含义如下:

  • FriendList:好友列表;
  • GroupList:群组列表;
  • QuickAnswerList:快捷回复列表;
  • RecentList: 最近联系人/群列表。

再接下来我们需要关注这个类里面的两个方法,也是这个类的核心功能,分别是LoadSave方法。Load 方法接受一个文件路径作为参数,将这个文件的内容读取出来并转化为UserLocalPersistence<TUser, TGroup>类型;Save 方法也是接受一个文件路径作为参数,将调用这个方法的对象转化为byte[],并存入指定文件路径的文件中。

以下是这两个方法的实现:

// 从文件读取数据
public static UserLocalPersistence<TUser, TGroup> Load(string filePath) {
try {
if (!File.Exists(filePath)) {
return null;
}
byte[] data = ESBasic.Helpers.FileHelper.ReadFileReturnBytes(filePath);
return (UserLocalPersistence<TUser, TGroup>)ESBasic.Helpers.SerializeHelper.DeserializeBytes(data, 0, data.Length);
}
catch {
return null;
}
}
// 将数据存储到文件
public void Save(string filePath) {
byte[] data = ESBasic.Helpers.SerializeHelper.SerializeObject(this);
ESBasic.Helpers.FileHelper.WriteBuffToFile(data, filePath);
}

了解到这里,我想你应该明白什么数据会被缓存到本地文件。没错,就是上述的四个属性,分别是好友列表、群组列表、快捷回复列表和最近联系人/群列表。

在了解完BaseGlobalCache类的字段后,我们回到主题,来看关于Initialize 方法的实现:

public virtual void Initialize(string curUserID, string persistencePath, IUnitTypeRecognizer recognizer, IAgileLogger _logger) {
//...
//自己的信息始终加载最新的
this.currentUser = this.DoGetUser(curUserID);
this.userManager.Add(this.currentUser.ID, this.currentUser);
this.persistenceFilePath = persistencePath;
this.originUserLocalPersistence = UserLocalPersistence<TUser, TGroup>.Load(this.persistenceFilePath);//返回null,表示该登录帐号还没有任何缓存
if (this.originUserLocalPersistence == null) {}
else {
this.quickAnswerList = this.originUserLocalPersistence.QuickAnswerList;
foreach (TUser user in this.originUserLocalPersistence.FriendList) {
if (user.ID == null) {
continue;
}
if (user.ID != this.currentUser.ID) {
user.UserStatus = UserStatus.OffLine;
user.CommentName = this.currentUser.GetUnitCommentName(user.ID);
this.userManager.Add(user.ID, user);
}
}
foreach (TGroup group in this.originUserLocalPersistence.GroupList) {
if (this.currentUser.GroupList.Contains(group.ID)) {
group.CommentName = this.currentUser.GetUnitCommentName(group.ID);
this.groupManager.Add(group.ID, group);
}
}
}
}

由于本篇文章介绍的是客户端全局缓存,故在此方法中的一些无关逻辑被有意隐藏,如果你想了解更完整的实现,建议配合GGTalk源码进行阅读。

来分析这段代码,首先通过调用DoGetUser方法,拿到当前登录的用户数据,然后通过userManager将其缓存到内存。接着接将本地缓存文件路径保存到persistenceFilePath 字段,接着通过调用UserLocalPersistence<TUser, TGroup>上的静态方法Load,读取本地缓存文件的内容。在本地缓存存在的情况下,去获取本地缓存文件中的快捷回复列表、好友列表和群组列表,将快捷回复列表保存到quickAnswerList 字段,并且将好友列表中的每一个好友的数据都通过userManager保存到内存中,将群组列表中的每一个群组的数据都通过groupManager保存到内存中。

综上所述:Initialize 方法的作用就是读取关于当前登录用户对应的本地缓存文件的数据,并将其保存到内存中。

现在有了文件 ——> 数据,那么数据 ——> 文件是在哪里实现的呢?还记得BaseGlobalCache类save 方法吗,我们顺着引用查看最终将数据存入文件的代码:

顺着引用我们找到了SaveUserLocalCache 方法,这个方法的作用就是将用户的好友列表数据、群组列表数据和快捷回复列表数据存入本地缓存文件。这个方法是在 MainForm_FormClosing 方法中调用的。

看到这里就串起来了,即在客户端窗体关闭时,就会将好友列表、群组列表和快捷回复列表数据缓存到本地文件中。

三. 更新本地缓存

想象这样一个场景,在某用户离线期间,此用户的好友或群组的信息发生了变更。比如,某好友资料发生了变化,或者有人从好友列表中删除了他,或者它所在的群组加入或移除了新成员,等等。那么在这名用户下次登录时,从本地存储拿到的缓存数据必然就是老版本的,那么GGTalk是如何解决这个问题的呢?

这里以好友列表数据为例(代码在路径GGTalk/GGTalk/TalkBase.Client/Core/BaseGlobalCache.cs):

public void StartRefreshFriendInfo()
{
//直接使用线程,可以快速启动。
this.updateThread = (this.userManager.Count > 1) ? new Thread(new ParameterizedThreadStart(this.RefreshContactRTData)) : new Thread(new ParameterizedThreadStart(this.LoadContactsFromServer));
this.updateThread.Start();
}

当用户登录后窗体显示时,或断线重连成功时,此方法会被调用。这个方法的作用就是通过判断缓存中是否存在用户来决定刷新部分联系人数据还是重新从服务器加载数据。

如果缓存中只有自己一个人,表示是第一次在该电脑上登录,此时将执行LoadContactsFromServer方法以从服务器加载所有联系人等数据。

如果缓存中只有多个人,表示不是第一次在该电脑上登录,此时将执行RefreshContactRTData方法以更新本地数据到最新版本。

在这里我们主要需要关注RefreshContactRTData方法:

private void RefreshContactRTData(object state)
{
try
{
this.BatchLoadStarted();
ContactRTDatas contract = this.DoGetContactsRTDatas(); //1000用户数据量大小为22k
foreach (string userID in this.userManager.GetKeyList())
{
if (userID != this.currentUser.ID && !contract.UserStatusDictionary.ContainsKey(userID)) //最新的联系人中不包含缓存用户,则将之从缓存中删除。
{
this.userManager.Remove(userID);
if (this.FriendRemoved != null)
{
this.FriendRemoved(userID);
}
}
} foreach (KeyValuePair<string, UserRTData> pair in contract.UserStatusDictionary)
{
if (pair.Key == this.currentUser.ID) {
continue;
}
TUser origin = this.userManager.Get(pair.Key);
if (origin == null) //不存在于本地缓存中
{
TUser user = this.DoGetUser(pair.Key);
user.CommentName = this.currentUser.GetUnitCommentName(user.ID);
this.userManager.Add(user.ID, user);
if (this.UserBaseInfoChanged != null)
{
this.UserBaseInfoChanged(user);
}
}
else
{
//资料变化
if (pair.Value.Version != origin.Version) {
TUser user = this.DoGetUser(pair.Key);
user.CommentName = this.currentUser.GetUnitCommentName(user.ID);
user.LastWordsRecord = origin.LastWordsRecord;
user.ReplaceOldUnit(origin);
this.userManager.Add(user.ID, user);
if (this.UserBaseInfoChanged != null) {
this.UserBaseInfoChanged(user);
}
}
else {
//状态变化
if (origin.UserStatus != pair.Value.UserStatus) {
origin.UserStatus = pair.Value.UserStatus;
if (this.UserStatusChanged != null) {
this.UserStatusChanged(origin);
}
}
}
}
}

这个方法会先去获取当前登录用户在服务器上最新的联系人列表数据(仅仅包含ID、版本号、状态),然后去遍历缓存中用户的ID,检查来自服务器最新的联系人列表数据是否包含此ID对应的用户,若不包含则需要将此ID对应的用户从缓存中去除。接着再遍历来自服务器上最新联系人的用户数据,若此用户不存在于本地缓存,则下载该用户数据并将其加入缓存。接下来就是根据版本号比较来判断联系人的资料是否发生变化,若发生变化则将其同步到本地缓存。

四. 总结

GGTalk客户端缓存流程:在用户登录后,首先会从本地缓存文件中读取用户的好友列表、群组列表和快捷回复列表数据,将这些数据保存到内存中。然后,从服务器获取最新的联系人版本信息,与本地缓存比较后,下载需要更新的联系人资料。而当客户端窗口关闭,也就是退出登录时,会将该用户的好友列表、群组列表和快捷回复列表数据缓存到本地文件。

以上就是关于GGTalk客户端全局缓存设计的核心了,在接下来的一篇我们将介绍GGTalk中是如何收发消息及处理消息的。

敬请期待:《GGTalk 开源即时通讯系统源码剖析之:消息收发及处理》

GGTalk 开源即时通讯系统源码剖析之:客户端全局缓存及本地存储的更多相关文章

  1. GGTalk——C#开源即时通讯系统源码介绍系列(一)

    坦白讲,我们公司其实没啥技术实力,之所以还能不断接到各种项目,全凭我们老板神通广大!要知道他每次的饭局上可都是些什么人物! 但是项目接下一大把,就凭咱哥儿几个的水平,想要独立自主.保质保量保期地一个个 ...

  2. 新一代开源即时通讯应用源码定制 运营级IM聊天源码

    公司介绍:我们是专业的IM服务提供商!哇呼Chat是一款包含android客户端/ios客户端/pc客户端/WEB客户端的即时通讯系统.本系统完全自主研发,服务器端源码直接部署在客户主机.非任何第三方 ...

  3. 即时通信系统中实现全局系统通知,并与Web后台集成【附C#开源即时通讯系统(支持广域网)——QQ高仿版IM最新源码】

    像QQ这样的即时通信软件,时不时就会从桌面的右下角弹出一个小窗口,或是显示一个广告.或是一个新闻.或是一个公告等.在这里,我们将其统称为“全局系统通知”.很多使用C#开源即时通讯系统——GGTalk的 ...

  4. GGTalk ——C#开源即时通讯系统

    http://www.cnblogs.com/justnow/ GGTalk ——C#开源即时通讯系统 下载中心   GGTalk(简称GG)是可在广域网部署运行的QQ高仿版,2013.8.7发布GG ...

  5. 即时通信系统中实现聊天消息加密,让通信更安全【低调赠送:C#开源即时通讯系统(支持广域网)——GGTalk4.5 最新源码】

    在即时通讯系统(IM)中,加密重要的通信消息,是一个常见的需求.尤其在一些政府部门的即时通信软件中(如税务系统),对即时聊天消息进行加密是非常重要的一个功能,因为谈话中可能会涉及到机密的数据.我在最新 ...

  6. 可在广域网部署运行的即时通讯系统 -- GGTalk总览(附源码下载)

      (最新版本:V6.2,2019.01.03 .Xamarin移动端版本已经推出,包括 Android 和 iOS) GGTalk开源即时通讯系统(简称GG)是QQ的高仿版,同时支持局域网和广域网, ...

  7. 【转】可在广域网部署运行的即时通讯系统 -- GGTalk总览(附源码下载)

    原文地址:http://www.cnblogs.com/justnow/p/3382160.html (最新版本:V6.0,2017.12.11 .即将推出Xamarin移动端版本,包括 Androi ...

  8. GGTalk即时通讯系统(支持广域网)终于有移动端了!(技术原理、实现、源码)

    首先要感谢大家一直以来对于GGTalk即时通讯系统的关注和支持!GGTalk即时通讯系统的不断完善与大家的支持分不开! 从2013年最初的GG1.0开放源码以来,到后来陆续增加了网盘功能.远程协助功能 ...

  9. 【转】GGTalk即时通讯系统(支持广域网)终于有移动端了!(技术原理、实现、源码)

    原文地址:http://www.cnblogs.com/justnow/p/4836636.html 首先要感谢大家一直以来对于GGTalk即时通讯系统的关注和支持!GGTalk即时通讯系统的不断完善 ...

  10. 轻量级C#网络通信组件StriveEngine —— C/S通信开源demo(附源码)

    前段时间,有几个研究ESFramework网络通讯框架的朋友对我说,ESFramework有点庞大,对于他们目前的项目来说有点“杀鸡用牛刀”的意思,因为他们的项目不需要文件传送.不需要P2P.不存在好 ...

随机推荐

  1. 2022-08-20:给定区间的范围[xi,yi],xi<=yi,且都是正整数, 找出一个坐标集合set,set中有若干个数字, set要和每个给定的区间,有交集。 求set的最少需要几个数。 比如给

    2022-08-20:给定区间的范围[xi,yi],xi<=yi,且都是正整数, 找出一个坐标集合set,set中有若干个数字, set要和每个给定的区间,有交集. 求set的最少需要几个数. ...

  2. 2021-05-09:给定数组hard和money,长度都为N;hard[i]表示i号的难度, money[i]表示i号工作的收入;给定数组ability,长度都为M,ability[j]表示j号人的

    2021-05-09:给定数组hard和money,长度都为N:hard[i]表示i号的难度, money[i]表示i号工作的收入:给定数组ability,长度都为M,ability[j]表示j号人的 ...

  3. 2021-08-11:按要求补齐数组。给定一个已排序的正整数数组 nums,和一个正整数 n 。从 [1, n] 区间内选取任意个数字补充到 nums 中,使得 [1, n] 区间内的任何数字都可以用

    2021-08-11:按要求补齐数组.给定一个已排序的正整数数组 nums,和一个正整数 n .从 [1, n] 区间内选取任意个数字补充到 nums 中,使得 [1, n] 区间内的任何数字都可以用 ...

  4. only仅显示一些字段

    only仅显示一些字段 仅显示nickname,age两列的数据 Student.objects.all().only('nickname','age')

  5. values_list() 元组形式显示查询结果

    values_list() 元组形式显示查询结果 name,age为数据库的两个列 Student.objects.values_list('name','age') values_list() 元组 ...

  6. 使用ONE.Abp快速开发微服务,再也不用加班了

    项目背景 公司采用项目制工作方式,因此在不同项目上可能存在多个团队开发独立的代码库,但通用的基础设施却是相同的,这可能导致每个项目都需要编写相同的代码,并重复造轮子.更严重的是,每个项目都有自己的用户 ...

  7. 【whale-starry-stl】01天 list学习笔记

    一.知识点 1. std::bidirectional_iterator_tag std::bidirectional_iterator_tag 是 C++ 标准库中定义的一个迭代器类型标签,用于标识 ...

  8. 前端树形结构图treeShapeStruct,可拖拽移动,点击展开收缩,无限添加子集

    快速实现树形结构图,可拖拽移动,点击展开收缩,无限添加子集; 下载完整代码请访问uni-app插件市场地址:https://ext.dcloud.net.cn/plugin?id=12650 效果图如 ...

  9. 【原创】xenomai内核解析-xenomai实时线程创建流程

    版权声明:本文为本文为博主原创文章,未经同意,禁止转载.如有错误,欢迎指正,博客地址:https://www.cnblogs.com/wsg1100/ 目录 问题概述 1 libCobalt中调用非实 ...

  10. Spring6 初始

    Spring6 初始 @ 目录 Spring6 初始 每博一文案: 1. 初始 Spring6 1.1 OCP开闭原则 1.2 依赖倒置原则DIP 1.3 控制反转IoC 2. Spring 初始 2 ...