Android SDK 体系架构

本文档将详细介绍融云的 SDK 产品架构和消息体系,以便于您更深入的了解融云并更快速的开发自己的产品。

image
融云 SDK 系统架构

IMKit

IMKit 的功能主要是封装各种界面对象,服务于开发者快速实现自己的产品,主要特点是是支持快速集成,支持丰富的界面定制功能。

IMLib

IMLib 的功能是提供基本通信能力库,封装了通信能力和 Conversation,Message 等各种对象,服务于需要根据自己的产品去自己实现界面的开发者。主要特点是封装清晰,轻量,便于使用。

Protocol

Protocol 是融云的核心协议栈,使用融云自定义的私有二进制协议。主要特点是是轻量化,有序可靠,不丢消息。Protocol 部分使用 Native 语言开发,在 Android 和 iOS 平台上保证业务一致性,便于开发者商用化自己的产品。

IMLib 体系架构

首先介绍 IMLib 的体系,对于真正使用融云 SDK 的用户,不管您选择 IMLib 还是 IMKit,您都需要了解一下体系概念。

image
融云 IMLib 体系架构
IMLib 的命名规则,在 iOS 上是 RCIMClient,在 Android 上是 RongIMClient。这是为了符合两个平台的命名习惯。

消息体系架构详解

会话与消息的关系

融云 SDK 中已经默认包含了消息数据的本地存储机制,开发者不再需要自己进行消息的存储。也就是说,在融云 SDK 收到消息后,先在本地进行了数据存储,然后再通过消息接收事件通知给开发者。开发者可以通过一系列接口读取和操作本地存储的消息数据。

会话实体和消息实体

会话实体类和消息实体类是用来存储本地会话和消息的容器类,除了包含会话内容和消息内容外,还包括了保存在本地的各种状态。

用来存储消息的实体类主要有 Conversation(会话) 和 Message(消息) 两个实体类,您在客户端读取消息时,获取的对象都和这两个类相关。会话有多种类型,可以是私聊会话,也可以是群组会话等,每一个 Conversation(会话)包含多条 Message(消息),关系如下图所示:

image

如何标识一个会话

通过 conversationType 和 targetId,可以唯一确定一个会话。ConversationType 枚举值意义和对应的 targetId 意义为:

会话类型枚举 ConversationType 说明 对应的 targetId
PRIVATE 单聊 用户的 Id(userId)。
GROUP 群组 群组的 Id(groupId)。
DISCUSSION 讨论组 讨论组的 Id(discussionId)。
CHATROOM 聊天室 聊天室的 Id(chatroomId)。
CUSTOMER_SERVICE 客服 客服的 Id(customerServiceId)。
SYSTEM 系统会话 系统账户 Id。可以理解为 QQ 的 10000 号的角色。
APP_PUBLIC_SERVICE 应用公众服务 应用公众服务的 Id(publicServiceId)。
PUBLIC_SERVICE 公众服务 公众服务的 Id(publicServiceId)。
请注意区分会话类型和消息类型,会话类型是针对会话的分类,不同的会话类型决定了不同的会话逻辑。

另:系统会话类型并不一定代表是“系统消息”,本质上与单聊会话类型没有区别,只是逻辑上做了不同的区分,便于展开不同的产品业务逻辑。

通过一个 conversationType 和 targetId 组合,您可以确定会话列表中一个唯一的对象。以获取某会话的未读消息数举例:获取某会话未读消息数的方法是 RongIM.getInstance().getUnreadCount(ConversationType conversationType, java.lang.String targetId)

获取一个私聊会话未读消息数的代码片段:

String userId = "9527";

// 注意:调用 RongIM.getInstance() 方法前,务必保证调用了 init 和 connect。
int unreadCount = RongIM.getInstance().getRongIMClient().getUnreadCount(ConversationType.PRIVATE, userId);

获取一个群组会话未读消息数的代码片段:

String groupId = "1234";

// 注意:调用 RongIM.getInstance() 方法前,务必保证调用了 init 和 connect。
int unreadCount = RongIM.getInstance().getRongIMClient().getUnreadCount(ConversationType.GROUP, groupId);

消息的定义

消息类(MessageContent 的子类)不同于消息实体类(Message),消息类代表一条具体的消息内容,消息实体类是消息类的外层容器,消息实体对象是消息对象在本地存储的外层对象,消息实体对象除了包含消息对象外,还包括消息的方向、接收状态、接收时间、发送者等。

消息基类

MessageContent 是融云的消息基类,所有消息类都继承于 MessageContent 类。需要注意的是,MessageContent 类和 Message 类之间的关系:Message 中包含了一个具体的继承自 MessageContent 的消息,就是 Content 属性,可以通过 setContent() 和 getContent() 进行存取。

约定:如果您要定义一个内容类消息(需要显示在聊天会话界面中,且不是通知类消息),请从 MessageContent 类继承,命名为 XxxxxMessage。

内容消息表示一个用户间发送的包含具体内容的消息,需要展现在聊天界面上,如文字消息、语音消息等。

通知消息基类

NotificationMessage 没有具体的功能定义,只是用来保证设计上良好的继承关系。

约定:如果需要定义一个通知类消息,请从 NotificationMessage 类继承,命名为 XxxxxNotificationMessage。

通知消息表示一个通知信息,可能展现在聊天界面上,如提示条通知。

状态消息基类

StatusMessage 没有具体的功能定义,只是用来保证设计上良好的继承关系。

约定:如果需要定义一个状态类消息,请从 StatusMessage 类继承,命名为 XxxxxStatusMessage。

状态消息表示一个状态,用来实现如“对方正在输入”的功能。

消息的分类

消息分类 消息行为状态标识
内容类消息 表示一个用户间发送的包含具体内容的消息,需要展现在聊天界面上,如文字消息、语音消息等。
通知类消息 表示一个通知信息,可能展现在聊天界面上,如提示条通知。
状态类消息 表示一个状态,用来实现如“对方正在输入”的功能。

内容类消息

消息类型 是否上线 ObjectName 类名 父类 是否计数 是否存储
文字消息 RC:TxtMsg TextMessage MessageContent
语音消息 RC:VcMsg VoiceMessage MessageContent
图片消息 RC:ImgMsg ImageMessage MessageContent
图文消息 RC:ImgTextMsg RichContentMessage MessageContent
位置消息 RC:LBSMsg LocationMessage MessageContent
表情贴纸消息 RC:StkMsg StickerMessage MessageContent
公众服务单图文消息 RC:PSImgTxtMsg PublicServiceRichContentMessage MessageContent
公众服务多图文消息 RC:PSMultiImgTxtMsg PublicServiceMultiRichContentMessage MessageContent

通知类消息

消息类型 是否上线 ObjectName 类名 父类 是否计数 是否存储
好友通知消息 RC:ContactNtf ContactNotificationMessage NotificationMessage
资料通知消息 RC:ProfileNtf ProfileNotificationMessage NotificationMessage
通用命令通知消息 RC:CmdNtf CommandNotificationMessage NotificationMessage
提示条通知消息 RC:InfoNtf InformationNotificationMessage NotificationMessage
群组通知消息 RC:GrpNtf GroupNotificationMessage NotificationMessage
讨论组通知消息 RC:DizNtf DiscussionNotificationMessage NotificationMessage
已读通知消息 RC:ReadNtf ReadReceiptMessage MessageContent
公众服务命令消息 RC:PSCmd PublicServiceCommandMessage NotificationMessage
命令消息 RC:CmdMsg CommandMessage NotificationMessage

状态类消息

消息类型 是否上线 ObjectName 类名 父类 是否计数 是否存储
对方正在输入状态消息 RC:TypSts TypingStatusMessage StatusMessage
是否计数:表示客户端收到消息后,是否进行未读消息计数(未读消息数增加 1),所有内容型消息都应该设置此值。
是否存储:表示客户端收到消息后,是否进行存储,并在之后可以通过接口查询。

内置内容类消息

文字消息

用来发送文字类消息,其中可以包括超链接,会自动识别。

消息类名:TextMessage

消息 ObjectName:RC:TxtMsg

消息状态行为标识:MessageTag.ISPERSISTED | MessageTag.ISCOUNTED

消息的结构:{"content":"Hello world!","extra":""}

其中 content 为文字消息的文字内容,extra 可以放置任意的数据内容,也可以去掉此属性。

语音消息

用来发送语音片段消息,其中可以包括超链接,会自动识别。

消息类名:VoiceMessage

消息 ObjectName:RC:VcMsg

消息状态行为标识:MessageTag.ISPERSISTED | MessageTag.ISCOUNTED

消息的结构:{"content":"bhZPzJXimRwrtvc=","duration":7,"extra":""}

其中 content 为语音消息录制转码成 AMR 格式后,进行 Base64 编码的结果值,duration 为语音消息的时长(单位:秒),extra 可以放置任意的数据内容,也可以去掉此属性。

图片消息

用来发送图片类消息。

消息类名:ImageMessage

消息 ObjectName:RC:ImgMsg

消息状态行为标识:MessageTag.ISPERSISTED | MessageTag.ISCOUNTED

消息的结构:{"content":"bhZPzJXimRwrtvc=","imageUri":"http://p1.cdn.com/fds78ruhi.jpg","extra":""}

图片消息包括两个主要部分:缩略图和大图,缩略图直接 Base64 编码后放入 content 中,大图首先上传到文件服务器(融云 SDK 中默认上传到七牛云存储),然后将云存储上的大图地址放入消息体中。流程示意如下:

App -> App: 压缩原图到大图尺寸 App -> File Server: 上传大图 File Server --> App: 返回上传大图的地址 App -> App: 压缩大图到缩略图并进行 Base64 转码 App -> RongCloud IM Server: 发送图片消息(内附缩略图内容和大图地址)

其中 content 为图片内容进行 Base64 编码的结果值,imageUri 为图片上传到图片存储服务器后的地址,extra 可以放置任意的数据内容,也可以去掉此属性。

缩略图尺寸为:240 x 240 像素,以宽度和高度中较长的边不超过 240 像素等比压缩。

大图尺寸为:960 x 960 像素,以宽度和高度中较长的边不超过 960 像素等比压缩。

图文消息

用来发送图文消息,包含一个标题,一段文字内容和一张图片。

消息类名:RichContentMessage

消息 ObjectName:RC:ImgTextMsg

消息状态行为标识:MessageTag.ISPERSISTED | MessageTag.ISCOUNTED

消息的结构: {"title":"Big News","content":"I'm Ironman.","imageUri":"http://p1.cdn.com/fds78ruhi.jpg","url":"http://www.rongcloud.cn","extra":""}

其中 title 为消息的标题,content 为消息的文字内容,imageUri 为图片的地址,url 为跳转的地址,extra 可以放置任意的数据内容,也可以去掉此属性。

图片尺寸为:120 x 120 像素。

位置消息

用来发送地理位置消息。

消息类名:LocationMessage

消息 ObjectName:RC:LBSMsg

消息状态行为标识:MessageTag.ISPERSISTED | MessageTag.ISCOUNTED

消息的结构:{"content":"bhZPzJXimRwrtvc=","latitude":39.9139,"longitude":116.3917,"poi":"北京市朝阳区北苑路北辰泰岳大厦","extra":""}

其中 content 为地图缩略图内容进行 Base64 编码的结果值,latitude 为位置的纬度值,longitude 为位置的经度值,poi 为位置兴趣点名称,extra 可以放置任意的数据内容,也可以去掉此属性。

文件消息

消息类名:FileMessage

消息 ObjectName:RC:FileMsg

消息状态行为标识:MessageTag.ISPERSISTED | MessageTag.ISCOUNTED

消息的结构:{"name”:”a.txt","size":190184,"type":"txt","fileUrl":"http://rongcloud-image.ronghub.com/text_plain_1471242002?e=2147"}

其中 name 为文件名,size 为文件大小,type 为文件类型,fileUrl 为文件地址,extra 可以放置任意的数据内容,也可以去掉此属性。

内置通知类消息

提示条(小灰条)通知消息

用来发送在聊天会话页面显示的提示条(小灰条)通知。

消息类名:InformationNotificationMessage

消息 ObjectName:RC:InfoNtf

消息状态行为标识:MessageTag.ISPERSISTED

消息的结构:{"message":"请在聊天中注意人身财产安全",extra:""}

其中 message 为提示条消息内容,extra 可以放置任意的数据内容,也可以去掉此属性。

联系人(好友)通知消息

用来发送联系人操作(加好友等)的通知消息。

消息类名:ContactNotificationMessage

消息 ObjectName:RC:ContactNtf

消息状态行为标识:MessageTag.ISPERSISTED

消息的结构:{"operation":"Request","sourceUserId":"123","targetUserId":"456","message":"我是小艾,能加一下好友吗?","extra":""}

其中 operation 为联系人操作的指令,sourceUserId 为发出通知的用户 Id,targetUserId 为接收通知的用户 Id,message 为通知附带的消息内容,extra 可以放置任意的数据内容,也可以去掉此属性。

官方针对 operation 属性定义了 "Request", "AcceptResponse", "RejectResponse" 几个常量,也可以由开发者自行扩展。

资料通知消息

用来发送用户资料变更通知消息。

消息类名:ProfileNotificationMessage

消息 ObjectName:RC:ProfileNtf

消息状态行为标识:MessageTag.ISPERSISTED

消息的结构:{"operation":"Update","data":"{\"nickname\":\"韩梅梅\", \"hometown\":\"beijing\"}","extra":""}

其中 operation 为资料通知操作,可以自行定义,data 为操作的数据,extra 可以放置任意的数据内容,也可以去掉此属性。

通用命令通知消息

用来发送通用的指令通知消息,消息内可以定义任意 JSON 内容。

消息类名:CommandNotificationMessage

消息 ObjectName:RC:CmdNtf

消息状态行为标识:MessageTag.ISPERSISTED

消息的结构:{"name":"AtPerson","data":"{\"sourceId\":\"9527\"}"}

其中 name 为命令名称,可以自行定义,data 为命令的内容。

群组通知消息

用来发送群组操作的通知消息。

消息类名:GroupNotificationMessage

消息 ObjectName:RC:GrpNtf

消息状态行为标识:MessageTag.ISPERSISTED

消息的结构:{"operatorUserId":"4324","operation":"Rename","data":"本地生活","message":"修改本群名为本地生活","extra":""}

其中 operatorUserId 为操作人用户 Id,operation 为操作名,data 为操作数据如:目标用户 Id 或修改后群名称,详细可参见内置消息类型说明message 为消息内容,extra 可以放置任意的数据内容,也可以去掉此属性。

讨论组通知消息

用来发送讨论组操作的通知消息。

消息类名:DiscussionNotificationMessage

消息 ObjectName:RC:DizNtf

消息状态行为标识:MessageTag.ISPERSISTED

消息的结构:{"type":1,"extension":"3213,4332","operator":"5435"}

其中 type 为讨论组操作类型 1:加入讨论组 2:退出讨论组 3:讨论组改名 4:讨论组管理员踢人,extension 为被加入讨论组用户 Id,多个用户 Id 以逗号分割,operator 为当前操作用户 Id。

已读通知消息

用来发送消息已经被接收到的状态消息。

消息类名:ReadReceiptMessage

消息 ObjectName:RC:ReadNtf

消息的结构:{"lastMessageSendTime":1408706337,"messageUId":"XXXXXX","type":1}

其中 lastMessageSendTime 为最后一条消息的发送时间,messageUId 为最后一条消息的 UId,type 为消息类型。

公众服务命令消息

公众服务中用来发送通用的指令通知消息,消息内可以定义任意 JSON 内容,与通用命令通知消息的区别是不存储、不计数。

消息类名:PublicServiceCommandMessage

消息 ObjectName:RC:PSCmd

消息的结构:{"cmd":"AtPerson","data":"{\"sourceId\":\"9527\"}"}

其中 cmd 为命令名称,可以自行定义,data 为命令的内容。

命令消息

用来发送通用的指令通知消息,消息内可以定义任意 JSON 内容,与通用命令通知消息的区别是不存储、不计数。

消息类名:CommandMessage

消息 ObjectName:RC:CmdMsg

消息的结构:{"name":"AtPerson","data":"{\"sourceId\":\"9527\"}"}

其中 name 为命令名称,可以自行定义,data 为命令的内容。

内置状态类消息

对方正在输入状态消息

用来发送对方正在输入时的状态消息,不存储、不计数。

消息类名:TypingStatusMessage

消息 ObjectName:RC:TypSts

消息的结构:{"typingContentType":"RC:TxtMsg"}

typingContentType 为正在输入消息类型。

自定义消息

要想自行定义消息,首先要了解 ObjectNameMessageTag 两个概念:

ObjectName 属性

ObjectName 是消息的全局标识,用来在各种场景下判断和区分不同的消息类型。

融云系统内置消息的命名规范为:RC:XxxMsg(内容类消息)或 RC:XxxNtf(通知类消息)或 RC:XxxSts(状态类消息),您在自定义消息时需要注意,不要以 "RC:" 开头,以避免与融云系统内置消息的 ObjectName 重名。

MessageTag 注解类型

MessageTag 定义约定了消息在客户端的行为和表现。MessageTag 的第一个参数为该消息的 ObjectName,第二个参数定义消息行为状态标识,可以对传值进行或运算:

枚举值 说明
MessageTag.NONE 为空值,不表示任何意义,发送的自定义消息不会在会话页面和会话列表中展示。
MessageTag.ISCOUNTED 表示客户端收到消息后,要进行未读消息计数(未读消息数增加 1),所有内容型消息都应该设置此值。非内容类消息暂不支持消息计数。
MessageTag.ISPERSISTED 表示客户端收到消息后,要进行存储,并在之后可以通过接口查询,存储后会在会话界面中显示。
消息类型 消息基类 消息行为状态标识
内容类消息 MessageContent MessageTag.ISPERSISTED | MessageTag.ISCOUNTED
通知类消息 NotificationMessage MessageTag.ISPERSISTED
状态类消息 StatusMessage MessageTag.NONE

定义消息

文字消息的定义举例,定义如下:

@MessageTag(value = "RC:TxtMsg", flag = MessageTag.ISCOUNTED | MessageTag.ISPERSISTED)
public class TextMessage extends MessageContent {

    private String content;
    private String extra;

    /**
     * 默认构造函数。
     */
    protected TextMessage() {

    }

    /**
     * 构造函数。
     *
     * @param in 初始化传入的 Parcel。
     */
    public TextMessage(Parcel in) {
        setExtra(ParcelUtils.readFromParcel(in));
        setContent(ParcelUtils.readFromParcel(in));
    }

    /**
     * 构造函数。
     *
     * @param data 初始化传入的二进制数据。
     * @param message 此参数代码中并没有调用,后续将废弃。
     */
    public TextMessage(byte[] data) {
        String jsonStr = null;

        try {
            jsonStr = new String(data, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            RLog.e("JSONException", e.getMessage());
        }

        try {
            JSONObject jsonObj = new JSONObject(jsonStr);

            if (jsonObj.has("content"))
                setContent(jsonObj.getString("content"));

            if (jsonObj.has("extra"))
                setExtra(jsonObj.getString("extra"));
        } catch (JSONException e) {
            RLog.e("JSONException", e.getMessage());
        }
    }

    /**
     * 构建一个文字消息实例。
     *
     * @return 文字消息实例。
     */
    public static TextMessage obtain(String text) {
        TextMessage model = new TextMessage();
        model.setContent(text);

        return model;
    }

    /**
     * 描述了包含在 Parcelable 对象排列信息中的特殊对象的类型。
     *
     * @return 一个标志位,表明 Parcelable 对象特殊对象类型集合的排列。
     */
    public int describeContents() {
        return 0;
    }

    /**
     * 将本地消息对象序列化为消息数据。
     *
     * @return 消息数据。
     */
    @Override
    public byte[] encode() {
        JSONObject jsonObj = new JSONObject();

        try {
            jsonObj.put("content", getContent());

            if (!TextUtils.isEmpty(getExtra()))
                jsonObj.put("extra", getExtra());

        } catch (JSONException e) {
            RLog.e("JSONException", e.getMessage());
        }

        try {
            return jsonObj.toString().getBytes("UTF-8");
        } catch (UnsupportedEncodingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        return null;
    }

    /**
     * 将类的数据写入外部提供的 Parcel 中。
     *
     * @param dest  对象被写入的 Parcel。
     * @param flags 对象如何被写入的附加标志,可能是 0 或 PARCELABLE_WRITE_RETURN_VALUE。
     */
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        ParcelUtils.writeToParcel(dest, getExtra());
        ParcelUtils.writeToParcel(dest, content);
    }

    /**
     * 读取接口,目的是要从Parcel中构造一个实现了Parcelable的类的实例处理。
     */
    public static final Creator<TextMessage> CREATOR = new Creator<TextMessage>() {
        @Override
        public TextMessage createFromParcel(Parcel source) {
            return new TextMessage(source);
        }

        @Override
        public TextMessage[] newArray(int size) {
            return new TextMessage[size];
        }
    };

    /**
     * 获取文字消息的内容。
     *
     * @return 文字消息的内容。
     */
    public String getContent() {
        return content;
    }

    /**
     * 设置文字消息的内容。
     *
     * @param content 文字消息的内容。
     */
    public void setContent(String content) {
        this.content = content;
    }

    /**
     * 获取文字消息的内容。
     *
     * @return 文字消息的内容。
     */
    public String getExtra() {
        return extra;
    }

    /**
     * 设置消息的附加内容。
     *
     * @param extra 消息的附加内容。
     */
    public void setExtra(String extra) {
        this.extra = extra;
    }
}

注意:整个消息编码后的大小不要超过 128 KByte。如消息内容为图片、音频类消息时,在对消息对象序列化前必须将图片、音频文件转化为字节数组字符串,并对其进行 Base64 编码处理后,才能进行序列化处理。融云提供的 IMKit 会话界面不支持自定义消息发送,可以通过其他方式发送后在会话界面展示。

消息的发送

从客户端发送消息

发送消息的方法很简单,你只需要调用 RongIMClientsendMessage(ConversationType conversationType, java.lang.String targetId, MessageContent message, String pushContent, SendMessageCallback callback) 方法,传入相应的参数即可。

conversationType 为要发送的会话类型,targetId 是要发送的目标 Id,即消息接收者的 Id,message 参数就是要发送的消息类,pushContent 是 push 的内容,为空时不 push 信息,参见本文消息定义节,callback 是发送消息后的回调。

我们对客户端消息发送的频率进行了限制,当前登录用户每秒中最多发送 5 条消息,超过频率后消息发送会失败。

代码如下:

// 发送文本消息。
private void sendTextMessage() {

    TextMessage txtMsg = TextMessage.obtain("Hello honey. " + new Date(System.currentTimeMillis()));

    sendMessage(txtMsg);
}

// 发送语音消息。
private void sendVoiceMessage() {

    File voiceFile = new File(getCacheDir(), "voice.amr");

    try {
        // 读取音频文件。
        InputStream is = getAssets().open("BlackBerry.amr");

        OutputStream os = new FileOutputStream(voiceFile);

        byte[] buffer = new byte[1024];

        int bytesRead;

        // 写入缓存文件。
        while((bytesRead = is.read(buffer)) !=-1){
            os.write(buffer, 0, bytesRead);
        }

        is.close();
        os.flush();
        os.close();
    } catch (IOException e) {
        e.printStackTrace();
    }

    VoiceMessage vocMsg = VoiceMessage.obtain(Uri.fromFile(voiceFile), 10);

    sendMessage(vocMsg);
}

// 发送图片消息。
private void sendImageMessage() {
    File imageFileSource = new File(getCacheDir(), "source.jpg");
    File imageFileThumb = new File(getCacheDir(), "thumb.jpg");

    try {
        // 读取图片。
        InputStream is = getAssets().open("emmy.jpg");

        Bitmap bmpSource = BitmapFactory.decodeStream(is);

        imageFileSource.createNewFile();

        FileOutputStream fosSource = new FileOutputStream(imageFileSource);

        // 保存原图。
        bmpSource.compress(Bitmap.CompressFormat.JPEG, 100, fosSource);

        // 创建缩略图变换矩阵。
        Matrix m = new Matrix();
        m.setRectToRect(new RectF(0, 0, bmpSource.getWidth(), bmpSource.getHeight()), new RectF(0, 0, 160, 160), Matrix.ScaleToFit.CENTER);

        // 生成缩略图。
        Bitmap bmpThumb = Bitmap.createBitmap(bmpSource, 0, 0, bmpSource.getWidth(), bmpSource.getHeight(), m, true);

        imageFileThumb.createNewFile();

        FileOutputStream fosThumb = new FileOutputStream(imageFileThumb);

        // 保存缩略图。
        bmpThumb.compress(Bitmap.CompressFormat.JPEG, 60, fosThumb);

    } catch (IOException e) {
        e.printStackTrace();
    }

    ImageMessage imgMsg = ImageMessage.obtain(Uri.fromFile(imageFileThumb), Uri.fromFile(imageFileSource));

    sendMessage(imgMsg);
}

// 发送地理位置消息
private void sendLocationMessage() {

    LocationMessage locMsg = LocationMessage.obtain(0, 0, "世界的原点", Uri.parse("http://map.com/map.jpg"));

    sendMessage(locMsg);
}

// 发送图文消息。
private void sendRichContentMessage() {

    RichContentMessage richMsg = RichContentMessage.obtain(
            "Iron Man",
            "Iron Man (Tony Stark) is a fictional character, a superhero that appears in books published by Marvel Comics.",
            "http://news.com/news.jpg"
    );

    sendMessage(richMsg);
}

// 发送消息的封装。
private void sendMessage(MessageContent messageContent) {
    RongIM.getInstance().sendMessage(ConversationType.PRIVATE, "1", messageContent, "您有一条新消息", new RongIMClient.SendMessageCallback() {
            @Override
            public void onSuccess(int messageId) {
                Log.d("Send:", "success: " + messageId);
            }

            @Override
            public void onError(int messageId, ErrorCode errorCode) {
                Log.d("Send:", "error: " + messageId);
            }

            @Override
            public void onProgress(int messageId, int percent) {
                Log.d("Send:", "on progress: " + messageId + ", " + percent + "%");
            }
        });
}

从服务端发送消息

从服务端发送消息,需要调用融云的服务端 API 接口。详细的文档说明请参见:Server 开发指南

服务端发消息目前共分如下几种类型:

会话类型 API 方法 用途
私聊 (PRIVATE) /message/private/publish (/message/publish) 向某用户发送消息。
群组 (GROUP) /message/group/publish 向某群组内发送消息,发送者可以为非群组成员。
聊天室 (CHATROOM) /message/chatroom/publish 向某聊天室内发送消息,发送者可以为非聊天室成员。
系统 (SYSTEM) /message/system/publish 向某用户发送消息。和私聊表现完全一致,只是会话类型不同。

代码示例如下(CoffeeScript 语言):

# 引用外部库
request = require 'request'
sha1 = require 'node-sha1'

# 开发者后台申请的 App Key 和 App Secret。
appKey = 'qd46yzrf4lf7f'
appSecret = 'v9phDBG9KoBG8e'

# 通用的 API 接口调用签名。
nonce = Math.floor(Math.random() * 100000 + 100000); # 生成随机数
timestamp = Math.floor(Date.now() / 1000) # 提取时间戳
signature = sha1(appSecret + nonce + timestamp) # 计算 SHA1 签名

# 定义消息类型。
objectName = 'RC:TxtMsg'

# 定义消息体。
jsonContent =
    'content': '你好!',
    'extra': '附加信息'

# 将消息体 JSON 转换为字符串。
content = JSON.stringify jsonContent

# 向服务器发送请求。
request.post
    url: 'https://api.cn.ronghub.com/message/publish.json', # API 接口地址。
    form: # 请求参数。
        'fromUserId': '2'
        'toUserId': '1'
        'objectName': objectName
        'content': content
        'pushContent': '这里是推送显示的内容。'
        'pushData': '{ "key": "这里是 APNS 的 payload 扩展。"}'
    headers: # 请求头签名。
        'App-Key': appKey
        'Nonce': nonce
        'Timestamp': timestamp
        'Signature': signature
    ,
    (error, response, body) -> # 请求回调。
        # 显示返回结果。
        console.log body

消息的编码

对于图片消息,需要在发送前生成一张小尺寸的缩略图,然后将缩略图文件的字节流进行 Base64 编码后放在 JSON 的 content 中发出。

对于语音消息,需要在发送前将录制的音频转为 AMR 格式,然后将 AMR 格式文件的字节流进行 Base64 编码后放在 JSON 的 content 中发出。

消息的接收

要想接收消息,主要是实现接收消息的监听器;另外,就是要注册接收到的消息,只有注册后的消息,系统才会识别。

注册消息类型

只有注册过的消息,系统才会正确的识别和分发。所以,接收消息前需要先注册消息。注意:系统内置的消息不需要注册,我们已经做了注册。

public class App extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        /**
         * 初始化 SDK。
         */
        RongIMClient.init(this);

        try {
            // 注册一个自定义消息类型。
            RongIMClient.registerMessageType(GroupInvitationNotification.class);
        } catch (AnnotationNotFoundException e) {
            e.printStackTrace();
        }
    }
}
当用户收到的消息,未在当前使用的版本上注册时,当前版本不能识别此消息,会默认按照 UnKnownMessage 处理,以小灰条方式默认提示“当前版本暂不支持查看此消息”。

注册监听器

注册监听器就是调用 RongIM.setOnReceiveMessageListener(this) 方法。

代码如下:

// 连接融云服务器。
RongIM.connect(token, new RongIMClient.ConnectCallback() {

    @Override
    public void onSuccess(String s) {
        // 此处处理连接成功。
        Log.d("Connect:", "Login successfully.");

        // 设置消息接收监听器。
        RongIM.setOnReceiveMessageListener(this);
    }

    @Override
    public void onError(ErrorCode errorCode) {
        // 此处处理连接出错。
        Log.d("Connect:", "Login failed.");
    }
});

/**
 * 接收消息的监听器:OnReceiveMessageListener 的回调方法,接收到消息后执行。
 *
 * @param message 接收到的消息的实体信息。
 * @param left    剩余未拉取消息数目。
 */
@Override
public boolean onReceived(Message message, int left) {

    MessageContent messageContent = message.getContent();

    if (messageContent instanceof TextMessage) {//文本消息
        TextMessage textMessage = (TextMessage) messageContent;
        Log.d(TAG, "onReceived-TextMessage:" + textMessage.getContent());
    } else {
        Log.d(TAG, "onReceived-其他消息,自己来判断处理");
    }

    return false;

}

判断消息类型

接收到消息后,需要对消息的类型进行判断,判断的方法主要依赖于 ObjectName。

本节完整的代码如下:

public class App extends Application {
    @Override
    public void onCreate() {
        super.onCreate();

        // 初始化。
        RongIM.init(this);

        // 从您的应用服务器请求,以获取 Token。在本示例中我们直接在下面 hardcode 给 token 赋值。
        // String token = getTokenFromAppServer();

        // 此处直接 hardcode 给 token 赋值,请替换为您自己的 Token。
        String token = "ptGbXapcyr+w8jTRzykWDBViWrePIfq0GuT89sdfDDgX+cDua7sRBAu4T2Id/k8vs/+ZK4KVTDkv4AhfjCAKgQ==";

        // 连接融云服务器。
        try {
            RongIM.connect(token, new RongIMClient.ConnectCallback() {

                @Override
                public void onSuccess(String s) {
                    // 此处处理连接成功。
                    Log.d("Connect:", "Login successfully.");

                    // 设置消息监听器。
                    RongIM.getInstance().getRongIMClient().setOnReceiveMessageListener(this);
                }

                @Override
                public void onError(ErrorCode errorCode) {
                    // 此处处理连接出错。
                    Log.d("Connect:", "Login failed.");
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

/**
 * 接收消息的监听器:OnReceiveMessageListener 的回调方法,接收到消息后执行。
 *
 * @param message 接收到的消息的实体信息。
 * @param left    剩余未拉取消息数目。
 */
@Override
public boolean onReceived(Message message, int left) {

    MessageContent messageContent = message.getContent();

    if (messageContent instanceof TextMessage) {//文本消息
        TextMessage textMessage = (TextMessage) messageContent;
        Log.d(TAG, "onReceived-TextMessage:" + textMessage.getContent());
    } else {
        Log.d(TAG, "onReceived-其他消息,自己来判断处理");
    }

    return false;

}

消息的解码

消息解码过程及代码示例同消息编码。

消息的读取和管理

获取会话列表和会话

会话列表就是最近聊天的联系人(好友)集合。

获取所有会话列表的方法是调用 RongIMClient.getInstance().getConversationList()

获取群组会话列表的方法是调用 RongIMClient.getInstance().getGroupConversationList()

获取某一会话的方法是调用 RongIMClient.getInstance().getConversation(RongIMClient.ConversationType conversationType, java.lang.String targetId)

移除会话

移除会话只是将会话从会话列表中移除,并不会移除会话中的消息,如果该会话收到新的消息,会重新在会话列表中显示出来。

移除会话的方法是调用 RongIMClient.getInstance().removeConversation(RongIMClient.ConversationType conversationType, java.lang.String targetId)

清空会话

清空会话不但会将会话从会话列表中移除,还会删除会话中的所有消息。注意区别于删除会话。

清空某一类或几类会话的方法是调用 RongIMClient.getInstance().clearConversations(RongIMClient.ConversationType... conversationTypes)

获取消息列表

消息列表就是一个会话中所有消息的集合。

获取最近历史消息的方法是调用 RongIMClient.getInstance().getLatestMessages(RongIMClient.ConversationType conversationType, java.lang.String targetId, int count)

获取分页的历史消息的方法是调用 RongIMClient.getInstance().getHistoryMessages(RongIMClient.ConversationType conversationType, java.lang.String targetId, int oldestMessageId, int count)

还可以按照消息的类型(ObjectName)获取分页的历史消息的方法 RongIMClient.getInstance().getHistoryMessages(RongIMClient.ConversationType conversationType, java.lang.String targetId, java.lang.String objectName, int oldestMessageId, int count)

删除消息

删除消息不同于删除会话,会直接清空本地的消息记录数据。

删除一条或多条消息的方法是调用 RongIMClient.getInstance().deleteMessages(int[] messageIds)

也可以直接清空某一个会话的所有消息 RongIMClient.getInstance().clearMessages(RongIMClient.ConversationType conversationType, java.lang.String targetId)


© 2016 RongCloud. All Rights Reserved. Version 2.8.5