本文由鞏鵬軍分享,原題“IM兼容性基建”,本文有修訂。
1、引言
一個成熟的IM成品,在運營過程中隨著時間的推移,會發布不同的版本,但為了用戶體驗并不能強制要求用戶必須升級到最新版本,而服務端此時已經是最新版本了,所以為了讓這些不同客戶端版本的用戶都能正常使用(尤其IM這種產品,不同版本可能通信協議都會有變動,這就更要命了),則必須要針對不同客戶端版本的兼容處理。
本文將基于筆者的IM產品開發和運營實踐,為你分享如何實現不同APP客戶端版本與服務端通信的兼容性處理方案。

學習交流:
(本文已同步發布于:http://www.52im.net/thread-4202-1-1.html)
2、關于作者
鞏鵬軍:專注移動開發十多年,熱愛即時通訊技術。個人微信公眾號:“鞏鵬軍”。
作者在即時通訊網分享的另一篇《知識科普:IM聊天應用是如何將消息發送給對方的?(非技術篇)》,感興趣的讀者也可以看看。
3、一個App時怎么辦?
提示:“一個App”指的是同一個IM服務端,只服務于一個特定的IM產品。
首先想到的就是直接使用App版本號判斷新老版本并進行兼容處理。
如下圖所示:
一般來說,不同的IM客戶端(如iOS、Android、Windows、Mac)都是同步迭代,多端發版時間一致,App版本號也一樣。
所以用跨多端的App版本號可以很容易地讓服務端只用寫一遍判斷和兼容邏輯。
示例:假設從V2.1.0開始應用紅包消息,那么判斷客戶端是否支持紅包的邏輯就很簡單。
偽代碼如下:
booleanisSupportRedEnvelop(String appVersion) {
returngte(appVersion, "2.1.0");
}
附:版本號比對邏輯(未充分考慮異常情況):
List<Integer> toNums(String version) {
Matcher matcher = Pattern
.compile("/[0-9]+\\.[0-9]+\\.[0-9]+")
.matcher(version);
String versionString = matcher.find()
? matcher.group(0).substring(1)
: "1.0.0";
List<Integer> verNums = Arrays
.stream(versionString.split("\\."))
.map(Integer::valueOf)
.collect(Collectors.toList());
returnverNums;
}
booleangte(String version, String target) {
List<Integer> appVerNums = toNums(version);
Integer appMajor = appVerNums.get(0);
Integer appMinor = appVerNums.get(1);
Integer appPatch = appVerNums.get(2);
List<Integer> targetNums = toNums(target);
Integer targetMajor = targetNums.get(0);
Integer targetMinor = targetNums.get(1);
Integer targetPatch = targetNums.get(2);
return(appMajor >= targetMajor) ||
(appMinor >= targetMinor) ||
(appPatch >= targetPatch);
}
4、多個App時怎么辦?
4.1概述
提示:“多個App”指的是同一個IM服務端,可能作為通用服務,作為多個不同APP產品中的聊天模塊使用的場景。
只有一個App時肯定是比較簡單的。但現實情況是一套IM系統通常會用于多個業務場景,這是很普遍的現象。業界的知名IM產品,比如釘釘、飛書、企業微信、美團大象等都是這樣。
底層邏輯大概是:IM系統比較復雜,功能繁多而且難以實現、更難以穩定,所以一個IM團隊維護一套IM系統,然后應用在多個業務場景就是最具性價比的選擇了。
4.2使用App版本
每個業務場景都會有自己的客戶端App,每個App都有自己的版本號,那么根據App版本號判斷新老版本的邏輯就不適用了(如下圖所示)。
一個App時可以這樣做兼容性判斷:
booleanisSupportRedEnvelop(String appVersion) {
returngte(appVersion, "2.1.0");
}
多個App時的兼容性判斷:
booleanisSupportRedEnvelop(String version) {
return
(app.equals("App1")&>e(version,"2.1.0"))||
(app.equals("App2")&>e(version,"2.2.3"))||
(app.equals("App3")&>e(version,"6.1"));
}
4.3使用App版本號的麻煩
隨著App的增多,需要的判斷也越多,這會很麻煩,也很容易出錯。
每個App推出新版本后,用戶不可能瞬間就升級到最新版本,根據經驗,每個App往往都會同時存在十個以上的不同版本。
這就會形成如下圖所示的局面:
5、多個App時,可將IM能力提煉為一套公用代碼
多個App時的問題總結起來就是:一套服務端代碼如何適應集成了不同IM能力的不同App客戶端?
我們來具體舉例分析一下,假設一個IM團隊維護的IM相關的客戶端模塊有IM Client SDK、聯系人、長連接、朋友圈等四個模塊(如下圖所示)。
如上圖所示:
- 1)App 1:集成了全部四個模塊;
- 2)App 2:只集成了三個模塊;
- 3)App 3:只集成了三個模塊。
因為三個App面向的客戶群不同,發版節奏不同,所以各自集成的IM的能力也不同。
比如下面這樣:
- 1)App 1:面向內部員工辦公溝通使用的App 1需要功能豐富,對于穩定性和Bug有一定的包容性,也容易溝通和修復再發版;
- 2)App 2:面向客服場景,用于企業的客服專員和企業的C端用戶溝通解決客訴問題,對于穩定性要求高,C端用戶升級率不好控制,發版節奏慢,最快只能和主業務App一致;
- 3)App 3:面向企業和B端供應商,比如美團和美團上的商戶,京東和京東平臺上的第三方商家,對于穩定性要求也比較高,B端商家的升級率好控制一點,發版節奏也可以快一些。
從上圖可以看出,因為IM核心能力是同一個團隊維護,所以Core包含的多個模塊的代碼必然是只有一套源代碼。不同App只是Core集成打包出來的產物,或者說不同App只是Core外面套了不同的殼而已,只要Core一樣,則App的IM能力就一樣(這就是本節標題所述的“多個App時,可將IM能力提煉為一套公用的代碼”這個意思)。
6、給每個App中使用的公用代碼(Core)一個版本號
如上節所述,我們將IM能力提煉為一套公用代碼(以下內容簡稱“Core”)。
那么,我們能不能給Core一個版本標識呢?
答案是肯定的:
站在App的角度,每個App相當于打上了Core版本標簽:
7、如何正確地解讀Core版呢?
7.1拋開App看Core版本
如果不看App版本,只看Core版本標簽:
7.2從一套服務端代碼看Core版本
同一個IM團隊,其IM Servers必然也是同一套代碼集,不考慮部署的區別。
那么上圖邏輯上等價于下圖:
7.3使用Core版本的兼容性判斷
站在Core的視角,多個App就像單個App類似,只是使用的版本標識不同。
具體如下:
- 1)單個App時,IM服務端要區分不同App版本;
- 2)多個App時,IM服務端要區分不同Core版本。
還拿是否支持紅包的判斷舉例。
一個App時:
booleanisSupportRedEnvelop(String appVersion){
returngte(appVersion, "2.1.0");
}
多個App時:
booleanisSupportRedEnvelop(Integer coreVersion){
returncoreVersion >= 2;
}
通過Core版本號,我們可以把兼容邏輯判斷簡化到和單個App一樣的簡單。
8、關于Core版本的命名和取值
關于Core版本號的取值,有下列可能的選項:
- 選項一:語義版本號 1.2.0;
- 選項二:整數 自然數 1 2 3;
- 選項三:整數 迭代日期 20220819 或 220819。
因為Core版本號不用給最終用戶看的,無需遵循常見的語義版本號規范。而且Core版本號只用于版本對比,所以整數會是一個比較好的選擇,方便比較,準確可靠。
用自然數 1、 2、 3作為Core版本號是可以的,每個迭代發布新的Core版本時遞增一下就可以了。
但是考慮到有多個終端平臺iOS、Android、Windows、Mac,如果某個平臺的Core發布后發現小Bug需要HotFix,那么要遞增版本號,就會擠占其它端的下一個自然數。究其原因,在于自然數是連續的,沒辦法在兩個常規的版本間插入一個HotFix版本。
選項三就可以解決這個問題:因為Core的迭代發布日期是稀疏的,若干天后才會發布一個Core版本,那么當某個端需要一個HotFix版本時,選擇HotFix當天的日期作為版本號即可。
總體上:多個端的主要版本號都是約定的統一的發布日期,多端一致,同時允許某個端臨時HotFix插入一個新的版本號,保留彈性。
參考 Google 對Android SDK API版本的實踐,我們可以把Core版本號命名為core_level,取值為Core的發布日期的整數表示。
9、多個App情況下的其它版本標識
1)platform:
一套Core,不同端在實際開發中,可能存在差異,為了針對具體端進行特定的兼容,需要知道當前是哪個端,可以約定platform字段表示端。取值可以是:ios、android、win、mac、linux等。
2)App版本號:
在IM相關邏輯的兼容性判斷中,只需使用跨App的多端一致的core_level了。但是為了和最終用戶、產品經理等溝通方便,保留App版本號app_version用于人和人之間溝通交流。core_level主要用于研發工程師之間,還有工程師和程序之間的溝通。兩者各取所長。
10、版本標識的傳輸方式
每個API和每條長連接數據包都攜帶Core版本,這樣服務端可以無狀態得處理每一個請求。如果需要在服務端主動推送時區分目標端的版本,可以在App登錄時將其攜帶的Core版本落庫存儲,然后推送時查詢使用。
10.1短連接(HTTP)
HTTP短連接通過新增Header字段方式傳輸:
curl "https://{domain}/api/v1/xxx"\
-H "platform: ios"\
-H "app_version: 8.0.25"\
-H "core_level: 220819"
10.2長連接(Socket)
長連接SDK通過類似HTTP Header的方式傳輸:
{
"platform":"ios",
"app_version":"8.0.25",
"core_level":"220819"
}
10.3短轉長
短轉長時HTTP Header會轉換為長連接數據body里的header通過長鏈傳遞。
這樣就同時存在長連接header和長連接body.header兩套字段,最終以長連接body.header為準即可。
10.4其它
IM系統里的瀏覽器和小程序,如果可以新增HTTP Header則新增Header傳輸,實在沒有辦法可以通過User-Agent傳輸該信息,服務端優先解析Header,沒有找到時再解析User-Agent。
服務端解析UA的正則表達式:
/ platform\/(ios|android|mac|win|linux) app_version\/([0-9]\.[0-9]+\.[0-9]+) core_level\/([1-9][0-9]+)( |$)/
以上正則表達式在線運行效果:點此查看。
11、本文小結
至此,我們找到了一個適用于多個App、多個子模塊、多個功能點、臨時BugFix的版本標識:Core版本號,這樣就可以很好地解決多App的IM能力兼容性問題。
以下是版本兼容性判斷偽碼:
booleanisSupportRedEnvelop(Integer coreLevel) {
returncoreLevel >= 220819;
}
12、參考資料
[1] Browser vs Engine Version
[2] Node.js ABI version number
[3] Android SDK API Level
[4] 零基礎IM開發入門(一):什么是IM系統?
[5] 一套海量在線用戶的移動端IM架構設計實踐分享(含詳細圖文)
[6] 一套原創分布式即時通訊(IM)系統理論架構方案
[7] 從零到卓越:京東客服即時通訊系統的技術架構演進歷程
[8] 一套億級用戶的IM架構技術干貨(上篇):整體架構、服務拆分等
[9] 基于實踐:一套百萬消息量小規模IM系統技術要點總結
[10] 一套十萬級TPS的IM綜合消息系統的架構實踐與思考
[11] 從新手到專家:如何設計一套億級消息量的分布式IM系統
[12] 閑魚億級IM消息系統的架構演進之路
[13] 深度解密釘釘即時消息服務DTIM的技術設計
[14] 一套高可用、易伸縮、高并發的IM群聊、單聊架構方案設計實踐
[15] 企業微信的IM架構設計揭秘:消息模型、萬人群、已讀回執、消息撤回等
(本文已同步發布于:http://www.52im.net/thread-4202-1-1.html)