巩鹏军的博客

HOME

IM008 - IM兼容性基础 (第二版)

27 Aug 2022

App随着时间推移,会发布不同版本,但不是每个App用户都会升级到最新版本,服务端只有最新版,所以需要做兼容处理。

1、一个App时怎么办?

首先想到的就是直接使用App版本号判断新老版本并进行兼容处理。

IM008-One IM, One App

一般来说,不同的客户端iOS、Android、Windows、Mac都是同步迭代,多端发版时间一致,App版本号也一样。

所以用跨多端的App版本号可以很容易地让服务端只用写一遍判断和兼容逻辑。

示例:假设从V2.1.0开始应用红包消息,那么判断客户端是否支持红包的逻辑伪代码如下

boolean isClientSupportRedEnvelopMessage(String appVersion) {
  return greaterThanOrEqual(appVersion, "2.1.0");
}

附: 版本号比对逻辑 (未充分考虑异常情况)

List<Integer> versionNumbers(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());
  return verNums;
}

boolean greaterThanOrEqual(String appVersion, String targetVersion) {
  List<Integer> appVerNums = versionNumbers(appVersion);
  Integer appMajorVerNum = appVerNums.get(0);
  Integer appMinorVerNum = appVerNums.get(1);
  Integer appPatchVerNum = appVerNums.get(2);
  List<Integer> targetVerNums = versionNumbers(targetVersion);
  Integer targetMajorVerNum = targetVerNums.get(0);
  Integer targetMinorVerNum = targetVerNums.get(1);
  Integer targetPatchVerNum = targetVerNums.get(2);
  return (appMajorVerNum >= targetMajorVerNum) || 
         (appMinorVerNum >= targetMinorVerNum) || 
         (appPatchVerNum >= targetPatchVerNum);
}

2、多个App时怎么办?

只有一个App比较简单。现实是一套IM系统用于多个业务场景是很普遍的现象。业界的知名IM产品:钉钉、飞书、企业微信、美团大象等都是这样。底层逻辑大概是:IM系统比较复杂,功能繁多而且难以实现,更难以稳定,所以一个IM团队维护一套IM系统,然后应用在多个业务场景就是最具性价比的选择了。

2.1、使用App版本号

每个业务场景都会有自己的客户端App,每个App都有自己的版本号,那么根据App版本号判断新老版本的逻辑就不适用了。

IM008-One IM, One App

一个App时

boolean isClientSupportRedEnvelopMessage(String appVersion) {
  return greaterThanOrEqual(appVersion, "2.1.0");
}

多个App时:

boolean isClientSupportRedEnvelopMessage(String appVersion) {
  return (yaml.appName.equals("App 1") && greaterThanOrEqual(appVersion, "2.1.0")) ||
         (yaml.appName.equals("App 2") && greaterThanOrEqual(appVersion, "2.2.3")) ||
         (yaml.appName.equals("App 3") && greaterThanOrEqual(appVersion, "6.1"));
}

2.2、使用App版本号的麻烦

随着App的增多,需要的判断也越多,这会很麻烦,也很容易出错。

每个App推出新版本后,用户不可能瞬间就升级到最新版本,根据经验,每个App往往都会同时存在十个以上的不同版本。这就会形成如下图所示的局面:

IM008-One IM, One App

3、多个App情况分析

多个App时的问题总结起来就是:一套服务端代码如何适应集成了不同IM能力的不同App客户端?

3.1、一套代码多个App共享

分析一下,如下图所示:假设一个IM团队维护的IM相关的客户端模块有IM Client SDK、联系人、长连接、朋友圈等四个模块。

IM008-One IM, Multiple Apps

因为三个App面向的客户群不同,发版节奏不同,所以各自集成的IM的能力也不同。比如:

从上图可以看出,因为IM核心能力是同一个团队维护,所以Core包含的多个模块的代码必然是只有一套源代码。不同App只是Core集成打包出来的产物,或者说不同App只是Core外面套了不同的壳而已,只要Core一样,则App的IM能力就一样。

3.2、给Core一个版本号

那么,我们能不能给Core一个版本标识呢?

IM008-One IM, Multiple Apps

3.3、给App打上Core版本标签

站在App的角度,每个App相当于打上了Core版本标签:

IM008-One IM, Multiple Apps

3.4、抛开App看Core版本

如果不看App版本,只看Core版本标签:

IM008-One IM, Multiple Apps

3.5、从一套服务端代码看Core版本

同一个IM团队,其IM Servers必然也是同一套代码集,不考虑部署的区别,那么上图逻辑上等价于下图:

IM008-One IM, Multiple Apps

3.6、使用Core版本的兼容性判断

站在Core的视角,多个App就像单个App类似,只是使用的版本标识不同。

还拿是否支持红包的判断举例:

一个App时

boolean isClientSupportRedEnvelopMessage(String appVersion) {
  return greaterThanOrEqual(appVersion, "2.1.0");
}

多个App时:

boolean isClientSupportRedEnvelopMessage(Integer coreVersion) {
  return coreVersion >= 2;
}

通过Core版本号,我们可以把兼容逻辑判断简化到和单个App一样的简单。

3.7、关于Core版本的命名和取值

关于Core版本号的取值,有下列可能的选项:

因为Core版本号不用给最终用户看的,无需遵循常见的语义版本号规范。而且Core版本号只用于版本对比,所以整数会是一个比较好的选择,方便比较,准确可靠。

用自然数 1、 2、 3作为Core版本号是可以的,每个迭代发布新的Core版本时递增一下就可以了。

但是考虑到有多个终端平台iOS、Android、Windows、Mac,如果某个平台的Core发布后发现小Bug需要HotFix,那么要递增版本号,就会挤占其它端的下一个自然数。究其原因,在于自然数是连续的,没办法在两个常规的版本间插入一个HotFix版本。

IM008-One IM, Multiple Apps

选项三就可以解决这个问题,因为Core的迭代发布日期是稀疏的,若干天才会发布一个Core版本,那么当某个端需要一个HotFix版本时,选择HotFix当天的日期作为版本号即可。

总体上,多个端的主要版本号都是约定的统一的发布日期,多端一致,同时允许某个端临时HotFix插入一个新的版本号,保留弹性。

参考 Google 对Android SDK API版本的实践,我们可以把Core版本号命名为core-level,取值为Core的发布日期的整数表示。

3.8、其它版本标识

3.8.1、platform

一套Core,不同端在实际开发中,可能存在差异,为了针对具体端进行特定的兼容,需要知道当前是哪个端,可以约定platform字段表示端。取值可以是:iosandroidwinmaclinux等。

3.8.2、App版本号

在IM相关逻辑的兼容性判断中,只需使用跨App的多端一致的core-level了。但是为了和最终用户、产品经理等沟通方便,保留App版本号app-version用于人和人之间沟通交流。core-level主要用于研发工程师之间,还有工程师和程序之间的沟通。两者各取所长。

3.9、版本标识传输方式

每个API和每条长连接数据包都携带Core版本,这样服务端可以无状态得处理每一个请求。如果需要在服务端主动推送时区分目标端的版本,可以在App登录时将其携带的Core版本落库存储,然后推送时查询使用。

3.9.1、短连接

HTTP短连接通过新增Header字段方式传输:

curl "https://{domain}/api/v1/xxx" \
  -H "platform: ios" \
  -H "app-version: 8.0.25" \
  -H "core-level: 220819" \
  -H "traceid: 0ad1348f1403169275002100356696"

3.9.2、长连接

长连接SDK通过类似HTTP Header的方式传输:

{
  "platform": "ios",
  "app-version": "8.0.25",
  "core-level": "220819",
  "traceid": "0ad1348f1403169275002100356696"
}

3.9.3、短转长

短转长时HTTP Header会转换为长连接数据body里的header通过长链传递。

这样就同时存在长连接header和长连接body.header两套字段,最终以长连接body.header为准即可。

3.9.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]+)( |$)/

4、多个App解决方案总结

至此,我们找到了一个适用于多个App、多个子模块、多个功能点、临时BugFix的版本标识:Core版本号,可以很好地解决多App兼容性问题。

boolean isClientSupportRedEnvelopMessage(Integer coreLevel) {
  return coreLevel >= 220819;
}

5、参考资料

关于我

专注于IM即时通讯全技术栈的程序员,关注获取更多IM技术文章。

gongpengjunblog

IM小蓝天

微信扫描二维码,关注我的公众号