聊天室應用開發實踐(一)

聲網Agora發表於2019-03-20

文章作者:monkeyHi

本文是 聲網 Agora 開發者的投稿。如有疑問,歡迎與作者交流

社會高度發展的今天,大家都離不開社交和社交網路。近幾年,直播行業的穩定高速發展,背後隱藏一個事實,大家需要一個實時性更高的網際網路環境,就像面對面溝通那樣的及時有效。

這次嘗試了一下 Agora SignalingSDK。

初識 Agora 信令 SDK

Agora Signaling 是Agora 全家桶一員,主要用來實現即時點對點通訊。Agora Signalling 是作為外掛的形式服務於 Agora 全家桶,也可以單獨用於實時訊息通訊的場景。

開發文件

Agora 官網已經提供了比較完善的文件資料。

以 Agroa Signaling 為例,我們可以看到官網分別就客戶端整合和服務端整合進行了介紹,而客戶端部分又針對常見客戶端實現進行的清晰簡單的講解。

擁有一定開發經驗的攻城獅很快便能上手。

當然我們也發現一個問題,文件上只有 quick start, 沒有進一步介紹介面使用的注意事項。帶著這個疑惑,筆者迅速瀏覽了API參考部分,所有介面都沒有提供具體的demo code 和注意事項。基本接入思路是這樣的:

  1. 初始化

  2. 登入

  3. 點對點訊息

  4. 頻道訊息

  5. 呼叫邀請

  6. 登出

官方 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.輸入自己的accout

run 選項卡中已經提示你輸入 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模式。

相關文章