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