上一篇简单的实现了一个聊天网页, 但这个太简单, 消息全广播, 没有用户认证和已读未读处理, 主要的意义是走通了 websocket-sharp 做服务端的可能性那么一个完整的 IM 还需要实现哪些部分?
一发消息
用户 A 想要发给用户 B, 首先是将消息推送到服务器, 服务器将拿到的 toid 和内容包装成一个完整的 message 对象, 分别推送给客户 B 和客户 A 为什么也要推送给 A 呢, 因为 A 也需要知道是否推送成功, 以及拿到了 messageId 可以用来做后面的已读未读功能
这里有两个问题还要解决, 第一个是 Server 如何推送到客户 B, 另外一个问题是群消息如何处理?
实现推送
先解决第一个问题, 在 Server 端, 每次连接都会创建一个 WebSocketBehavior 对象, 每个 WebSocketBehavior 都有一个唯一的 Id, 如果用户在线我们就可以推送过去:
Sessions.SendTo(userKey, Json.JsonParser.Serialize(msg));
需要解决的是需要将用户的 Id 和 WebSocketBehavior 的 Id 关联起来, 所以这就要求每个用户连接之后需要马上验证所以用户的流程如下:
由于 JavaScript 和 Server 交互的主要途径就是 onmessage 方法, 暂时不能像 socketio 那样可以自定义事件让后台执行完成后就触发, 我们先只能约定消息类型来实现验证和聊天的区分
- function send(obj) {
- // 必须是对象, 还有约定的类型
- ws.send(JSON.stringify(obj))
- }
- socketSDK.sendTo = function (toId,msg) {
- var obj = {
- toId:toId,
- content: msg,
- type: "002"// 聊天
- }
- send(obj);
- }
- socketSDK.validToken = function (token) {
- var obj = {
- content: token || localStorage.token,
- type: "001"// 验证
- }
- send(obj);
- }
在后端拿到 token 就可以将用户的 guid 存下来, 所有用户的 guid 与 WebSocketBehavior 的 Id 关系都保存在缓存里面
- var infos = _userService.DecryptToken(token);
- UserGuid = infos[0];
- if (!cacheManager.IsSet(infos[0]))
- {
- cacheManager.Set(infos[0], Id, 60);
- }
- // 告之 client 验证结果, 并把 guid 发过去
- SendToSelf("token 验证成功");
调用 WebSocketBehavior 的 Send 方法可以将对象直接发送给与其连接的客户端接下来我们只需要判断 toid 这个用户在缓存里面, 我们就能把消息推送给他如果不在线, 就直接保存消息
群消息
群是一个用户的集合, 发一条消息到群里面, 数据库也只需要存储一条, 而不是每个人都存一条, 但每个人都会收到一次推送这是我的 Message 对象和 Group 对象
- public class Message
- {
- private string _receiverId;
- public Message()
- {
- SendTime = DateTime.Now;
- MsgId = Guid.NewGuid().ToString().Replace("-", "");
- }
- [Key]
- public string MsgId { get; set; }
- public string SenderId { get; set; }
- public string Content { get; set; }
- public DateTime SendTime { get; set; }
- public bool IsRead { get; set; }
- public string ReceiverId
- {
- get
- {
- return _receiverId;
- }
- set
- {
- _receiverId = value;
- IsGroup=isGroup(_receiverId);
- }
- }
- [NotMapped]
- public Int32 MsgIndex { get; set; }
- [NotMapped]
- public bool IsGroup { get; set; }
- public static bool isGroup(string key)
- {
- return !string.IsNullOrEmpty(key) && key.Length == 20;
- }
- }
- View Code
- public class Group
- {
- private ICollection<User.User> _users;
- public Group()
- {
- Id = Encrypt.GenerateOrderNumber();
- CreateTime=DateTime.Now;
- ModifyTime=DateTime.Now;
- }
- [Key]
- public string Id { get; set; }
- public DateTime CreateTime { get; set; }
- public DateTime ModifyTime { get; set; }
- public string GroupName { get; set; }
- public string Image { get; set; }
- [Required]
- // 群主
- public int CreateUserId { get; set; }
- [NotMapped]
- public virtual User.User Owner { get; set; }
- public ICollection<User.User> Users
- {
- get { return _users??(_users=new List<User.User>()); }
- set { _users = value; }
- }
- public string Description { get; set; }
- public bool IsDeleteD { get; set; }
- }
- View Code
对于 Message 而言, 主要就是 SenderId,Content 和 ReceiverId, 我通过 ReceiverId 来区分这条消息是发给个人的消息还是群消息对于群 Id 是一个长度固定的字符串区别于用户的 GUID 这样就可以实现群消息和个人消息的推送了:
- case "002":// 正常聊天
- // 先检查是否合法
- if (!IsValid)
- {
- SendToSelf("请先验证!","002");
- break;
- }
- // 在这里创建消息 避免群消息的时候多次创建
- var msg = new Message()
- {
- SenderId = UserGuid,
- Content = obj.content,
- IsRead = false,
- ReceiverId = toid,
- };
- // 先发送给自己 两个作用 1 告知对方服务端已经收到消息 2 用于对方通过 msgid 查询已读未读
- SendToSelf(msg);
- // 判断 toid 是 user 还是 group
- if (msg.IsGroup)
- {
- log("群消息:"+obj.content+", 发送者:"+UserGuid);
- // 那么要找出这个 group 的所有用户
- var group = _userService.GetGroup(toid);
- foreach (var user in group.Users)
- {
- // 除了发消息的本人
- // 群里的其他人都要收到消息
- if (user.UserGuid.ToString() != UserGuid)
- {
- SendToUser(user.UserGuid.ToString(), msg);
- }
- }
- }
- else
- {
- log("单消息:" + obj.content + ", 发送者:" + UserGuid);
- SendToUser(toid, msg);
- }
- //save message
- //_msgService.Insert(msg);
- break;
而 SendToUser 就可以将之前的缓存 Id 拿出来了
- private void SendToUser(string toId, Message msg)
- {
- var userKey = cacheManager.Get<string>(toId);
- // 这个判断可以拿掉 不存在的用户肯定不在线
- //var touser = _userService.GetUserByGuid(obj.toId);
- if (userKey != null)
- {
- // 发送给对方
- Sessions.SendTo(userKey, Json.JsonParser.Serialize(msg));
- }
- else
- {
- // 不需要通知对方
- //SendToSelf(toId + "还未上线!");
- }
- }
二收消息
收消息包含两个部分, 一个是发送回执, 一个是页面消息显示回执用来做已读未读显示的问题在于, 有历史消息, 有当前的消息有未读的消息, 不同人发的不同消息, 怎么呈现呢? 先说回执
回执
我定义的回执如下:
- public class Receipt
- {
- public Receipt()
- {
- CreateTime = DateTime.Now;
- ReceiptId = Guid.NewGuid().ToString().Replace("-", "");
- }
- [Key]
- public string ReceiptId { get; set; }
- public string MsgId { get; set; }
- /// <summary>
- /// user 的 guid
- /// </summary>
- public string UserId { get; set; }
- public DateTime CreateTime { get; set; }
- }
回执不同于消息对象, 不需要考虑是否是群的, 回执都是发送到个人的, 单聊的时候这个很好理解, A 发给 B,B 读了之后发个回执给 A,A 就知道 B 已读了那么 A 发到群里一条消息, 读了这条消息的人都把回执推送给 AA 就可以知道哪些人读了哪些人未读
js 的方法里面我传了一个 toid, 本质上是可以通过 message 对象查到用户的 id 的但我不想让后端去查询这个 id, 前端拿又很轻松
- // 这个 toid 是应该可以省略的, 因为可以通过 msgId 去获取
- // 目前这么做的理由就是避免服务端进行一次查询
- //toId 必须是 userId 也就是对应的 sender
- socketSDK.sendReceipt = function(toId, msgId) {
- var obj = {
- toId: toId,
- content: msgId,
- type: "003"
- }
- send(obj)
- }
- case "003":
- key = cacheManager.Get<string>(toid);
- var recepit = new Receipt()
- {
- MsgId = obj.content,
- UserId = UserGuid,
- };
- // 发送给 发回执的人, 告知服务端已经收到他的回执
- SendToSelf(recepit);
- if (key != null)
- {
- // 发送给对方
- await Sessions.SendTo(key, Json.JsonParser.Serialize(recepit));
- }
- // save recepit
- break;
这样前端拿到回执就能处理已读未读的效果了
消息呈现:
我采用的是每个对话对应一个 div, 这样切换自然, 不用每次都要渲染
当用户点击左边栏的时候, 就会在右侧插入一个. messages 的 div 包括当收到了消息还没有页面的时候, 也需要创建页面
- function leftsay(boxid, content, msgid) {
- // 这个 view 不一定打开了
- $box = $("#" + boxid);
- // 可以先放到隐藏的页面上去,
- word = $("<div class='msgcontent'>").html(content);
- warp = $("<div class='leftsay'>").attr("id", msgid).append(word);
- if ($box.length != 0) {
- $box.append(warp);
- } else {
- $box = $("<div class='messages'id=" + boxid + ">");
- $box.append(word);
- $("#messagesbox").append($box);
- }
- }
未读消息
当前页面不在 active 状态, 就不能发已读回执
- function unreadmark(friendId, count) {
- $("#" + friendId).find("span").remove();
- if (count == 0) {
- return;
- }
- var span = $("<span class='unreadnum'>").html(count);
- $("#"+friendId).append(span);
- }
- sdk.on("messages", function (data) {
- if (sdk.isSelf(data.senderid)) {
- // 自己说的
- // 肯定是当前对话
- // 照理说还要判断是不是当前的对话框
- data.list = [];// 为 msg 对象增加一个数组 用来存储回执
- if (data.isgroup)
- selfgroupmsg[data.msgid] = data;// 缓存群消息 用于处理回执
- rightsay(data.content, data.msgid);
- } else {
- // 别人说的
- // 不一定是当前对话, 就要从 ReceiverId 判断
- var _toid = data.senderid;
- if (!sdk.isSelf(data.receiverid)) {
- // 接受者不是自己 说明是群消息
- _toid = data.receiverid;
- }
- var boxid = _toid + viewkey;
- // 如果是当前会话就发送已读回执
- if (_toid == currentToId) {
- sdk.sendReceipt(data.senderid, data.msgid);
- } else {
- if (!msgscache[_toid]) {
- msgscache[_toid] = [];
- }
- // 存入未读列表
- msgscache[_toid].push(data);
- unreadmark(_toid, msgscache[_toid].length);
- }
- leftsay(boxid, data.content, data.msgid);
- }
- });
单聊的时候已读未读比较简单, 就判断这条消息是否收到了回执
$("#" + msgid).find(".unread").html("已读").addClass("ed");
但是群聊的时候, 显示的是几人未读, 而且要能够看到哪些人读了哪些人未读, 为了最大的减少查询, 在最初获取联系人列表的时候就需要将群的成员也一起带出来, 然后前端记录下每一条群消息的所收到的回执这样每收到一条就一个人而前端只需要缓存发送的群消息即可
- function readmsg(data) {
- // 区分是单聊还是群聊
- // 单聊就直接是已读
- var msgid = data.msgid;
- var rawmsg = selfgroupmsg[msgid];
- if (!rawmsg) {
- $("#" + msgid).find(".unread").html("已读").addClass("ed");
- }
- else {
- rawmsg.list.push(data);
- // 得到了这个群的信息
- var ginfo = groupinfo[rawmsg.receiverid];
- // 总的人数
- var total = ginfo.Users.length;
- // 找到原始的消息
- // 已读的人数
- var readcount = rawmsg.list.length;
- // 未读人数
- var unread = total - readcount-1;// 除去自己
- var txt = "已读";
- if (unread != 0) {
- txt = unread + "人未读";
- $("#" + msgid).find(".unread").html(txt);
- } else {
- $("#" + msgid).find(".unread").html(txt).addClass("ed");
- }
- }
- }
这样就可以显示几人未读了:
小结: 大致的流程已经走通, 但还有些问题, 比如历史消息和消息存储还没有处理, 文件发送, 另外还有对于一个用户他可能不止一个端, 要实现多屏同步, 这就需要缓存下每个用户所有的 WebSocketBehavior 对象 Id 后续继续完善
来源: https://www.cnblogs.com/stoneniqiu/p/8626931.html