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