关于IM的一些思考与实践
上一篇简单的实现了一个聊天网页,但这个太简单,消息全广播,没有用户认证和已读未读处理,主要的意义是走通了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[];
if (!cacheManager.IsSet(infos[]))
{
cacheManager.Set(infos[], Id, );
}
//告之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 == ;
}
}
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; }
}
对于Message而言,主要就是SenderId,Content和ReceiverId,我通过ReceiverId来区分这条消息是发给个人的消息还是群消息。对于群Id是一个长度固定的字符串区别于用户的GUID。这样就可以实现群消息和个人消息的推送了:
case ""://正常聊天
//先检查是否合法
if (!IsValid)
{
SendToSelf("请先验证!","");
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发到群里一条消息,读了这条消息的人都把回执推送给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 "":
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。 后续继续完善。
关于IM的一些思考与实践的更多相关文章
- 腾讯IVWEB前端工程化工具feflow思考与实践
本篇文章主要介绍腾讯IVWEB团队从0到1在工程化的思考和实践.feflow的全称是Front-end flow(前端工作流),致力于提升研发效率和规范的工程化解决方案.愿景是通过feflow,可以使 ...
- 平安银行在开源技术选型上的思考和实践 RocketMQ
小结: 1. https://mp.weixin.qq.com/s/z_c5D8fvHaYvHSczm0nYFA 平安银行在开源技术选型上的思考和实践 平安银行·吴建峰 阿里巴巴中间件 3月7日 随着 ...
- [转] 基于NodeJS的前后端分离的思考与实践(五)多终端适配
前言 近年来各站点基于 Web 的多终端适配进行得如火如荼,行业间也发展出依赖各种技术的解决方案.有如基于浏览器原生 CSS3 Media Query 的响应式设计.基于云端智能重排的「云适配」方案等 ...
- Java Web前后端分离的思考与实践
第一节 Java Web开发方式的变化 Web开发虽然是我们常说的B/S模式,其实本质上也是一种特殊的C/S模式,只不过C和S的选择余地相对要窄了不少,而且更标准化.不论是采用什么浏览器和后端框架,W ...
- 【微信支付】分享一个失败的案例 跨域405(Method Not Allowed)问题 关于IM的一些思考与实践 基于WebSocketSharp 的IM 简单实现 【css3】旋转倒计时 【Html5】-- 塔台管制 H5情景意识 --飞机 谈谈转行
[微信支付]分享一个失败的案例 2018-06-04 08:24 by stoneniqiu, 2744 阅读, 29 评论, 收藏, 编辑 这个项目是去年做的,开始客户还在推广,几个月后发现服务器已 ...
- 【数据库】_由2000W多条开房数据引发的思考、实践----给在校生的一个真实【练耙场】,同学们,来开始一次伟大的尝试吧。
× 缘起---闲逛博客园 前几天的时候,在某一QQ群看到一条消息“XXX酒店开房XXXBTXX迅雷BT下载”,当时是一目十行的心态浏览,目光掠过时, 第一反应我想多了~以为是XX种子(你懂的~ ...
- 微信小程序web-view的简单思考和实践
微信小程序的组件web-view推出有一段时间了,这个组件的推出可以说是微信小程序开发的一个重要事件,让微信小程序不会只束缚在微信圈子里了,打开了一个口子,这个口子或许还比较小,但未来有无限可能. 简 ...
- 红灯区:DevOps 建设的思考和实践
点击关注"有赞coder" 获取更多技术干货哦- 作者:费解 团队:效能改进 背景 众所周知,在丰田精益生产中,核心观念包含对人的尊重.消除浪费.持续改善,只有这样,企业才能保持良 ...
- 万字长文---关于PKM收集与整理系统的思考和实践
PKM闭环中有一个很重要的环节就是信息输入,包括各种信息来源,例如微信公众号.博客.知乎.RSS等等,因此也就诞生了一大堆稍后读软件,如何真正有效的获取输入而不是做一只仓鼠是需要思考的.最近看了< ...
- 《前端们,贺老 Live 面试你了!》所引发的思考与实践
贺老在知乎live中提到了一个这样的问题: 产品经理提出了一个需求:用户点击文章阅读,返回之后阅读其他文章.当用户看得多了,容易点到自己看过的文章,造成时间浪费.所以想给点击过的文章加一个标记,如:& ...
随机推荐
- Nashorn——在JDK 8中融合Java与JavaScript之力
从JDK 6开始,Java就已经捆绑了JavaScript引擎,该引擎基于Mozilla的Rhino.该特性允许开发人员将JavaScript代码嵌入到Java中,甚至从嵌入的JavaScript ...
- 读取含有BOM头的文件遇到的问题
需求是读取一个csv文件,然后解析成对应的数据结构.csv必须包含指定的某些列,通过列名header来进行校验. 解析配置文件的方法. public List<QuestionData> ...
- FusionCharts封装-Category
Categories.java: /** * @Title:Categories.java * @Package:com.fusionchart.model * @Description:Fusion ...
- Android View绘制回调方法流程
Android中View的性命周期,挪用 invalidate() 战 requestLayout() 会触收哪些方式,一张图就可以讲解的很详细. 该图确切一看便特别很是清楚.让人简略的懂得View的 ...
- Java获取当前的时间
Java获取当前的时间 1.利用Java中的Calendar获取当前的时间 具体实现如下: /** * @Title:NowTime.java * @Package:com.you.model * @ ...
- python urllib和urllib3包使用
urllib包 urllib是一个包含几个模块来处理请求的库.分别是: urllib.request 发送http请求 urllib.error 处理请求过程中,出现的异常. urllib.parse ...
- Angular通过订阅观察者对象实现不同组件中数据的实时传递
在angular官方定义中,组件直接的数据交换只要在父子直接传递,但是我们在项目中经常需要在各种层级之间传递数据,下面介绍关于订阅可观察对象实现的数据传递. 首先定义一个服务app.sevice.ts ...
- [BZOJ1207] [HNOI2004] 打鼹鼠 (dp)
Description 鼹鼠是一种很喜欢挖洞的动物,但每过一定的时间,它还是喜欢把头探出到地面上来透透气的.根据这个特点阿Q编写了一个打鼹鼠的游戏:在一个n*n的网格中,在某些时刻鼹鼠会在某一个网格探 ...
- Spring Boot Security 基于角色的访问控制
@Override protected void configure(HttpSecurity http) throws Exception { //如果配置为需要登录 if (needLogin) ...
- datatable 多字段 排序;
没有找到datatable的排序方面运用案例,根据接口,自己实现 所以记录一二,小伙伴们有更好的方法,欢迎讨论 1.需求图 2.需求 1)默认 未开启 灰色下箭头 2)第一次点击 :启动排序,降序- ...