Android-IM架構設計
###1. 架構總覽
###2. 模組介紹
####2.1 協議封裝與任務流程
#####1) 協議與任務的封裝
a. 協議有協議頭(協議頭因為格式相同,被抽象出來)和協議體組成,協議有兩類:請求協議(request)和回覆協議(response);
b. 任務(action)由請求協議、回覆協議和任務回撥(callback)組成;
c. callback是針對客戶端主動請求協議的相應處理,分別是成功回撥、超時回撥和失敗回撥;
#####2) 訊息(任務)流程
a. 由UI或SYSTEM觸發一個訊息的生成,隨之將其投遞到傳送佇列中,等待發送; b. 訊息傳送執行緒會不停的從傳送佇列中拉取一個訊息併發送出去,同時放入超時監測佇列;而當網路斷開時會等待若干時間並重新迴圈,若檢測該訊息在佇列等待時間過長,則丟棄該訊息並觸發相應的失敗回撥; d. 對於客戶端主動請求,在收到服務端給予回覆時,呼叫該訊息的成功回撥處理相應回覆; e. 對於超時執行緒監測超時的訊息,移除超時監測佇列,並呼叫超時回撥。
####2.2 定時任務
定時任務的實現主要由TimerHelper類與ITimerProcessor兩個類,第一個類主要實現了Timer的功能,ITimerProcessor為一個介面,當要建立一個Timer的時候,需要新建立一個類實現ITimerProcessor中得process介面來處理具體的業務。
TimerHelper類的內部封裝了系統的Timer及TimerTask來實現,對外提供startTimer以及stopTimer介面。startTimer需要一個布林型別的引數標誌啟動定時任務是需要執行一次,還是永久執行。
新生成TimerHelper例項的時候需要制定定時任務的時間以及定時觸發處理介面ITimerProcessor的具體實現。
當然在Timer的設計中,我們並沒有完全採用系統提供的Timer類實現,有些Timer是我們採用執行緒模擬實現,這個主要是基於Java 中Timer的一些缺陷來考慮。下面簡單介紹下四種Timer
#####2.2.1 心跳Timer
心跳Timer 是維持客戶端與服務端長連一個強有力的保證。網路中接收發送都是使用socket的recv與send進行傳送與接收,如果此套接字已經斷開,則傳送與接收資料都會出現問題,建立心跳機制,就是為了及時檢測該套接字是否有效。
所謂心跳就是給服務端傳送一個自定義的包,來告訴服務端,自己線上,以確保長連的有效性。
#####2.2.2 傳送超時Timer
在開發網路應用程式的時候,處理業務和通訊流程之間經常會出現矛盾。這種矛盾主要是由於兩者之間的不同步造成的。比如,網路的延遲較大,而實際業務處理的速度則相對比較快,那麼如果處理完某一事務然後等待發送成功再處理下一個事務則會大大降低效率。所以我們建立了一個傳送訊息佇列。這是一個典型的“生產者消費者”模型,業務邏輯將需要傳送的資料放到訊息佇列中,SendPacketMonitor從訊息佇列中取出資料,併發送出去。
由於使用了訊息佇列的模式,傳送就變成了一種非同步操作,業務邏輯將訊息放入訊息佇列後,就可以進行其他的操作,而無法知道該訊息是否真正傳送成功。因此,我們在設計訊息佇列的時候採用了回撥機制,業務方在放入訊息佇列的時候,必須實現onSuccess介面與onTimeOut介面,分別在傳送成功與傳送超時呼叫。
傳送超時Timer是自己採用執行緒來模擬實現的,在SendPacketMonitor類,作為消費者,會不停的從訊息佇列中取出資料,取出資料後,會判斷該訊息產生的時刻與當前時間相比較,如果發現時差已經超過系統定義的最大超時時間,則直接呼叫“生產者”的onTimeOut介面,通知其傳送超時。
#####2.2.3 重連“Timer”
重連是另一個保證長連的機制,雖然我們使用了心跳機制來保證長連,但是由於網路環境的複雜性,我們無法保證在一個連線開啟後,就永遠保持連線,因此,重連就成了另外一個保證。
重連主要是為了當連線斷開的時候,客戶端能夠自動快速的連線到服務端。為了系統的穩定性,及相應快速,重連Timer採用的是執行緒模擬Timer實現。
重連的邏輯中,會去檢測服務端的心跳包,如果發現長時間沒有收到服務端的任何資料包,則認為該socket已經失效,並進行重連。
在重連Timer中,為了防止雪崩效應的出現,我們在檢測到socket失效,並不是立馬進行重連,而是讓客戶端隨機Sleep一段時間再去連線服務端,這樣就可以使不同的客戶端在服務端重啟的時候不會同時去連線,從而造成雪崩效應。
#####2.2.4 好友狀態Timer
雖然在實現的邏輯中,服務端在好友狀態變化的時候,會主動推送訊息給客戶端,但是我們還是設計了好友狀態Timer。因為在網路複雜的環境中,有太多的未知因素。
好友狀態Timer的基本實現就是每隔一段時間傳送一個數據包請求獲得好友的狀態資訊。當收到響應資料包的時候,就會去更新Cache中的好友狀態資訊。
####2.3狀態管理
#####2.3.1 狀態管理StateManager
狀態管理就是一個多狀態的狀態機,其中包括Net狀態管理(指硬體網路是否可用),Socket狀態管理(指與MsgServer的連線是否可用)。
狀態管理主要功能就是採集Net狀態與Socket狀態,提供兩個介面notifyNetState與notifySocketState兩個介面供Net狀態管理與Socket狀態管理呼叫,當接收到狀態變化的時候會呼叫NetDispach進行網路狀態變更分發。
#####2.3.2 Net狀態管理NetStateManager
Net狀態管理指的是物理網路狀態的管理,主要管理當前物理網路是否可用以及進行變化時進行監聽,當監聽到網路斷開或者連上事件的時候,會呼叫狀態管理notifyNetState介面。
在應用啟動的時候我們會註冊網路變化廣播接收
public class ConnectionChangeReceiver extends BroadcastReceiver
Override他的onReceive函式,在onReceive 函式中獲取網路連線服務,然後呼叫NetStateManager的setState介面,通知狀態變化。
#####2.3.3 Socket狀態管理
Socket狀態管理指的是客戶端與MsgServer之間的連線狀態。當檢測到連線不可用時,會呼叫該介面的setState介面設定狀態。當socket的channelConnected、exceptionCaught與channelDisconnected函式被呼叫的時候以及在重連出現異常或失敗的時候會通知進行狀態變更。
#####2.3.4 狀態變更分發
狀態變更分發對外提供三個介面register,unregister與dispachMsg介面。外界如果關心網路狀態變化事件,可以註冊自己的Handler到該類,當網路狀態發生變化的時候,會根據註冊的Handler進行事件通知。
register介面為提供註冊的介面。
unregister介面為取消註冊的忌口。
dispachMsg為事件分發介面,當網路狀態發生變化的時候,該介面會被呼叫,通知各個Handler進行處理。
####2.4 斷線重連
斷線重連機制是當IM與MsgServer斷開後能夠自動連線。斷線重連為一個單獨的執行緒,進行迴圈,當檢測到NetState為不可用的時候,會隨機睡眠1-9秒然後繼續檢測。同時會檢測心跳包,當發現最後一次收到心跳包超過MAX_HEART_BEAT_TIME時間會認為Socket連線不可用而重置SocketState的狀態。當檢測到網路可用,Socket狀態不可用的時候,就會啟動斷線重連機制,在進行斷線重連之前,會進行連線次數判斷,如果為第一次重連,則隨機睡眠1秒多,這個機制主要是為了防止服務端出現異常而重啟的時候大量的客戶端同時連線上來而發生雪崩現象。如果不為第一次重連,則睡眠指定時間,該時間的計算公式如下:
nSleep = (long) Math.pow(2, mnReconnectCount);
if(nSleep > 16) {
nSleep = 16;
}
該計算公式主要是為了防止大量的客戶端不停的進行重連從而對服務端造成大量的壓力,另外從節省客戶端的能耗考慮。
每次進行重連都會將重連次數累加,這個主要是為了防止以後需要對重連次數進行限制。
重連過程如下圖:
####2.5 登陸
登陸流程主要分為以下幾個流程:1、認證;2、獲取MsgServer地址;3、登陸MsgServer。下面依次介紹。
#####2.5.1 認證
認證過程主要是對使用者合法身份的驗證,包括如下兩個方面:
從主客獲取AppToken。該過程是一個反射呼叫,IM程式呼叫主客的獲取Token介面獲取到主客的AppToken,
Dao等資訊。
拿從主客獲取到得AppToken及Dao資訊到IM的驗證伺服器去換取一個IMToken,該過程為一個Http呼叫。伺服器會對上傳的AppToken及Dao資訊進行校驗,如果校驗成功,則會返回一個IMToken,以及LoginServer地址等資訊,後期需要拿該Token到MsgServer進行登陸驗證。
認證的過程主要在TokenManager中實現,該類中還對Token時效進行了管理,當獲取IMToken的時候會先對判斷IMToken是否為空,如果為空則去獲取AppToken等資訊,再去服務端換取IMToken,否則判斷Token的時效是否失效效,如果失效則獲取AppToken並換取IMToken資訊,如果有效,則直接返回IMToken。
#####2.5.2 獲取MsgServer地址
該過程是客戶端通過驗證時拿到的LoginServer地址建立Socket連線,併發送獲取MsgServer請求,LoginServer會返回一個可用的MsgServer的Ip及Port。
#####2.5.3 登陸MsgServer
當經過3.2獲取到MsgServer的IP及Port後,會更具給定的IP與Port與MsgServer建立一個Socket連線,當連線建立成功後。會攜帶獲取到得IMToken,使用者名稱等資訊傳送一個登陸請求包,如果登陸請求驗證通過,客戶端啟動一個Timer與服務端傳送心跳包保持長連線。
以下為整個登陸的流程:
####2.6 非同步實現
非同步的封裝有兩種實現方式:1、需要更新介面的非同步;2、不需要更新介面的非同步。兩種實現的方式是不同的。同時非同步還有一個非同步管理類。
#####2.6.1 TaskTrigger類
TaskTrigger類主要用來註冊以及管理各個Task,每生成一個非同步例項,都需要通過trigger介面如果對於需要更新介面的非同步,則直接呼叫AsyncTask的execute介面執行任務,否則將其放入Task的任務佇列中,後臺會通過process介面呼叫dotrigger介面執行具體的任務,執行結束後,呼叫task的callback介面。
#####2.6.2 需要更新介面的非同步
該方式主要整合Android自有的AsyncTask類,對其進行了一個簡單的封裝,該非同步實現方式主要解決一些需要更新UI介面的非同步,解決Android中飛UI執行緒不能更新UI的問題。
#####2.6.3 不需要更新介面的非同步
該非同步主要提供不需要更新UI的一些非同步操作,該實現方式為新開啟一個執行緒執行任務,當任務執行完成之後呼叫回撥函式。
####2.7 本地快取
存訊息時,根據快取中之前一條訊息的ID,獲得新訊息的唯一自增ID,將該訊息存入DB。存訊息時依賴三張表,聯絡人表,主訊息表和附加訊息表。首先會根據當前收發使用者的ID從聯絡人表中得到兩者的唯一聯絡ID,並設定訊息中的聯絡ID,將該訊息存入主訊息表和附加訊息表。
讀訊息時,直接從DB中拉取即可,根據引數的不同拉取的訊息不同。可以根據訊息ID,拉取某一天訊息,也可以根據起止訊息ID,拉取指定偏移量和指定條數的訊息列表。
存使用者資訊時,判斷該使用者資訊是否為空或合法,若為空或不合法,直接返回;若合法,則更新 快取中的使用者資訊,使用者資訊只保留在快取中。
取使用者資訊時,先判斷快取中是否存在該使用者資訊,若存在,則直接返回該使用者資訊;否則返回null,由業務端選擇是否發起取使用者資訊的操作。
收發訊息時,得到好友ID,判斷快取中得最近聯絡人ID列表中是否存在,若不存在則新增,否則 忽略;同樣地,在獲取最近聯絡人列表時也如此。
⾸首先獲得最近聯絡人ID列表,然後從快取中讀取必要地資訊,組合成最近聯絡人列表。另外,啟用一個執行緒,每500毫秒來檢測是否有新訊息來通知UI主執行緒來更新介面
* 作者:蘑菇街IM團隊四小虎:納納、藍狐、語鬼、舒沉,和移動團隊強力攻城獅支援:海豬