文章作者:monkeyHi
本文是 声网 Agora 开发者的投稿。如有疑问,欢迎与作者交流。
社会高度发展的今天,大家都离不开社交和社交网络。近几年,直播行业的稳定高速发展,背后隐藏一个事实,大家需要一个实时性更高的互联网环境,就像面对面沟通那样的及时有效。
这次尝试了一下 Agora SignalingSDK。
Agora Signaling 是Agora 全家桶一员,主要用来实现即时点对点通信。Agora Signalling 是作为插件的形式服务于 Agora 全家桶,也可以单独用于实时消息通信的场景。
Agora 官网已经提供了比较完善的文档资料。
以 Agroa Signaling 为例,我们可以看到官网分别就客户端集成和服务端集成进行了介绍,而客户端部分又针对常见客户端实现进行的清晰简单的讲解。
拥有一定开发经验的攻城狮很快便能上手。
当然我们也发现一个问题,文档上只有 quick start, 没有进一步介绍接口使用的注意事项。带着这个疑惑,笔者迅速浏览了API参考部分,所有接口都没有提供具体的demo code 和注意事项。基本接入思路是这样的:
初始化
登录
点对点消息
频道消息
呼叫邀请
注销
Agroa 官网提供了关于 Agora 信令的各种demo,初略浏览一番,比较容易看懂,没有什么很奇怪的写法。
但是,这些demo都有一个问题,没有注释。这对不曾接触Agora产品的新手不是特别友好,可能要花比较多精力来熟悉这些接口。
通过Agora 官网及已经公布的API 。我们可以了解到,常见带身份信息的文本聊天完全不在话下,基于Agora Signaling的demo,我们只要关心一下自己的业务模型,端上套个皮就能实现聊天室、留言板等互动交流场景。
直播间的弹幕聊天
直播间聊天和弹幕聊天,本质上就是一个留言板和即时通讯的合体。而Agora 信令 本身就是为实时通信互动而生,实现这样的功能只要加一个聊天数据库来保留历史记录即可。
医患远程诊断
现实生活中,受距离、时间、心理等诸多因素影响,病患并不一定能及时到达医院,医生也未必能及时到达现场,这时候及时通讯网络可以提供诸多方便。病患或病患家属可以通过一个App 将患情通过影像、声音、文字传递给医生,同时可以随时的沟通,就像现场问诊一样,病患可能也需要一个病友群或频道来分享交流。
消息通知
相信大家对手机短信、微信消息、qq消息都不陌生,我们借助 Agora 信令 也是可以实现简单版本的网络短信功能的。
客服功能
有些产品可能需要一个客服功能,这样遇到使用问题时,可以随时通过聊天窗口咨询,而且不需要额外的添加客服人员的微信。有效沟通,同时保护彼此隐私。
实时性比较高的设备间通信
比如我在A省有一批矿机,需要及时的了解机房状况,那么我在机房可以设置一个通信机,将采集到的数据通过 Agora 信令 及时传回并记录在数据库。虽然这个场景可能并不是Agora 信令 设计初衷,但作为一种可行的备选也是不错的。
课堂在线互动
各种在线学堂的远程授课方案,包括远程考试等,课堂互动可不局限于文字、语音、图像,通常要结合起来。
直播导购互动
如果有这样一种直播活动,画面上和电视导购没什么区别,但是可以通过更方便的方式下单,扫码,沟通,填写信息,付款,获取订单状态,以及端上的现场互动等。
科研领域
需要远程采集观测的各种数据等。实验展示等。实验数据实时采集处理等。
几乎能想到的任何需要实时通信、点对点通信、或者分频道通信的场景,都尝试着去实现。
在实际做自己的应用之前,我先上手跑了一下官方的 demo,开启踩坑之旅。
笔者体验环境:
解压 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
前面我们简单预览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算法实现
关于鉴权机制及算法 详情见
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里的业务怎么样,需要大家花些心思来学习。
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 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 users; // 用户表
private HashMap> accountDialogueRecords = null; // 账号会话记录
private HashMap> channelDialogueRecords = null; // 频道会话记录
List currentAccountDialogueRecords = null; // 当前账号会话记录
List 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 users, List 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模式。