[TOC]php
準備工做(協議選型)redis
xxx項目架構算法
IM 關鍵技術點 & 策略機制數據庫
典型IM業務場景json
存儲結構簡析後端
udp協議雖然實時性更好,可是如何處理安全可靠的傳輸而且處理不一樣客戶端之間的消息交互是個難題,實現起來過於複雜. 目前大部分IM架構都不採用UDP來實現.緩存
可是爲啥還須要HTTP呢?安全
核心的TCP長鏈接,用來實時收發消息,其餘資源請求不佔用此鏈接,保證明時性服務器
http能夠用來實現狀態協議(能夠用php開發)
IM進行圖片/語言/大塗鴉聊天的時候: http可以很方便的處理 斷點續傳和分片上傳等功能.
TCP: 維護長鏈接,保證消息的實時性, 對應數據傳輸協議.
IM協議選擇原則通常是:易於拓展,方便覆蓋各類業務邏輯,同時又比較節約流量。節約流量這一點的需求在移動端IM上尤爲重要 !!!
xmpp: 協議開源,可拓展性強,在各個端(包括服務器)有各類語言的實現,開發者接入方便。可是缺點也是很多:XML表現力弱,有太多冗餘信息,流量大,實際使用時有大量天坑。
MQTT: 協議簡單,流量少,可是它並非一個專門爲IM設計的協議,多使用於推送. 須要本身在業務上實現羣,好友相關等等(目前公司有用MQTT實現通用IM框架).
SIP: 多用於VOIP相關的模塊,是一種文本協議. sip信令控制比較複雜
私有協議: 本身實現協議.大部分主流IM APP都是是使用私有協議,一個被良好設計的私有協議通常有以下優勢:高效,節約流量(通常使用二進制協議),安全性高,難以破解。 xxx項目基本屬於私有定製協議<參考了蘑菇街開源的TeamTalk>, 後期通用IM架構使用MQTT
協議設計的考量:
網絡數據大小 —— 佔用帶寬,傳輸效率:雖然對單個用戶來講,數據量傳輸很小,可是對於服務器端要承受衆多的高併發數據傳輸,必需要考慮到數據佔用帶寬,儘可能不要有冗餘數據,這樣纔可以少佔用帶寬,少佔用資源,少網絡IO,提升傳輸效率;
網絡數據安全性 —— 敏感數據的網絡安全:對於相關業務的部分數據傳輸都是敏感數據,因此必須考慮對部分傳輸數據進行加密(xxx項目目前提供C++的加密庫給客戶端使用)
編碼複雜度 —— 序列化和反序列化複雜度,效率,數據結構的可擴展性
協議通用性 —— 大衆規範:數據類型必須是跨平臺,數據格式是通用的
經常使用序列化協議
提供序列化和反序列化庫的開源協議: pb,Thrift. 擴展至關方便,序列化和反序列化方便(xxx項目目前使用pb)
文本化協議: xml,json. 序列化,反序列化容易,可是佔用體積大(通常http接口採用json格式).
...
...
同時支持TCP 和 HTTP 方式, 關聯性不大的業務服務獨立開來
服務支持平行擴展,平行擴展方便且對用戶無感知
cache db層的封裝,業務調用方直接調用接口便可.
除了Access server是有狀態的,其餘服務無狀態
各個服務之間,經過rpc通訊,能夠跨機器.
oracle裏面都是模塊化,有點相似MVC模式, 代碼解耦, 功能解耦.
oracle 太過龐大, 能夠把某些業務抽取出來
缺點
改進
push server 沒有業務,僅僅是轉發Access和oracle之間的請求
缺點
改進
Access server和用戶緊密鏈接,維持長鏈接的同時,還有部分業務
缺點
改進:
爲何有可能會亂序?
對於在線消息, 一發一收,正常狀況固然不會有問題
對於離線消息, 可能有不少條.
怎麼保證不亂序?
每條消息到服務端後,都會生成一個全局惟一的msgid, 這個msgid必定都是遞增增加的(msgid的生成會有同步機制保證併發時的惟一性)
針對每條消息,會有消息的生成時間,精確到毫秒
拉取多條消息的時候,取出數據後,再根據msgid的大小進行排序便可.
消息爲何可能會重複呢?
這種狀況下,就可能會須要有重發機制. 客戶端和服務端均可能須要有這種機制.
既然有重複機制,就有可能收到的消息是重複的.
怎麼解決呢? 保證不重複最好是客戶端和服務端相關處理
消息meta結構裏面增長一個字段isResend. 客戶端重複發送的時候置位此字段,標識這個是重複的,服務端用來後續判斷
服務端爲每一個用戶緩存一批最近的msgids(所謂的localMsgId),如緩存50條
服務端收到消息後, 經過判斷isResend和此msgid是否在localMsgId list中. 若是重複發送,則服務端不作後續處理.
由於僅僅靠isResend不可以準備判斷,由於可能客戶端確實resend,可是服務端確實就是沒有收到......
最簡單的就是服務端每傳遞一條消息到接收方都須要一個ack來確保可達
服務端返回給客戶端的數據,有可能客戶端沒有收到,或者客戶端收到了沒有迴應.
考慮一個帳號在不一樣終端登陸後的狀況.
這裏提供兩種方案供參考(本質思想同樣,實現方式不一樣)
每一個用戶的每條消息都必定會分配一個惟一的msgid
服務端會存儲每一個用戶的msgid 列表
客戶端存儲已經收到的最大msgid
優勢:
根據服務器和手機端之間sequence的差別,能夠很輕鬆的實現增量下發手機端未收取下去的消息
對於在弱網絡環境差的狀況,丟包狀況發生機率是比較高的,此時常常會出現服務器的回包不能到達手機端的現象。因爲手機端只會在確切的收取到消息後纔會更新本地的sequence,因此即便服務器的回包丟了,手機端等待超時後從新拿舊的sequence上服務器收取消息,一樣是能夠正確的收取未下發的消息。
因爲手機端存儲的sequence是確認收到消息的最大sequence,因此對於手機端每次到服務器來收取消息也能夠認爲是對上一次收取消息的確認。一個賬號在多個手機端輪流登陸的狀況下,只要服務器存儲手機端已確認的sequence,那就能夠簡單的實現已確認下發的消息不會重複下發,不一樣手機端之間輪流登陸不會收到其餘手機端已經收取到的消息。
假如手機A拿Seq_cli = 100 上服務器收取消息,此時服務器的Seq_svr = 150,那手機A能夠將sequence爲[101 - 150]的消息收取下去,同時手機A會將本地的Seq_cli 置爲150
每一個用戶的每條消息都必定會分配一個惟一的msgid
服務端會存儲每一個用戶的msgid 列表
客戶端存儲已經收到的最大msgid
這兩種方式的優缺點?
方式二中,確認機制都是多一次http請求. 可是可以保證及時淘汰數據
方式一中,確認機制是等到下一次拉取數據的時候進行肯定, 不額外增長請求, 可是淘汰數據不及時.
心跳功能: 維護TCP長鏈接,保證長鏈接穩定性, 對於移動網絡, 僅僅只有這個功能嗎?
心跳其實有兩個做用
心跳保證客戶端和服務端的鏈接保活功能,服務端以此來判斷客戶端是否還在線
心跳還須要維持移動網絡的GGSN
運營商經過NAT(network adddress translation)來轉換移動內網ip和外網ip,從而最終實現連上Internet,其中GGSN(gateway GPRS support Node)模塊就是來實現NAT的過程,可是大部分運營商爲了減小網關NAT的映射表的負荷,若一個鏈路有一段時間沒有通訊就會刪除其對應表,形成鏈路中斷,所以運營商採起的是刻意縮短空閒鏈接的釋放超時,來節省信道資源,可是這種刻意釋放的行爲就可能會致使咱們的鏈接被動斷開(xxx項目以前心跳有被運營商斷開鏈接的狀況,後面改進了心跳策略,後續還將繼續改進心跳策略)
NAT方案說白了就是將過去每一個寬帶用戶獨立分配公網IP的方式改成分配內網IP給每一個用戶,運營商再對接入的用戶統一部署NAT設備,NAT的做用就是將用戶網絡鏈接發起的內網IP,以端口鏈接的形式翻譯成公網IP,再對外網資源進行鏈接。
從mobile 到GGSN都是一個內網,而後在GGSN上作地址轉換NAT/PAT,轉換成GGSN公網地址池的地址,因此你的手機在Internet 上呈現的地址就是這個地址池的公網地址
最多見的就是每隔固定時間(如4分半)發送心跳,可是這樣不夠智能.
4分半的緣由就是綜合了各家移動運營商的NAT超時時間
心跳時間過短,消耗流量/電量,增長服務器壓力.
心跳時間太長,可能會被由於運營商的策略淘汰NAT表中的對應項而被動斷開鏈接
智能心跳策略
維護移動網GGSN(網關GPRS支持節點)
參考微信的一套自適應心跳算法:
爲了保證收消息及時性的體驗,當app處於前臺活躍狀態時,使用固定心跳。
app進入後臺(或者前臺關屏)時,先用幾回最當心跳維持長連接。而後進入後臺自適應心跳計算。這樣作的目的是儘可能選擇用戶不活躍的時間段,來減小心跳計算可能產生的消息不及時收取影響。
精簡心跳包,保證一個心跳包大小在10字節以內, 根據APP先後臺狀態調整心跳包間隔 (主要是安卓)
掉線後,根據不一樣的狀態須要選擇不一樣的重連間隔。若是是本地網絡出錯,並不須要定時去重連,這時只須要監聽網絡狀態,等到網絡恢復後重連便可。若是網絡變化很是頻繁,特別是 App 處在後臺運行時,對於重連也能夠加上必定的頻率控制,在保證必定消息實時性的同時,避免形成過多的電量消耗。
斷線重連的最短間隔時間按單位秒(s)以四、八、16...(最大不超過30)數列執行,以免頻繁的斷線重連,從而減輕服務器負擔。當服務端收到正確的包時,此策略重置
有網絡但鏈接失敗的狀況下,按單位秒(s)以間隔時間爲二、二、四、四、八、八、1六、16...(最大不超過120)的數列不斷重試
爲了防止雪崩效應的出現,咱們在檢測到socket失效(服務器異常),並非立馬進行重連,而是讓客戶端隨機Sleep一段時間(或者上述其餘策略)再去鏈接服務端,這樣就可使不一樣的客戶端在服務端重啓的時候不會同時去鏈接,從而形成雪崩效應。
用戶A發送消息給用戶B
用戶A發送消息到羣C
未讀消息索引存在的意義在於保證消息的可靠性以及做爲離線用戶獲取未讀消息列表的一個索引結構。
未讀消息索引由兩部分構成,都存在redis中:
假設A有三個好友B,C,D。A離線。B給A發了1條消息,C給A發了2條消息,D給A發了3條消息,那麼此時A的未讀索引結構爲:
hash結構
zset結構
User | MsgId 1 | MsgId 2 | MsgId 3 |
---|---|---|---|
B | 1 | - | - |
C | 4 | 7 | - |
D | 8 | 9 | 10 |
消息上行以及隊列更新未讀消息索引是指,hash結構對應的field加1,而後將消息id追加到相應好友的zset結構中。
接收ack維護未讀消息索引則相反,hash結構對應的field減1,而後將消息id從相應好友中的zset結構中刪除。
該流程用戶在離線狀態的未讀消息獲取。
該流程主要由sessions/recent接口提供服務。流程以下:
和在線的流程相同,離線客戶端讀取了未讀消息後也要發送接收ack到業務端,告訴它未讀消息已經下發成功,業務端負責維護該用戶的未讀消息索引。
和在線流程不一樣的是,這個接收ack是經過調用messages/lastAccessedId接口來實現的。客戶端須要傳一個hash結構到服務端,key爲經過sessions/recent接口下發的好友id,value爲sessions/recent接口的未讀消息列表中對應好友的最大一條消息id。
服務端收到這個hash結構後,遍歷它
這樣就完成了離線流程中未讀消息索引的維護。
若是消息標記爲offline,則將消息入庫,寫緩存(只有離線消息才寫緩存),更新未讀消息索引,而後調用apns進行推送。
若是消息標記爲online,則直接將消息入庫便可,由於B已經收到這條消息。
若是消息標記爲redeliver,則將消息寫入緩存,而後調用apns進行推送。
拆分出來的目的:
真的可以起到這樣的效果麼?
鏈接層更穩定 - - - 須要有硬性指標來判斷才能肯定更穩定,由於Access的服務不重,目前也不是瓶頸點.
目前Access服務不重, 拆分出來真有必要嗎?
真要拆分, 那也不是這麼拆分, 是在Oracle上作拆分, 相似微服務的那種概念
穩定性不是這麼體現,原來 connd 的設計,更薄不承擔業務,而如今的 access 仍是有一些業務邏輯,那麼它升級的可能性就比較高。
access 拆分,目的就是讓保持鏈接的那一層足夠薄,薄到怎麼改業務,它都不用升級代碼(tcp 不會斷)。
減小重啓,方便Access服務升級 - - - 不能經過增長一層服務來實現重啓升級,須要有其餘機制來確保服務端進行升級而不影響TCP長鏈接上的用戶
拆分出來的connd server 仍是有可能會須要重啓的, 這時候怎麼辦呢 ?關鍵性問題仍是沒有解決
加一層服務,是打算經過共享內存的方式,connd 只管理鏈接。access 更新升級的時候,用戶不會掉線。
增長一個服務,就多了一條鏈路, 就可能會致使服務鏈路過長,請求通過更多的服務,會致使服務更加不可用. 由於要保證每一個服務的可用性都到99.999%(5個9)是很難的,增長一個服務,就會下降整個服務的可用性.
架構改進必定要有數據支撐, 要確實起到效果, 要有數據輸出才能證實這個改進是有效果的,要否則花了二個月時間作改進,結果沒有用,浪費人力和時間,還下降開發效率
方案: 增長一條信令交互,服務端若是要重啓/縮容, 告知鏈接在此Access上的全部客戶端,服務端要升級了,客戶端須要重連其餘節點
等肯定當前Access節點上的全部客戶端都鏈接到其餘節點後, 當前Access節點再進行重啓/下線/縮容.
怎麼擴容? 若是須要擴容,則增長新的節點後,經過etcd進行服務發現註冊.客戶端經過router server請求數據後,拉取到相關節點.
若是當前3個節點扛不住了,增長2個節點, 這個時候,要可以立刻緩解當前3個節點壓力,須要怎麼作?
按照以前的方式,客戶端從新登陸請求router server,而後再進行鏈接的話,這是不可以立刻緩解壓力的,由於新增的節點後, 當前壓力仍是在以前幾個節點
因此, 服務端須要有更好的機制,來由服務端控制
服務端發送命令給當前節點上的客戶端,讓客戶端鏈接到新增節點上.
服務端還須要肯定是否有部分鏈接到其餘節點了,而後再有相應的策略.
線上機器都有防火牆策略(包括硬件防火牆/軟件防火牆)
硬件防火牆: 硬件防火牆設備,很貴,目前有采購,可是用的少
軟件防火牆: 軟件層面上的如iptable, 設置iptable的防火牆策略
TCP 通道層面上
socket建連速度的頻率控制, 不能讓別人一直創建socket鏈接,要否則socket很容易就爆滿了,撐不住了
收發消息頻率控制, 不能讓別人一直可以發送消息,要否則整個服務就掛掉了
要可以發送消息, 必需要先登陸
要登陸, 必須有token,有祕鑰
收發消息也能夠設置頻率控制
爲啥xmpp不適合,僅僅是由於xml數據量大嗎 ?
目前也有方案是針對xmpp進行優化處理的. 所以流量大並非主要缺點
還有一點就是消息不可靠,它的請求及應答機制也是主要爲穩定長連網絡環境所設計,對於帶寬偏窄及長連不穩定的移動網絡並非特別優化
所以設計成支持多終端狀態的XMPP在移動領域並非擅長之地
爲啥mqtt不適合? 爲啥xxx項目沒有用mqtt ?
mqtt 適合推送,不適合IM, 須要業務層面上額外多作處理, 目前已經開始再用
xxx項目不用mqtt是歷史遺留問題,由於剛開始要迅速開展,迅速搭建架構實現,所以用來蘑菇街的teamtalk.
若是後續選型的話, 若是沒有歷史遺留問題,那麼就會選擇使用mqtt
除了數據量大, 還要考慮協議的複雜度, 客戶端和服務端處理協議的複雜度?
協議要考慮容易擴展, 方便後續新增字段, 支持多平臺
要考慮客戶端和服務端的實現是否簡單
編解碼的效率
服務須要可以跨機房,尤爲是有狀態的節點.
須要儲備多機房容災,防止整個機房掛掉.
維持TCP長鏈接,包括心跳/超時檢測
收包解包
防攻擊機制
等待接收消息迴應(這個以前沒有說到,就是把消息發送給接收方後還須要接收方迴應)
消息爲何可能會亂序? 怎麼保證消息不亂序?
對於離線消息,存儲方式/存儲結構要怎麼設計?
如何保證消息不丟,不重? 怎麼設計消息防丟失機制?
對於長鏈接, 怎管理這些長鏈接?
接入層節點有多個,並且是有狀態的.經過什麼機制保證從節點1下發的請求,其對應的響應仍是會回到節點1呢?
【"歡迎關注個人微信公衆號:Linux 服務端系統研發,後面會大力經過微信公衆號發送優質文章"】