XCC API

开发指南

XSwitch XCC API 功能强大,使用起来非常灵活,可以很简单的使用任何语言写出一些 Demo IVR。但是,XCC API 是基于 RPC 调用的,不管 API 设计多完善,由于网络和系统的复杂性,写出一个生产级别、高可靠的系统也不是一件容易的事。本章,我们就来看一下在面向生产环境开发应用程序时应该注意的问题。


我应该使用什么语言开发?

简单的回答是:使用你(以及你的团队)最擅长的语言。

XCC API 通过 NATS 承载,NATS 有各种语言的客户端库,因此,你几乎肯定可以找到你使用的语言的客户端库。

NATS 支持同步调用和异步调用,XCC 也是。NATS 的 API 设计的很好,因此我们没有在 NATS API 上又包装一层 XCC SDK,主要是因为不是太有必要,而且,我们不一定能包装好。如果只是进行简单封装,那么,你无法用到所有的参数和特性;如果彻底封装、精心检查每一个参数和返回值,那么,最终就会是包装的特别臃肿,而你的应用层还是免不了检查所有可能的返回值以打印特定的日志或者进行对应的处理。比如我们使用Play播放一个媒体文件,最简单的用法只有两个参数:当前 Channel 的 UUID 以及待播放文件的路径,但是当你播放 TTS 时,就需要传入更多的参数。哪些参数(比如 TTS 引擎)合法与不合法,可能只有到了 XSwitch 侧才能知道,或者在应用层(通过事先配置)知道,而处于中间Play函数却不容易知道。另外,对于 Play 的返回值,在 XSwitch 侧,通常有以下情况:

  • 成功:播放完成
  • 失败:文件不存在,或不运行的文件等
  • 中断:被 API 打断,或用户提前挂机

但在控制侧(你的应用程序侧),却又多了很多情况,如网络中断、消息超时、XSwitch 过载或崩溃、NATS 过载丢消息或崩溃等。当发生这些情况时,你的应用就会异常,你必须小心地检查各种异常以便清除缓存、打印相关的日志用于问题追踪等。当然,你写的程序也会崩溃,如果很不幸,你的程序崩溃了,那么,失去控制的 XSwitch 何去何从、你的程序恢复后又怎么收拾前面的烂摊子,也是需要考虑的问题。

所幸,NATS 已经帮我们 Cover 了很多问题,NATS 各种语言实现的客户端也都尽量发挥了相关的语言的特性,并能提供一致的调用逻辑。

因此,我们并没有再在 NATS 基础上包装一层客户端 SDK。除了上述原因,还有就是我们并不是对所有语言都非常擅长,而且,不同的用户有不同的应用场景,即使对同一种语言也有不同的偏好,不同的调用习惯,因此,我们决定把这些自由留给开发者。

当然,我们本身也是开发者,在使用 XCC API 的过程中也逐渐形成了自己的 Go 语言 SDK,我们也开源出来供大家参考,这些 SDK 我们会一直维护,因而大家也可以直接使用,并欢迎提出各种改进建议。关于该 SDK 会再后文提到。

同步调用

不管是本地函数调用还是 RPC,同步调用都会比较简单。但是 RPC 由于需要经过网络传输的特殊性,实际上所有的 RPC 都是异步的,客户端发出一个消息,服务端收到并进行处理,然后将内容返回,客户端再根据服务端返回的数据返回给调用者。

NATS 使用 Reqeust 机制做同步调用。同步订阅一个一次性的 Subject,在发送请求的同时,带上这个 Subject,对方在处理完后就可以直接将消息回复到这个 Subject 上,客户端收到回复后调用完成,在此之前,客户端就一直阻塞等待。如图:

同步调用同阻塞的,因而需要超时机制,一般在 Request 请求中都有一个timeout参数用于超时。在 C 语言中,是使用pthread/condition同步机制实现的,它会阻塞掉整个线程。还是以 Play 函数为例,为了方便调用者使用,它是阻塞的,只能等文件播放完成后才能返回。如果文件的长度是 20 秒,则必须保证timeout大于 20 才行,这样才能防止调用超时。如果对方提前挂机或 Play 被 API 人为中断,则该函数也会提前返回,状态码为410(Gone,表示 Channel 主体已不存在)或206(表示文件只播放了一部分)。

timeout在满足播放条件的情况下肯定是越短越好。为了能更精确的使用timeout,需要提前知道文件的播放长度。在播放时长不可预知的情况下,只能使用经验参数,保证不能太长,又不能太短。但无论如何,正常情况下,该函数或者返回服务端的结果,或者超时。

在网络发生问题、过载等导致消息丢失的情况下,发出的请求无法得到响应,调用者就会一直阻塞直到超时。这在timeout比较长的情况下,可能会导致过多的资源占用。所以使用同步机制要求网络比较可靠,XSwitch 和 NATS 也不要过载(导致丢消息)。

同步调用的好处是可以很方便的写连续控制的代码,下面是一个调用方法的伪代码:

Play(file1)
Play(file2)
Play(file2)

但是,对方可能会提前挂机,Play也可能出错,所以,在真正应用中,都需要检查返回值:

code, message = Play(file1)
if code != 200
    // 打印日志,错误处理代码 ...
    return
end
code, message = Play(file2)
...

由于同步调用是阻塞的,一般来说,都需要在独立的线程中调用。线程在不同的语言中有不同的表现方式,比如,在 C 语言中是线程,在 Go 语言中是协程(Goroutine),而在 Javascript 中,所有调用都是异步非阻塞的。

值一得的是,Javascript 的 NATS 客户端库也提供了 Request 和 Publish 两种调用方法,区别是前者可以指定超时时间,并可以在收到结果后调用回调函数。

异步调用

异步调用使用起来更复杂一些,但对于调用者有更大的灵活性。异步调用通过使用 NATS 的 Publish 方法发送请求,Publish 会立即返回而不等待执行结果,因此,要想获得结果,就必须订阅一个 Subject 用于接收返回值。同时。为了知道请求是哪个函数发出的,还需要“记住”请求的 ID,如:

nats.sub('cn.xswitch.ctrl.ivr', onMessage) // 收到消息会执行onMessage回调函数

request_id = 0
requests = {} // 记录所有请求

function onMessage(msg) {
    if (msg.method == 'Event.Channel') { // Channel 事件
        if (state == 'START') { // 来话的第一个消息
            request_id++        // 请求id
            requests[request_id] = 'play file1'
            Publish(Play, request_id, file1) // 发送请求,播放file1
        } else if (state == 'DESTROY') { // 挂机了
            delete(requests[request_id]) // 清理现场
        }
    } else if msg.result { 结果
        state = requests[msg.result.id] // 哪个请求的返回结果

        if (state == 'play file1') {
            // file1 播放完成
            request_id++        // 请求id
            requests[request_id] = 'play file2'
            Publish(Play, request_id, file2) // 发送请求,播放file2
        } else if (state == 'play file2') {
            // file2播放完成...
        }
    }
}

从上述伪代码中可以看出,异步执行需要异步的等待执行结果,然后进行下一步。为了能匹配请求和返回值,还需要将请求id存起来。当然,挂机后需要清除缓存中的内容,或者,需要实现一种垃圾回收机制(以面对各种异步情况,如收不到DESTROY消息的情况,严谨的网络编程会认为网络永远会有不可靠的情况的)。其实,同步调用也是这么实现的,只不过将这些复杂性隐藏到了阻塞等待之后。

语音识别

在通话中可以启动语音识别。语音识别使用DetectSpeech接口实现,在识别时,可以指定一个Media参数,指定在开启语音识别的开始,播放一个声音文件或 TTS。在播放声音的过程中,支持打断。

  • DetectSpeech在指定Media参数的情况下是同步的,它只有在识别完成时才会返回结果。
  • DetectSpeech如果不指定Media参数,则是异步的,它不会阻塞,而是立即返回,但很显然,它无法立即返回识别结果,识别结果在DetectedSpeech事件中返回。

不管是同步还是异步,DetectSpeech都可以通过breakable参数指定是否打断。在打断情况下,如果正在播放语音文件,会立即停止当前的播放,并继续识别。流程如下:

DetectSpeech() // 进行语音识别

// 在另一个线程或协程中
for {
	DetectSpeechFeedTTS(uuid, engine, voice, text)
}

同步语音识别

如果有Media参数,则是同步模式。同步语音识别比较容易开发,示例伪代码如下:

for {
	text = "你好"
	result = DetectSpeech(text)
	if (result.text) {
		text = "我听到你说的是" + result.text
	} else {
		text = "没听清,请重说"
	}
}

同步语音识别支持打断和非打断模式,一次识别完成后,必须循环再次调用DetectSpeech才可以继续识别。

在实际使用时,如果不想播放任何声音,又想使用同步语音视频,则可以传入silence_stream://1000这样的文件名,其中,1000是静音毫秒数,取值范围为20~3600000

异步语音识别

异步语音识别支持连续识别。

subscribe(DetectedSpeech Event, function(event) {
	console.log(event)
}

DetectSpeech(不传text参数) // 进入连续识别

打断

DetectSpeech支持打断。可以使用PlayDetectSpeech组合支持即打断又不打断的方式。

Play(welcome_tex) // 欢迎语,不可打断
for {
	DetectSpeech(text) // 可打断
}

系统层打断

通过DetectSpeechbreakable: true参数,可以让系统自己识别并打断。

应用层打断

如果breakable: false,则可以由应用层自行决定什么时候打断。以下仅讨论应用层的打断。

在异步识别的过程中,可以通过Play API 播放语音,如果需要打断,可以通过DetectedSpeech中的Speech.Begin事件来判断,收到该事件后,调用Stop API 接口(需要使用sync: true参数)打断当前的Play

不管是同步还是异步识别,都可以通过DetectSpeechFeedTTS参数补充 TTS,在识别的过程中可以连续 Feed TTS。Feed 的 TTS 也可以通过Stop接口打断。

事件

XSwitch 会发出事件通知,在 JSON-RPC 中,事件就是一个不带id的请求,且不需要回复。

在控制侧的事件接收通常是在独立的线程中执行的。控制侧可以根据接收到的事件执行一些控制,如收到语音识别的内容后打断放音操作。注意在收到事件后,不要阻塞当前的事件接收进程(比如不要执行阻塞的 Play 调用)。

在实际应用中,可以使用独立的线程或协程处理每一路通话,收到的事件也可以推到当前线程中处理,也可以在专门的线程中处理。在多线程(或协程)编程中,不可避免的要考虑对同一资源的竞争性访问,这一般可以通过 Mutex 或其它机制实现,具体的实现因程序语言而异(在 Javascript 中只有一个线程,因而不需要考虑这种情况)。

XSwitch 有原生的事件,通常字段比较多,不建议使用。如果有可能,就建议使用Event.Channel事件。

Event.Channel

Channel 事件,在 Channel 状态发生变化时发出。参见Channel State

Channel 事件会发送到被AcceptAnswer接管的 Controller 上。

Event.CDR

CDR 事件,在通话完毕后发出。每一个 Channel 都会有一个 CDR 事件,如果参与通话的是两条腿(alegbleg),则会有两个 CDR 事件,并分别有leg标志。CDR 事件一般会在Event.Destroy之前发出。

CDR 事件默认会送到与Event.Channel相同的 Subject 上,但也可以通过全局配置参数cdr-subject配置单独的 Subject。

参见CDR 相关说明

Event.XCC

XCC 事件,通过产生CUSTOM xcc::publish事件,可以发送自定义事件。

Lua 示例:

local ctrl_uuid = 'test'

local event = freeswitch.Event("CUSTOM", "xcc::publish");
event:addHeader('XCC-Control-UUID', ctrl_uuid)
event:addHeader('param1', 'value1')
event:addHeader('param2', 'value2')
event:addHeader('param3', 'value3')
-- event:addBody(body)

event:fire();

同步和异步调用相结合

在实际应用中,通常可以根据情况使用同步和异步结合使用。

Channel 缓存

一般来说,简单的应用不需要对 Channel 信息进行缓存。todo

如果需要缓存,

gRPC 和 Protobuf

Protobuf 是 Google 推出的消息序列化格式,配合 gRPC 使用更能发挥使用。XCC 使用 JSON 而不是并没有使用 Protobuf 进行对象的序列化。这主要是因为 Protobuf 相对来说更重一些,而且,XCC 传输的消息内容大部分是字符串,Protobuf 的优势不是特别明显。另外,gPRC 更重,且 C 语言没有官方的实现,C++语言的实现在 XSwitch 中表现不是很好(reload模块会有问题,一些网络资源无法释放干净),因此我们在 XCC API 中没有使用 gRPC 和 Protobuf。

不过,XCC API 有一个 Protobuf 描述(xctrl.proto,参见xswitch/xctrl),这主要是因为很多语言(如 Go 或 Java)可以通过它转换成目标语言的对象,并序列化成 JSON。该xctrl.proto是会一直维护的,可以在项目中使用。

XCtrl Proto 参见:

关于 Protobuf 参见:

JSON-RPC 序列化和反序列化

XCC 使用 JSON-RPC 消息承载。各种语言都有 JSON-RPC 的实现,但是,它们通常都耦合了传输层相关的代码实现(如 HTTP),比较重。如果你需要自己实现 JSON-RPC 的消息序列化和反序列化,本节给出一些语言的参考。

Javascript

在 Javascript 语言中,JSON-RPC 消息的序列化和反序列化都很简单,因为它几乎可以跟 Javascript 的对象一对一的转换,相当于 Javascript 中的一等公民,因而,无需特殊的处理。

值得一提的是,我们提倡在序列化 JSON 时使用“Pretty”格式,也就是有正常的缩进和换行,这样便于阅读和调试,对字节数的增加引起的影响也可以忽略不计。如果你特别在意在生产环境中的效率,那么可以使用条件编译技术仅在生产环境中使用“紧凑”格式。在 Javascript 中使用“Pretty”格式的将对象序列化的方法如下:

var str = JSON.stringify(obj, null, 2) // spacing level = 2

反序列化也很简单:

var rpc = JSON.parse(str)

Go

在 Go 语言中,习惯将 JSON 与 Go 语言的结构体相对应。由于 JSON-RPC 只是一个“信封”,在收到 JSON-RPC 消息时并不知道里面的内容应该对应哪个对象,因而一般需要两步或多步解析。以Event.Channel消息为例:

{
  "jsonrpc": "2.0",
  "method": "Event.Channel",
  "params": {
    "state": "START",
    "...": "..."
  }
}

当收到上述消息时,我们先解析“信封”,因为有method,所以是一个请求,没有id,说明是一个事件请求,不需要回复。根据method才知道params的结构,可以将params中的内容反序列化成ChannelEvent结构体对象。

import (
	"encoding/json"
)

type RPC struct {
	Version string           `json:"jsonrpc"`
	ID      *json.RawMessage `json:"id"`
	Method  string           `json:"method"`
	Params  *json.RawMessage `json:"params"`
	Result  *json.RawMessage `json:"result,omitempty"`
	Error   *json.RawMessage `json:"error,omitempty"`
}

type ChannelEvent struct {
	NodeUUID    string `json:"node_uuid"`
	UUID        string `json:"uuid"`
	State       string `json:"state"`
	CidName     string `json:"cid_name"`
	CidNumber   string `json:"cid_number"`
	DestNumber  string `json:"dest_number"`
}

var rpc RPC // 定义`rpc`变量为RPC类型
err := json.Unmarshal(msg.Data, &rpc) // 反序列化为RPC对象
if err != nil { // 错误处理
}
switch rpc.Method {
case "Event.Channel": // 根据`method`决定使用什么对象反序列化
	{
		var channelEvent ChannelEvent
		err := json.Unmarshal(*(rpc.Params), &channelEvent)
	}
}

在上述代码中,我们定义RPC为一个宽泛的类型,即可以接收 JSON-RPC 请求消息,也可以接收响应消息,它的的Paramsjson.RawMessage类型,因而它会保存原来 JSON 的内容而不深度解析,直到我们根据method知道params的类型后再进行解析。

下列代码是 Ctrl 侧发送应答请求的序列化代码示例:

type RPCRequest struct {
	Version string `json:"jsonrpc"`
	ID      string `json:"id"`
	Method  string `json:"method"`
	Params interface{} `json:"params"`
}

type AnswerParams struct {
	CtrlUUID string `json:"ctrl_uuid"`
	UUID     string `json:"uuid"`
}

rpc := RPCRequest{
	Version: "2.0",
	ID: "1",
	Method: "XNode.Answer",
	Params: AnswerParams{
		CtrlUUID: "ctrl_uuid",
		UUID:     "uuid",
	},
}

bytes, err := json.Marshal(rpc)

在上述代码中,RPCRequestRPC更具体,它只是一个请求,同时,它也比较宽泛,使用interface{}可以接受任何类型的参数,因而在构造请求结构体时我们可以传入AnswerParams类型的对象,序列化后的对象类型是byte[],可以直接通过网络函数发送,也可以转换成字符串打印输出。

由于 Go 语言允许在结构体定义时加入“json:”相关的注释,因而序列化后的字段名称可以根据情况指定。

除此之外,Go 语言对 Protobuf 的支持非常完善,我们也提供xctrl.proto转换生成的 Go 语言对象和函数,这样就不需要自定义各种函数的结构体,使用起来就方便些。

当然,前面也提到,我们也有更深度的包装,做成了 Go 语言 SDK,更方便使用,但也有更多的规矩和约束,这些约束适用于我们的代码架构,供大家参考。这些 SDK 会一直维护,如果也适合你使用,也可以直接拿来用。

Java

Java 中有json-simplegson等可以直接序列化和反序列化 JSON。与上一节中 Go 语言示例中对应的 Java 示例代码如下:

import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;

String rpc = new String(json_request_string, StandardCharsets.UTF_8);
JSONObject m = (JSONObject) parser.parse(rpc);  // 将JSON字符串解析成Java对象
String method = (String) m.get("method");
JSONObject params = (JSONObject)m.get("params");
String state = (String) params.get("state");

if (method != null && method.equals("Event.Channel") &&
	state != null && state.equals("START")) { // 新来话事件
	String node_uuid = (String) params.get("node_uuid");
	// 构造应答请求
	JSONObject rpc = new JSONObject();
	rpc.put("jsonrpc", "2.0");
	rpc.put("id", "test-id");
	rpc.put("method", "XNode.Answer");
	JSONObject p = new JSONObject();
	p.put("uuid", (String) params.get("uuid"));
	p.put("ctrl_uuid", "ctrl_uuid");
	rpc.put("params", p);
	StringWriter request = new StringWriter();
	rpc.writeJSONString(request);
	System.out.println(request);
	// 通过NATS发送请求 ...
}

从上述代码可以看出,直接操作 JSON 通用对象的代码也不复杂,但与序列化成具体的对象类相比,显得不太直观,而且如果 JSON 中的字符比较多的情况下,代码就比较冗长了。

Java 中不支持类似 Go 语言中的json.RawMessageinterface{}机制,因而使用起来要复杂些。gson支持对象的序列化和反序列化,可以配合使用。xctrl.proto也可以直接生成 Java 类(Xctrl.java),只是 Protobuf 对 JSON 的支持没有对 Protobuf 原生协议支持的好。下面是使用gson和 Protobuf 的示例:

import com.google.protobuf.util.JsonFormat;
import com.google.protobuf.*;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import xctrl.Xctrl.AcceptRequest;
import xctrl.Xctrl.Request;
import xctrl.Xctrl.PlayRequest;
import xctrl.Xctrl.Media;
import xctrl.Xctrl.ChannelEvent;
import xctrl.Rpc.*;


private static RPCRequest.Builder rpc(String method, String id) {
	return RPCRequest.newBuilder()
		.setJsonrpc("2.0")
		.setMethod(method)
		.setId(id);
}

// 请求字符串
String event = new String(json_request_string, StandardCharsets.UTF_8);
System.out.println(event);
JsonElement e = parser.parse(event);  // 将请求字符串解析成`gson`对象
JsonObject root = e.getAsJsonObject();// 找到根对象
String method = root.get("method").getAsString(); // 找到`method`
JsonObject params = root.get("params").getAsJsonObject(); // 找到`params`
if (method.equals("Event.Channel")) {
	// 将params重新变成string,注意这一点与Go语言中不同
	// gson中应该有方法直接将gson对象直接转换成Protobuf对像,但未找到怎么用
	String sparams = params.toString();
	ChannelEvent.Builder cevent = ChannelEvent.newBuilder();
	// 反序列化成`ChannelEvent`类
	JsonFormat.parser().ignoringUnknownFields().merge(sparams, cevent);
	if (cevent.getState().equals("START")) { // 来话请求
		// 下列代码应试在新线程中执行(因为是阻塞的),但简单起见,我们直接写在这里
		JsonFormat.Printer printer = JsonFormat.printer().preservingProtoFieldNames();
		String node_uuid = cevent.getNodeUuid();

		AcceptRequest accept = AcceptRequest.newBuilder()
			.setUuid(params.get("uuid").getAsString())
			.setCtrlUuid("ctrl_uuid")
			.build();

		RPCRequest xrpc = rpc("XNode.Accept", "0") // 创建RPC请求
			.setParams(Any.pack(accept)) // params被定义为Protobuf的Any类型
			.build();

		// Any类型必须提供一个TypeRegistry才能正常序列化,它会生成一个额外的`@type`字段
		TypeRegistry registry = TypeRegistry.newBuilder()
			.add(accept.getDescriptorForType()).build();
		String rpc_accept = printer.usingTypeRegistry(registry).print(xrpc);
		System.out.println(rpc_accept);
		// 通过NATS发送请求 ...
	}
}

在上述示例中,我们通过JsonFormat将对像序列化,它是 Protobuf 中标准的序列化方法,但对于Any类型的请求会生成额外的@type字段,理论上对方通过@type能正确的反序列化。但 XSwitch 不支持@type,因而生成的消息无法在 Ctrl 侧用 Java 正确的反序列化,因此上述代码中我们仍然使用了gson

我们相信 Java 语言如此成熟且应用广泛,一定有更好的方法。

Subject 及各种 ID 及 UUID 详解

使用 XCC 开发时,会遇到好多 Subject、ID 及 UUID,虽然我们在本文档中给出了解释,但是初学者还是容易混,因此,在此我们再详细解释一下。

RPC

RPC 中的id是 JSON-RPC 要求的,它代表一个请求。XSwitch 要求该id必须是一个字符串类型。XSwitch 本身不关心该 ID,会在 Response 中原样返回。所以,该id可以是任意字符串。但是,为了便于跟踪消息和排错,建议使用真正的 UUID 字符串,每个请求一个,保证唯一。

对于 NATS,如果使用异步消息(Publish 而不是 Request),则需要根据 JSON-RPC 的语义匹配返回结果。

Node UUID

每个 XSwitch 媒体节点又称为一个 Node,因此有一个唯一的 Node UUID,在事件消息中写为node_uuid。该node_uuid必须加上cn.xswitch.node前缀,才是媒体节点订阅的 Subject。一般来说,根据业务属性,媒体节点也会以队列方式订阅一些其它的业务 Subject,如cn.xswitch.node.ivrcn.xswitch.node.test等,这样在有很多媒体节点的情况下便于按业务分组。

Ctrl UUID

与 Node UUID 类似,每一个 Controller 都需要有一个唯一的 Ctrl UUID,用于与 Node UUID 建立虚拟对应关系。Ctrl UUID 在 JSON 消息中以ctrl_uuid表示,每个 Controller 对需要加上cn.xswitch.ctrl.前缀订阅自己的主题,如:cn.xswitch.ctrl.f76f6931-38c1-123b-4786-0242ac120003

多个 Ctrl 可以以 Queue 订阅的方式订阅一个业务 Subject,如cn.xswitch.ctrl、或cn.xswitch.ctrl.ivr等,多个 Ctrl 之间会竞争的接收发到这个 Subject 上的消息。

在一些高级应用中,同一个 Ctroller 也可以订阅很多 UUID。如,在批量外呼应用中,可以对每路通话产生一个 Channel UUID,进而订阅cn.xswitch.ctrl.$channel_uuid,当呼叫完成后取消订阅。

Channel UUID

每个呼叫(Channel)都有一个 UUID,唯一表示这路呼叫。对于来话,Channel UUID 是由 XNode 产生的。对于去话,Channel UUID 可以由 Controller 产生,或者由 XNode 产生。

XNode 侧的 Subject

XNode 侧的 Subject 必须以cn.xswitch.node前缀开头。对于从 Controller 侧发起的外呼或者命令(如status命令)而言,XNode 属于服务的一方,Controller 属于客户端,因而 Controller 会向cn.xswitch.node发 Request。

XCtrl 侧的 Subject

Controller 侧的 Subject 必须以cn.xswitch.ctrl前缀开头。多个 Controller 可以以 Queue 的方式竞争订阅cn.xswitch.ctrl主题。对于从 XNode 侧的来话而言,XNode 是客户端,XCtrl 是服务器。但是,在 XCtrl 接收到来话消息以后,它要命令 XNode 做事情,如AnswerPlay等,则 XCtrl 又是客户端,而 XNode 是服务器,此时所有命令应该发到cn.xswitch.node.$node_uuid上。

状态 Subject

在 Request/Replay 模式的 RPC 调用中,Node 侧订阅cn.xswitch.node,Ctrl 侧订阅cn.xswitch.ctrl,这样,Node 侧发起的请求发送到cn.xswitch.ctrl上,而 Ctrl 侧发起的请求则发送到cn.xswitch.node上。每个请求里填的 Subject 都是“对端”订阅的主题,这样看起来很合理。

但在状态发布类的主题上,如 Node 的状态,则应该发到cn.xswitch.status.node上,前缀cn.xswitch.status代表是状态发布,后面的node代表 Node 自己的状态。在这种情况下,遵循消息队列的 Pub/Sub 方式,即 Node 将自己的状态发布到cn.xswitch.status.node上,而 Ctrl 则将自己的状态发布到cn.xswitch.status.ctrl上。如果谁关心对方的状态,就订阅对方的状态主题,这主要是一个状态可能会被不同组件订阅。如对于cn.xswitch.status.node主题上的相当状态,在有多个微服务(如 IVR、会议、话单)关注它的状态时,它们都可以订阅该主题。

总之:

  • 在 Request/Reply 模式的 RPC 调用中,订阅主题是自己(如 Node 订阅cn.xswitch.node),而请求(Request)的主题是对方,如对方是 Ctrl 订阅了cn.xswitch.ctrl
  • 在状态订阅中,订阅的主题是对方(如自己是 Node,订阅cn.xswitch.status.ctrl),而发布(Publish)的主题是自己(如cn.xswitch.status.node),如果对方关注我们的状态,对方应该订阅这个主题。

分机同振与顺振

XCC 的 Dial 和 Bridge 接口都支持一个Destination,Destination 中有多个CallParam,每一个代码一条腿,可以支持多个不同的分机同振。

如果使用一个 SIP 分机号,但有多个不同的设备注册,则可以在 SIP Profile 中开启multiple-registration参数,支持在分机做被叫时多个设备同振。

在 XSwitch 中,默认分机类型是 SIP,仅对 SIP 分机振铃,如果分机类型是 WebRTC,则 SIP 和 WebRTC(Verto)分机同振。

如果让 SIP 分机和 WebRTC 分机顺振,可以在CallParam.Params中添加find_sip_device_onlyfind_webrtc_device_only参数。参见 Dial 接口相关说明。

感知分机注册状态

很多时候需要知道分机的注册状态,虽然这是一个误区(我们以后将详细分析)。感知分机注册状态有以下两种方式:

  • 订阅相关事件
  • 直接通过 API 查询分机状态

两种方式其实差不多,第一种方式订阅事件后,Ctrl 侧还是要存到缓存(如 Redis)或数据库中,以备以后查询。两种方式的区别就是 Ctrl 侧还是 Node 侧查的问题。

订阅相关事件

  • 订阅cn.xswitch.ctrl.event.custom.sofia::register事件,可以获取 SIP 分机注册状态
  • 订阅cn.xswitch.ctrl.event.custom.sofia::unregister事件,可以获取 SIP 分机注销状态
  • 订阅cn.xswitch.ctrl.event.custom.sofia::expire事件,可以获取 SIP 分机注册过期信息
  • 订阅cn.xswitch.ctrl.event.custom.verto::login事件,可以获取 WebRTC 分机的注册注销状态

如果 Ctrl 侧订阅了这些事件,那么 Ctrl 侧需要在收到分机注册事件后记录相关状态,在收到注销或超时事件后,删除相关状态(不管存到 Redis 还是数据库)。注意:关于冷启动支持:如果 Ctrl 侧重启,可以通过 API(NativeAPI show registrations as json)获取分机注册数据,也可以直接读取数据库(如果 Node 侧的分机注册数据存到 MySQL 或 PostgreSQL 中时,SQLite 不要直接查询,有潜在的冲突)。

通过 API 查询

可以通过以下 Native API 查询分机注册状态:

  • sofia status profile default reg 1000:查询 SIP 分机 1000 的注册状态
  • sofia_contact 1000@xswitch.cn:查询该 SIP 分机注册状态
  • verto_contact 1000@xswitch.cn:查询 WebRTC 分机注册状态

也可以直接连接数据库查询,具体的数据库表参见数据库表相关文档或联系我们的工程师。

示例

请参考 https://git.xswitch.cn/xswitch/xcc-examples 中的相关示例,示例大部分以 Node.js(Javascript)语言提供,因为 Node.js 可以比较方便的描述 JSON,做前后端的也都熟悉。

示例中也有 Go、Java 等语言的参考,其中的 README 也能提供更多的信息。

XCC事件说明