博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
关于IM的一些思考与实践
阅读量:6892 次
发布时间:2019-06-27

本文共 10373 字,大约阅读时间需要 34 分钟。

简单的实现了一个聊天网页,但这个太简单,消息全广播,没有用户认证和已读未读处理,主要的意义是走通了做服务端的可能性。那么一个完整的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
_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
Users { get { return _users??(_users=new List
()); } 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
(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; }       ///        /// user的guid       ///        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
(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 = $("
").html(content); warp = $("
").attr("id", msgid).append(word); if ($box.length != 0) { $box.append(warp); } else { $box = $("
"); $box.append(word); $("#messagesbox").append($box); } }

未读消息

当前页面不在active状态,就不能发已读回执。

function unreadmark(friendId, count) {        $("#" + friendId).find("span").remove();        if (count == 0) {            return;        }        var span = $("").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。 后续继续完善。

 

转载地址:http://fnhbl.baihongyu.com/

你可能感兴趣的文章
jsf开发心得(3)-jsf应用中css运用背景图片显示不了的问题
查看>>
IOS UIAlertController 弹出框中添加视图(例如日期选择器等等)
查看>>
ubuntu 12.04 开启root
查看>>
WAR包制作
查看>>
XSS
查看>>
Java 线程学习
查看>>
acl_cpp 编程之 xml 流式解析与创建
查看>>
基于域的无线安全认证方案
查看>>
Thread类常用方法
查看>>
我的友情链接
查看>>
几乎所有编程语言的hello, world程序(3)
查看>>
CentOs 设置静态IP 方法
查看>>
Windows上Python2.7安装Scrapy过程
查看>>
学习记录:浏览器JAVASCRIPT里的WINDOWS,DOCUMNET
查看>>
Nginx内置变量以及日志格式变量参数详解
查看>>
Linux简单了解
查看>>
Importing Swift into Objective-C
查看>>
oracle merge同时包含增、删、改
查看>>
Docker 命令
查看>>
如何在andorid native layer中加log function.【转】
查看>>