關於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。 後續繼續完善。

相關文章
相關標籤/搜索