該文基于開源項(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)方式:
用戶/客服和服務(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ā)送給綁定的客服突倍。
關(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ù)造寝。
超時(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ǔ)漏构资。