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
可以为不存在(注意不是null
,null
是一个值),如果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.Channel
(state=START
)事件到消息队列。关注该消息队列的控制服务可以接管这个呼叫。
控制服务称为 XCtrl。多个 XCtrl 可以协同工作。第一个发送接管指令(Accept
)并接管成功的 XCtrl 将接管该呼叫。后续 XSwitch 内所有跟当前通话相关的事件都会发到这个节点上。
如果一个来话在10
秒内没有被接管,则呼叫将会被挂断。
如果想对来话直接应答,则可以直接使用Answer
应答,Answer
会隐含进行Accept
。流程图如下:
去话处理
去话,即外呼,使用Dial
方法实现,由于外呼可能持续比较长的时间,外呼在 XSwitch 侧是在单独的线程中处理的,所以外呼请求会收到code = 202
消息表示该外呼请求已被接受正在排队。如果外呼不成功,或者有成功进展,都会有后续的Event.Channel
或Event.Result
消息。
简单外呼的流程图如下:
Channel State
Channel 状态。
来话 Channel 状态
START
:来话第一个事件,XCtrl 应该从该事件开始处理,第一个指令必须是 Accept 或 AnswerRINGING
:振铃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
事件,这时可以根据ANSWERED
或BRIDGE
事件处理业务逻辑。
在 XNode 中,一个 Channel 从创建开始(state = START
或state = 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
不存在,则会发送到Accept
或Answer
接管的 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_name
,caller_id_number
等。这些变量通常在Event.Channel
事件中发送。对于每一路通话,XSwitch Channel 上会有数百个相关的变量,然而,大多数时候我们并不需要所有变量,因而,为了简洁起见,XSwitch 并不发送所有变量,而是只发送很少的变量。
如果在开发应用中有特殊需求,可以在 Controller 侧用以下方式“订阅”更多变量:
- 在
xcc.conf
中静态配置,这个配置是全局的,即所有 Channel 事件都会带这些变量 - 对于入局呼叫,可以在
Accept
或Answer
时,通过channel_params
字符串数组动态订阅变量 - 对于出局呼叫,可以在
Dial
或Bridge
的Destination
参数中通过channel_params
数组动态订阅变量 - 在呼叫过程中,可以通过
SetVar
中的channel_params
数组动态订阅变量。
订阅的变量会在 Channel 生命期内一直有效,xcc.conf
中静态配置的变量会与动态订阅的变量合并(去重)发送。Event.Channel
事件中,会在params.params
参数中携带,参数和值全部都是字符串。
订阅的变量也会体现在Event.CDR
中,但会与Event.CDR
中的变量混在一起,没有二级params.params
。
:变量列表
Info/Event 显示名称 | 通道变量名称 | 说明 |
---|---|---|
Channel-State | state | 当前 Channel 的状态 |
Channel-State-Number | state_number | Channel 状态整数值 |
Channel-Name | channel_name | Channel 名称 |
Unique-ID | uuid | Channel UUID,唯一标志 |
Call-Direction | direction | 呼叫方向,inbound/outbound |
Answer-State | state | 应答状态 |
Channel-Read-Codec-Name | read_codec | 接收语音编码 |
Channel-Read-Codec-Rate | read_rate | 接收采样率 |
Channel-Write-Codec-Name | write_codec | 发送语音编码 |
Channel-Write-Codec-Rate | write_rate | 发送采样率 |
Caller-Username | username | 鉴权用户名 |
Caller-Dialplan | dialplan | Dialplan 名称,如 XML、Lua 等 |
Caller-Caller-ID-Name | caller_id_name | 主叫名称 |
Caller-Caller-ID-Number | caller_id_number | 主叫号码 |
Caller-ANI | ani | ANI,一般与主叫号码相同 |
Caller-ANI-II | aniii | ANI II [^aniii] |
Caller-Network-Addr | network_addr | 主叫 IP 地址 |
Caller-Destination-Number | destination_number | 被叫号码 |
Caller-Unique-ID | uuid | Channel UUID |
Caller-Source | source | 源模块,如 mod_sofia |
Caller-Context | context | Dialplan Context |
Caller-RDNIS | rdnis | 转移后的 DNIS 信息,见 transfer Application |
Caller-Channel-Name | channel_name | |
Caller-Profile-Index | profile_index | . |
Caller-Channel-Created-Time | created_time | 创建时间 |
Caller-Channel-Answered-Time | answered_time | 应答时间 |
Caller-Channel-Hangup-Time | hangup_time | 挂机时间 |
Caller-Channel-Transfer-Time | transfer_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