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

文章做者:monkeyHijava

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

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

此次嘗試了一下 Agora SignalingSDK。github

初識 Agora 信令 SDK

Agora Signaling 是Agora 全家桶一員,主要用來實現即時點對點通訊。Agora Signalling 是做爲插件的形式服務於 Agora 全家桶,也能夠單獨用於實時消息通訊的場景。算法

開發文檔

Agora 官網已經提供了比較完善的文檔資料。數據庫

以 Agroa Signaling 爲例,咱們能夠看到官網分別就客戶端集成和服務端集成進行了介紹,而客戶端部分又針對常見客戶端實現進行的清晰簡單的講解。windows

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

固然咱們也發現一個問題,文檔上只有 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模式。

相關文章
相關標籤/搜索