移動(dòng)IM開發(fā)技術(shù)選型(包括通訊方式, 網(wǎng)絡(luò)連接方式, 協(xié)議選擇) 和常見問題匯總。
1.通訊方式選擇
IM 通訊方式無非兩種選擇: 設(shè)備直連 (P2P) 和通過服務(wù)器中轉(zhuǎn)。
1.1 P2P
P2P 多見于局域網(wǎng)內(nèi)聊天工具,典型的應(yīng)用有: 飛鴿傳書,飛秋等。這類軟件在啟動(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) 下無法建立連接。
1.2 服務(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ā)要求高。
2.網(wǎng)絡(luò)連接方式IM 主流網(wǎng)絡(luò)連接方式有兩種:
基于 TCP 的長(zhǎng)連接基于 HTTP 短連接 PULL 的方式后者常見于 WEB IM 系統(tǒng)(當(dāng)然現(xiàn)在很多 WEB IM 都是基于 WebSocket 實(shí)現(xiàn)),它的優(yōu)點(diǎn)是實(shí)現(xiàn)簡(jiǎn)單,方便開發(fā)上手,問題是流量大,服務(wù)器負(fù)載較大,消息及時(shí)性無法很好地保證,對(duì)大規(guī)模的用戶量支持不夠,比較適合小型的 IM 系統(tǒng), 如小網(wǎng)站的客戶系統(tǒng)。
基于 TCP 長(zhǎng)連接則能夠更好地支持大批量用戶,問題是客戶端和服務(wù)器的實(shí)現(xiàn)比較復(fù)雜。當(dāng)然也還有一些變種,如下行使用 MQTT 進(jìn)行服務(wù)器通知 / 消息的下發(fā),上行使用 HTTP 短連接進(jìn)行指令和消息的上傳。這種方式能夠保證下行消息 / 指令的及時(shí)性,但是在弱網(wǎng)絡(luò)下上行慢的問題還是比較嚴(yán)重。早期的來往就是基于這種方式。
3.協(xié)議選擇IM 協(xié)議選擇原則一般是: 易于拓展,方便覆蓋各種業(yè)務(wù)邏輯,同時(shí)又比較節(jié)約流量。后一點(diǎn)的需求在移動(dòng)端 IM 上尤其重要。
常見的協(xié)議有:
XMPPSIPMQTT私有協(xié)議XMPP 協(xié)議的優(yōu)點(diǎn)在于: 協(xié)議開源,可拓展性強(qiáng),在各個(gè)端 (包括服務(wù)器) 有各種語言的實(shí)現(xiàn),開發(fā)者接入方便。但是缺點(diǎn)也是不少: XML 有太多冗余信息,流量大,實(shí)際使用時(shí)有大量天坑。
SIP 協(xié)議多用于 VOIP 相關(guān)的模塊,是一種文本協(xié)議,由于我并沒有實(shí)際用過,所以不做評(píng)論,但從它是文本協(xié)議這一點(diǎn)幾乎可以斷定它的流量不會(huì)小。
MQTT 的優(yōu)點(diǎn)是協(xié)議簡(jiǎn)單,流量少,但是它并不是一個(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ǎn)潔,可讀性好,節(jié)約流量,易于拓展,同時(shí)又能夠匹配當(dāng)前團(tuán)隊(duì)的技術(shù)堆棧?;谌缟显瓌t,我們可以推出: 如果團(tuán)隊(duì)小,團(tuán)隊(duì)技術(shù)在 IM 上積累不夠可以考慮使用 XMPP 或者 MQTT+HTTP 短連接的實(shí)現(xiàn)。反之可以考慮自己設(shè)計(jì)和實(shí)現(xiàn)私有協(xié)議。
4.私有協(xié)議的設(shè)計(jì)4.1序列化選擇
移動(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é)議的序列化和反序列化過程。
4.2 協(xié)議格式設(shè)計(jì)
基于 TCP 的應(yīng)用層協(xié)議一般都分為包頭和包體(如 HTTP),IM 協(xié)議也不例外。包頭一般用于表示每個(gè)請(qǐng)求 / 反饋的公共部分,如包長(zhǎng),請(qǐng)求類型,返回碼等。 而包頭則填充不同請(qǐng)求 / 反饋對(duì)應(yīng)的信息。
一個(gè)最簡(jiǎn)單的包頭可以定義為
struct PackHeader
{
int32_t length_; // 包長(zhǎng)度
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)然這是最簡(jiǎn)單的一個(gè)例子,面對(duì)真正的業(yè)務(wù)邏輯時(shí),包體里面會(huì)需要塞入更多地信息,這個(gè)需要開發(fā)根據(jù)自己的業(yè)務(wù)邏輯總結(jié)公共部分, 如為了兼容加入的協(xié)議版本號(hào), 為了負(fù)載均衡加入的模塊 id 等。
5.其他問題上面就是一個(gè) IM 系統(tǒng)大致的選型過程: 通訊方式,連接方式,協(xié)議選擇,協(xié)議設(shè)計(jì)。但是實(shí)際開發(fā)過程中還有大量的問題需要處理。
5.1協(xié)議加密
為了保證協(xié)議不容易被破解,市面上幾乎所有主流 IM 都會(huì)對(duì)協(xié)議進(jìn)行加密傳輸。常見的流程和 HTTPS 加密相似: 建立連接后,客戶端和服務(wù)器進(jìn)行進(jìn)行協(xié)商,最終客戶端獲得一個(gè)當(dāng)前 Session 秘鑰,后續(xù)的數(shù)據(jù)傳輸都通過這個(gè)秘鑰進(jìn)行加解密。一般出于效率的考慮都會(huì)采用流式加密,如 RC4。而前期協(xié)商過程則推薦使用 RSA 等非對(duì)稱加密以增加破解難度。
5.2快速連接(登錄)
對(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í)間。簡(jiǎn)化登錄后的同步請(qǐng)求,部分同步請(qǐng)求可以推遲到 UI 操作時(shí)進(jìn)行,如群成員信息刷新。5.3連接保持
一般 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)線 (移動(dòng)端沒有網(wǎng)線…) 直接被拔除移動(dòng)端在實(shí)際操作時(shí)為了節(jié)約流量和電量一般會(huì)在心跳包上做一些小優(yōu)化
精簡(jiǎn)心跳包,保證一個(gè)心跳包大小在 10 字節(jié)之內(nèi)心跳包只在空閑時(shí)發(fā)送根據(jù) APP 前后臺(tái)狀態(tài)調(diào)整心跳包間隔 (主要是安卓)5.4消息可達(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)。
5.5文件上傳優(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ù)傳