IM聊天实现思路及其采用 node+socket.io+elasticsearch实现的代码片段

本文将以问题的形式展示自己在IM开发项目中所遇到的问题及其相应解决方案.

一,技术选型
1.我采用了node运行环境(8.x)+socket.io(用于长连接)+elasticsearch(用于存储和检索消息).
2.由于js对于所有的IO操作都是异步的,并且socket.io(2.x)对于js有较好的执行性,能够通过十几行代码就能实现一个简单的聊天功能。
3.考虑到我们的聊天消息没有大量的更新和关联操作,我采用了基于lucene框架的elasticsearch,对消息进行批量存储和读取,并对消息进行持久化操作。并且可以很方便的对消息进行分析和实时搜索。

二,几点建议.

1.如果你想按照本文操作去做,应该熟练掌握ES6的语法,socket.io的2.x js的api用法,elasticsearch的DSL查询

2.请参考我的笔记和相关博客

1.node
1.es6入门 http://es6.ruanyifeng.com/
2.node.js官方api http://nodejs.cn/api/
(其中es6的async语法,node的订阅事件模型很重要)

2.socket
1.socket.io与js api 调用示例
http://blog.csdn.net/qq_26026975/article/details/75675132

3.elasticsearch
1.elasticsearch api 文档
https://es.xiaoleilu.com/
2.es学习笔记
http://note.youdao.com/noteshare?id=186c696920e08bcab54b2cf7f096a827

三,IM单聊解决方案

1.单聊整个过程

1.A用户登录至服务器 然后与服务器通过socket建立长连接

2.对A用户进行初始化操作,包括是否更新好友列表,是否有离线消息等,操作完成后,将删除离线消息标记

3.A用户发送消息至B用户,通过调用ajax请求至服务器,服务器进行消息存储,并判断B是否在线,若在线,则通过长连接推送请求告知B,有新消息,然后B请求服务器获取其消息详情.若B不在线,则直接存储离线标记

4.A用户退出登录时,删除用户缓存内的相关信息

2.怎样判断用户是否在线

1.当用户登录时,记录用户的socketId(若支持多个客户端登录,需存储多个socketId),退出时删除其socketId即可

2.在这里,我采用了redis进行存储处理,按照key(用户id)-value(socketId)的形式进行存储。

3.在初始化服务时,应该删除其redis内的所有socketid,避免服务器中断后,socketid还存在的情况。建议批量操作用lua脚本进行删除

    /**
     * 通过lua脚本  批量删除指定key值
     * 
     */
    static async delKeys(keys) {
        return await new Promise((resolve, reject) => {
            client.evalsha(keys, (err, data) => {
                if (err) {
                    reject(err);
                } else {
                    resolve(data);
                }
            });
        });
    }

3.消息id 为什么要递增

1.在存储消息时,消息id我采用了递增的形式,这样的好处在于,当我去读取离线标记文档时,我只需要存储第一条未读消息id即可,这样筛选时,我只需要大于等于其未读消息id即可。(我不用发送时间的原因在于,因为存储消息时异步操作,并且有网络原因,很有可能我的第二条发送消息时间会优先存储,所以用redis可以避免消息的顺序(因为redis单进程,并发时不会有影响))

2.实现方案,调用redis的incr key命令进行操作即可

    /**
     * @param  key 
     * @version 1.0 自增id 2017-7-12
     * 
     */
    static async incrKey(key) {

        return await new Promise((resolve, reject) => {
            client.incr(key, (err, data) => {
                if (err) {
                    reject(err);
                } else {
                    resolve(data);
                }
            });
        });
    }

4.个人离线消息怎么处理

1.对于个人离线消息,我单独建立了一个离线消息文档,包含信息如下

/**
 * 消息读取标记(消息列表)
 * 记录用户读取的消息
 * 
 * 当用户读取群消息时,更新标记已读消息id(若查找不到则视为刚加入群)
 * 
 * 
 * @param
 *  receiveID: Number,//接收人id
    senderID: Number,//群id或者发送人id
    lastReadMsgID: Number,// 最后一条已读消息id 或最前一条的未读消息id  0 表示刚加入群未有可读信息
    msgType : Number,//消息类型  0 个人   1群组  2全站
    msgLastTime : date,//最后一条读取消息时间 
    joinMsgId: Number,//加入时消息id
 * 
 */
class msgOfflineClass extends obj {

    constructor({ id,mid,receiveID, senderID,joinMsgId,lastReadMsgID = 0, msgType = 0, msgLastTime = new Date() }) {
        super({ id,mid,receiveID, senderID, joinMsgId,lastReadMsgID, msgType, msgLastTime: msgLastTime.getTime(), });
    }
}

2.当发送消息时,若离线会去校验对方是否存有离线标记,若有,则说明对方未读取消息,不做处理.否则则存储其离线标记信息

3.当登录时,仅获取其离线消息数目及其最新一条离线消息内容,不会做更新操作.只有进入消息详情时,才会去删除该用户对应的离线消息标记。

5.断线之后,怎样处理

1.对于前端,如果断线之后,可以通过监听reconnect方法即可


        //重连时自动请求是否有新消息
        socket.on('reconnect', () => {
            try {
                console.log('reconnect');
            } catch (e) {
            }
        });

        //断网提示
        socket.on('disconnect', function () {
            try {
                console.log('disconnect');
            } catch (e) {
            }
        });

2.重连后,后端会重新走一遍connection内自动执行的方法

6.消息处理机制怎样才能保证其可靠性

1.应该明确消息只有送达成功的概念,没有读取成功的状态。因为在读取消息后,不会再发送请求给服务器说,已去读取成功。因为如果接受到了消息,但是发送获取成功状态通知时,请求失败了.这样会陷入死循环。

2.针对第一点出现的情况,我们一般采用这样的方式解决,我们在每次通知客户端去获取消息的时候,会给客户端发送一个上一条消息的token(自定义的序列码)。这样在每次读取下一条消息的时候,我们会去核对上一条消息是否已送达,如果没有,则会向客户端补发消息。

3.我们应该明确用socket长连接仅仅是传送一些简单的状态指令消息,不应该用它去传递字节数据较多的数据。

四,IM群聊解决方案

1.单聊和群聊的区别有哪几点

1.群聊时,是一对多的关系,所以我们去通知群成员获取消息时,要是像单聊那样,还需要一个个判断其群成员是否在线,则显得过于繁琐
2.离线标记处理方式和单聊有区别,见第四点
3.群成员角色更加丰富,有成员等级,成员类型,权限等操作
4.在存储群消息时,仅存储一份,并且接收人设置为群id即可

2.怎样发送一对多的群消息

1.在用户登录时,会自动去查询所在群,然后利用socket特性,将该socket加入其群内(具体详见socket.io的rooms和namespace部分)

2.参考我的博文
http://blog.csdn.net/qq_26026975/article/details/75675132

3.群聊整个过程

1.当A用户登录时,会自动查询加入了哪些群,然后将其加入socket内

2.当A用户发送消息至群1时,服务器先存储该消息(发送人A,接收人群1id),然后调用socket方法(socket.to(群id))就能通知给加入该群的其他用户

3.若群1其他成员在线,则收到新消息通知后,会去请求服务器,获取其群消息内容,然后将离线标记更新至最新的一条已读消息id

4.在离线情况下,当该群1成员登录或重连后,获取其离线群消息列表。当读取群消息详情时,自动更新离线标记至最新一条已读消息id

4.群聊离线消息怎么处理

1.当用户加群时,会自动增加一条离线标记文档,并且与单聊离线标记文档不同的是,群离线标记会有一个加入时的消息id。所以该成员读取的消息范围为(加入时的上一条消息id,最新一条消息id]

2.当群成员获取详情时,会更新离线标记的已读消息id为最新消息id,无需和单聊一样,在发送时做判断处理

五,项目还需优化的几点

1.消息的缓存处理

我们应该对用户的好友列表,消息列表,接收到的消息做缓存处理.不应该每次登录时都从服务器端请求。比如,每次服务器端发送器好友列表时,应该给客户端一个好友列表版本号,每次仅需比对版本号,从而决定是否从服务器重新获取好友列表信息。另外对于消息,由于消息id时递增的,我们仅需比对其消息id和服务器端是否一致即可。

2.高并发情况下发送消息时解决方案

1.该项目是有痛点的,虽然我在发送消息和存储消息时,都做了异步处理,但对于高并发的请求,依然会有可能出现雪崩现象(如果你仅仅只是十万级的请求数量,可以不用考虑)

2.我们需要对发送和查询消息的请求,做队列化处理,防止雪崩事件的产生,并且对群消息的查询做缓存化处理。

3.控制用户发送消息的频率

对于群成员较多,如一万人的大群而言,我们需要限制其发送消息的频率,不然服务器发送群消息时虽然只执行了一次存储操作,但是执行了一万次的查询操作(需分发给一万个用户(若同时在线))。所以我们需要对用户进行频率限制,来确保消息的可靠性和稳定性。

六,结语

1.本次IM的demo,我花了将近一个月的时间来实现群聊和单聊(包含其学习成本),我也查阅了很多资料,在此我表示,如果大家想要提高自己的水平,最好将官方api当做自己的第一手资料。然后我想说,elasticsearch虽然很不错,但是坑很多,学习成本比较大,需要静下心来好好看

2.在做这种项目之前,最好上网查查,看看别人的思路和设计,会避免很多弯路。这也是我为什么侧重讲思路而不是大篇幅贴代码的原因。另外,在确定好思路后,我们必须得将项目分成一个个的点,然后逐个攻破,这样就能控制项目进度了

3.作为一名合格的开发,需要对压力测试,单元测试有深入的了解

发布了21 篇原创文章 · 获赞 18 · 访问量 9万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览