LeanMessage 移动开发 SDK 是由 LeanCloud 提供的,专为 iOS、Android 和 WindowsPhone® 等客户端程序提供应用内聊天的 API 和服务,而且也提供了 JavaScript API,方便开发者打通网页和客户端应用,给最终用户提供统一的使用体验。使用 LeanMessage API,您能够极快地以最少工做量让您的移动应用支持实时聊天,获得一种如微信通常的沟通体验。html
开始以前
出于本文的目的,我假设您已经很是熟悉使用 JSON、Android 和 Eclipse 进行移动应用编程的基本概念。在您继续阅读本文以前,请访问 leancloud.cn 并建立您的应用程序。只需遵循注册页面中的简单指令便可。android
本文介绍了包含单聊、群聊、历史记录和应用鉴权的核心 API 类。您将学习如何简单进行用户间一对一单聊,以及如何建立群组让多用户进行群聊,还有如何经过签名来对聊天通道进行控制,以保护应用和用户的隐私。示例均构建于 LeanMessage SDK for Android 之上(请参阅Android开发指南)。算法
在 LeanMessage 实时消息世界中,每个参与者(通常而言是「人」)都是一个 Peer。Peer 拥有一个在应用内惟一标识本身的 ID(称为 PeerID,字符串类型,长度不超过 50 字节,具体数值由应用自身肯定),系统中的每一条消息都来自于一个 Peer,发送到一个或多个 Peer。而且,LeanMessage 的消息服务容许一个 Peer 在多个不一样设备上登陆,也容许一个设备上同时登陆多个 Peer,究竟该如何使用,由应用根据使用场景本身选择。编程
这里要注意的是,PeerID 是由应用开发者本身賦值的,LeanMessage 自己并无任何强制要求,因此:json
为了防止骚扰,一个 Peer 须要先关注(watch)了对方才能给对方发送消息;由于 LeanMessage 提供了更细粒度的权限控制,应用开发者能够在关注(watch)动做上增长签名来保证安全性。这一点后面会进行详细说明。缓存
在 LeanMessage 中全部的消息都是 AVMessage 的实例,AVMessage 只支持文本,而且长度不能超过 5KB。消息分为暂态(transient)和持久消息两种类型。全部持久消息都会在 LeanMessage 云端保存,因此用户离线以后也能够获得通知和接收,而暂态消息并不会离线保存,适合开发者用来进行协议控制。安全
AVMessage 的定义如清单 1 所示:服务器
public class AVMessage implements Parcelable { private List<String> toPeerIds; // 消息接收方的 PeerID,支持一个或多个 String groupId; // 消息所属群组的ID,对于普通一对一聊天消息而言,此值为空 String message; // 消息体 long timestamp; // 消息发送时间戳 boolean isTransient; // 是不是暂态消息 String fromPeerId; // 消息发送方的 PeerID public AVMessage(); public AVMessage(String message); public AVMessage(String message, List<String> toPeerIds, boolean isTransient); public AVMessage(String message, boolean isTransient); }
LeanMessage 为全部历史消息都提供了存储和查询的功能,存储时间则根据开发者的类型有所不一样。微信
每个 Peer 经过开启(open)一个会话(Session)而加入实时消息服务,Peer 能够在一个会话中关注(watch)一个或多个 Peer,当被关注者上下线时,会收到通知。Peer 在开启会话后只能向本身关注的其余 Peers 发送消息,但能够收到任何 Peer 发来的消息,也就是说单向关注时,消息能够顺利地由关注者发往被关注者。网络
Session 有以下几种状态:
Session 上能够进行的操做有:
明白了这三个概念以后,咱们就能够开始进入实际聊天环节了。
首先咱们须要在 application 的 onCreate 函数中进行 LeanCloud 最基本的初始化:
@Override public void onCreate() { super.onCreate(); AVOSCloud.initialize(this, "pleaseReplaceWithYourAppId", "pleaseReplaceWithYourAppKey"); }
接下来咱们来看一下怎么样进行一对一的基本聊天。首先,咱们须要开启一个会话(Session),示例代码如清单 2 所示:
SessionManager session = SessionManager.getInstance(selfId);//获取SessionManager实例,以便于后续的操做。这里的 selfId 能够是用户的实际 id,也能够是其余惟一的字符串,譬如「Tom」。 List<String> watchedIds = new LinkedList<String>(); session.open(watchedIds); //打开Session,同时关注一些 PeerID。此时没有关注对象
注意!
通常而言,会话的开启是在用户登陆以后的 RootActivity 中进行的。对于支持匿名聊天的应用,也能够在 Application 启动的时候进行。千万不要在一个临时或短命的 Activity 中开启聊天会话。上面代码中 SessionManager 也是 Session 的子类,因此能够直接调用 Session 的方法。
接下来,咱们开始跟「Bob」这个用户进行聊天。为了给他发送消息,咱们先要关注(watch)他,代码以下:
List<String> peerIds = new LinkedList<String>(); peerIds.add("Bob"); session.watchPeers(peerIds);
以后咱们给「Bob」发送一条消息:
List<String> peerIds = new LinkedList<String>(); peerIds.add("Bob"); session.sendMessage(new AVMessage("嗨,你好,我是 Tom", peers, false));
好了,这样一条消息就发送过去了。可是问题来了,对于「Bob」而言,他怎么才能收到别人发给他的消息呢?
上面对于 Session 的全部操做都是异步的。与通常 Android 异步方法调用不一样,LeanMessage SDK 的异步并非经过 Callback 或者相似 RsyncTask 的机制实现的,而是经过继承 AVMessageReceiver 这一 BoardcastReceiver,实现如下方法来处理来自服务器端的响应的。AVMessageReceiver 接口定义如清单 3 所示:
/** * 当服务器成功与客户端打开session时产生本次回调 */ public abstract void onSessionOpen(Context context, Session session); /** * 在 session 暂停时调用,通常都是由网络链接丢失致使的隐性调用 */ public abstract void onSessionPaused(Context context, Session session); /** * Session 恢复时,通常都是网络链接恢复之后的 * 这个时候你能够处理一些因为网络异常致使的失败消息 */ public abstract void onSessionResumed(Context context, Session session); /** * 从某个Peer接收到消息时,会收到一次调用 */ public abstract void onMessage(Context context, Session session, AVMessage msg); /** * 服务器反馈消息已经成功发送时,会收到调用 */ public abstract void onMessageSent(Context context, Session session, AVMessage msg); /** * 在消息发送失败时,产生的调用 在这里你能够保存一下发送失败的消息以便将来重发 */ public abstract void onMessageFailure(Context context, Session session, AVMessage msg); /** * 当关注的一些peers上线时,产生的调用 */ public abstract void onStatusOnline(Context context, Session session, List<String> peerIds); /** * 当关注的一些peers下线时,产生的调用 */ public abstract void onStatusOffline(Context context, Session session, List<String> peerIds); /** * 当与服务器发生交互的过程当中的任何错误,都在这里被返回 */ public abstract void onError(Context context, Session session, Throwable e);
从上面接口的定义中,咱们能够看到,要接收到别人发过来的消息,只须要响应 onMessage() 方法便可。代码示例如清单 4 所示:
public class CustomeMsgReceiver extends AVMessageReceiver { @Override public void onMessage(final Context context, Session session, AVMessage avMsg) { Logger.d("onMessage "+avMsg.getMessage()); // 进行上层逻辑处理,譬如 UI 展现,或者消息提醒。 } } // 在 AndroidManifest.xml 文件中声明这一 BoardcastReceiver。 <receiver android:name=".receiver.CustomeMsgReceiver"> <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="com.avoscloud.session.action" /> </intent-filter> </receiver>
上面的代码演示了如何发送文本信息,可是如今的交互方式已经愈来愈多样化,图片、语音、视频已经是很是广泛的媒体类型。而从 AVMessage 的定义来看,只支持不超过 5KB 大小的文本,那么 LeanMessage 又如何能支持富媒体的聊天消息呢?
记得 LeanStorage 中的 AVFile 吗?
AVFile 是 LeanStorage 提供的非结构化数据存储解决方案,可让你的应用程序将二进制文件存储到云端服务器中,而且自动提供 CDN 加速服务,能带给用户更迅捷的下载体验。好比常见的文件类型图像文件、影像文件、音乐文件和任何其余二进制数据均可以使用。具体说明能够参见Android 开发文档。
对于图片、语音、视频这类较大的非结构化数据,存储到云端文件系统以后,在消息中发送 url 已经是业界惯例,而且 LeanMessage 中 AVMessage 类的定义并无规定消息体是什么类型,因此咱们能够充分利用这一扩展空间,结合 AVFile 来发送、接收富媒体的聊天消息。实现方法如清单 5 所示:
AVFile file = AVFile.withAbsoluteLocalPath("test.jpg", Environment.getExternalStorageDirectory() + "/test.jpg"); file.saveInBackground(new SaveCallback() { // override public void done(AVException ex) { if (null != ex) { // error } else { // construct message body under json format. HashMap<String, Object> params = new HashMap<String, Object>(); params.put("type", "image"); params.put("context", "嗨,你好,我是 Tom"); params.put("attachment", file.getUrl()); List<String> peerIds = new LinkedList<String>(); peerIds.add("Bob"); session.sendMessage(new AVMessage(JSON.toJSONString(params), peers, false)); } } });
新版本的 LeanMessage SDK 会支持富媒体消息,避免让每一个开发者都重复作相似的工做。
在聊天的需求里,还有一个很重要的场景,就是群组聊天。从前面 AVMessage 的定义咱们能够猜到,LeanMessage 应该是支持群聊的,那实际上该如何实现呢?下面咱们一步一步来尝试一下。
与普通的单聊相比,群聊增长了以下两个基本概念:
AVGroup 表明一个聊天群组,能够对应到实际的多人聊天、聊天群、聊天室等,每一个 AVGroup 有一个惟一的 ID(groupID,由 LeanMessage 云端分配),其定义如清单 6 所示:
public class AVGroup implements Group { String roomId; String selfId; Session session; }
一个 Peer 加入群后向群发送的消息能够被全部群成员收到。当有新成员加入或者既有成员退出时,全部群成员都会获得通知。AVGroup 上能够进行的操做有:
public interface Group{ public void join(); public void sendMessage(AVMessage msg); public void kickMember(List<String> peerIds); public void inviteMember(List<String> peerIds); public void quit(); public String getGroupId(); public String getSelfId(); public AVHistoryMessageQuery getHistoryMessageQuery(); }
与 AVMessageReceiver 相似,AVGroupMessageReceiver 主要用来处理群组操做的结果。其详细定义如清单 7 所示:
// 在加入聊天室成功后被调用 若是join时,没有带groupId,您能够在返回的group中间获取groupId public abstract void onJoined(Context context, Group group); //当你被别人邀请进入某个聊天室之后 // @param group // @param byPeerId 这我的邀请了你 public abstract void onInvited(Context context, Group group, String byPeerId); // 当你被别人踢出聊天室之后 // @param group // @param byPeerId 是他踢了你 public abstract void onKicked(Context context, Group group, String byPeerId); // 处理消息发送成功事件 public abstract void onMessageSent(Context context, Group group, AVMessage message); // 用来处理消息发送失败事件.能够缓存起来,过后重发 public abstract void onMessageFailure(Context context, Group group, AVMessage message); // 收到消息之后被调用,通常经过这个接口来处理和接受来自Group的消息 // @param context // @param group // @param message // @param fromPeerId 发消息者 public abstract void onMessage(Context context, Group group, AVMessage message); // 处理退出成功事件 public abstract void onQuit(Context context, Group group); // 处理Group操做被拒绝的时间 // @param context // @param group // @param op 这里可能存在的操做有 "join","invite","kick" // @param targetIds // 通常来讲是指被操做的对象,在join操做中间就是指groupId自己, // invite和kick中则指被邀请或者被踢除的peerIds public abstract void onReject(Context context, Group group, String op, List<String> targetIds); // 处理新用户加入事件 public abstract void onMemberJoin(Context context, Group group, List<String> joinedPeerIds); // 处理用户退出事件 public abstract void onMemberLeft(Context context, Group group, List<String> leftPeerIds); // 处理全部Group相关的异常 public abstract void onError(Context context, Group group, Throwable e);
因为整个实时通讯功能都是创建在 Session 的基础上,因此咱们要加入一个聊天室也须要创建在一个已经打开的 Session 上。 已经打开一个 Session 之后,能够经过如下操做来加入一个 Group:
Group group = SessionManager.getInstance(selfId).getGroup();//准备新建一个聊天室 //Group group = SessionManager.getInstance(selfId).getGroup(groupId); 加入一个已经存在的聊天室 group.join(); // LeanMessage 云端会判断 groupId 是否存在,若是不存在就新建一个 Group,不然加入已有 Group
加入成功以后 AVGroupMessageReceiver 子类中的 onJoined 方法就会被调用。
发送消息很是简单,经过以下代码就能够向特定聊天室发送消息了:
Group group = SessionManager.getInstance(selfId).getGroup(groupId); group.sendMessage(new AVGroupMessage("hello world"));
发送成功以后,AVGroupMessageReceiver 子类中的 onMessageSent 方法会被调用,反之则 onMessageFailure 方法会被调用。
接收一个聊天室的消息,与接收单聊的消息同样,须要开发者实现 AVGroupMessageReceiver 接口,并在 AndroidManifest.xml 中注册便可,如代码清单 8 所示:
public class CustomeGroupMsgReceiver extends AVGroupMessageReceiver { ... @Override public void onMessage(final Context context, Group group, AVMessage avMsg) { Logger.d("onMessage "+avMsg.getMessage()); // 进行上层逻辑处理,譬如 UI 展现,或者消息提醒。 } ... } // 在 AndroidManifest.xml 文件中声明这一 BoardcastReceiver。 <receiver android:name=".receiver.CustomeGroupMsgReceiver"> <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="com.avoscloud.session.action" /> </intent-filter> </receiver>
在加入一个聊天室以后,咱们第一步就是看看有哪些人在这个群组里面。LeanMessage 和 LeanStorage 是结合在一块儿的,经过使用 LeanStorage 的数据存储功能,来保存一个聊天室的基本信息(表名:AVOSRealtimeGroups),在 LeanStorage 应用管理平台的数据中心,咱们能够看到 AVOSRealtimeGroups 的全部字段。
LeanStorage 的数据中心
LeanStorage 也是 LeanCloud 平台的核心服务之一,提供了应用内数据和文件数据的存储功能。对于应用内数据,LeanStorage 支持 schema free 的存储,开发者不须要事先定义数据的模式,只要符合 JSON 格式的 Object 均可以自由存储到 LeanStorage 云端。同时,LeanStorage 也提供一个 Web 版的数据管理界面,能够很是方便地增、删、改、查任何数据。
固然,在咱们知道一个聊天室的 groupId 的时候,也能够在代码中,经过 AVObject 的 fetch 接口来查看这个聊天室的组员状况,代码如清单 9 所示:
AVObject groupObject = AVObject.createWithoutData("AVOSRealtimeGroups",groupId); groupObject.fetch();//若是您在UI进程中,请使用异步方法调用 List groupMembers= groupObject.getList("m");
谨防系统线程阻塞!
回想一下,在移动应用程序中,长时间的操做(如网络、文件或长的计算)不该该在主系统线程上完成。相反,应在一个单独的工做线程中执行它们。阻塞系统线程会对应用程序的用户界面的响应能力产生负面影响,有可能致使强行关闭您的应用程序。
在查询到聊天室成员以后,可让用户邀请一些本身的朋友加入,做为管理员也能够剔除一些「可怕」的成员。代码如清单 10 所示:
Group group = SessionManager.getInstance(selfId).getGroup(groupId); List<String> toInvite = Arrays.asList("peerId1","peerId2","peerId3"); group.inviteMember(toInvite); List<String> toKickOff = Arrays.asList("badBoy1","badBoy2"); group.kickMembers(toKickOff);
邀请成功之后,通知的流程是这样的:
操做者(管理员) 被邀请者 其余人 1,发出请求 inviteMember 2,AVGroupMessageReceiver.onInvited 3, AVGroupMessageReceiver.onJoined 4,AVGroupMessageReceiver.onMemberJoin AVGroupMessageReceiver.onMemberJoin
相应地,踢人的流程以下:
操做者(管理员) 被踢者 其余人 1,发出请求 kickMember 2,AVGroupMessageReceiver.onKicked 3, AVGroupMessageReceiver.onQuit 4,AVGroupMessageReceiver.onMemberLeft AVGroupMessageReceiver.onMemberLeft
LeanMessage 会将非暂态消息自动保存在云端,以后开发者能够经过 AVHistoryMessageQuery 这个对象来进行查询。AVHistoryMessageQuery 定义如清单 11 所示:
public class AVHistoryMessageQuery { int limit; String convid; String from; long timestamp; /** * 设置查询返回集合的大小 * 默认100,最大1000 */ public void setLimit(int limit); /** * 设定聊天的发起人是谁 */ public void setFrom(String from); /** * 设置查询从哪一个时间开始的聊天记录 */ public void setTimestamp(long timestamp); /** * 指定聊天记录查询条件中,聊天发送的对象条件 */ public void setPeerIds(List<String> peerIds); /** * 同步方法查询聊天记录 * 请确保在一个异步方法中调用此方法,不然会出现UI线程中的网络请求而致使的UI卡死 */ public List<AVHistoryMessage> find() throws AVException; /** * 异步方法查询聊天记录 */ public void findInBackground(HistoryMessageCallback callback); /** * 此接口为异步查询聊天记录的回调类 */ public static interface HistoryMessageCallback; }
经过 AVHistoryMessageQuery 查询获得的结果是 AVHistoryMessage,该类的定义如清单 12 所示:
public class AVHistoryMessage extends AVMessage { /** * 查看是否属于聊天室聊天记录 */ public boolean isRoom(); /** * 查看聊天记录所在的conversation Id,对于 Group 来讲等于 GroupID,对于单聊来讲,是内部生成的一个值。 */ public String getConvid(); }
聊天记录的查询的基本方法跟 AVQuery 相似可是略有不一样。 针对 Session 的聊天记录和聊天室 Group 的聊天记录查询略有不一样,可是基本流程是同样(代码清单 12):
String selfId = "Tom"; SessionManager sm = SessionManager.getInstance(selfId); List<String> peers = new ArrayList<String>(); peers.add(selfId); peers.add("Bob"); AVHistroyMessageQuery sessionHistoryQuery = sm.getHistroyMessageQuery(); sessionHistoryQuery.setLimit(100); //设置查询结果大小 sessionHistoryQuery.setPeerIds(peers); // 设置单聊的参与方,多个参与者之间是「与」的关系 sessionHistoryQuery.setTimestamp(1413184345686); //查询时间片1413184345686之前的聊天记录 sessionHistoryQuery.findInBackground(new HistoryMessageCallback() { @Override public void done(List<AVHistoryMessage> messages, AVException error) { System.out.println(messages.size()); } });//查询session里的聊天记录 Group group = sm.getGroup("140a534fd092809500e6d651e73400c7"); AVHistroyMessageQuery groupHistoryQuery = group.getHistoryMessageQuery();//获取AVHistoryMessageQuery对象来查询聊天室的聊天记录 groupHistoryQuery.findInBackground(new HistoryMessageCallback(){ @Override public void done(List<AVHistoryMessage> messages,AVException error){ for(AVHistoryMessage msg:messages){ System.out.println(msg.getMessage()); } } })
上面第一个查询会拿到「Tom」和「Bob」在特定时间点之前的 100 条聊天记录;第二个查询会拿到特定聊天室的全部聊天记录(若是总数不超过 1000 条的话)。
查看全部聊天室的方法和查看单个聊天室成员的方法相似,都是直接经过 AVQuery 或者 AVObject 来遍历 AVOSRealtimeGroups 表实现的,这里再也不赘述。
前面实现了单聊、群聊、富媒体聊天诸多功能,可是开发者可能已经发现了,这都是直接调用 LeanMessage SDK 来实现的,对于咱们开发者来讲,能控制的东西不多,在安全性上会存在一些担忧。譬如:万一不怀好意的人破解了个人 appId 和 appKey,是否是就能够在个人聊天社区里面随心所欲?
为了知足开发者对权限和认证的要求,LeanMessage 还设计了操做签名的机制。咱们能够在 LeanCloud 应用控制台、设置、应用选项中强制启用签名(强烈推荐这样作)。启用后,全部的 Session open 和 watch 行为都须要验证签名,这样开发者就能够对用户登陆以及他能够关注哪些人,进而能够给哪些人发消息进行充分的控制。
签名采用 Hmac-sha1 算法,输出字节流的十六进制字符串 (hex dump),签名的消息格式以下:
app_id:peer_id:watch_peer_ids:timestamp:nonce
其中:
在群组操做中,LeanMessage 对加群、邀请和踢出群这三个动做也容许加入签名,它的签名格式是:
app_id:peer_id:group_id:group_peer_ids:timestamp:nonce:action
其中:
签名的 key 是应用的 master key。开发者能够实现本身的 SignatureFactory,调用远程的服务器的签名接口得到签名。若是没有本身的服务器,能够直接在 LeanCloud 的云代码上经过 Web Hosting 动态接口实现本身的签名接口。在移动应用中直接作签名是很是危险的,它可能致使你的 master key 泄漏。
LeanCloud 的 appKey 分类
在 LeanCloud 平台上申请了应用以后,LeanCloud 会分配给咱们两个 key:一个 appKey,一个 master Key。其中 appKey 能够执行一些普通的操做,而且受到 LeanCloud 平台安全设置的限制,相似于操做系统中的普通 User 帐号,因此能够直接用在客户端;master Key 则拥有全部权限,相似于操做系统中的 Root/Administrator 帐号,因此请妥善保管。
好,有了签名机制以后,咱们究竟该如何使用呢?咱们只须要实现本身的 SignatureFactory,而后在开启 session 的时候,把这个 signatureFactory 传进去便可。示例代码如清单 13 所示:
// Signature 定义以下,由 LeanMessage 提供 public class Signature { private String signature; private long timestamp; private String nonce; private List<String> signedPeerIds; // getter / setter for properties ...... } // customise signature factory,由开发者实现 public MySignatureFactory implements SignatureFactory { @override public Signature createSignature(String selfId, List<String> watchIds) { // call remote server for correct signature. } @override public Signature createGroupSignature(String groupId, String selfId, List<String> targetPeerIds, String action) { // call remote server for correct group signature. } } // open session with signature factory. SignatureFactory signatureFacatory = new MySignatureFactory(); SessionManager sm = SessionManager.getInstance(selfId); sm.setSignatureFactory(signatureFactory); sm.open(selfId);
设定了 SignatureFactory 以后,对于须要鉴权的操做,LeanMessage SDK 与服务器端通信的时候都会带上应用本身生成的 Signature 信息,这样 LeanMessage 服务器端就会使用 app 的 masterKey 来验证信息的有效性,保证聊天渠道的安全。
LeanMessage 是一个很是稳定可靠的聊天服务平台,提供的功能也足以知足咱们应用开发者的需求。这里我经过介绍 LeanMessage API 来实现应用内的单聊、群聊、富媒体消息等基本功能,可是 LeanMessage 还支持更多高阶功能,譬如 Super Peer、敏感词过滤、消息实时监听等等,有兴趣的朋友能够继续探索。