本文由有赞技术团队原创分享,原题“有赞 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)