转载

基于声网 Agora 信令 SDK 开发聊天室应用(一)

文章作者: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

基于声网 Agora 信令 SDK 开发聊天室应用(一) b.输入自己的accout

run 选项卡中已经提示你输入 account ,我们随便输入一个 Roman

后续可以尝试自己实现用户中心

基于声网 Agora 信令 SDK 开发聊天室应用(一)

c.选择模式并发送消息

然后, 会看到提示 successd

基于声网 Agora 信令 SDK 开发聊天室应用(一)

这里,先一起试试 点对点通信 ,输入 2 ,回车

我们输入聊天的对象,hello

基于声网 Agora 信令 SDK 开发聊天室应用(一) 顺便开个linux虚拟机运行linux客户端demo

基于声网 Agora 信令 SDK 开发聊天室应用(一)

互相发消息

基于声网 Agora 信令 SDK 开发聊天室应用(一)

这里比较奇怪,demo可能有些功能业务省略掉了,java端可以发点对点消息,却收不到。

尝试发频道消息,发现群聊频道模式完全没问题。

基于声网 Agora 信令 SDK 开发聊天室应用(一)

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模式。

原文  https://blog.csdn.net/agora_cloud/article/details/89029120
正文到此结束
Loading...