最近在做一個iOS IM SDK,內(nèi)測版已出爐,詳見http://netease.im。在內(nèi)部試用的階段,不斷有兄弟部門或者合作伙伴過來問各種技術(shù)細節(jié),所以統(tǒng)一寫一篇文章記錄,統(tǒng)一介紹下一個IM APP的方方面面,包括技術(shù)選型(包括通訊方式,網(wǎng)絡(luò)連接方式,協(xié)議選擇)和常見問題。
通訊方式選擇
IM通訊方式無非兩種選擇:設(shè)備直連(P2P)和通過服務(wù)器中轉(zhuǎn)。
P2P
P2P多見于局域網(wǎng)內(nèi)聊天工具,典型的應(yīng)用有:飛鴿傳書,天網(wǎng)Maze(你懂的)等。這類軟件在啟動后一般做兩件事情
進行UDP廣播:發(fā)送自己信息和接受同局域網(wǎng)內(nèi)其他端信息
開啟TCP監(jiān)聽:等待其他端進行連接
詳細的流程可以參考飛鴿傳書源碼。但是這種方式在有種種限制和不便:一方面它只適合在線的點對點消息傳輸,而對離線,群組等業(yè)務(wù)支持不夠。另一方面由于 NAT 的存在,使得不同局域網(wǎng)內(nèi)機器互聯(lián)的難度大大上升,在某些網(wǎng)絡(luò)類型(對稱NAT)下無法建立連接。
服務(wù)器中轉(zhuǎn)
幾乎所有互聯(lián)網(wǎng)IM產(chǎn)品都采用服務(wù)器中轉(zhuǎn)這種方式進行消息傳輸,相對于P2P的方式,它有如下的優(yōu)點:
能夠支持更多P2P無法支持或支持不好的業(yè)務(wù),如離線消息,群組,聊天室服務(wù)
方便業(yè)務(wù)邏輯的拓展和新舊版本的兼容
當(dāng)然它也有自己的問題:服務(wù)器架構(gòu)復(fù)雜,并發(fā)要求高。
網(wǎng)絡(luò)連接方式
IM主流網(wǎng)絡(luò)連接方式有兩種:
基于TCP的長連接
基于HTTP短連接PULL的方式
后者常見于WEB IM系統(tǒng)(當(dāng)然現(xiàn)在很多WEB IM都是基于WebSocket實現(xiàn)),它的優(yōu)點是實現(xiàn)簡單,方便開發(fā)上手,問題是流量大,服務(wù)器負(fù)載較大,消息及時性無法很好地保證,對大規(guī)模的用戶量支持不夠,比較適合小型的IM系統(tǒng),如一個小網(wǎng)站的客戶系統(tǒng)。
基于TCP長連接則夠更好地支持大批量用戶,問題是客戶端和服務(wù)器的實現(xiàn)比較復(fù)雜。當(dāng)然也還有一些變種,如下行使用MQTT進行服務(wù)器通知/消息的下發(fā),上行使用HTTP短連接進行指令和消息的上傳。這種方式能夠保證下行消息/指令的及時性,但是在弱網(wǎng)絡(luò)下上行慢的問題還是比較嚴(yán)重。早期的來往就是基于這種方式。
協(xié)議選擇
IM協(xié)議選擇原則一般是:易于拓展,方便覆蓋各種業(yè)務(wù)邏輯,同時又比較節(jié)約流量。后一點的需求在移動端IM上尤其重要。
常見的協(xié)議有:
XMPP
SIP
MQTT
私有協(xié)議
XMPP協(xié)議的優(yōu)點在于:協(xié)議開源,可拓展性強,在各個端(包括服務(wù)器)有各種語言的實現(xiàn),開發(fā)者接入方便。但是缺點也是不少:XML表現(xiàn)力弱,有太多冗余信息,流量大,實際使用時有大量天坑。
SIP協(xié)議多用于VOIP相關(guān)的模塊,是一種文本協(xié)議,由于我并沒有實際用過,所以不做評論,但從它是文本協(xié)議這一點幾乎可以斷定它的流量不會小。
MQTT的優(yōu)點是協(xié)議簡單,流量少,但是它并不是一個專門為IM設(shè)計的協(xié)議,多使用于推送。
而市面上幾乎所有主流IM APP都是是使用私有協(xié)議,一個被良好設(shè)計的私有協(xié)議一般有如下優(yōu)點:高效,節(jié)約流量(一般使用二進制協(xié)議),安全性高,難以破解。缺點則是在開發(fā)初期沒有現(xiàn)有樣列可以參考,對于設(shè)計者的要求比較高。
一個好的協(xié)議需要滿足如下條件:高效,簡潔,可讀性好,節(jié)約流量,易于拓展,同時又能夠匹配當(dāng)前團隊的技術(shù)堆棧。基于如上原則,我們可以推出: 如果團隊小,團隊技術(shù)在IM上積累不夠可以考慮使用XMPP或者MQTT+HTTP短連接的實現(xiàn)。反之可以考慮自己設(shè)計和實現(xiàn)私有協(xié)議。
私有協(xié)議的設(shè)計
序列化選擇
移動互聯(lián)網(wǎng)相對于有線網(wǎng)絡(luò)最大特點是:帶寬低,延遲高,丟包率高和穩(wěn)定性差,流量費用高。所以在私有協(xié)議的序列化上一般使用二進制協(xié)議,而不是文本協(xié)議。常見的二進制序列化庫有protobuf和MessagePack,當(dāng)然你也可以自己實現(xiàn)自己的二進制協(xié)議序列化和反序列的過程,比如蘑菇街的TeamTalk。但是前面二者無論是可拓展性還是可讀性都完爆TeamTalk(TeamTalk連Variant都不支持,一個int傳輸時固定占用4個字節(jié)),所以大部分情況下還是不推薦自己去實現(xiàn)二進制協(xié)議的序列化和反序列化過程。
協(xié)議格式設(shè)計
基于TCP的應(yīng)用層協(xié)議一般都分為包頭和包體(如HTTP),IM協(xié)議也不例外。包頭一般用于表示每個請求/反饋的公共部分,如包長,請求類型,返回碼等。 而包頭則填充不同請求/反饋對應(yīng)的信息。
一個最簡單的包頭可以定義為
struct PackHeader { int32_t length_; //包長度 int32_t serial_; //包序列號 int32_t command_; //包請求類型 int32_t code_; //返回碼 };以心跳包為例,假設(shè)當(dāng)前的serial為1,心跳包的command為10,那么使用MessagePack做序列化時:length=4,serial=1,command=10,code=0,每個字段各占一個字節(jié),包體為空,僅需要4個字節(jié)。
當(dāng)然這是最簡單的一個例子,面對真正的業(yè)務(wù)邏輯時,包體里面會需要塞入更多地信息,這個需要開發(fā)根據(jù)自己的業(yè)務(wù)邏輯總結(jié)公共部分,如為了兼容加入的協(xié)議版本號,為了負(fù)載均衡加入的模塊id等。
其他問題
上面就是一個IM系統(tǒng)大致的選型過程:通訊方式,連接方式,協(xié)議選擇,協(xié)議設(shè)計。但是實際開發(fā)過程中還有大量的問題需要處理。
協(xié)議加密
為了保證協(xié)議不容易被破解,市面上幾乎所有主流IM都會對協(xié)議進行加密傳輸。常見的流程和HTTPS加密相似:建立連接后,客戶端和服務(wù)器進行進行協(xié)商,最終客戶端獲得一個當(dāng)前Sessino的秘鑰,后續(xù)的數(shù)據(jù)傳輸都通過這個秘鑰進行加解密。一般出于效率的考慮都會采用流式加密,如RC4。而前期協(xié)商過程則推薦使用AES等非對稱加密以增加破解難度。
快速連接(登錄)
對iOS APP而言,因為沒有真后臺的存在,APP每次啟動基本都需要一次重連登錄(短時間內(nèi)切換除外),所以如何快速重連重登就非常重要。常見的優(yōu)化思路如下:
本地緩存服務(wù)器IP并定期刷新。移動網(wǎng)絡(luò)調(diào)優(yōu)可以參考《iOS移動網(wǎng)絡(luò)調(diào)優(yōu)那些事》。
合并部分請求。如加密和登錄操作可以合并為同一個操作,這樣就可以減少一次不必要的網(wǎng)絡(luò)請求來回的時間。
簡化登錄后的同步請求,部分同步請求可以推遲到UI操作時進行,如群成員信息刷新。
連接保持
一般APP實現(xiàn)連接保持的方式無非是采用應(yīng)用層的心跳,通過心跳包的超時和其他條件(網(wǎng)絡(luò)切換)來執(zhí)行重連操作。那么問題來了:為什么要使用應(yīng)用層心跳和如何設(shè)計應(yīng)用層心跳。
眾所周知TCP協(xié)議是有KEEPALIVE這個設(shè)置選項,設(shè)置為KEEPALIVE后,客戶端每隔N秒(默認(rèn)是7200s)會向服務(wù)器發(fā)送一個發(fā)送心跳包。但實際操作中我們更多的時是使用應(yīng)用層心跳。原因如下:
KEEPALIVE對服務(wù)器負(fù)載壓力比較大(服務(wù)器大大是這么說的...)
socks代理對KEEPALIVE并不支持
部分復(fù)雜情況下KEEPALIVE會失效,如路由器掛掉,網(wǎng)絡(luò)直接被拔除
移動端在實際操作時為了節(jié)約流量和電量一般會在心跳包上做一些小優(yōu)化
盡量精簡心跳包,保證一個心跳包大小在10字節(jié)之內(nèi)
心跳包只在空閑時發(fā)送 (收到最后一個數(shù)據(jù)包n秒內(nèi)再也沒有收到包則進行一次心跳)
根據(jù)APP前后臺狀態(tài)調(diào)整心跳包間隔 (主要是安卓)
消息可達
在移動網(wǎng)絡(luò)下,丟包,網(wǎng)絡(luò)重連等情況非常之多,為了保證消息的可達,一般需要做消息回執(zhí)和重發(fā)機制。參考易信,每條消息會最多會有3次重發(fā),超時時間為15秒,同時在發(fā)送之前會檢測當(dāng)前連接狀態(tài),如果當(dāng)前連接并沒有正確建立,緩存消息切定時檢查(每隔2秒檢查一次,檢查15次)。所以一條消息在最差的情況下會有2分多的重試時間,以保證消息的可達。
因為重發(fā)的存在,接受端偶爾會收到重復(fù)的兩條消息,這種情況下就需要接收端進行去重。一般的做法是每條消息都有自己唯一的message id(一般是uuid)。
文件上傳優(yōu)化
IM消息(包括SNS模塊)內(nèi)包含大量的文件上傳的需求,如何優(yōu)化文件的上傳就成了一個比較大的主題。常見有下面這些優(yōu)化思路:
將上傳流程提前:音頻提供邊錄邊傳。朋友圈的圖片進行預(yù)上傳,選擇圖片后用戶一般會進行文本輸入,在這段時間內(nèi)后臺就可以默默將選好的圖片進行上傳。
提供閃電上傳的方式:服務(wù)器根據(jù)MD5進行文件去重。
優(yōu)化和上傳服務(wù)器的連接(參考快速連接),提供連接重用的功能。
文件分塊上傳:因為移動網(wǎng)絡(luò)丟包嚴(yán)重,將文件分塊上傳可以使得一個分組包含合理數(shù)量的TCP包,使得重試概率下降,重試代價變小,更容易上傳到服務(wù)器。
在分包的前提下支持上傳的pipeline,避免不必要的網(wǎng)絡(luò)等待時間。
支持?jǐn)帱c續(xù)傳