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、敏感詞過濾、消息實時監聽等等,有興趣的朋友能夠繼續探索。