XCC API

设计架构及交互流程

系统整体控制架构和弹性伸缩基于 NATS 消息队列实现。


NATS 简介

NATS[^nats]是一个消息队列,它实现了 Pub/Sub(生产者-消费者)消息机制,通过它也可以实现“请求-响应”式的 RPC(Remote Procedure Call,远程过程调用)交互。

[^nats]: 参见 https://nats.io/

Subject

NATS 使用主题(Subject)对消息进行标记,实际消息内容可以是任何文本或二进制消息。XSwitch 使用 JSON-RPC 消息封装。

在使用 NATS 前,需要了解 NATS 的核心概念,参考文档:https://docs.nats.io/nats-concepts/core-nats。文档都是英文的,下面是简要的中文解释。

Pub/Sub

在实际使用时,XNode 和 Ctrl 分别向 NATS 服务器订阅自己关心的主题,然后就可以互相发送消息了。消息本身是异步的,也就是说,生产者产生一条消息,发送出去就完了,而消费者可以有 0 个或多个,订阅同一个主题的消费者都能收到这条消息,如果没有消费者,消息就会被丢弃。

参考文档:https://docs.nats.io/nats-concepts/core-nats/pubsub

Request/Reply

通过订阅一个一次性的主题,可以实现阻塞的“请求-响应”调用。这个一次性的主题称为“邮箱”(Mailbox),如下图:

其中,XNode 发出一条消息前订阅了一个邮箱,Ctrl 收到这条消息后往这个邮箱里回复了一条响应消息。

当然,这个一次性的主题也可以扩展为 N(N>1)的场景,在此不做讨论。

参考文档:https://docs.nats.io/nats-concepts/core-nats/reqreply

Queue Groups

NATS 通过队列组可以执行类似负载均衡的分发策略。对于多个消费者来说,订阅了相同 Queue 的一组消费者中,只有一个可以收到消息,也就是说消息分发是互斥的。如下图,控制器 1、2、3 以同一个队列(queue-1)方式订阅了同一个 Subject,只有一个能收到消息,而控制器 4 没有以队列方式订阅,控制器 5 使用了另一个队列(queue-2),它们也能收到消息。

通过这种方式,可以实现 Round Robin(轮循)方式的消息分发。比如同时来了 300 路通话,每个 Ctrl 可以处理 100 路。

参考文档:https://docs.nats.io/nats-concepts/core-nats/queue

集群

NATS 支持集群,多个 NATS 间也可以自由转发消息。如果连接到其中一个 NATS 服务器失败,NATS 客户端库会自动尝试连接下一个 NATS 服务器。

基于 NATS 实现的 XSwitch 集群将在后面的章节讨论。

SDK

NATS 有各种语言的客户端 SDK,因此使用起来很方便,具体的例子见后文。

JSON-RPC

远程过程调用使用 JSON-RPC 2.0 格式定义。下面是一个 JSON-RPC 请求:

{
  "jsonrpc": "2.0",
  "id": "1",
  "method": "XNode.Answer",
  "params": {
    "ctrl_uuid": "ctrl_uuid ... ",
    "uuid": "..."
  }
}

JSON-RPC 中,所有请求都有一个id,代表期望返回一个结果。且返回结果中的id与请求中的id一致。JSON-RPC 规定id可以是null、数字或字符串,但为了简单起见,我们只使用字符串。

id可以为不存在(注意不是nullnull是一个值),如果id不存在,则认为是一个事件(或称通知,Notification),事件其实也是一个请求,但是不需要对方返回结果。

如果返回结果中有result,则为正常返回,否则,应该返回error。两者都可以是任何合法的 JSON 对像或值。

{
	"jsonrpc": "2.0",
	"id": "...",
	"result": {
		...
	}
}

{
	"jsonrpc": "2.0",
	"id": "...",
	"error": {
		...
	}
}

详细的 JSONRPC 规则可参考:

XCC API

通过 NATS 使用 JSON-RPC 进行远程过程调用的 API 称为 XCC API。

JSON-RPC 相当于一个信封。在信封内部,是 XCC API 具体请求的内容,统一放到params中。信封中的出错信息通常是因为信封出错导致的,如必选参数不存在、方法不存在等。如果信封合法,则具体的返回结果都放到result中,返回结果中均有如下参数:

  • code:代码,参照 HTTP 代码规范,如2xx代表成功,4xx代表客户端错误,5xx代表服务端错误等。
  • message:对代码的解释。有一些message中会带有一些00开头的错误码,这些错误码没有任何含义,仅为方便追踪错误而设。
  • uuid:如果请求中有uuid,则结果中也有uuid,代表当前的 Channel UUID。

通用code返回值说明

  • 100:临时响应,实际的响应消息将在后续以异步的方式返回。
  • 200:成功。
  • 202:请求已成功收到,但尚不知道结果,后续的结果将以事件(NOTIFY)的形式发出。
  • 206:成功,但是数据不全,如发生在放音过程中通过 API 暂停的情况。
  • 400:客户端错误,多发生在参数不全或不合法的情况。
  • 410:Gone。发生在放音或 ASR 检测过程中用户侧挂机的情况。
  • 419:冲突。如一个电话被接管,另一个控制器又尝试接管的情况。
  • 500:内部错误。
  • 6xx:系统错误,如发生在关机或即将关机的情况下,拒绝呼叫。

XCC API 即控制指令。控制指令分类两类:

  • Channel API:作用于一个 Channel,必须有一个uuid参数,uuid即为当前 Channel 的uuid
  • 普通 API:普通 API 没有uuid参数。如对 FreeSWITCH 的控制,发起外呼等。

API 在 XSwitch 后台使用多线程调度,所有 API 在前面看起来都是阻塞的,但由于消息队列的异步特性,可以被同步或异步地调用。

Session 与 Channel

在 XSwitch 中,Session 与 Channel 通用,代表一路呼叫。XSwitch 是个 B2BUA,如果是单路的 IVR,则是一个 Channel,在 a 呼 b 的场景中,是两个 Channel。每个 Channel 在 XSwitch 中也称为一条“腿”(Leg),呼入的腿一般称为a-leg,呼出的腿称为b-leg

所有的 XCC API 都是针对 Channel 进行操作(通过uuid参数),如 Play、TTS 等。有的 API(如ChannelBridge)可以同时操作两条腿,甚至更多的腿。

来话处理

XSwitch 侧所有来话均会被置于park(暂停、挂起)状态,然后 XSwitch 广播一个Event.Channelstate=START)事件到消息队列。关注该消息队列的控制服务可以接管这个呼叫。

控制服务称为 XCtrl。多个 XCtrl 可以协同工作。第一个发送接管指令(Accept)并接管成功的 XCtrl 将接管该呼叫。后续 XSwitch 内所有跟当前通话相关的事件都会发到这个节点上。

如果一个来话在10秒内没有被接管,则呼叫将会被挂断。

如果想对来话直接应答,则可以直接使用Answer应答,Answer会隐含进行Accept。流程图如下:

去话处理

去话,即外呼,使用Dial方法实现,由于外呼可能持续比较长的时间,外呼在 XSwitch 侧是在单独的线程中处理的,所以外呼请求会收到code = 202消息表示该外呼请求已被接受正在排队。如果外呼不成功,或者有成功进展,都会有后续的Event.ChannelEvent.Result消息。

简单外呼的流程图如下:

Channel State

Channel 状态。

来话 Channel 状态

  • START:来话第一个事件,XCtrl 应该从该事件开始处理,第一个指令必须是 Accept 或 Answer
  • RINGING:振铃
  • ANSWERED:应答
  • BRIDGE:桥接
  • UNBRIDGE: 断开桥接
  • DESTROY:挂机

去话 Channel 状态

  • CALLING:去话第一个事件
  • RINGING:振铃
  • ANSWERED:应答
  • MEDIA:媒体建立
  • BRIDGE:桥接
  • READY:就绪
  • UNBRIDGE: 断开桥接
  • DESTROY: 挂机

在调用XNode.Dial外呼的时候,在ignore_early_media=false(默认)的情况下,收到MEDIA就会触发 READY 事件。如果为true,则需要等到ANSWERED以后才会触发READY状态。不管什么情况,都需要在收到READY状态后才可以对 Channel 进行控制。

在执行XNode.Bridge时,没有READY事件,这时可以根据ANSWEREDBRIDGE事件处理业务逻辑。

在 XNode 中,一个 Channel 从创建开始(state = STARTstate = CALLING),到销毁(state = DESTROY),是一个完整的生命周期。销毁前,会发送Event.CDR事件,通常会在单独的 Topic 上发出(可配置)。

一般来说,只要 Channel 被创建,总会有对应的DESTROY消息。但是,在 XNode 发生崩溃的情况下,需要准备超时垃圾回收机制。

同步调用

使用 NATS 客户端库的request函数发起同步调用。同步调用在 Ctrl 侧会阻塞直到返回结果。同步调用有一个超时参数timeout,如果发生超时,不一定代表出错。考虑以下场景,播放一个声音文件(file):

request(play, file, (timeout = 10));

如果文件长度为 5 秒,则该函数会在 5 秒播放完成后返回。

如果文件长度为15秒,则该函数会在10秒后超时。但文件的播放仍在进行。客户端可以选择继续等待播放完成的事件(Event.PLAYBACK_STOP),或停止当前播放(调用Stop方法)。

注意:在实际开发时,要尽量避免出现 NATS timeout,也就是说要让timeout足够长,因为在出现timeout的情况下(如上述场景),实际上文件可能没有播放完成,也可能是网络断了或 XSwitch 崩溃了等各种原因,因而,Channel 的状态在 Ctrl 侧是是未知的,这就需要很小心的错误处理以应对各种情况(如,可以主动查询一下 Channel 状态,但查询可能又会发生超时……)。

expect_100_trying

为了方便在同步调用时快速知道执行结果,在同步调用中有一个实验性的参数params.exepect_100_trying。使用该参数时必须提供params.ctrl_uuid。如果expect_100_trying = true,则调用会立即返回(code = 100或错误消息),如果返回的是错误消息,则错误消息为最终响应,不会有后续结果。如果返回的是100,则实际的调用结果将发送到异步订阅的 Controller 主题上,该主题规则为ctrl_uuid的拼接,如cn.xswitch.ctrl.$ctrl_uuid。如果ctrl_uuid不存在,则会发送到AcceptAnswer接管的 Controller 指定的位置。

目前支持该操作的接口有:

  • Bridge
  • ChannelBridge
  • Play
  • Record(仅同步模式(action = RECORD)时可用)
  • ReadDTMF
  • DetectSpeech
  • RingBackDetection

异步调用

使用 NATS 的publish函数可以发起异步调用。异步调用时,如果rpc.id为空,表示 Ctrl 不需要看到结果,XSwitch 不会发送 API 的执行结果消息,但是有可能产生相关的事件,如(PLAYBACK_STOP)。

如果异步调用时rpc.id为非空,则 FreeSWITCH 会返回Event.Result消息,Ctrl 应该订阅该消息(Subject 为cn.xswitch.ctrl.<ctrl_uuid>),以获取执行结果。结果中的rpc.id与请求时中的rpc.id一致。

Channel 变量

每一个 Channel 上都有很多参数(param),也叫变量(variable),如caller_id_namecaller_id_number等。这些变量通常在Event.Channel事件中发送。对于每一路通话,XSwitch Channel 上会有数百个相关的变量,然而,大多数时候我们并不需要所有变量,因而,为了简洁起见,XSwitch 并不发送所有变量,而是只发送很少的变量。

如果在开发应用中有特殊需求,可以在 Controller 侧用以下方式“订阅”更多变量:

  • xcc.conf中静态配置,这个配置是全局的,即所有 Channel 事件都会带这些变量
  • 对于入局呼叫,可以在AcceptAnswer时,通过channel_params字符串数组动态订阅变量
  • 对于出局呼叫,可以在DialBridgeDestination参数中通过channel_params数组动态订阅变量
  • 在呼叫过程中,可以通过SetVar中的channel_params数组动态订阅变量。

订阅的变量会在 Channel 生命期内一直有效,xcc.conf中静态配置的变量会与动态订阅的变量合并(去重)发送。Event.Channel事件中,会在params.params参数中携带,参数和值全部都是字符串。

订阅的变量也会体现在Event.CDR中,但会与Event.CDR中的变量混在一起,没有二级params.params

:变量列表

Info/Event 显示名称通道变量名称说明
Channel-Statestate当前 Channel 的状态
Channel-State-Numberstate_numberChannel 状态整数值
Channel-Namechannel_nameChannel 名称
Unique-IDuuidChannel UUID,唯一标志
Call-Directiondirection呼叫方向,inbound/outbound
Answer-Statestate应答状态
Channel-Read-Codec-Nameread_codec接收语音编码
Channel-Read-Codec-Rateread_rate接收采样率
Channel-Write-Codec-Namewrite_codec发送语音编码
Channel-Write-Codec-Ratewrite_rate发送采样率
Caller-Usernameusername鉴权用户名
Caller-DialplandialplanDialplan 名称,如 XML、Lua 等
Caller-Caller-ID-Namecaller_id_name主叫名称
Caller-Caller-ID-Numbercaller_id_number主叫号码
Caller-ANIaniANI,一般与主叫号码相同
Caller-ANI-IIaniiiANI II [^aniii]
Caller-Network-Addrnetwork_addr主叫 IP 地址
Caller-Destination-Numberdestination_number被叫号码
Caller-Unique-IDuuidChannel UUID
Caller-Sourcesource源模块,如 mod_sofia
Caller-ContextcontextDialplan Context
Caller-RDNISrdnis转移后的 DNIS 信息,见 transfer Application
Caller-Channel-Namechannel_name
Caller-Profile-Indexprofile_index.
Caller-Channel-Created-Timecreated_time创建时间
Caller-Channel-Answered-Timeanswered_time应答时间
Caller-Channel-Hangup-Timehangup_time挂机时间
Caller-Channel-Transfer-Timetransfer_time转移时间

[^aniii]: ANI II (OLI - Originating Line Information),参见:http://www.nanpa.com/number_resource_info/ani_ii_digits.html

详细的通道变量参见以下链接:

https://freeswitch.org/confluence/display/FREESWITCH/Channel+Variables

综述