GGTalk 开源即时通讯系统源码剖析之:客户端全局缓存及本地存储
继上篇《GGTalk 开源即时通讯系统源码剖析之:虚拟数据库》详细介绍了 GGTalk 内置的虚拟的数据库,无需部署真实数据库便能体验GGTalk的全部功能,虚拟数据库将极大地简化服务端的部署过程,能使服务端立即运行起来。接下来我们将进入GGTalk的客户端,此篇将介绍GGTalk 客户端全局缓存及本地存储。
GGTalk V8.0 对需要频繁请求服务器的数据做了客户端全局缓存处理,大大减少了向服务器的请求次数,降低了服务器的压力,而且,这也使得客户端的运行速度更快、用户操作体验更流畅。
这篇文章将会详细的介绍GGTalk客户端的全局缓存以及客户端的本地持久化存储。还没有GGTalk源码的朋友,可以到 GGTalk源码下载中心 下载。
一. GGTalk 客户端缓存设计
1. ClientGlobalCache类
ClientGlobalCache 类是GGTalk客户端全局缓存的核心实现,其代码位置如下图所示:

然后来到这个类的定义:

这个类的核心作用是在内存中保存用户和群组的数据。首先这个类接受两个泛型参数,分别为TUser和TGroup,并且限定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;
//...
}
首先我们能够看到两个字段,userManager和groupManager,它们作用分别是用来缓存用户和群组的数据,它们的类型都是ObjectManager(看到这里如果你了解GGTalk服务端虚拟数据库设计和GGTalk服务端全局缓存的话,你会发现他们都用到了这个类型),这里就不再赘述了。
二. GGTalk 客户端本地持久化存储
接下来我们再来看 BaseGlobalCache 的 originUserLocalPersistence字段,这个字段的作用是将用户和群组的数据缓存到本地文件。它的类型是UserLocalPersistence<TUser, TGroup>,来到定义:

在这个类的内部定义了四个属性,分别为FriendList、GroupList、QuickAnswerList和RecentList,其代表含义如下:
FriendList:好友列表;GroupList:群组列表;QuickAnswerList:快捷回复列表;RecentList: 最近联系人/群列表。
再接下来我们需要关注这个类里面的两个方法,也是这个类的核心功能,分别是Load和Save方法。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 开源即时通讯系统源码剖析之:客户端全局缓存及本地存储的更多相关文章
- GGTalk——C#开源即时通讯系统源码介绍系列(一)
坦白讲,我们公司其实没啥技术实力,之所以还能不断接到各种项目,全凭我们老板神通广大!要知道他每次的饭局上可都是些什么人物! 但是项目接下一大把,就凭咱哥儿几个的水平,想要独立自主.保质保量保期地一个个 ...
- 新一代开源即时通讯应用源码定制 运营级IM聊天源码
公司介绍:我们是专业的IM服务提供商!哇呼Chat是一款包含android客户端/ios客户端/pc客户端/WEB客户端的即时通讯系统.本系统完全自主研发,服务器端源码直接部署在客户主机.非任何第三方 ...
- 即时通信系统中实现全局系统通知,并与Web后台集成【附C#开源即时通讯系统(支持广域网)——QQ高仿版IM最新源码】
像QQ这样的即时通信软件,时不时就会从桌面的右下角弹出一个小窗口,或是显示一个广告.或是一个新闻.或是一个公告等.在这里,我们将其统称为“全局系统通知”.很多使用C#开源即时通讯系统——GGTalk的 ...
- GGTalk ——C#开源即时通讯系统
http://www.cnblogs.com/justnow/ GGTalk ——C#开源即时通讯系统 下载中心 GGTalk(简称GG)是可在广域网部署运行的QQ高仿版,2013.8.7发布GG ...
- 即时通信系统中实现聊天消息加密,让通信更安全【低调赠送:C#开源即时通讯系统(支持广域网)——GGTalk4.5 最新源码】
在即时通讯系统(IM)中,加密重要的通信消息,是一个常见的需求.尤其在一些政府部门的即时通信软件中(如税务系统),对即时聊天消息进行加密是非常重要的一个功能,因为谈话中可能会涉及到机密的数据.我在最新 ...
- 可在广域网部署运行的即时通讯系统 -- GGTalk总览(附源码下载)
(最新版本:V6.2,2019.01.03 .Xamarin移动端版本已经推出,包括 Android 和 iOS) GGTalk开源即时通讯系统(简称GG)是QQ的高仿版,同时支持局域网和广域网, ...
- 【转】可在广域网部署运行的即时通讯系统 -- GGTalk总览(附源码下载)
原文地址:http://www.cnblogs.com/justnow/p/3382160.html (最新版本:V6.0,2017.12.11 .即将推出Xamarin移动端版本,包括 Androi ...
- GGTalk即时通讯系统(支持广域网)终于有移动端了!(技术原理、实现、源码)
首先要感谢大家一直以来对于GGTalk即时通讯系统的关注和支持!GGTalk即时通讯系统的不断完善与大家的支持分不开! 从2013年最初的GG1.0开放源码以来,到后来陆续增加了网盘功能.远程协助功能 ...
- 【转】GGTalk即时通讯系统(支持广域网)终于有移动端了!(技术原理、实现、源码)
原文地址:http://www.cnblogs.com/justnow/p/4836636.html 首先要感谢大家一直以来对于GGTalk即时通讯系统的关注和支持!GGTalk即时通讯系统的不断完善 ...
- 轻量级C#网络通信组件StriveEngine —— C/S通信开源demo(附源码)
前段时间,有几个研究ESFramework网络通讯框架的朋友对我说,ESFramework有点庞大,对于他们目前的项目来说有点“杀鸡用牛刀”的意思,因为他们的项目不需要文件传送.不需要P2P.不存在好 ...
随机推荐
- 2022-04-02:你只有1*1、1*2、1*3、1*4,四种规格的砖块。 你想铺满n行m列的区域,规则如下: 1)不管那种规格的砖,都只能横着摆, 比如1*3这种规格的砖,3长度是水平
2022-04-02:你只有11.12.13.14,四种规格的砖块. 你想铺满n行m列的区域,规则如下: 1)不管那种规格的砖,都只能横着摆, 比如1*3这种规格的砖,3长度是水平方向,1长度是竖直方 ...
- 2021-11-08:扁平化嵌套列表迭代器。给你一个嵌套的整数列表 nestedList 。每个元素要么是一个整数,要么是一个列表;该列表的元素也可能是整数或者是其他列表。请你实现一个迭代器将其扁平化
2021-11-08:扁平化嵌套列表迭代器.给你一个嵌套的整数列表 nestedList .每个元素要么是一个整数,要么是一个列表:该列表的元素也可能是整数或者是其他列表.请你实现一个迭代器将其扁平化 ...
- extra别名,即给列取别名
extra别名,即给列取别名 Student.objects.all().extra(select={"name":"nickname"}) nickname为 ...
- .NET 通过源码深究依赖注入原理
依赖注入 (DI) 是.NET中一个非常重要的软件设计模式,它可以帮助我们更好地管理和组织组件,提高代码的可读性,扩展性和可测试性.在日常工作中,我们一定遇见过这些问题或者疑惑. Singleton服 ...
- Dapr v1.11 版本已发布
Dapr是一套开源.可移植的事件驱动型运行时,允许开发人员轻松立足云端与边缘位置运行弹性.微服务.无状态以及有状态等应用程序类型.Dapr能够确保开发人员专注于编写业务逻辑,而不必分神于解决分布式系统 ...
- 大数据实战手册-开发篇之spark实战案例:实时日志分析
2.6 spark实战案例:实时日志分析 2.6.1 交互流程图 2.6.2 客户端监听器(java) @SuppressWarnings("static-access") pri ...
- 【python基础】文件-文件路径
1.文件路径 我们发现不管是写入还是写出操作,我们提供的都是文件名,其实这里准确说应该是文件路径.当我们简单把文件名传递给open函数时,Python将在当前执行程序的文件所在的目录中查找文件名所代表 ...
- 记一次Native memory leak排查过程
1 问题现象 路由计算服务是路由系统的核心服务,负责运单路由计划的计算以及实操与计划的匹配.在运维过程中,发现在长期不重启的情况下,有TP99缓慢爬坡的现象.此外,在每周例行调度的试算过程中,能明显看 ...
- 一次oracle行级锁导致的问题
分析问题:我在plsql/developer是用的system用户连接的数据库,而在crt用 sqlplus / as sysdba 连接数据库,是sys用户.现在在plsql/developer ...
- 瞬间抠图!揭秘 ZEGO 绿幕抠图算法背后的技术
抠图是图像处理中最常见的操作之一,指的是将图像中需要的部分从画面中精确的提取出来. 抠图的主要功能是为了后期的合成做准备.在 Photoshop 中,抠图的方法有很多种,最常见的有通道抠图.蒙版抠图. ...