基于netty+websocket的客服聊天IM系统

该文基于开源项目分析,总结了IM相关的一些知识点,如何实现,以及针对客服业务需要补充的几个点。
开源系统使用netty+websocket/socket搭建IM系统,前端实现了jsp和layui,服务端内容较完整,前端可根据自己实际情况搭建。
感谢开源项目的贡献。地址:
https://gitee.com/qiqiim/qiqiim-server

IM服务

1.网络协议

传输层

tcp

面向连接的、可靠的、基于字节流的传输层通信协议,keepalive 机制、ack机制保障连接和消息的可靠性。

应用层

websocket

在TCP连接上进行全双工通信的协议,允许服务端主动向客户端推送数据。适用于IM即时通讯。

http

短连接,业务操作类接口。

2.数据传输格式

protobuf,适用于高并发场景下的消息传输

使用场景:
用户A发送消息时,前端通过protobuf序列化消息,将其send到服务端,服务端接收到后反序列化消息,处理完成后再次序列化消息,发送给客服B,接收到消息时同样也要反序列化消息展示。

ps:客服聊天系统中用到的消息类型为绑定、心跳、普通消息,不同场景下发送的消息类型不同。

项目中用到的消息格式:
消息包 Message.proto,其中content为下面的消息内容:

syntax = "proto3";
package com.black.services.customerIm.common.model.proto;
option java_outer_classname="MessageProto";
message Model {
     string version = 1;//接口版本号
     string deviceId = 2;//设备uuid
     uint32 cmd = 3;//请求接口命令字  1绑定  2心跳  3上线  4下线 5消息
     string sender = 4;//发送人
     string receiver = 5;//接收人
     string groupId =6;//用户组编号(暂时可忽略)
     uint32 msgtype = 7;//请求1,应答2,通知3,响应4  format
     uint32 flag = 8;//1 rsa加密 2aes加密
     string platform = 9;//mobile-ios mobile-android pc-windows pc-mac
     string platformVersion = 10;//客户端版本号
     string token = 11;//客户端凭证
     string appKey = 12;//客户端key
     string timeStamp = 13;//时间戳
     string sign = 14;//签名
     bytes content = 15;//请求数据
}

消息内容 MessageBody.proto:

syntax = "proto3";
package com.black.services.customerIm.common.model.proto;
option java_outer_classname="MessageBodyProto";
 
message MessageBody {
    string title = 1; //标题
    string content = 2;//内容
    string time = 3;//发送时间
    uint32 type = 4;//0 文字   1 文件
    string extend = 5;//扩展字段
}

开发可以自定义proto格式(上面.proto那种格式),然后通过protoc命令生成对应的java文件,protoc安装方式:http://google.github.io/proto-lens/installing-protoc.html

前端js库:https://github.com/protocolbuffers/protobuf/tree/master/js

3.连接可靠性

实现心跳保活

websocket受到nginx缺省为60秒的proxy_read_timeout的影响,超过时间没有发送任何消息,连接会自动断开。

解决办法:服务端在连接没有消息传输后,到达一定时间后发送心跳包,客户端收到心跳包回一个响应包,如果心跳发送一定时间后还未收到响应,则关闭连接。

netty使用IdleStateHandler处理,设置readIdleTime(读超时时间)和writeIdleTime(写超时时间),当读超时触发后发送心跳包到客户端(浏览器),客户端(浏览器)收到心跳包后回复一个心跳回应包(需要前端监听类型为心跳包的消息,收到后发送心跳回应);如果服务端心跳请求发出后一定时间内未收到回复,可断开连接。

服务端超时触发的代码:

/**
 * 超时触发此方法
 *
 * @param ctx
 * @param o
 * @throws Exception
 */
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object o) throws Exception {
    // 服务端发个心跳包,客户端要回一个才行(需要前端实现)
    if (o instanceof IdleStateEvent && ((IdleStateEvent) o).state().equals(IdleState.WRITER_IDLE)) {
        if (StringUtils.isNotEmpty(sessionId)) {
            MessageProto.Model.Builder builder = MessageProto.Model.newBuilder();
            builder.setCmd(NettyConstants.CmdType.HEARTBEAT);//心跳包
            builder.setMsgtype(NettyConstants.ProtobufType.SEND);
            ctx.channel().writeAndFlush(builder);
        }
    }
    //如果心跳请求发出70秒内没收到响应,则关闭连接
    if (o instanceof IdleStateEvent && ((IdleStateEvent) o).state().equals(IdleState.READER_IDLE)) {
        //服务端收到上一次的心跳响应后会设置这个响应时间
        Long lastTime = (Long) ctx.channel().attr(NettyConstants.SessionConfig.SERVER_SESSION_HEARBEAT).get();
        if (lastTime == null || ((System.currentTimeMillis() - lastTime) / 1000 >= 70)) {
            connertor.close(ctx);
        }
    }
}

前端心跳响应:

socket.onmessage = function(event) {
      //后端发送的是二进制帧,protobuf反序列化
      var msg = proto.Model.deserializeBinary(event.data);
      //心跳消息
      if(msg.getCmd()==2){//对应服务端的NettyConstants.CmdType.HEARTBEAT
          //发送心跳回应
          var message1 = new proto.Model();
          message1.setCmd(2);
          message1.setMsgtype(4);
          socket.send(message1.serializeBinary());
      }else {
          //...
      }
};

断线重连

当网络情况不稳定,或者用户从移动网切换到无线网等场景下,长连接会断开。需要前端监听websocket的onclose事件,当连接断开后重新创建连接。

socket.onclose = function(event) {
    //重新创建websocket连接
};

4.消息可靠性

1.基于TCP,传输层已经保证了消息可靠性

2.应用层消息可靠性,实现Ack消息机制(待补充)

5.安全

ssl,http协议升级为https,对应的ws协议升级为wss

注意:当升级ssl后,前端通过"ws://"开头的url创建不了连接,需要修改成wss;并且nginx增加对应配置。

6.负载均衡

nginx

nginx应用层负载,支持websocket,原因是websocket在创建长连接之前,会通过一次http握手升级。

lvs(待研究)

抗负载能力强,工作在网络四层,仅作流量分发,几乎可以对所有应用做负载均衡。

7.netty服务

服务端由netty搭建:

1.基于nio,支持高并发,可维持大数量的长连接

2.本身支持websocket协议,自带websocket的处理器,方便开发

im聊天实现方式:

image.png

用户/客服和服务端之间的连接是netty中的channel,所有聊天的消息写入到channel中,当A给B发送消息后,ChannelInboundHandler从A和netty服务端连接的channel中读取到数据,然后解析消息获取消息的接收者B,再将消息写入B和服务端连接的channel。反之亦然。

8.数据库

1.mysql消息持久化

2.消息较多,需要考虑分表分库

9.缓存

用户进入聊天页面时,是可以和机器人或者人工客服聊天的;默认是机器人聊天,当用户输入“人工”时,切换成人工客服聊天,此时需要在缓存中保存用户的会话状态,来区别用户发送的消息是触达机器人还是客服。
考虑到集群部署,可选择在redis中维护会话状态以及客服的在线状态。
|

业务实现

1.app端开始聊天

1.用户进入聊天页面,new WebSocket,创建和服务端端的长连接。
2.前端监听连接到成功事件,并给连接的服务端发送一条消息,该消息包括用户的信息,消息类型是“绑定”
3.服务端判断此用户的会话状态,如果处于人工客服中,将缓存中的会话id取出,通过会话id查询出消息历史,发送给前端;如果不处于人工客服,给用户发送欢迎语。
4.用户发送消息,消息类型是“普通”,服务端接收到消息,判断用户会话状态。

a.用户不处于人工会话状态,且用户没有发送“人工”二字,此时调用机器人服务,将消息发送给机器人,得到的结果写到用户的channel中,结果发送给前端。
b.用户不处于人工会话状态,但用户发送“人工”二字,通过redis中客服在线状态,获取空闲客服,和用户之间创建绑定关系,关系存入redis中。(需要考虑队列排队,等待空闲客服的场景)
c.用户处于人工会话状态,直接将消息发送给绑定的客服。

image.png

关闭会话状态

分为两种,客服主动关闭,会话超时关闭。

客服主动关闭

1.客服主动发起关闭会话请求,该请求协议为http/https。前端设置聊天框无法输入,消息无法点击发送。
2.在redis中删除用户和客服绑定关系,删除当前客服的聊天用户列表中的对应用户。
3.给用户推送一条满意度调查消息。(如果不希望重复发送满意度调查,可以维护满意度调查的发送次数)
4.用户可选择填写满意度,满意度调查为http/https接口,后续场景有如下情况:

a.离开聊天页面,会断开连接,心跳响应超时服务端会将用户从redis的登录人员集中删除。
b.留在当前页面,继续聊天则会路由到机器人回复。


image.png
超时关闭

设置超时关闭时长,系统自动删除用户会话状态

定时遍历客服聊天用户集合,获取到用户最近一次聊天的时间戳(可以将用户最近一次聊天的时间戳放入redis的有序集合中,设置member为userId,时间戳为score值),当超过超时时间后,关闭会话状态,流程和客服主动关闭相同。

消息推送

对接push厂商通道,在非聊天页面进行消息推送。

总结

针对于项目实际业务场景,还是有很多地方需要完善。比如所有客服繁忙时,用户需要在队列中排队等待;比如如何实现应用层的ack机制保证消息不丢失。
该文需要补充的地方还有很多,才能完成真实的业务场景。欢迎大家查缺补漏。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容

  • 什么是tcp?tcp简称传输控制协议,提供的是面向连接,可靠的字节流服务。客户和服务器彼此交换数据前,必须先在双方...
    影子1997阅读 288评论 0 0
  • 点击查看原文 Web SDK 开发手册 SDK 概述 网易云信 SDK 为 Web 应用提供一个完善的 IM 系统...
    layjoy阅读 13,336评论 0 15
  • https://blog.csdn.net/rogerjava/article/details/9418211 H...
    Albert陈凯阅读 230评论 0 0
  • 计算机网络概述 网络编程的实质就是两个(或多个)设备(例如计算机)之间的数据传输。 按照计算机网络的定义,通过一定...
    蛋炒饭_By阅读 1,174评论 0 10
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,099评论 18 139