文章作者:monkeyHi
本文是 聲網 Agora 開發者的投稿。如有疑問,歡迎與作者交流。
社會高度發展的今天,大家都離不開社交和社交網路。近幾年,直播行業的穩定高速發展,背後隱藏一個事實,大家需要一個實時性更高的網際網路環境,就像面對面溝通那樣的及時有效。
這次嘗試了一下 Agora SignalingSDK。
初識 Agora 信令 SDK
Agora Signaling 是Agora 全家桶一員,主要用來實現即時點對點通訊。Agora Signalling 是作為外掛的形式服務於 Agora 全家桶,也可以單獨用於實時訊息通訊的場景。
開發文件
Agora 官網已經提供了比較完善的文件資料。
以 Agroa Signaling 為例,我們可以看到官網分別就客戶端整合和服務端整合進行了介紹,而客戶端部分又針對常見客戶端實現進行的清晰簡單的講解。
擁有一定開發經驗的攻城獅很快便能上手。
當然我們也發現一個問題,文件上只有 quick start, 沒有進一步介紹介面使用的注意事項。帶著這個疑惑,筆者迅速瀏覽了API參考部分,所有介面都沒有提供具體的demo code 和注意事項。基本接入思路是這樣的:
-
初始化
-
登入
-
點對點訊息
-
頻道訊息
-
呼叫邀請
-
登出
官方 Demo
Agroa 官網提供了關於 Agora 信令的各種demo,初略瀏覽一番,比較容易看懂,沒有什麼很奇怪的寫法。
但是,這些demo都有一個問題,沒有註釋。這對不曾接觸Agora產品的新手不是特別友好,可能要花比較多精力來熟悉這些介面。
可能的一些應用場景
通過Agora 官網及已經公佈的API 。我們可以瞭解到,常見帶身份資訊的文字聊天完全不在話下,基於Agora Signaling的demo,我們只要關心一下自己的業務模型,端上套個皮就能實現聊天室、留言板等互動交流場景。
直播間的彈幕聊天
直播間聊天和彈幕聊天,本質上就是一個留言板和即時通訊的合體。而Agora 信令 本身就是為實時通訊互動而生,實現這樣的功能只要加一個聊天資料庫來保留歷史記錄即可。
醫患遠端診斷
現實生活中,受距離、時間、心理等諸多因素影響,病患並不一定能及時到達醫院,醫生也未必能及時到達現場,這時候及時通訊網路可以提供諸多方便。病患或病患家屬可以通過一個App 將患情通過影像、聲音、文字傳遞給醫生,同時可以隨時的溝通,就像現場問診一樣,病患可能也需要一個病友群或頻道來分享交流。
訊息通知
相信大家對手機簡訊、微信訊息、qq訊息都不陌生,我們藉助 Agora 信令 也是可以實現簡單版本的網路簡訊功能的。
客服功能
有些產品可能需要一個客服功能,這樣遇到使用問題時,可以隨時通過聊天視窗諮詢,而且不需要額外的新增客服人員的微信。有效溝通,同時保護彼此隱私。
實時性比較高的裝置間通訊
比如我在A省有一批礦機,需要及時的瞭解機房狀況,那麼我在機房可以設定一個通訊機,將採集到的資料通過 Agora 信令 及時傳回並記錄在資料庫。雖然這個場景可能並不是Agora 信令 設計初衷,但作為一種可行的備選也是不錯的。
課堂線上互動
各種線上學堂的遠端授課方案,包括遠端考試等,課堂互動可不侷限於文字、語音、影像,通常要結合起來。
直播導購互動
如果有這樣一種直播活動,畫面上和電視導購沒什麼區別,但是可以通過更方便的方式下單,掃碼,溝通,填寫資訊,付款,獲取訂單狀態,以及端上的現場互動等。
科研領域
需要遠端採集觀測的各種資料等。實驗展示等。實驗資料實時採集處理等。
幾乎能想到的任何需要實時通訊、點對點通訊、或者分頻道通訊的場景,都嘗試著去實現。
在實際做自己的應用之前,我先上手跑了一下官方的 demo,開啟踩坑之旅。
準備
筆者體驗環境:
- windows10 x64
- IntelliJ IDEA 2018.3.2 x64
- SDK
- jdk1.8
SDK 目錄
解壓 SDK,得到如下目錄結構,我們後續會基於其中的samples : Agora-Signaling-Turorial-Java 來學習和理解server端SDK和api。
└─Agora_Signaling_Server_SDK_Java // SDK根目錄
├─lib // 信令的jar包
├─libs-dep // 行令依賴的jar包
└─samples // 一個栗子
└─Agora-Signaling-Tutorial-Java
├─gradle // 由此可以判斷時gradle專案
│ └─wrapper
├─lib // 這裡已經又全部需要的jar包了,需要用SDK中 lib、libs中的jar包覆蓋
└─src
└─main
└─java
├─mainclass
├─model
└─tool
複製程式碼
匯入為idea專案
前面我們簡單預覽SDK目錄,一個gradle專案。非常容易匯入idea。這裡就以idea搭建demo執行環境。
1.進入 Agora-Signaling-Tutorial-Java 2.右鍵--> Open Folder as InterlJ Idea project 3.等待匯入完成,通常都很快
配置
1.配置SDK
確保SDK目錄下的lib、libs-dep 中的所有jar包到專案的lib目錄下。
2.檢視並修改build.gradle,要注意其中第14行
dir: 'lib', include: ['* .jar']
複製程式碼
修改為:
dir: 'lib', include: ['*.jar']
複製程式碼
星號*後沒有空格。修改後的build.gradle:
group 'com.agora'
version '1.0-SNAPSHOT'
apply plugin: 'java'
sourceCompatibility = 1.5
repositories {
jcenter()
}
dependencies {
testCompile group: 'junit', name: 'junit', version: '4.11'
compile fileTree (dir: 'lib', include: ['*.jar'])
}
複製程式碼
gradle配置發生變化時,idea提示 import Changes ,點一下 import Changes .
確保gradle成功引入了依賴jar包。
3.配置appid
tip: 這裡需要注意, agora 有兩種鑑權機制。直接用appid,或者使用token。為方便演示,我們直接用appid完成鑑權,但是,筆者也同時搬來了java的token演算法。具體看 第 4 步介紹。
切換到 Pancages 視角,找到 tool/COnstant,注意 8 ~ 11 行 ,
static {
//app_ids.add("Your_appId");
//app_ids.add("Your_appId");
}
複製程式碼
這裡我們取消一行註釋, 替換其中的Your_appId 為真實的appid。
static {
//app_ids.add("Your_appId");
app_ids.add("");
}
複製程式碼
4.計算token
tips: 只有在開啟app認證時,才會用到token。這裡方便演示,筆者決定暫時不開啟app認證。筆者僅僅模仿並貼出相關程式碼
具體實現:
package tool;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class SignalingToken {
public static String getToken(String appId, String certificate, String account, int expiredTsInSeconds) throws NoSuchAlgorithmException {
StringBuilder digest_String = new StringBuilder().append(account).append(appId).append(certificate).append(expiredTsInSeconds);
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update(digest_String.toString().getBytes());
byte[] output = md5.digest();
String token = hexlify(output);
String token_String = new StringBuilder().append("1").append(":").append(appId).append(":").append(expiredTsInSeconds).append(":").append(token).toString();
return token_String;
}
public static String hexlify(byte[] data) {
char[] DIGITS_LOWER = {'0', '1', '2', '3', '4', '5',
'6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
char[] toDigits = DIGITS_LOWER;
int l = data.length;
char[] out = new char[l << 1];
// two characters form the hex value.
for (int i = 0, j = 0; i < l; i++) {
out[j++] = toDigits[(0xF0 & data[i]) >>> 4];
out[j++] = toDigits[0x0F & data[i]];
}
return String.valueOf(out);
}
}
複製程式碼
更具體的可以參考 java版token演算法實現
關於鑑權機制及演算法 詳情見
執行demo
1.在啟動前,有必要來一起看看 mainclass目錄。
啟動類有兩個, 一個是啟動點對點通訊server的, 另一個是頻道訊息。
怎麼理解呢,其實很簡單,點對點通訊,你可以理解為倆人竊竊私語。頻道通訊則是群聊(像微信群)。
└─src
└─main
└─java
├─mainclass
│ MulteSignalObjectMain2.java // 頻道訊息 啟動類
│ SingleSignalObjectMain.java // 點對點通訊 啟動類
│ WorkerThread.java // 核心業務流程
複製程式碼
2.嘗試通訊
a.啟動
選中 SingleSignalObjectMain.java --> ctrl + shift + f10
b.輸入自己的accoutrun 選項卡中已經提示你輸入 account ,我們隨便輸入一個 Roman
後續可以嘗試自己實現使用者中心
c.選擇模式併傳送訊息
然後, 會看到提示 successd
這裡,先一起試試 點對點通訊 ,輸入 2 ,回車
我們輸入聊天的物件,hello
順便開個linux虛擬機器執行linux客戶端demo互相發訊息
這裡比較奇怪,demo可能有些功能業務省略掉了,java端可以發點對點訊息,卻收不到。
嘗試發頻道訊息,發現群聊頻道模式完全沒問題。
3.小結
啟動demo沒有什麼難度,不過demo裡的業務怎麼樣,需要大家花些心思來學習。
code review (java)
demo跑起來了,但是我們並不是很明白這個程式具體業務。換自己來寫,可能還是一臉懵。所以,筆者決定review code,學習一下SDK用法。
檔案src\main\java\tool\Constant.java中大部分寫死的和預定義的引數值都在這裡
package tool;
import java.util.ArrayList;
public class Constant {
public static int CURRENT_APPID = 0;
public static ArrayList<String> app_ids = new ArrayList();
// 申明一些 命令,這些命令通常都是些常量
public static String COMMAND_LOGOUT;
public static String COMMAND_LEAVE_CHART;
public static String COMMAND_TYPE_SINGLE_POINT;
public static String COMMAND_TYPE_CHANNEL;
public static String RECORD_FILE_P2P;
public static String RECORD_FILE_CHANEEL;
public static int TIMEOUT;
public static String COMMAND_CREATE_SIGNAL;
public static String COMMAND_CREATE_ACCOUNT;
public static String COMMAND_SINGLE_SIGNAL_OBJECT;
public static String COMMAND_MULTI_SIGNAL_OBJECT;
public Constant() {
}
static {
// 前面宣告的變數名,這裡複製
// app_ids 是陣列格式的,意味你可以新增多個appid
app_ids.add("073e6cb4f3404d4ba9ad454c6760ec0b");
// 一些命令 定義
// 退出登陸
COMMAND_LOGOUT = "logout";
// 離開當前聊天繪畫
COMMAND_LEAVE_CHART = "leave";
// 私聊模式輸入2
COMMAND_TYPE_SINGLE_POINT = "2";
// 群聊模式輸入3
COMMAND_TYPE_CHANNEL = "3";
// 快取檔案定義
RECORD_FILE_P2P = "test_p2p.tmp";
RECORD_FILE_CHANEEL = "test_channel.tmp";
// 超時
TIMEOUT = 20000;
// 新建 一個signal
COMMAND_CREATE_SIGNAL = "0";
// 新建一個使用者
COMMAND_CREATE_ACCOUNT = "1";
// 進入點對點模式
COMMAND_SINGLE_SIGNAL_OBJECT = "0";
// 進入頻道群聊模式
COMMAND_MULTI_SIGNAL_OBJECT = "1";
}
}
複製程式碼
啟動類
以 點對點 為例:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package mainclass;
import tool.Constant;
// 一個點對點啟動類
public class SingleSignalObjectMain {
// 構造方法
public SingleSignalObjectMain() {
}
// main 方法接受 字串陣列作為引數
public static void main(String[] args) {
// new 一個workerThread ,核心業務都在workerThread 類中
WorkerThread workerThread = new WorkerThread(Constant.COMMAND_SINGLE_SIGNAL_OBJECT);
// 啟動這個workerThread 執行緒。
(new Thread(workerThread)).start();
}
}
複製程式碼
model目錄中定義了一些資料類和類方法,比較容易理解。
main/java/mainclass/WorkerThread.java檔案裡定義了一個執行緒類,繼承Runable。
限於篇幅,這裡摘部分程式碼出來解讀一下。
首先, WorkerThread類中定義:
private boolean mainThreadStatus = false; // 主執行緒狀態 預設false
private String token = "_no_need_token"; // 預設未開啟token認證,而是直接使用appid
private String currentUser; // 當前會話使用者
private boolean timeOutFlag; // 超時標記,是否超時
private DialogueStatus currentStatus; // 當前訊息狀態
private HashMap<String, User> users; // 使用者表
private HashMap<String, List<DialogueRecord>> accountDialogueRecords = null; // 賬號會話記錄
private HashMap<String, List<DialogueRecord>> channelDialogueRecords = null; // 頻道會話記錄
List<DialogueRecord> currentAccountDialogueRecords = null; // 當前賬號會話記錄
List<DialogueRecord> currentChannelDialogueRecords = null; // 當前頻道會話記錄
複製程式碼
重點看一下構造方法
public WorkerThread(String mode) {
currentMode = mode; //傳入mode
init(); // 初始化
String appid = Constant.app_ids.get(0); // 獲取配置檔案的裡的app_id
// 如果傳入mode值等於COMMAND_SINGLE_SIGNAL_OBJECT的值(點對點),用appid new 一個信令,更新會話狀態為為登陸狀態
// 否則判斷是否為頻道模式,更新狀態。 這裡,大家可以根據自己情況修改邏輯。
// 這裡有個疑問,兩個分支裡,為啥一個需要 new Signal 一個不需要呢?
if (currentMode.equals(Constant.COMMAND_SINGLE_SIGNAL_OBJECT)) {
sig = new Signal(appid);
currentStatus = DialogueStatus.UNLOGIN;
} else {
if (currentMode.equals(Constant.COMMAND_MULTI_SIGNAL_OBJECT)) {
currentStatus = DialogueStatus.SIGNALINSTANCE;
}
}
}
複製程式碼
init() function
則初始化一個必要的需要互動輸入來初始化的資料
run() function
會根據currentStatus的值來呼叫不同的業務函式
makeSignal()
中非常關鍵的一步
Signal signal = new Signal(appId); //用id例項化信令
複製程式碼
joinChannel(String channelName)中用到LoginSession類和Channel類
public void joinChannel(String channelName) {
final CountDownLatch channelJoindLatch = new CountDownLatch(1);
// 例項化Channel 類 ,裡面override幾個事件監聽
Channel channel = users.get(currentUser).getSession().channelJoin(channelName, new Signal.ChannelCallback() {
// 當加入頻道時
@Override
public void onChannelJoined(Signal.LoginSession session, Signal.LoginSession.Channel channel) {
channelJoindLatch.countDown();
}
// 頻道使用者列表發生變化時
@Override
public void onChannelUserList(Signal.LoginSession session, Signal.LoginSession.Channel channel, List<String> users, List<Integer> uids) {
}
// 收到頻道訊息時
@Override
public void onMessageChannelReceive(Signal.LoginSession session, Signal.LoginSession.Channel channel, String account, int uid, String msg) {
if (currentChannelDialogueRecords != null && currentStatus == DialogueStatus.CHANNEL) {
PrintToScreen.printToScreenLine(account + ":" + msg);
DialogueRecord dialogueRecord = new DialogueRecord(account, msg, new Date());
currentChannelDialogueRecords.add(dialogueRecord);
}
}
// 當頻道使用者加入會話時
@Override
public void onChannelUserJoined(Signal.LoginSession session, Signal.LoginSession.Channel channel, String account, int uid) {
if (currentStatus == DialogueStatus.CHANNEL) {
PrintToScreen.printToScreenLine("..." + account + " joined channel... ");
}
}
@Override
public void onChannelUserLeaved(Signal.LoginSession session, Signal.LoginSession.Channel channel, String account, int uid) {
if (currentStatus == DialogueStatus.CHANNEL) {
PrintToScreen.printToScreenLine("..." + account + " leave channel... ");
}
}
@Override
public void onChannelLeaved(Signal.LoginSession session, Signal.LoginSession.Channel channel, int ecode) {
if (currentStatus == DialogueStatus.CHANNEL) {
currentStatus = DialogueStatus.LOGINED;
}
}
});
timeOutFlag = false;
wait_time(channelJoindLatch, Constant.TIMEOUT, channelName);
if (timeOutFlag == false) {
// 未超時,加入頻道
users.get(currentUser).setChannel(channel);
}
}
複製程式碼
這裡篇幅有限,不能貼出全部程式碼。大家可以對著api文件來 著重看一下如何認證,如何登陸,如何收發訊息。
後續,筆者會上傳註釋過的到github。
可能會遇到的問題及應對方法
1.demo的build.gradle 中多了一個空格,導致提示找不到lib
解決方法: * .jar --> *.jar
2.例項化signal時失敗
解決方法: 檢查appid是否正確,檢查是否開啟了token認證
如果開啟了token認證,需要增加token計算演算法,可以參考這個文件。
3.筆者發現兩個啟動類雖然預設啟動命令值不一樣,但是其實啟動效果一樣,都可以選擇切換p2p或者channel模式。