关于即时通讯:IM开发干货分享有赞移动端IM的组件化SDK架构设计实践

100次阅读

共计 9184 个字符,预计需要花费 23 分钟才能阅读完成。

本文由有赞技术团队原创分享,原题“有赞 APP IM SDK 组件架构设计”,即时通讯网收录时有订正和改变,感激原作者的自私分享。

1、引言

本文次要以 Android 客户端为例,记录了有赞旗下 App 中应用自研 IM,并将 IM 提炼成组件化 SDK 的设计思路。此项工作由有赞挪动开发组 IM SDK 团队独特探讨实现。

在有赞产品中,存在大量须要交易单方沟通交流的场景,比方,客户征询商家产品信息,售前售后简略的答疑和维权等。另外,有赞业务还存在一些非凡的简单场景,如供应商、分销商、客户三方之间须要同步沟通,会同时存在多种沟通角色。

此时须要较为欠缺的即时通信(IM)解决方案,然而因为有赞针对不同的商户和应用场景有多个 APP,APP 自行实现 IM 性能代价较大,且保护起来人力扩散,于是,IM SDK 我的项目便应运而生了,APP 通过接入此给件化 SDK,能够疾速实现 IM 基本功能。

学习交换:

– 即时通讯 / 推送技术开发交换 5 群:215477170[举荐]
– 挪动端 IM 开发入门文章:《新手入门一篇就够:从零开发挪动端 IM》

本文已同步公布于“即时通讯技术圈”公众号,欢送关注:

▲ 本文在公众号上的链接是:https://mp.weixin.qq.com/s/ANp1kuj65Ww5RpABl2M9RQ,原文链接是:http://www.52im.net/thread-3088-1-1.html

2、相干文章

《从游击队到正规军 (一):马蜂窝旅游网的 IM 零碎架构演进之路》
《从游击队到正规军(二):马蜂窝旅游网的 IM 客户端架构演进和实际总结》(* 举荐)
《从游击队到正规军(三):基于 Go 的马蜂窝旅游网分布式 IM 零碎技术实际》
《一套海量在线用户的挪动端 IM 架构设计实际分享(含具体图文)》
《从零到卓越:京东客服即时通讯零碎的技术架构演进历程》
《一套原创分布式即时通讯(IM) 零碎实践架构计划》
《蘑菇街即时通讯 /IM 服务器开发之架构抉择》
《自已开发 IM 有那么难吗?手把手教你自撸一个 Andriod 版繁难 IM (有源码)》
《适宜老手:从零开发一个 IM 服务端(基于 Netty,有残缺源码)》
《拿起键盘就是干:跟我一起徒手开发一套分布式 IM 零碎》

3、设计指标

本次 IM 组件化 SDK 的设计指标有以下几点:

  • 1)IM 主流程稳固可用:音讯传输具备高可靠性;
  • 2)UI 组件间接集成进入 SDK,并反对可定制化;
  • 3)富媒体发送集成进入 SDK,并可按需定制须要的富媒体类型;
  • 4)实现音讯传输层 SDK,与带有 UI 的 SDK 的性能拆散,业务调用方既能够应用音讯传输 SDK,解决音讯,而后自行处理 UI,也能够应用带有 UI 组件的 SDK,一步实现较为齐备的 IM 性能。

4、整体构造

下图中简要形容了有赞客户端中 IM 零碎的根本构造:

如上图所示,各分层的职责分工如下:

  • 1)音讯通道层:保护 Socket 长连贯作为音讯通道,音讯收发流程次要在这一层中实现;
  • 2)长久化层:次要将音讯存入数据库中,富媒体文件存入文件缓存中,不便第二次展现音讯时候,从本地加载,而不是网络层获取;
  • 3)逻辑解决层:实现各种音讯相干的逻辑解决,如排序,富媒体文件的预处理等;
  • 4)UI 显示层:将数据在 UI 上进行出现。

5、设计要点 1:Socket 长连贯的创立与保护

IM SDK 所有数据收发流程,均通过 Socket 长连贯实现,如何保护一个稳固 Socket 通道,是 IM 零碎是否稳固的重要一环。

上面形容下 Socket 通道几个重要的流程。

1)创立流程(连贯):

如图上所示,当 IM SDK 初始化后,业务调用连贯申请接口,会开始连贯的创立过程,创立胜利后,会实现鉴权操作,当创立和鉴权都实现后,会开启音讯收发线程,为了维持长连贯,会有心跳机制,特地的,会开启一个心跳轮询线程。

2)心跳机制:

心跳机制,是 IM 零碎设计中的常见概念,简略的解释就是每隔若干工夫发送一个固定信息给服务端,服务端收到后及时回复一个固定信息,如果服务端若干工夫内没有收到客户端心跳信息则视客户端断开,同理如果客户端若干工夫没有收到服务端心跳回值则视服务端断开。

 

当长连贯创立胜利后,会开启一个轮询线程,每隔一段时间发送心跳音讯给服务器端,以维持长连贯。

无关 IM 心跳方面的专项文章,请见:

《手把手教你用 Netty 实现网络通信程序的心跳机制、断线重连机制》
《为何基于 TCP 协定的挪动端 IM 依然须要心跳保活机制?》
《挪动端 IM 实际:实现 Android 版微信的智能心跳机制》
《挪动端 IM 实际:WhatsApp、Line、微信的心跳策略剖析》
《一文读懂即时通讯利用中的网络心跳包机制:作用、原理、实现思路等》
《正确理解 IM 长连贯的心跳及重连机制,并入手实现(有残缺 IM 源码)》
《一种 Android 端 IM 智能心跳算法的设计与实现探讨(含样例代码)》
《手把手教你用 Netty 实现网络通信程序的心跳机制、断线重连机制》

3)重连流程:

重连被触发时,如果该次连贯胜利,退出重连。反之重连失败后,会判断以后重连的次数是否超过预期值(这里设为 6 次),并对重连次数计数,如果超过就会退出重连,反之休眠预设的工夫后再次进行重连操作。

重连触发条件分为三种:

  • a. 被动连贯不胜利(被动连贯 Socket,如果连贯失败,会触发重连机制);
  • b. 网络被被动断开(失常建设连贯,操作过程中,网络被断开,通过零碎播送触发重连);
  • c. 服务器没响应,心跳没回值(服务端心跳预设工夫内没回值,客户端认为服务端曾经断开,触发重连)。

无关重连机制的深刻学习,能够浏览以下两篇:

  • 《正确理解 IM 长连贯的心跳及重连机制,并入手实现(有残缺 IM 源码)》
  • 《手把手教你用 Netty 实现网络通信程序的心跳机制、断线重连机制》

4)网络状态判断:

TCP API 并没有提供一个牢靠的办法判断以后长连贯通道状态,isConnected()和 isClosed()仅仅通知你以后的 Socket 状态,不是是长连贯断开是一回事。isConnected()通知你是否 Socket 与 Romote host 放弃连贯,isClosed()通知你是否 Socket 被敞开。

如果你判断长连贯通道是否被敞开,只能通过和流操作相干的以下办法:

  • a. read() return -1;
  • b. readLine() return null;
  • c. readXXX() throw EOPException for any other XXX;
  • d. write 将抛出 IOException: Broken pipe(通道被敞开)。

所以 SDK 封装 isConnected(办法的时候,是依据这几种状况综合判断以后的通道状态,而不是仅仅通过 Socket.isConnected()或者 Socket.isClosed()。

6、设计要点 2:音讯发送流程

音讯发送流程次要有两大类:

1)一类是 IM 相干数据的申请,例如:历史音讯列表,会话列表等;

2)另一类是 IM 音讯的发送,次要是文字音讯。

(富媒体音讯发送,会将富媒体文件先上传服务器后,拿到文件 URL, 通过文字音讯,将此 URL 发给接管方,接管方下载后进行 UI 展现)。

以上两类音讯发送,均应用上图的流程进行发送,可通过发送回调感知申请的后果。

如上图所示,音讯发送流程,须要先封装音讯申请,在通过发送队列发送至服务器,发送前,在将申请 id 和对应回调存入本地 Map 数据结构中。

if(requestCallBack != null) {
  mCallBackMap.put(requestId, requestCallBack);
}

之后接管服务器推送音讯(此音讯带有发送申请时的申请 id),在本地的 Map 数据找到申请 id 对应的回调,而后通过回调返回服务器推送过去的数据。

申请能够通过泛型指定返回值类型,SDK 中会自行解析服务器数据返回的数据,间接返回给业务调用方 model 对象,方便使用。(目前反对 json 格局的数据解析)

private void IMResponseOnSuccess(String requestid, String response) {
        if(mCallBackMap != null) {
           IMCallBack callBack = mCallBackMap.get(requestid);
           if(callBack == null) {
               return;
           }
           if(callBack instanceofJsonResultCallback) {
               finalJsonResultCallback resultCallback = (JsonResultCallback) callBack;
               if(resultCallback.mType == String.class) {
                   callBack.onResponse(response);
               } else{
                   Object object = newGson().fromJson(response, resultCallback.mType);
                   callBack.onResponse(object);
               }
               removeCallBack(requestid);
           }
        }
}

如下的示例中,展现了一个获取会话列表的申请,能够看出目前的申请封装,和一些第三方的的网络库相似,应用起来较为不便。

RequestApi requestApi = new RequestApi(IMConstant.REQ_TYPE_GET_CONVERSATION_LIST, Enums Manager.IMType.IM_TYPE_WSC.getRequestChannel());
requestApi.addRequestParams(“limit”, 100); 
requestApi.addRequestParams(“offset”, 0);
IMEngine.getInstance().request(requestApi, newJsonResultCallback<List<ConversationEntity>>() {
    @Override
    publicvoidonResponse(List<ConversationEntity> response) {
        mSwipeRefreshLayout.setRefreshing(false);
        mAdapter.mDataset.clear();
        mAdapter.mDataset.addAll(response);
        mAdapter.notifyDataSetChanged();
    }
    @Override
    publicvoidonError(intstatusCode) {
        //do something
    }
});

能够看出,该申请间接返回了一个会话类型的 List 汇合,业务方可间接应用。

7、设计要点 3:音讯接管流程

音讯的监听流程次要应用了一个全局监听的形式来进行,须要先注册监听器,监听器中有默认的回调。

public interface IMListener {
    /**
     * 连贯胜利
     */
    void connectSuccess();
    /**
     * 连贯失败
     */
    void connectFailure(EnumsManager.DisconnectType type);
    /**
     * 鉴权胜利
     */
    void authorSuccess();
    /**
     * 鉴权失败
     */
    void authorFailure();
    /**
     * 接收数据胜利
     */
    void receiveSuccess(int reqType, String msgId, String requestChannel, String message, int statusCode);
    /**
     * 接收数据失败
     */
    void receiveError(int reqType, String msgId, String requestChannel, int statusCode);
}

该监听器中能够接管如下类型的音讯:

  • 1)Socket 连贯状态的返回后果;
  • 2)鉴权状态的返回后果,(鉴权流程因有赞业务须要);
  • 3)接管的 IM 音讯,或者其余类型的返回音讯。可依据音讯类型进行后续的散发解决。

业务如需应用此全局监听器,须要自行实现此接口,并在业务初始化时,注册此监听器即可。SDK 中会依据注册的监听器,在读取到服务器推送音讯后,间接通过监听器到回调进行散发。

private void distributeData(IMEntity imEntity) {
        if(mIMListener != null&& imEntity != null) {
       // 省略局部逻辑代码
       ……
       if(status == Response.SUCCESS) {
           switch(responseModel.reqType) {
               caseIMConstant.REQ_TYPE_AUTH: // 鉴权胜利
                   mIMListener.authorSuccess();
                   return;
               caseIMConstant.REQ_TYPE_OFFLINE: //  服务端踢客户端下线
                   mIMListener.connectFailure(EnumsManager.DisconnectType.SERVER);
                   break;
               caseIMConstant.REQ_TYPE_HEARTBEAT: // 心跳胜利
               caseIMConstant.REQ_TYPE_RECEIVER_MSG: // 收到回调音讯
                   handleMessageID(responseModel.body);
                   break;
               default:
                   break;
           }
           mIMListener.receiveSuccess(responseModel.reqType, msgId, responseModel
                   .requestChannel, responseModel.body, 0);
       } else{
           mIMListener.receiveError(responseModel.reqType, msgId, responseModel
                   .requestChannel, status);
       }
   }
}

局部接管音讯,如心跳,多端登录时被踢下线告诉等,sdk 外部会自行处理,业务根本无感知。

8、设计要点 4:可定制化的 UI

随着公司规模的扩充与业务线的疾速迭代,可能新的业务也须要 IM 这个性能,家喻户晓,IM UI 性能的嵌入会占据大量的开发与调试工夫, 为了解决这个痛点,决定将 IM UI 局部抽成一个 Library,实现可定制与独自保护,做到真正的麻利开发与疾速迭代。

8.1 UIKit 设计 

IM UIKit 裸露相应的 api 接口,业务方注入相应的性能定制项,针对 UI 的点击回调通过 EventBus 总线 post 散发,缩小了业务方与 UIKit 的耦合,底层业务方通过 MVP 模式对 View 与 Model 进行解耦。

定制项个别通过如下几种形式。

1)XML(定制业务信息,资源信息,显示条数,各个业务性能开关等):

<?xml version=”1.0″ encoding=”utf-8″?>
<resources>
    <stylename=”limit”>
        <!– 每屏展现的条数 –>
        <itemname=”swiplimit”>5</item>
        ……
    </style>
    ……
    ……
    <stylename=”itembox”>
        <itemname=”showvoice”>true</item>
        ……
        ……
        <itemname=”more”show=”true”>
            <more>
                <icon>im_plus_image</icon>
                <itemname> 测试 </itemname>
                <callback>false</callback>
            </more>
             ……
             ……
            <more>
                <icon>ic_launcher</icon>
                <itemname> 测试 </itemname>
                <callback>true</callback>
            </more>
        </item>
        ……
        ……
    </style>
</resources>

2)Style(定制 UI 背景,气泡色彩,字体大小等):

<?xml version=”1.0″ encoding=”utf-8″?>
<resources>
    <!–im 聊天背景 –>
    <stylename=”imui_background”>
        <itemname=”android:background”>@android:color/holo_red_dark</item>
    </style>
    ……
    ……
    <!– 气泡背景 –>
    <stylename=”bubble_background”>
        <itemname=”android:background”>@mipmap/bubble_right_green</item>
    </style>
       <!– 背景和和字段色彩定制 –>
    <stylename=”bg_and_textcolor”parent=”bubble_background”>
        <itemname=”android:textColor”>@android:color/holo_red_dark</item>
    </style>
    ……
    ……
</resources>

3)Model 定制(传入预设的定制 Model 模板填入相应参数,UIKit 外面做相应解析):

public class Entity {
    publicString action1;
    publicString action2;
    publicString aciton3;
    ……
}

8.2 UIKit 反对的富媒体类型

除了文字音讯之外,当初支流的 IM 零碎中也反对各种富媒体发送,在有赞 IM SDK UIKit 中,目前也反对几种富媒体发送。以下是发送流程图和两类常见富媒体音讯简介。

  • 1)语音音讯:除了应用常见的录制和解码播放的技术之外。还利用了 AudioManager 中 requestAudioFocus,abandonAudioFocus 相干办法,实现了录制和播放语音音讯,如果有第三方播放音乐,会主动暂停,录制和播放语音音讯完结后,声音会自动播放。
  • 2)图片音讯:通过七牛服务器设置了缩略图,接管方收到音讯后,会先下载缩略图,当用户再点击进入图片详情页时,会下载大图,Andorid 客户端应用 Picasso 加载库加载图片,并做本地缓存。

9、设计要点 5:UI 中聊天会话数据加载策略

参考业界支流的 IM 零碎计划,用户聊天时,须要将曾经发送和接管到的聊天信息保留到本地,而不是每次都拉取历史数据。以达到节约流量和无网络状态下也查看数据的成果。

为此 IM SDK 长久化层的数据库中,也实现了简略存储加载机制,上面形容典型的数据加载场景。

1)IM 会话首次申请数据流程:

2)IM 下拉获取历史数据流程:

3)IM 单条音讯发送长久化计划:

4)IM 单条数据重发流程:

10、设计不足之处

1)音讯回执:

以后的设计方案中,没有音讯回执的机制,也就是说接受方收到音讯后,不会返回服务器收到音讯的告诉,服务器无奈判断音讯是否推送胜利,这样在忽然断网,网络模式切换,或者弱网环境下,会影响音讯的达到率。

一种可行的设计形式是,发送方减少已送到和未送达的状态,接管方收到音讯后,给服务器返回已收到音讯的告诉,服务器再推送给发送方该状态,如果没有收到接管方回执,服务器可尝试从新推送。发送方承受到接管方的收到回执后,更新发送状态已发送,如果未收到,则显示未送达。为了避免接管方回执失落,接管方接管音讯时候,可保护本地去重队列。

2)本地申请超时的判断:

本地发动的申请,没有用定时器,齐全依赖服务器返回或者呈现 Socket 通道异样后上抛的告诉作为超时判断,局部场景可能笼罩不到,须要对申请减少固定的超时解决机制,固定时候未收到申请,即认为超时。

* 举荐学习:针对以上两点有余,感兴趣的读者,能够钻研一下 MobileIMSDK 开源工程源码 https://github.com/JackJiang2011/MobileIMSDK,MobileIMSDK 曾经实现了残缺的音讯送达保障机制(包含:ACK 回执、重传、去重、超时断定等等)。

(本文同步公布于:http://www.52im.net/thread-3088-1-1.html)

正文完
 0