- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
继上篇《 GGTalk 开源即时通讯系统源码剖析之:虚拟数据库 》详细介绍了 GGTalk 内置的虚拟的数据库,无需部署真实数据库便能体验GGTalk的全部功能,虚拟数据库将极大地简化服务端的部署过程,能使服务端立即运行起来。接下来我们将进入GGTalk的客户端,此篇将介绍GGTalk 客户端全局缓存及本地存储.
GGTalk V8.0 对需要频繁请求服务器的数据做了客户端全局缓存处理,大大减少了向服务器的请求次数,降低了服务器的压力,而且,这也使得客户端的运行速度更快、用户操作体验更流畅.
这篇文章将会详细的介绍GGTalk客户端的全局缓存以及客户端的本地持久化存储。还没有GGTalk源码的朋友,可以到 GGTalk源码下载中心 下载.
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 方法定义在其父类里面.
我们找到 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服务端全局缓存的话,你会发现他们都用到了这个类型),这里就不再赘述了.
接下来我们再来看 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开源即时通讯系统源码剖析之:客户端全局缓存及本地存储的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我阅读了有关 JSR 107 缓存 (JCache) 的内容。 我很困惑:据我所知,每个 CPU 都管理其缓存内存(无需操作系统的任何帮助)。 那么,为什么我们需要 Java 缓存处理程序? (如果C
好吧,我是 jQuery 的新手。我一直在这里和那里搞乱一点点并习惯它。我终于明白了(它并不像某些人想象的那么难)。因此,鉴于此链接:http://jqueryui.com/sortable/#dis
我正在使用 Struts 2 和 Hibernate。我有一个简单的表,其中包含一个日期字段,用于存储有关何时发生特定操作的信息。这个日期值显示在我的 jsp 中。 我遇到的问题是hibernate更
我有点不确定这里发生了什么,但是我试图解释正在发生的事情,也许一旦我弄清楚我到底在问什么,就可能写一个更好的问题。 我刚刚安装了Varnish,对于我的请求时间来说似乎很棒。这是一个Magneto 2
解决 Project Euler 的问题后,我在论坛中发现了以下 Haskell 代码: fillRow115 minLength = cache where cache = ((map fill
我正试图找到一种方法来为我网络上的每台计算机缓存或存储某些 python 包。我看过以下解决方案: pypicache但它不再被积极开发,作者推荐 devpi,请参见此处:https://bitbuc
我想到的一个问题是可以从一开始就缓存网络套接字吗?在我的拓扑中,我在通过双 ISP 连接连接到互联网的 HAProxy 服务器后面有 2 个 Apache 服务器(带有 Google PageSpee
我很难说出不同缓存区域 (OS) 之间的区别。我想简要解释一下磁盘\缓冲区\交换\页面缓存。他们住在哪里?它们之间的主要区别是什么? 据我了解,页面缓存是主内存的一部分,用于存储从 I/O 设备获取的
1.题目 请你为最不经常使用(LFU)缓存算法设计并实现数据结构。 实现 LFUCache 类: LFUCache(int capacity) - 用数据结构的容量 capacity 初始化对象 in
1.题目 请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。 实现 LRUCache 类: ① LRUCache(int capacity) 以正整数作为容量 capacity
我想在访问该 View 时关闭某些页面的缓存。它适用于简单查询模型对象的页面。 好像什么时候 'django.middleware.cache.FetchFromCacheMiddleware', 启
documents为 ExePackage element state Cache属性的目的是 Whether to cache the package. The default is "yes".
我知道 docker 用图层存储每个图像。如果我在一台开发服务器上有多个用户,并且每个人都在运行相同的 Dockerfile,但将镜像存储为 user1_myapp . user2 将其存储为 use
在 Codeigniter 中没有出现缓存问题几年后,我发现了一个问题。我在其他地方看到过该问题,但没有适合我的解决方案。 例如,如果我在 View 中更改一些纯 html 文本并上传新文件并按 F5
我在 Janusgraph 文档中阅读了有关 Janusgraph Cache 的内容。关于事务缓存,我几乎没有怀疑。我在我的应用程序中使用嵌入式 janusgrah 服务器。 如果我只对例如进行读取
我想知道是否有来自终端的任何命令可以用来匹配 Android Studio 中执行文件>使缓存无效/重新启动的使用。 谢谢! 最佳答案 According to a JetBrains employe
我想制作一个 python 装饰器来内存函数。例如,如果 @memoization_decorator def add(a, b, negative=False): print "Com
我经常在 jQuery 事件处理程序中使用 $(this) 并且从不缓存它。如果我愿意的话 var $this = $(this); 并且将使用变量而不是构造函数,我的代码会获得任何显着的额外性能吗?
是的,我要说实话,我不知道varnish vcl,我可以解决一些基本问题,但是我不太清楚,这就是为什么我遇到问题了。 我正在尝试通过http请求设置缓存禁止,但是该请求不能通过DNS而是通过 Varn
在 WP 站点上加载约 4000 个并发用户时遇到此问题。 这是我的配置: F5 负载均衡器 ---> Varnish 4,8 核,32 Gb RAM ---> 9 个后端,4 个核,每个 16 RA
我是一名优秀的程序员,十分优秀!