关于IM的一些思考与实践

上一篇简单的实现了一个聊天网页,但这个太简单,消息全广播,没有用户认证和已读未读处理,主要的意义是走通了websocket-sharp作服务端的可能性。那么一个完整的IM还须要实现哪些部分?html

1、发消息

用户A想要发给用户B,首先是将消息推送到服务器,服务器将拿到的toid和内容包装成一个完整的message对象,分别推送给客户B和客户A。为何也要推送给A呢,由于A也须要知道是否推送成功,以及拿到了messageId能够用来作后面的已读未读功能。前端

这里有两个问题还要解决,第一个是Server如何推送到客户B,另一个问题是群消息如何处理?git

实现推送

先解决第一个问题,在Server端,每次链接都会建立一个WebSocketBehavior对象,每一个WebSocketBehavior都有一个惟一的Id,若是用户在线咱们就能够推送过去:github

 Sessions.SendTo(userKey, Json.JsonParser.Serialize(msg));

须要解决的是须要将用户的Id和WebSocketBehavior的Id关联起来,因此这就要求每一个用户链接以后须要立刻验证。因此用户的流程以下:web

因为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 + "还未上线!");
            }
        }

2、收消息

收消息包含两个部分,一个是发送回执,一个是页面消息显示。回执用来作已读未读。显示的问题在于,有历史消息,有当前的消息有未读的消息,不一样人发的不一样消息,怎么呈现呢?先说回执

回执

我定义的回执以下:

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发到群里一条消息,读了这条消息的人都把回执推送给A。A就能够知道哪些人读了哪些人未读。

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。 后续继续完善。

相关文章
相关标签/搜索