本文將以問題的形式展示自己在IM開發(fā)項(xiàng)目中所遇到的問題及其相應(yīng)解決方案.
一im軟件1個聊天就是一個長連接嗎?,技術(shù)選型
1.我采用了node運(yùn)行環(huán)境(8.x)+.io(用于長連接)+(用于存儲和檢索消息).
2.由于js對于所有的IO操作都是異步的,并且.io(2.x)對于js有較好的執(zhí)行性,能夠通過十幾行代碼就能實(shí)現(xiàn)一個簡單的聊天功能。
3.考慮到我們的聊天消息沒有大量的更新和關(guān)聯(lián)操作,我采用了基于框架的,對消息進(jìn)行批量存儲和讀取,并對消息進(jìn)行持久化操作。并且可以很方便的對消息進(jìn)行分析和實(shí)時搜索。
二,幾點(diǎn)建議.
1.如果你想按照本文操作去做,應(yīng)該熟練掌握ES6的語法,.io的2.x js的api用法,的DSL查詢
2.請參考我的筆記和相關(guān)博客
1.node
1.es6入門
2.node.js官方api
(其中es6的async語法,node的訂閱事件模型很重要)
2.
1..io與js api 調(diào)用示例
3.
1. api 文檔
2.es學(xué)習(xí)筆記
三,IM單聊解決方案
1.單聊整個過程
1.A用戶登錄至服務(wù)器 然后與服務(wù)器通過建立長連接
2.對A用戶進(jìn)行初始化操作,包括是否更新好友列表,是否有離線消息等,操作完成后,將刪除離線消息標(biāo)記
3.A用戶發(fā)送消息至B用戶,通過調(diào)用ajax請求至服務(wù)器,服務(wù)器進(jìn)行消息存儲,并判斷B是否在線,若在線,則通過長連接推送請求告知B,有新消息,然后B請求服務(wù)器獲取其消息詳情.若B不在線,則直接存儲離線標(biāo)記
4.A用戶退出登錄時,刪除用戶緩存內(nèi)的相關(guān)信息
2.怎樣判斷用戶是否在線
1.當(dāng)用戶登錄時,記錄用戶的(若支持多個客戶端登錄,需存儲多個),退出時刪除其即可
2.在這里,我采用了redis進(jìn)行存儲處理,按照key(用戶id)-value()的形式進(jìn)行存儲。
3.在初始化服務(wù)時,應(yīng)該刪除其redis內(nèi)的所有,避免服務(wù)器中斷后,還存在的情況。建議批量操作用lua腳本進(jìn)行刪除
/**
* 通過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我采用了遞增的形式,這樣的好處在于,當(dāng)我去讀取離線標(biāo)記文檔時,我只需要存儲第一條未讀消息id即可,這樣篩選時,我只需要大于等于其未讀消息id即可。(我不用發(fā)送時間的原因在于,因?yàn)榇鎯οr異步操作,并且有網(wǎng)絡(luò)原因,很有可能我的第二條發(fā)送消息時間會優(yōu)先存儲,所以用redis可以避免消息的順序(因?yàn)閞edis單進(jìn)程,并發(fā)時不會有影響))
2.實(shí)現(xiàn)方案im軟件1個聊天就是一個長連接嗎?,調(diào)用redis的incr key命令進(jìn)行操作即可
/**

* @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.對于個人離線消息,我單獨(dú)建立了一個離線消息文檔,包含信息如下
/**
* 消息讀取標(biāo)記(消息列表)
* 記錄用戶讀取的消息
*
* 當(dāng)用戶讀取群消息時,更新標(biāo)記已讀消息id(若查找不到則視為剛加入群)

*
*
* @param
* receiveID: Number,//接收人id
senderID: Number,//群id或者發(fā)送人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.當(dāng)發(fā)送消息時,若離線會去校驗(yàn)對方是否存有離線標(biāo)記,若有,則說明對方未讀取消息,不做處理.否則則存儲其離線標(biāo)記信息
3.當(dāng)?shù)卿洉r,僅獲取其離線消息數(shù)目及其最新一條離線消息內(nèi)容,不會做更新操作.只有進(jìn)入消息詳情時,才會去刪除該用戶對應(yīng)的離線消息標(biāo)記。
5.斷線之后,怎樣處理
1.對于前端,如果斷線之后,可以通過監(jiān)聽方法即可
//重連時自動請求是否有新消息

socket.on('reconnect', () => {
try {
console.log('reconnect');
} catch (e) {
}
});
//斷網(wǎng)提示
socket.on('disconnect', function () {
try {
console.log('disconnect');
} catch (e) {
}
});
2.重連后,后端會重新走一遍內(nèi)自動執(zhí)行的方法
6.消息處理機(jī)制怎樣才能保證其可靠性
1.應(yīng)該明確消息只有送達(dá)成功的概念,沒有讀取成功的狀態(tài)。因?yàn)樵谧x取消息后,不會再發(fā)送請求給服務(wù)器說,已去讀取成功。因?yàn)槿绻邮艿搅讼?但是發(fā)送獲取成功狀態(tài)通知時,請求失敗了.這樣會陷入死循環(huán)。
2.針對第一點(diǎn)出現(xiàn)的情況,我們一般采用這樣的方式解決,我們在每次通知客戶端去獲取消息的時候,會給客戶端發(fā)送一個上一條消息的token(自定義的序列碼)。這樣在每次讀取下一條消息的時候,我們會去核對上一條消息是否已送達(dá),如果沒有,則會向客戶端補(bǔ)發(fā)消息。
3.我們應(yīng)該明確用長連接僅僅是傳送一些簡單的狀態(tài)指令消息,不應(yīng)該用它去傳遞字節(jié)數(shù)據(jù)較多的數(shù)據(jù)。
四,IM群聊解決方案
1.單聊和群聊的區(qū)別有哪幾點(diǎn)
1.群聊時,是一對多的關(guān)系,所以我們?nèi)ネㄖ撼蓡T獲取消息時,要是像單聊那樣,還需要一個個判斷其群成員是否在線,則顯得過于繁瑣
2.離線標(biāo)記處理方式和單聊有區(qū)別,見第四點(diǎn)
3.群成員角色更加豐富,有成員等級,成員類型,權(quán)限等操作
4.在存儲群消息時,僅存儲一份,并且接收人設(shè)置為群id即可
2.怎樣發(fā)送一對多的群消息
1.在用戶登錄時,會自動去查詢所在群,然后利用特性,將該加入其群內(nèi)(具體詳見.io的rooms和部分)
2.參考我的博文
3.群聊整個過程
1.當(dāng)A用戶登錄時,會自動查詢加入了哪些群,然后將其加入內(nèi)
2.當(dāng)A用戶發(fā)送消息至群1時,服務(wù)器先存儲該消息(發(fā)送人A,接收人群1id),然后調(diào)用方法(.to(群id))就能通知給加入該群的其他用戶
3.若群1其他成員在線,則收到新消息通知后,會去請求服務(wù)器,獲取其群消息內(nèi)容,然后將離線標(biāo)記更新至最新的一條已讀消息id
4.在離線情況下,當(dāng)該群1成員登錄或重連后,獲取其離線群消息列表。當(dāng)讀取群消息詳情時,自動更新離線標(biāo)記至最新一條已讀消息id
4.群聊離線消息怎么處理
1.當(dāng)用戶加群時,會自動增加一條離線標(biāo)記文檔,并且與單聊離線標(biāo)記文檔不同的是,群離線標(biāo)記會有一個加入時的消息id。所以該成員讀取的消息范圍為(加入時的上一條消息id,最新一條消息id]
2.當(dāng)群成員獲取詳情時,會更新離線標(biāo)記的已讀消息id為最新消息id,無需和單聊一樣,在發(fā)送時做判斷處理
五,項(xiàng)目還需優(yōu)化的幾點(diǎn)
1.消息的緩存處理
我們應(yīng)該對用戶的好友列表,消息列表,接收到的消息做緩存處理.不應(yīng)該每次登錄時都從服務(wù)器端請求。比如,每次服務(wù)器端發(fā)送器好友列表時,應(yīng)該給客戶端一個好友列表版本號,每次僅需比對版本號,從而決定是否從服務(wù)器重新獲取好友列表信息。另外對于消息,由于消息id時遞增的,我們僅需比對其消息id和服務(wù)器端是否一致即可。
2.高并發(fā)情況下發(fā)送消息時解決方案
1.該項(xiàng)目是有痛點(diǎn)的,雖然我在發(fā)送消息和存儲消息時,都做了異步處理,但對于高并發(fā)的請求,依然會有可能出現(xiàn)雪崩現(xiàn)象(如果你僅僅只是十萬級的請求數(shù)量,可以不用考慮)
2.我們需要對發(fā)送和查詢消息的請求,做隊(duì)列化處理,防止雪崩事件的產(chǎn)生,并且對群消息的查詢做緩存化處理。
3.控制用戶發(fā)送消息的頻率
對于群成員較多,如一萬人的大群而言,我們需要限制其發(fā)送消息的頻率,不然服務(wù)器發(fā)送群消息時雖然只執(zhí)行了一次存儲操作,但是執(zhí)行了一萬次的查詢操作(需分發(fā)給一萬個用戶(若同時在線))。所以我們需要對用戶進(jìn)行頻率限制,來確保消息的可靠性和穩(wěn)定性。
六,結(jié)語
1.本次IM的demo,我花了將近一個月的時間來實(shí)現(xiàn)群聊和單聊(包含其學(xué)習(xí)成本),我也查閱了很多資料,在此我表示,如果大家想要提高自己的水平,最好將官方api當(dāng)做自己的第一手資料。然后我想說,雖然很不錯,但是坑很多,學(xué)習(xí)成本比較大,需要靜下心來好好看
2.在做這種項(xiàng)目之前,最好上網(wǎng)查查,看看別人的思路和設(shè)計(jì),會避免很多彎路。這也是我為什么側(cè)重講思路而不是大篇幅貼代碼的原因。另外,在確定好思路后,我們必須得將項(xiàng)目分成一個個的點(diǎn),然后逐個攻破,這樣就能控制項(xiàng)目進(jìn)度了
3.作為一名合格的開發(fā),需要對壓力測試,單元測試有深入的了解