聊天室快速访问
继上次完成聊天室的历史记录功能后,我又想着实现聊天记录的已读未读功能。(轻喷。。)
开始之前
首先我看了抖音和钉钉这两款应用的消息已读未读功能的呈现效果。首先是抖音,在聊天界面,给好友发送完消息后,消息界面的最右下角有一个“已发送”标记,这时候是属于对方未读,如果对方上线并别点开了和你的聊天界面,你和他的聊天界面上,那个“已发送”就变成了“已读”,所以抖音并不会每一条消息上面都显示“已读”或者“未读”。而钉钉,由于其专注于办公,这个已读未读功能就“变态”多了,每一条发出去的消息都会显示对方是已读还是未读。
学谁?
当然了,我只能通过抖音或者钉钉在功能上呈现出来的效果来结合自己掌握的知识来推断他们大概的实现过程,真正的实现可比想的复杂多了,所以我也是照葫芦画瓢,做一个能用的精简版已读未读功能。既然是照葫芦画瓢,那就学钉钉吧,做一个“变态版”,每一条消息都显示“已读”或者“未读”。
思考
A和B聊天,如果A发送一条消息给B,怎么知道B已读呢?当然是B收到消息后,发送一个“已读回执”,A接收到已读回执后,更新自己的UI,把“未读”改为“已读”。
啊!挺简单啊。。。
可,,可是,如果B不在线,或者B在线,但是没有打开和A的聊天界面,那B不也是未读A的消息吗?如果B不在线,A发完消息后下线了,这时B上线了,查看消息后,发送已读回执给A,可是A已经下线了啊,怎么保持已读状态?此外,消息的已读和未读状态要保持的话,是该给消息添加一个属性,标识已读未读吗?如果添加属性,意味着我以前的代码也得改?比如一些操作数据库的代码和逻辑。
啊!
我怎么做
首先,并不需要给消息添加一个已读未读标识,只记录一个B最近已读A消息的时间即可。这样怎么就行呢?
1.设计“已读回执”结构
已读回执很简单,就是两个id和一个时间戳,id分别是读消息的A用户id、发消息的B用户id,时间戳是A最近一次读取B的消息的时间。这个已读回执是存到Redis中去的,最终会通过定时任务在凌晨三点持久化到数据库。例如id为3的用户发送已读回执给id为5的用户,则Redis中保存一个Value(key:'3-4-readTime', ×××)
2.“已读回执”怎么用
给一个场景A和B两个用户,进行对话。我们来分情况讨论一下吧:
- A上线,A点击好友列表中的好友B,这个动作即会触发发送一个已读回执给B(即使没有未读消息),同时存在Redis中。
- B在线,B收到回执会把和A的聊天中的A未读的消息更改为已读
- B不在线,B不能即时收到这个回执。等到B下次上线的时候,首先会去Redis中获取每个好友给自己的回执缓存(如果缓存没有则取数据库中获取,同时更新到缓存),这里能获取到好友A的回执,将回执里面的时间和要渲染到界面上的B发的每条消息的时间去比较,如果回执中的时间大于消息时间,则该消息标记为“已读”,否则标记为“未读”。
- A上线,点击列表中的好友B,执行完上面的过程,A发消息给B,A的界面显示这条消息,并显示此消息未读。
- B在线:
- B这时开着的是和A的聊天界面。B收到A的消息,立刻显示了出来,随即发送一条已读回执给A,同时更新Redis。如果A在线,则收到这条回执,把界面上的所有未读消息更改为“已读”。如果A不在线了,下次上线也可以通过缓存的回执或者持久化到数据库中的回执来渲染界面,显示“已读”。
- B这时开着的不是和A的聊天界面,则不发送回执给A,A界面继续显示“未读”。当B打开和A的聊天界面,才发送回执,同时更新Redis。
- B不在线:A界面这条消息继续显示“未读”。只有在下次B上线的时候,B点开和A的聊天界面,才会给A发送已读回执。A在线则获取实时的回执,如果不在线则下次上线的时候获取回执渲染界面。
- B在线:
Redis宕机了怎么办
和历史记录的实现一样,对于我这个单服务器系统,那就只能直接保存到数据库了。另外Redis持久化策略可以在下次启动的时候恢复数据。
遇到的坑
这个思路是没什么复杂的,但是实施起来就有很多小细节了。在一个坑上面花了很多时间。就是js获取的时间戳,在java中转成Timestamp的时候出错,导致我在调试的时候一旦发送已读回执,接收已读回执的那个客户端就会断连。最后发现是Timestemp这个东东,不能用强转字符串来得到,大意了,需要通过Timestamp.valueOf(×××)来把一个字符串转为时间戳,而且这个传入的字符串需要以yyyy-MM-dd HH:mm:ss的格式来的。当时写太快了,就用(Timestamp)强制转换。。。
截图: