此文根據【QCON高可用架構群】分享內容,由群內【編輯組】志願整理,轉發請註明出處。

沈劍,目前任58同城技術委員會主席,高階架構師,優秀講師。負責過百度hi,58幫幫等im系統的架構設計。

一、什麼是IM

1、IM概述

IM 是“instant messaging”的簡稱,翻譯成即時通訊。說到即時通訊,我們可能最先想到的是一款叫 ICQ的聊天軟體 ,後來者還有微信/skype/msn/momo等 。IM包含即時和通訊是兩個關鍵詞。

  • 即時
    英文為“instant”或“real time”,表示“立刻”,就是響應非常快,速度的快慢由人的預期決定,而不是絕對時間。IM訊息的發出和送達,達到秒級、毫秒級即可認為是快的。

  • 通訊
    通訊就是雙方按照既定協議交換資訊。所謂“協議”,是雙方約定達成一致的一個某種約定。

所以“即時通訊”,從字面上看,就是快速,按照一定協議交換資訊。 下圖是一個ICQ的截圖:


2、IM系統特性

IM系統相對其他系統而言有自己的幾個特點:

  • 實時性
    以Web IM為例,Web站點通常是以請求/響應互動的 ,當Web IM一端接收訊息時,最直觀的做法就是輪詢請求收取訊息,實時性和輪詢間隔有關。其實Web IM也可以做到完全實時,後續會講到如何實現。

  • 推送性
    IM不是請求/響應的方式,而是主動推送訊息,所以這個“推”是IM系統與其他系統有區別的地方,比如電商或搜尋,都是一請求、一響應的方式。

以股市變化曲線為例,客戶端不主動傳送請求,曲線會隨時間自動變化 :


  • 訊息可達性

訊息可達性即訊息的可靠投遞 ,有一個著名的定理:SMC定理,Single-Message Communication,Published in : Communications, IEEE Transactions on (Volume:24 , Issue: 2 ) ,很短的一個論文,文章的結論是,任何端到端的訊息傳遞協議 ,訊息既不丟失,同時也不重複是不可能的。後續會分享,系統層面的重複,可以換取業務層面的不丟失和不重複。

  • 狀態一致性

大部分人可能瞭解XMPP(Extensible Messaging and Presence Protocol )協議 ,即可擴充套件訊息和Presence協議 ,這個Presence直接翻譯就是“出席“,何為出席? 大家都到群內簽到了,叫出席 ,沒來聽講座叫缺席 ,在IM中出席缺席表示狀態 。登入線上叫出席,沒登入叫缺席 。
幾乎所有的IM請求都和狀態相關(比如:線上轉發,不線上存離線) ,所以狀態的一致性尤為重要 。

舉個例子:

假設使用者A有100個好友,使用者A加入了10個群,每個群有100個人。使用者A在登入的一瞬間,狀態由“不線上”變為了“線上”,他的狀態的變更需要通知要通知1100個人,包括100個好友和1000個群友,這個擴散係數是非常龐大,

如何做這1100個人的狀態推送? 常見的做法和Feed,微博類似 ,你的100個好友(實時性要求很高),採用推的模式 ,你的1000個群友採用拉的模式(實時性要求不高) 。當然,這裡還只是說狀態很很複雜,還沒涉及到狀態一致性問題。

二、協議設計

網路協議是由三個要素組成,語義、語法、時序。語義表示要做什麼,語法表示要怎麼做,時序表示做的順序。 本文主要講解重點語法的設計。

IM的協議分為3層,如下圖:


1、應用層

常用的IM應用層協議有3種 :

1.1、文字協議

文字協議是“貼近人類書面語言”的協議,典型例子是MSN ,另外HTTP協議也是文字協議,如下圖所示:

文字協議有幾個特點:

  • 可讀性好/便於除錯

  • 擴充套件性也好(key:value)

  • 解析效率一般(一行一行讀入,按照冒號分割,解析key和value),

  • 對二進位制的支援不好 ,比如語音/視訊

1.2、 二進位制協議

二進位制協議最典型的是IP協議,如下圖所示:


二進位制協議一般定長包頭和可擴充套件變長包體 ,每個欄位固定了含義 ,例如IP協議的前4個bit表示協議版本號 (Version)。IM中,QQ使用的時二進位制協議。

二進位制協議有幾個特點:

  • 可讀性差/難於除錯

  • 擴充套件性不好 ,如果要擴充套件欄位,舊版協議就不相容了,所以一般設計時會有一個Version欄位

  • 解析效率超高(幾乎沒有解析代價)

  • 對二進位制的支援不好 ,比如語音/視訊

1.3、流式XML

Xmpp協議就是使用流式XML,像Gtalk,校內通都是基於Xmpp的。

舉一個羅密歐給朱麗葉發訊息的例子:

< message to='[email protected]' from='[email protected]' type='chat' xml:lang='en' >
Wherefore art thou, Romeo?

Xmpp用 [email protected] 來標示一個賬號,稱為JID 。JID由兩個部分組成 ,前半部分是該域中的賬號體系 ,後半部分是域標識(編者注:JID還包含一個Resource部分)。Xmpp協議可以實現跨域的互通。例如Gtalk和校內通使用者聊天。只要服務端實現了s2s服務(server to server) ,不過現在的IM基本沒有互通需求 ,所以 這個服務基本沒有人實現。

Xmpp協議有幾個特點:

  • 它是準標準協議,可以跨域互通

  • XML的優點,可讀性好,擴充套件性好

  • 解析代價超高 (dom解析)

  • 有效資料傳輸率超低(大量的標籤 )

我個人強烈不建議使用Xmpp,特別是無線端IM,這個是google上個世紀的產物了,如果要用,一定要自己做壓縮 ,減少網路流量。

實際例子

下面來看一個IM協議的實際例子 ,一般常見的做法是:定長二進位制包頭,可擴充套件變長包體可以使用用文字、XML等。 百度Hi 的IM包體用的是XML ,58同城的IM包體用的是protobuffer,包頭負責傳輸和解析效率,包體與業務無關負責保證擴充套件性。


這是一個簡單例子,我們來看一下包頭和包體 。

定長包頭(16個位元組)

  • 前4個位元組是Version

  • 接下來的4個位元組是個“魔法數字(magic_num)“,用來保證資料錯位或丟包問題,常見的做法是,包頭放幾個約定好的特殊字元,包尾放幾個約定好的特殊字元 約定好,發給你的協議,某幾個位元組位置,是0x 01020304 。才是正常報文

  • 接下來是command(命令號),用來區分是keepalive報文、業務報文、金鑰交換報文等

  • len(包體長度),告知服務端要接收多長的包體

變長包體

可選擇 Xml、protobuffer、mcpack等 ,某些公司使用protobuffer,作者也強烈推薦,主要有幾個原因:

  • 現成的解析庫種類多,可以生成C++、Java、php等程式碼

  • 自帶壓縮功能

  • 在工業界已廣泛應用

  • Google製造

    下面貼一個protobuffer寫的使用者登入協議的示例:



2、安全層

IM協議,訊息的保密性非常重要 ,誰都不希望自己聊天內容被看到

1、HTTPS

稍微複雜,代價有點高 。

2、自行加解密

金鑰管理方式有多種

1、固定金鑰
服務端和客戶端約定好一個金鑰,同時約定好一個加密演算法(eg:AES ),每次客戶端IM在傳送前,就用約定好的演算法,以及約定好的金鑰加密 再傳輸 ,服務端收到報文後,用約定好的演算法,約定好的金鑰再解密 。這種方式,金鑰和演算法 對程式設計師都是透明的 。有些公司cookie就是這麼使用的。

2、一人一金鑰
簡單說來就是每個人的金鑰是固定的,但是每個人之間又不同,其實就是在固定金鑰的演算法中包含使用者的某一特殊屬性,比如使用者uid、手機號等。

據傳,QQ是這麼使用這種方式(未核實)

3、動態金鑰

大家瞭解ssl的過程麼,動態金鑰 一Session一金鑰的安全性更高,每次傳輸互動前協商金鑰, 客戶端第一個報文:服務端,請告訴我這次通話的金鑰 ,伺服器就隨機生成一個,返回給客戶端 然後這次會話用這個金鑰來通訊 ,這樣可以簡單的做到動態金鑰,但是一旦攻擊者擷取報文,就能知道你的動態金鑰。

SSL的用法,是2次生成非對稱加密金鑰,1次生成對稱加密金鑰 ,具體詳情這裡不展開,有興趣的可以深入研究一下。

3、傳輸層

傳輸層:TCP/UDP

現在的IM傳輸層基本都是使用TCP。有了epoll/kqueue等技術後 ,單機多連線就不是瓶頸了,單機幾十萬連結沒什麼問題。58同城現在線上單機連線好像是10w?(可能單機效能測試可以到百萬,線上一般跑到幾十萬)

關於QQ是使用UDP問題(作者觀點)

10多年前,一臺伺服器支撐不了1W個TCP連線 ,騰訊的同時線上量高,沒辦法,只有用UDP了 ,但UDP又不可靠,騰訊使用UDP實現了TCP的超時/重傳/確認等機制

三、WEB聊天室

1、需求

下面是一個Web聊天室的截圖


Web聊天室需求主要有一下幾點:

1)使用者可以設定自己的名字

2)進入聊天室後,可以看到所有其他人的名字

3)可以看到所有人的聊天

4)可以發言給所有人

上面是Web聊天室的一些簡單業務(像群吧?) ,設定名字/拉取其他人的資訊 都是簡單的請求響應式的需求這裡不展開,主要重點講怎麼看歷史聊天訊息,怎麼發訊息給別人 。

2、系統架構

Web站點三層架構大家都很熟悉,大致是這個樣子


LAMP是個典型解決方案 ,更典型的是這個樣子

  • 資料層

對於聊天室的簡單需求,一個存使用者資訊,一個存訊息兩個表就可以了,下面是一個簡化了的示例:

user( name vchar(16) unique );

message( time timestamp, name vchar(16), msg vchar(140) );

有人進入,就往user裡插入 ,有人退出,就從user裡刪除 ,有人發訊息,就往message裡插入 ,有人進入,往message里拉取,就能查詢歷史資訊。

3、技術核心點

Web聊天室的核心店在於如何把傳送的訊息通知User表裡的所有人?訊息實時性,主要有三種方式,websocket、flashsocket、http輪詢,本文只講http輪詢。

1、輪詢

什麼是輪詢?
舉個例子,在火車上想上洗手間,擠到洗手間旁,卻發現洗手間有人,於是你只能回座位繼續等。過了N分鐘,又朝洗手間的方向擠過去,卻發現洗手間還是有人,又只能回坐等。這麼一而再,再而三的每隔N分鐘去洗手間檢視洗手間是否有蹲位,這就是輪詢。

程式程式碼:while(1){sleep 500ms; get msg;}

大部分人最容易想到的解決方案就是輪詢(poll) 。十幾年前,四通利方/碧海銀沙就是用的輪詢,輪詢拉取訊息,每隔幾秒往message表裡,拉取最新的聊天室訊息 ,這樣做能簡單實現一個聊天室。

但輪詢問題也顯而易見 ,每隔N分鐘,輪詢呼叫 “獲取訊息”介面,有可能出現訊息的延時,某一時刻剛剛拉取完訊息,突然又產生了一條新訊息,這條訊息就必須等到N分鐘之後,再次發起“獲取訊息”輪詢時,才有機會獲取到。 可以降低時間間隔來降低延時,但絕大部分的輪詢呼叫,都沒有訊息返回,造成服務端極大的資源浪費

2、訊息連線

Web聊天室通過“http訊息連線”來保證訊息的絕對實時性,何謂訊息連線?

使用者和伺服器建立一條http連線,專門用來傳遞Notify 。例如,手機上,web聊天室裡,有一個B使用者 專門有一條http訊息連線,用來投遞Notify(訊息),如下圖 。



訊息連線如何保證 web聊天室訊息的實時性呢?它具有以下幾個特性:

1、沒有訊息到達的時候,這個http訊息連線將被夯住,不返回,由於http是短連線,這個http訊息連線最多被夯住90秒,就會被斷開(這是瀏覽器或者webserver的行為)。

2、如果http訊息連線被斷開,立馬再發起一個http訊息連線


如上圖,http訊息連線90s超時了,伺服器返回空了,web瀏覽器會立馬再次發起一個新的訊息連線 。目的是,保證一個使用者一直有一條訊息連線連著,可以接受訊息

3、每次收到訊息時,這個訊息連線就能及時將訊息帶回瀏覽器頁面,並且在返回後,會立馬再發起一個http訊息連線


如上圖,某人傳送了一條聊天室訊息 ,這個訊息要投遞給user裡的所有人,B是其中之一,此時會有一條訊息連線在,訊息連線就直接將訊息帶回 ,帶會訊息後,立馬再發起訊息連線 。

4、訊息池

上面三大特性保證了:

a)任何時間都有訊息連線在

b)訊息能在第一時間通過訊息連線返回

但這裡有個小概率事件,正在返回訊息的時候(可以認為此時沒有訊息連線),瞬間又到達了一條訊息怎麼辦,此時服務端要有一個類似於“訊息池”的東西,將這個訊息暫存起來 。訊息連線到達後,從訊息池中將訊息取回,再通過訊息連線返回。


看上圖中的步驟1-7

1)訊息要投遞給聊天室中的所有人,B是其中之一

2)此時沒有訊息連線,msg放入訊息池

3)訊息連線來晚了

4)從訊息池中獲取訊息

5)獲取到訊息了

6)返回訊息給web瀏覽器裡的B

7)新的訊息連線發起

結論:

Web聊天室通過http長輪詢可以保證訊息的實時性。這種實時性的保證不是通過增加輪詢頻率來保證的,而是通過夯住http訊息連線來保證的,在大部分時間沒有實時訊息的情況下,這個http訊息連線對於webserver的請求壓力是90秒1次,能夠大大節省了web伺服器資源。

這個訊息連線的思想,是一個觀察者模式 ,所有的聊天室使用者是observer,聊天室是subject,observer訂閱subject ,subject儲存有所有observer的集合,當有訊息發出,即subject發生改變時,通過http訊息連線反向通知subject

這裡重點討論了 web聊天室 訊息的實時性,聊天訊息的可靠投遞,暫時也先不展開了

即時,是相對時間而不是絕對時間 ,m的即時,訊息投遞在幾百毫秒,1秒內投遞完成,一般都可以接受(站點應用這個時間就接受不了解),這個時間對未來我講im服務的網路模型,影響很大 。 im的所有業務邏輯,都是在這幾百毫秒內完成的 。

4、IM典型業務場景

IM業務邏輯有一定的複雜度,舉例IM中一個典型業務場景,使用者A將使用者B加入到分組G中,如下圖:

這裡包含多少業務邏輯呢

第一步,判斷A是否是正常im使用者
第二步,判斷B是否是正常im使用者
第三步,判斷分組G是否存在
第四步,是否A和B已經是好友
第五步,使用者A是否加黑了B
第六步,使用者B是否加黑了A
第七步/使用者A的加好友頻率是否過快
第八步,加好友的驗證文字是否合法。“我是xx,請加我為好友”就是這個文字的驗證。
第九步,檢查B的加好友策略 ,是“允許所有”還是“需要驗證”還是“禁止所有”

都9步了,真正的加好友步驟卻還沒開始,這裡的每一個步驟,都不是一個簡單的本地cpu計算能完成的,都需要訪問資料庫或者後端服務 ,所以im的業務邏輯是很複雜的,做過的人懂。

上面有一個步驟,是檢查加好友的驗證短語的合法性 。IM系統中,所有能被別人看到的話,都要經過antispam驗證 ,antispam一般有敏感詞(政治,黃色)過濾,訊息頻率過濾,廣告過濾 ,每一個步驟,都有很複雜的詞庫過濾,或者分析過濾(分析一句話是不是廣告,非常複雜)

四、Q&A

Q1: xmpp中,在跨域通訊網路沒有保證的情況下,xmpp如何保證跨域訊息的可靠送達的。

A1:im訊息的可達性,是通過超時重傳確認保證的(未來會再次重點講細節),跨域只是提供了一種不同域的通訊機制

Q2:應用層協議設計中,為啥 version 會放在 magic_num 之前呢? 為啥magic_num 不是第一個呢? 是考慮不同版本的 magic_num 不一樣?

A2:是的

Q3:我沒懂magic_num的作用,這個是常量嗎?另外,為啥不直接採用http,而要自定義協議?

A3:web上可以選擇http作為應用層協議,直接tcp長連線搞的話協議得自己來

Q4:聊天室怎麼保證訊息的可靠性?

A4: 可靠性以後講,或者可以看下剛才群內有同學發的那篇文章 webim如何保證訊息的可靠投遞

Q5:安全層加密, 是對應用層的:定長二進位制包頭 + 可擴充套件變長包體 都做加密?

A5:包體和業務相關,要加密

Q6: 現在通訊行業的IM(RCS等業務)基本都用SIP(單獨或結合MSRP)來做,協議安全用TCP/TLS,WebRTC也用SIP,請問@58沈劍 不用這些公共協議而用私有協議的主要出發點除了效能,還有什麼?

A6:自己做更可控,結合自己業務做優化。另,im行業準標準協議是xmpp。

Q7:訊息推送的話,用長連線保持的話,現在也比較通用了吧?

A7:能用tcp就用tcp,有些場景,例如web,選擇http訊息連線是被逼的

Q8:實時推送,http輪詢和websocket如何選擇呢?

A8:websocket有相容性問題,很多瀏覽器不支援,據我瞭解,大規模高線上的im,好像web端還木有用websocket搞的

Q9:手機端的聊天室如何處理使用者頻繁斷線的問題?如何在斷線後獲取伺服器端聊天曆史並與本地聊天曆史合併?

A9:web聊天室,訊息連線不上,訊息就放在訊息池裡,上來再給他,一定時間上不來,就丟掉。訊息id可以去重。

Q10:能不能介紹一下注冊/IM/Subscribe/Presence等伺服器結合起來的部署架構?

A10:架構後續課程分享

Q11:websocket有什麼弊端?目前單機最高業務承載多少?

A11:同A8回答。

Q12:移動端im的注意事項

A12:後續專門講移動優化

Q13:聊天室達到10萬級別的時候,在效率推送上是怎麼考慮的?

A13:聊天室和群一樣,訊息擴散係數很影響效能,一般群不能到這個級別,這個級別的群對伺服器影響很大(微信群上限是多少?qq群上限是多少?)

Q14:我想問下,剛說的訊息池是全域性的還是針對每個使用者單獨的?

A14:訊息池本質是個map,key是uid

Q15:為什麼不用BOSH(Bidirectional-streams Over Synchronous HTTP)來描述剛才web HTTP機制?有什麼細微區別嗎?

A15:我猜bosh的本質也是訊息連線,bosh/comet還是什麼,叫法不一樣,實現方式應該是類似的

Q16:另外你說的移動IM不建議XMPP協議,是單指XML格式本身的缺陷,有效資料太少,資料解析慢,僅僅是這個原因嗎?解析這方面有做過具體的效能測試嗎?另外如果是有效資料太少,是否可以稍微擴充套件就可以大幅度減少無效資料問題。還是有其它方面原因,比如XMPP協議本身互動協商次數太多的問題?

A16:xmpp解析慢,有效傳輸率低(無線端的表現就是耗流量),你們猜用xmpp發一個登入包多大?

Q17:websocket效能會比long-polling好多少呢?

A17:tcp長連線 和 http短連線 的差異

Q18:聊天訊息中含html、css、js相關程式碼是如何處理的?

A18:這是一個富文字訊息的好問題,伺服器不管訊息的內容,只進行投遞,富文字訊息的內容,是客戶端需要理解的

Q19:im怎麼保證時序性?當伺服器接收msgpack時處理不過來,客戶端就會阻塞傳送訊息。但沒阻塞接收的訊息。這樣傳送與接著就不同步了

A19:一言難盡,簡單說,單對單的訊息,用uid+msgid進行時序保證;群訊息,用gid+msgid進行時序保證。