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