基于netty+websocket的客服聊天IM系統(tǒng)

該文基于開源項(xiàng)目分析慕淡,總結(jié)了IM相關(guān)的一些知識點(diǎn)品洛,如何實(shí)現(xiàn)叭莫,以及針對客服業(yè)務(wù)需要補(bǔ)充的幾個(gè)點(diǎn)肯夏。
開源系統(tǒng)使用netty+websocket/socket搭建IM系統(tǒng)经宏,前端實(shí)現(xiàn)了jsp和layui,服務(wù)端內(nèi)容較完整驯击,前端可根據(jù)自己實(shí)際情況搭建烁兰。
感謝開源項(xiàng)目的貢獻(xiàn)。地址:
https://gitee.com/qiqiim/qiqiim-server

IM服務(wù)

1.網(wǎng)絡(luò)協(xié)議

傳輸層

tcp

面向連接的徊都、可靠的沪斟、基于字節(jié)流的傳輸層通信協(xié)議,keepalive 機(jī)制暇矫、ack機(jī)制保障連接和消息的可靠性币喧。

應(yīng)用層

websocket

在TCP連接上進(jìn)行全雙工通信的協(xié)議轨域,允許服務(wù)端主動(dòng)向客戶端推送數(shù)據(jù)。適用于IM即時(shí)通訊杀餐。

http

短連接干发,業(yè)務(wù)操作類接口。

2.數(shù)據(jù)傳輸格式

protobuf史翘,適用于高并發(fā)場景下的消息傳輸

使用場景:
用戶A發(fā)送消息時(shí)枉长,前端通過protobuf序列化消息,將其send到服務(wù)端琼讽,服務(wù)端接收到后反序列化消息必峰,處理完成后再次序列化消息,發(fā)送給客服B钻蹬,接收到消息時(shí)同樣也要反序列化消息展示吼蚁。

ps:客服聊天系統(tǒng)中用到的消息類型為綁定、心跳问欠、普通消息肝匆,不同場景下發(fā)送的消息類型不同。

項(xiàng)目中用到的消息格式:
消息包 Message.proto顺献,其中content為下面的消息內(nèi)容:

syntax = "proto3";
package com.black.services.customerIm.common.model.proto;
option java_outer_classname="MessageProto";
message Model {
     string version = 1;//接口版本號
     string deviceId = 2;//設(shè)備uuid
     uint32 cmd = 3;//請求接口命令字  1綁定  2心跳  3上線  4下線 5消息
     string sender = 4;//發(fā)送人
     string receiver = 5;//接收人
     string groupId =6;//用戶組編號(暫時(shí)可忽略)
     uint32 msgtype = 7;//請求1旗国,應(yīng)答2,通知3注整,響應(yīng)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;//時(shí)間戳
     string sign = 14;//簽名
     bytes content = 15;//請求數(shù)據(jù)
}

消息內(nèi)容 MessageBody.proto:

syntax = "proto3";
package com.black.services.customerIm.common.model.proto;
option java_outer_classname="MessageBodyProto";
 
message MessageBody {
    string title = 1; //標(biāo)題
    string content = 2;//內(nèi)容
    string time = 3;//發(fā)送時(shí)間
    uint32 type = 4;//0 文字   1 文件
    string extend = 5;//擴(kuò)展字段
}

開發(fā)可以自定義proto格式(上面.proto那種格式)能曾,然后通過protoc命令生成對應(yīng)的java文件,protoc安裝方式:http://google.github.io/proto-lens/installing-protoc.html

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

3.連接可靠性

實(shí)現(xiàn)心跳敝坠欤活

websocket受到nginx缺省為60秒的proxy_read_timeout的影響寿冕,超過時(shí)間沒有發(fā)送任何消息,連接會自動(dòng)斷開椒袍。

解決辦法:服務(wù)端在連接沒有消息傳輸后驼唱,到達(dá)一定時(shí)間后發(fā)送心跳包,客戶端收到心跳包回一個(gè)響應(yīng)包槐沼,如果心跳發(fā)送一定時(shí)間后還未收到響應(yīng)曙蒸,則關(guān)閉連接捌治。

netty使用IdleStateHandler處理岗钩,設(shè)置readIdleTime(讀超時(shí)時(shí)間)和writeIdleTime(寫超時(shí)時(shí)間),當(dāng)讀超時(shí)觸發(fā)后發(fā)送心跳包到客戶端(瀏覽器)肖油,客戶端(瀏覽器)收到心跳包后回復(fù)一個(gè)心跳回應(yīng)包(需要前端監(jiān)聽類型為心跳包的消息兼吓,收到后發(fā)送心跳回應(yīng));如果服務(wù)端心跳請求發(fā)出后一定時(shí)間內(nèi)未收到回復(fù)森枪,可斷開連接视搏。

服務(wù)端超時(shí)觸發(fā)的代碼:

/**
 * 超時(shí)觸發(fā)此方法
 *
 * @param ctx
 * @param o
 * @throws Exception
 */
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object o) throws Exception {
    // 服務(wù)端發(fā)個(gè)心跳包审孽,客戶端要回一個(gè)才行(需要前端實(shí)現(xiàn))
    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);
        }
    }
    //如果心跳請求發(fā)出70秒內(nèi)沒收到響應(yīng),則關(guān)閉連接
    if (o instanceof IdleStateEvent && ((IdleStateEvent) o).state().equals(IdleState.READER_IDLE)) {
        //服務(wù)端收到上一次的心跳響應(yīng)后會設(shè)置這個(gè)響應(yīng)時(shí)間
        Long lastTime = (Long) ctx.channel().attr(NettyConstants.SessionConfig.SERVER_SESSION_HEARBEAT).get();
        if (lastTime == null || ((System.currentTimeMillis() - lastTime) / 1000 >= 70)) {
            connertor.close(ctx);
        }
    }
}

前端心跳響應(yīng):

socket.onmessage = function(event) {
      //后端發(fā)送的是二進(jìn)制幀浑娜,protobuf反序列化
      var msg = proto.Model.deserializeBinary(event.data);
      //心跳消息
      if(msg.getCmd()==2){//對應(yīng)服務(wù)端的NettyConstants.CmdType.HEARTBEAT
          //發(fā)送心跳回應(yīng)
          var message1 = new proto.Model();
          message1.setCmd(2);
          message1.setMsgtype(4);
          socket.send(message1.serializeBinary());
      }else {
          //...
      }
};

斷線重連

當(dāng)網(wǎng)絡(luò)情況不穩(wěn)定佑力,或者用戶從移動(dòng)網(wǎng)切換到無線網(wǎng)等場景下,長連接會斷開筋遭。需要前端監(jiān)聽websocket的onclose事件打颤,當(dāng)連接斷開后重新創(chuàng)建連接。

socket.onclose = function(event) {
    //重新創(chuàng)建websocket連接
};

4.消息可靠性

1.基于TCP漓滔,傳輸層已經(jīng)保證了消息可靠性

2.應(yīng)用層消息可靠性编饺,實(shí)現(xiàn)Ack消息機(jī)制(待補(bǔ)充)

5.安全

ssl,http協(xié)議升級為https响驴,對應(yīng)的ws協(xié)議升級為wss

注意:當(dāng)升級ssl后透且,前端通過"ws://"開頭的url創(chuàng)建不了連接,需要修改成wss豁鲤;并且nginx增加對應(yīng)配置秽誊。

6.負(fù)載均衡

nginx

nginx應(yīng)用層負(fù)載,支持websocket畅形,原因是websocket在創(chuàng)建長連接之前养距,會通過一次http握手升級。

lvs(待研究)

抗負(fù)載能力強(qiáng)日熬,工作在網(wǎng)絡(luò)四層棍厌,僅作流量分發(fā),幾乎可以對所有應(yīng)用做負(fù)載均衡竖席。

7.netty服務(wù)

服務(wù)端由netty搭建:

1.基于nio耘纱,支持高并發(fā),可維持大數(shù)量的長連接

2.本身支持websocket協(xié)議毕荐,自帶websocket的處理器束析,方便開發(fā)

im聊天實(shí)現(xiàn)方式:

image.png

用戶/客服和服務(wù)端之間的連接是netty中的channel,所有聊天的消息寫入到channel中憎亚,當(dāng)A給B發(fā)送消息后员寇,ChannelInboundHandler從A和netty服務(wù)端連接的channel中讀取到數(shù)據(jù),然后解析消息獲取消息的接收者B第美,再將消息寫入B和服務(wù)端連接的channel蝶锋。反之亦然。

8.數(shù)據(jù)庫

1.mysql消息持久化

2.消息較多什往,需要考慮分表分庫

9.緩存

用戶進(jìn)入聊天頁面時(shí)扳缕,是可以和機(jī)器人或者人工客服聊天的;默認(rèn)是機(jī)器人聊天,當(dāng)用戶輸入“人工”時(shí)躯舔,切換成人工客服聊天驴剔,此時(shí)需要在緩存中保存用戶的會話狀態(tài),來區(qū)別用戶發(fā)送的消息是觸達(dá)機(jī)器人還是客服粥庄。
考慮到集群部署丧失,可選擇在redis中維護(hù)會話狀態(tài)以及客服的在線狀態(tài)。
|

業(yè)務(wù)實(shí)現(xiàn)

1.app端開始聊天

1.用戶進(jìn)入聊天頁面惜互,new WebSocket利花,創(chuàng)建和服務(wù)端端的長連接。
2.前端監(jiān)聽連接到成功事件载佳,并給連接的服務(wù)端發(fā)送一條消息炒事,該消息包括用戶的信息,消息類型是“綁定”
3.服務(wù)端判斷此用戶的會話狀態(tài)蔫慧,如果處于人工客服中挠乳,將緩存中的會話id取出,通過會話id查詢出消息歷史姑躲,發(fā)送給前端睡扬;如果不處于人工客服,給用戶發(fā)送歡迎語黍析。
4.用戶發(fā)送消息卖怜,消息類型是“普通”,服務(wù)端接收到消息阐枣,判斷用戶會話狀態(tài)马靠。

a.用戶不處于人工會話狀態(tài),且用戶沒有發(fā)送“人工”二字蔼两,此時(shí)調(diào)用機(jī)器人服務(wù)甩鳄,將消息發(fā)送給機(jī)器人,得到的結(jié)果寫到用戶的channel中额划,結(jié)果發(fā)送給前端妙啃。
b.用戶不處于人工會話狀態(tài),但用戶發(fā)送“人工”二字俊戳,通過redis中客服在線狀態(tài)揖赴,獲取空閑客服,和用戶之間創(chuàng)建綁定關(guān)系抑胎,關(guān)系存入redis中燥滑。(需要考慮隊(duì)列排隊(duì),等待空閑客服的場景)
c.用戶處于人工會話狀態(tài)圆恤,直接將消息發(fā)送給綁定的客服突倍。

image.png

關(guān)閉會話狀態(tài)

分為兩種,客服主動(dòng)關(guān)閉盆昙,會話超時(shí)關(guān)閉羽历。

客服主動(dòng)關(guān)閉

1.客服主動(dòng)發(fā)起關(guān)閉會話請求,該請求協(xié)議為http/https淡喜。前端設(shè)置聊天框無法輸入秕磷,消息無法點(diǎn)擊發(fā)送。
2.在redis中刪除用戶和客服綁定關(guān)系炼团,刪除當(dāng)前客服的聊天用戶列表中的對應(yīng)用戶澎嚣。
3.給用戶推送一條滿意度調(diào)查消息。(如果不希望重復(fù)發(fā)送滿意度調(diào)查瘟芝,可以維護(hù)滿意度調(diào)查的發(fā)送次數(shù))
4.用戶可選擇填寫滿意度易桃,滿意度調(diào)查為http/https接口,后續(xù)場景有如下情況:

a.離開聊天頁面锌俱,會斷開連接晤郑,心跳響應(yīng)超時(shí)服務(wù)端會將用戶從redis的登錄人員集中刪除。
b.留在當(dāng)前頁面贸宏,繼續(xù)聊天則會路由到機(jī)器人回復(fù)造寝。


image.png
超時(shí)關(guān)閉

設(shè)置超時(shí)關(guān)閉時(shí)長,系統(tǒng)自動(dòng)刪除用戶會話狀態(tài)

定時(shí)遍歷客服聊天用戶集合吭练,獲取到用戶最近一次聊天的時(shí)間戳(可以將用戶最近一次聊天的時(shí)間戳放入redis的有序集合中诫龙,設(shè)置member為userId,時(shí)間戳為score值)鲫咽,當(dāng)超過超時(shí)時(shí)間后签赃,關(guān)閉會話狀態(tài),流程和客服主動(dòng)關(guān)閉相同分尸。

消息推送

對接push廠商通道姊舵,在非聊天頁面進(jìn)行消息推送。

總結(jié)

針對于項(xiàng)目實(shí)際業(yè)務(wù)場景寓落,還是有很多地方需要完善括丁。比如所有客服繁忙時(shí),用戶需要在隊(duì)列中排隊(duì)等待伶选;比如如何實(shí)現(xiàn)應(yīng)用層的ack機(jī)制保證消息不丟失史飞。
該文需要補(bǔ)充的地方還有很多,才能完成真實(shí)的業(yè)務(wù)場景仰税。歡迎大家查缺補(bǔ)漏构资。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市陨簇,隨后出現(xiàn)的幾起案子吐绵,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件己单,死亡現(xiàn)場離奇詭異唉窃,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)纹笼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進(jìn)店門纹份,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人廷痘,你說我怎么就攤上這事蔓涧。” “怎么了笋额?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵元暴,是天一觀的道長。 經(jīng)常有香客問我兄猩,道長昨寞,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任厦滤,我火速辦了婚禮援岩,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘掏导。我一直安慰自己享怀,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布趟咆。 她就那樣靜靜地躺著添瓷,像睡著了一般。 火紅的嫁衣襯著肌膚如雪值纱。 梳的紋絲不亂的頭發(fā)上鳞贷,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天,我揣著相機(jī)與錄音虐唠,去河邊找鬼搀愧。 笑死,一個(gè)胖子當(dāng)著我的面吹牛疆偿,可吹牛的內(nèi)容都是我干的咱筛。 我是一名探鬼主播,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼杆故,長吁一口氣:“原來是場噩夢啊……” “哼迅箩!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起处铛,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤饲趋,失蹤者是張志新(化名)和其女友劉穎拐揭,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體奕塑,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡堂污,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了爵川。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,703評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡息楔,死狀恐怖寝贡,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情值依,我是刑警寧澤圃泡,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站愿险,受9級特大地震影響颇蜡,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜辆亏,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一风秤、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧扮叨,春花似錦缤弦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至衷蜓,卻和暖如春累提,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背磁浇。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工斋陪, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人置吓。 一個(gè)月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓鳍贾,卻偏偏與公主長得像,于是被迫代替她去往敵國和親交洗。 傳聞我的和親對象是個(gè)殘疾皇子骑科,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評論 2 353

推薦閱讀更多精彩內(nèi)容

  • 什么是tcp?tcp簡稱傳輸控制協(xié)議,提供的是面向連接构拳,可靠的字節(jié)流服務(wù)咆爽×禾模客戶和服務(wù)器彼此交換數(shù)據(jù)前,必須先在雙方...
    影子1997閱讀 337評論 0 0
  • 點(diǎn)擊查看原文 Web SDK 開發(fā)手冊 SDK 概述 網(wǎng)易云信 SDK 為 Web 應(yīng)用提供一個(gè)完善的 IM 系統(tǒng)...
    layjoy閱讀 13,758評論 0 15
  • https://blog.csdn.net/rogerjava/article/details/9418211 H...
    Albert陳凱閱讀 254評論 0 0
  • 計(jì)算機(jī)網(wǎng)絡(luò)概述 網(wǎng)絡(luò)編程的實(shí)質(zhì)就是兩個(gè)(或多個(gè))設(shè)備(例如計(jì)算機(jī))之間的數(shù)據(jù)傳輸斗埂。 按照計(jì)算機(jī)網(wǎng)絡(luò)的定義符糊,通過一定...
    蛋炒飯_By閱讀 1,220評論 0 10
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)呛凶,斷路器男娄,智...
    卡卡羅2017閱讀 134,651評論 18 139