阅读《FreeSWITCH 权威指南》笔记。
简单使用
windows 安装 FreeSwitch
自己的电脑系统是 xubuntu 22.10 Ubuntu Kinetic Kudu (development branch) 版本,linx 安装 FreeSwitch 需要自己编译安装,里面安装编译会出错,所以采用虚拟机 windows 安装。windows 安装很方便,不再赘述。
freeswitch 默认开启了 1000~1019 的号码,默认密码为 1234。
可通过配置文件 安装目录下\conf\directory\default\
内查看各个号码的配置信息,其中的变量如 $${default_password}
在 安装目录\conf\vas.xml
中定义。
启动 freeswitch 在开始菜单的列表中 右键程序-以管理员身份运行 进行启动,当出现如下界面时即启动完成。
sip客户端安装、登陆、呼叫
在 Windows 中用的是 MicroSip,安装和使用都很方便(仅使用于 Windoes)。下载链接
手机端用的是 SipDroid,在手机浏览器中找的下载链接。配置登陆号码见下方截图(域名即 FreeSwitch 所安装机器的 IP 地址)。
手机端和 MicroSip 的配置几乎是一样的,不过需要在同一个局域网中。我是用手机分出了 Wifi 让主机连接,然后 VirtualBox 使用桥接的方式连上主机网络。多个终端也可以通过多开几个虚拟机安装 MircoSip 的方式实现。
不同客户端通过登陆不同的号码,就可以通过电话进行呼叫和通话了。
启动 FS 后,在安装包中还有一个 fs-cli 是可以连接到 FS 的客户端,和 FS 一样的输入输出,但关闭不影响服务程序。
FreeSwitch 基本命令
sofia status profile internal reg # 查看已注册(登陆)设备(号码)
originate user/1000 &park # 通过 1000 拨打电话到 park 程序
# 程序(APP)其实为 freeswitch 内置的函数(注意使用时加上 & 符号):
# park 挂起(听不到任何声音)
# hold 挂起(能听到声音,Music On Hold, MOH)
# playback(/root/welcome.wav) 播放特定的声音文件
# record(/tmp/rec.wav) 录音文件
# bridge(user/1001) 转接到 1001
show channels # 显示通话中的一些信息,包含 UUID
uuid_bridge <uuid1> <uuid2> # 将两个 channel 桥接起来
help # 帮助
sofia help # 模块帮助
sofia global siptrace on # 开启 sip 信息的显示,用 off 可以关闭。
FreeSWITCH 架构
以下 FS 表示 FreeSWITCH 的缩写。
总体架构
总体来说包括 核心 和 外围模块 组成。核心短小精悍高稳定高安全,外围模块通过调用核心提供的 API 与核心进行通信,核心通过让外围模块注册回调函数执行外围模块代码。
核心主要有四部分:DB、公共接口(Public API)、抽象接口和事件(Event)。
DB 默认使用 SQLite,SQLite 是一种嵌入式数据库。FS 使用核心数据库(在
安装目录/db/core.db
)来记录系统接口、任务(tasks)及当前的通道(channels)、通话(calls)等实时数据。模块也有自己的数据库(表)在 db 目录下。公共接口(Public API)可以被外围模块调用。如创建或释放媒体流、Json处理函数等等。
抽象接口,抽象接口是核心没有实现的接口,一般由外围模块负责实现并向核心层 注册,核心通过 回调 的方式调用具体的实现。
事件(Event),FS 在内部使用消息和 事件机制 进行进程间和模块间通信。事件的产生和消费是异步的,事件可以在 FS 中通过 绑定(Bind)回调函数 进行捕获,即 FS 在时间发生时会依次回调这些函数。
外围接口实现
- Endpoint,是 FS 的最外围(再向外就不是 FS 了),主要 包含了不同 呼叫控制协议的接口,实现与不同电话系统的通信。
- Dialplan,拨号计划,提供电话路由功能。系统默认是由 mod_dialplan_xml 提供。
- Chatplan,聊天计划,提供对文本消息的路由,由 mod_sms 实现。
- APP,应用程序,FS 提供了一些内置的 APP。如 mod_voicemail 实现语音留言;mod_conference 实现多方会议。
- FSAPI,命令接口,是对外的命令接口。
- XML 接口,支持多种 XML 的读取,如 本地文件、DB、远程 HTTP 请求等。但对于应用和扩展在外围模块中完成,如 mod_xml_rpc、mod_xml_curl 等。
- Codec,编解码器(是 COde 与 DECode 的组合)。FS 的实现可以桥接不同采样频率的电话或会议电话。
- 语音识别及语音合成(ASR/TTS)。
- 格式、文件接口(Format, File Interface),支持不同格式文件的回放、录音。
- 日志(Logger),控制日志写到 控制台、文件、系统日志(syslog)、远程日志服务器等。
- 定时器(Timer),FS 最理想的工作始终频率是 1000Hz,许多默认 Linux 发行版内核默认是 100Hz 或 250 Hz,在这种情况下可以重新编译内核调整始终频率。
- 嵌入式语言(Embeded Language),支持 Lua、Javascript、Perl 等控制呼叫流程。
- 事件套接字(Event Socket),可以使用任何其他语言通过事件套接字控制呼叫流程、扩展 FS 功能。
目录结构
在安装目录下
sounds 提供各种声音文件,sounds/music 提供 MOH(Music On Hold,保持音乐)
storage 存放从其他 HTTP 服务器下载下来的语音文件缓存及录音留言文件
conf 存放配置文件
着重介绍下配置文件,配置文件由众多 XML 组成系统装载时,会将 XML 组织在一起 Load 到内存,成为 XML 注册表。
-
conf/freeswitch.xml
是主入口,是所有 XML 文件的黏合剂。标签X-PRE-PROCESS
是预处理命令,是在加载阶段只进行简单的替换不会被解析,所以对它进行注释仍然会发生替换,需要注意一些影响。 -
vars.xml
是通过X-PRE-PROCESS
定义的一些全局变量,在后续以$${var}
的方式进行引用。可以通过global_getvar
命令来查看变量值。 - autoload_configs 目录下是模块级的配置文件,命令方式
模块名.conf.xml
(无前缀 mod_),会在系统启动的时候 Load。各模块查找是通过 configuration 标签的 name 属性查找的。如 mod_sofia 在启动时会向 XML 注册表中查找 configuration 标签为 sofia.conf 的配置。- autoload_configs 下的 modules.conf.xml 定义了启动时自动加载哪些模块。
- autoload_configs 下的 post_load_modules.conf.xml 中定义的模块是最后加载的。
-
conf/dialplan
目录中的 XML 是路由计划 -
conf/ivr_menues
中存放了默认的 IVR 菜单 -
conf/directory
中存放了用户配置目录(用户目录)。FS 的 用户目录 支持多个域(Domain)。
XML 用户目录
SIP 不要求一定要注册才可以打电话,但通话前仍需用户认证,认证参数即中用户目录中进行配置。即用户目录决定了哪些用户能注册到 FS 中。
<include>
<domain name="$${domain}">
<params>
<param name="dial-string" value="..."/>
</params>
<variables>
<variable name="record_stereo" value="true"/>
</variables>
<groups>
<group name="sales">
<users>
<user id="1000" type="pointer"/>
</users>
</group>
</groups>
</domain>
</include>
default.xml 是自带默认的配置文件。其中 $${domain}
默认变量值是主机 IP 地址,可以将他修改为一个域名。
-
params
标签中定义 domain 下所有用户的公共参数。 - params 定义的
dial-string
变量很重要,FS 会使用 user/username 或 sofia/internal/username@domain 呼叫时会根据 username 等信息找到 dial-string 最终扩展成用户实际 SIP 地址。 -
variables
标签定义了一些 Channel 级别的公共变量,在通话中会绑定到相应的 Channel 上形成 Channel Variables。 -
groups
、group
组标签,是不必要的,但可以方便地进行群呼、代接之类的业务。 -
users
、user
标签可以是完整的 XML,也可以是指向已存在用户的“指针”(type="pointer"
,通过<user id="xxx">
来找到)。 - 注:params 和 variables 可以出现在 user、group 或 domain 中,优先级按作用域的减小而增大。
呼叫相关概念
两种典型流程:
- Bob -> FS -> Alice
- FS -> Bob && FS -> Alice
市场上有对方式二的变种,流程为 a) B -> FS 随即 FS 挂掉电话;b) FS -> B && FS -> A。好处:接电话不会被收费。(华为中 Welink 呼叫就是这个流程)。
- 来话,针对与 FS 是到达 FS 的呼叫
- 去话,针对于 FS 是从 FS 出去的呼叫
- Session,无论来话去话 FS 都会启动一个 Session(会话)用于控制整个呼叫
- Channel,每个 Session 控制这一个 Channel(信道、通道),是一对 UA 间通信的实体,相当于 FS 的一条腿。每个 Channel 都用一个唯一的 UUID 来标识,称为 Channel UUID;每个 Channel 上可以绑定一些呼叫参数,称为 Channel Variables(通道变量)。
- Call,FS 的作用是将两个 Channel 桥接 到一起组成一个通话,称为一个 Call。
- 回铃音和 Early Media,假设A、B 不在同一交换机(服务器)上通话,中间会经过两台交换机 a、b:
A <-> a <-> b <-> B
。在早期,A 呼 B 在 B 开始振铃时,A 能听到单一的回铃音(Ring Back Tone),这里 b 只向 a 传送了个信令传达到 B 的信号,由 a 交换机生成来铃流;后来,为了支持让 A 听 B 端定制的铃流,必须由交换机 b 返回铃流,这就是 Early Media(早期媒体)。在 SIP 通信中是由 183 消息(带有 SDP)描述的。
Early Media 的流量不包含在通信费中,一般是在月租或套餐中的收费的,所以可以将真正的话音数据伪装成 Early Media 实现“免费通话”。但这种应用有一定的限制,大多数交换机允许的 Early Media 不会太常,如 1 分钟,以避免这种免费通话。
- 全局变量和局部变量,全局变量在服务加载时只求值一次,用
$${var}
形式引用;局部变量即 Channel Variables,在每次创建 Channel 时求值(生命周期),用${var}
引用。部分变量在显示时有 variable_ 前缀,但在使用时不需要此前缀。
拨号计划 DialPlan
找到了本章的在线读书笔记可参考。
从配置文件看工作流程
配置文件的拨号计划又叫 XML DialPlan,下文还会降到 内联拨号计划。
拨号计划默认用 XML 格式配置。DailPlan 的完整结构的配置是这样的嵌套结构(简写了 xml 文件):
- document
- section(name: dialplan)
- context
- extension # extension 与 extension 之间在逻辑上是隔离的
- condition(field="xxx" expression="^echo|1234$") # 测试条件,指定表达式
- action(application="info" data="xxx") # 执行动作
- 修改配置文件后要用
reloadxml
命令或 F6键 重载配置文件 - DailPlan 的匹配顺序是按先后顺序进行匹配的,所以注意(不清楚整体情况时)自己修改的放到前面
- 注意修改权限,我在 sublime 中修改保存成功但实际 reloadxml 不成功,在整个文件夹的上加修改权限后成功
- 按 F8 或
console loglevel debug
设置 console 的 log 级别为 debug -
extension
标签有个continue="true"
属性,表示匹配完当前 extension 后还可以匹配后方的 extension,默认 continue 为 false。 -
condition
标签,为空表示匹配所有条件- 验证用户信息的 field(可以是内置变量,用户目录中的外置变量要用
${}
)与 expression(正则)是否匹配 -
condition
标签可叠加,表示与关系 -
condition
标签中break
参数可以做逻辑判断,如break="on-true"
表示 在执行完本 cond 中的 action 后if true break
,另外有 on-false(默认),always,never
- 验证用户信息的 field(可以是内置变量,用户目录中的外置变量要用
-
action
标签是执行动作,application
表示设置执行的应用,data
为传进去的字段,现知的应用有:- info:输出所有变量到日志中
- answer:接听电话,相当于 FS 响应应答消息,后面可以设置一些媒体流如放音、转到语音信箱等。
- echo(数字号码 9196):回声,可以听到自己的声音,另外还有个延迟回声
delay_echo(9195)
。 - log:输出日志,
- hangup:挂断电话
- set:设置一个当前 leg 的变量绑定到 Channel 上。如设置 greeting 变量的 data:
data="greeting=good-morning.wav"
- export:设置一个双方 leg 的变量,同时还设置一个
export_vars=greeting
,当 data 开头为nolocal:
时表示只设置到对方 leg 上。 - hash:设置变量到内存哈希表中,data 的格式为
operator/key/scope/value
- bridge:作用,将两条腿桥接起来(在这里创建 b-leg),data 是标准的呼叫字符串(Dial string),内部
{c=d}
可以设置 b-leg 的通道变量 c = d 。birdge 会一直阻塞等待 b-leg 接听或挂机等操作。 - sleep:表示暂停的 ms 数
- conference,会议,可以配置 condition 将呼叫某一(类)号码的用户都转到会议中(会议分组看 data 格式)。
- bind_meta_app,根据用户输入的 number(加星号)执行操作。
- transfer,将当前通话重新转移到 Routing 阶段。
- playback(9664),播放一个声音文件
- record,开启录音,data 中传保存位置
<extension name="My Echo Test"> <!-- 测试时名字没改,忽略其意义就好 -->
<condition expression="^1234(\d+)$" field="destination_number">
<action application="log" data="INFO you called ${destination_number}"/> <!-- log 数据首单词可设置级别 -->
<action application="log" data="NOTICE the suffix is $1"/>
<action application="hangup"/>
</condition>
</extension>
- 注意执行阶段,解析阶段,执行阶段
- 日志级别:CONSOLE、ALERT、CRIT、ERR、WARNING、NOTICE、INFO、DEBUG
- 反动作标签
anti-action
,当 condition 不匹配时可以执行 内部 的anti-action
标签(神奇的设定)。
工作机制
Channel 状态机转换工程: NEW - INIT - ROUTING/HUNTING - EXECUTE - HANGUP - REPORTING - DESTROY
新建 Channel - 初始化 - 路由(查找解析 Dialplan) - 执行动作 - 挂机(某一方执行)- 包好(统计计费) - 销毁(释放资源)
在 执行 阶段,也可以发生转移(Transfer),转移到同一个 Context 下不同 Extension,转移后会重新进入 Routing 阶段。
注意:
- 默认情况下,Routing 阶段会查到到 执行计划 中的所有 Extension,并把 action 放到一个队列中,然后才进入 Execute 阶段执行。所以在 action 标签中改变某值去影响路由的逻辑是不对的(除非用 inline 属性)。
- action 标签上
inline="true"
属性可以让 action 在 Routing 阶段执行 - 可用
inline
属性的 app 不多,一般都是很快地存取变量的操作。
内联拨号计划
可以把前面讲到的拨号计划成为“XML 拨号计划”。内联拨号计划(Inline Dialplan),用于快速测试不同的 action,可以直接在命令行中写出对应的命令:
originate user/1000 answer,playback:/tmp/a.wav,record:/tmp/b.wav inline
# 解释:使用 1000 拨打,首先 answer,然后放音 a.wav,然后录音到 b.wav
如上注释中的解释,不再重复了,其中:
- 不同于之前用
&
指定的 APP,用内联形式不需加&
且可以指定多个 APP(是个流程) - 带参数的 APP 用
APP:args
格式来书写 - 多个 APP 间默认用 逗号 分隔
- 当参数中有空格时,用单引号括住参数
- 当参数中有逗号时,可以用 m 语法修改分隔符为其他字符,如
m:^:xxx
表示 xxx 中用 ^ 表示分隔符
调用 API
在拨号计划中可以调用一些 API,使用方法和引用变量一样,不过变量为函数,如 ${version()}
获取版本。 ${expr(1+1)}
计算一个表达式。
调试技巧
一般流程:发现问题 - 定位问题 - 分析问题 - 解决问题。
- 拨打内置的 APP 看现象缩小定位范围,如 echo(9196)、playback(9664)
- 通过
uuid_debug_media <uuid> both on
打开媒体调试开关(uuid 通过show channels
查看) - 通过
console loglevel debug
打开 FS debug 日志,检查消息的到达 - 检查日志中挂机原因(Hangup Cause),一般 CALL_REJECTED 表示呼叫拒绝,可能是认证错误,USER_NOT_REGISTERED 说明对方未注册。
- 经过网关通过
bgapi originate sofia/gateway/gw1/Bob &echo
“分段”查看网关后半段通信,bgapi 在后台执行线程,不阻塞控制台 -
sofia profile external siptrace on
打开 external 的 profile siptrace,sofia global siptrace on
打开所有 Profile 的。 -
sofia loglevel all 9
打开 Sofia 底层协议栈的调试,换为 0 为关闭 - 使用 fs_cli 进行如上操作,可以随时关闭打开调试信息、固定消息内容等操作,而不影响主进程
- 抓包工具:tcpdump、wireshark、tshark(wireshark 的命令行版,参考)、ngrep(类似于 grep 在文本界面中方便),pcapsipdump(能将不同通话 IP 包存到不同的文件中,在通话量大时很好用)
-
tcpdump -nq -s 0 -A -vvv -i eth0 -w abc.pcap port 5060
,-n、-q 表示不进行域名翻译及减少输出内容,-s 0 表示不限制包长,-A 表示以易读的 ASCII 方式输出,-v 表示详细程度,v 越多越详细,-i eth0 表示指定网卡 etho,-w 为写出到指定文件。对于搜索条件udp
指定抓 udp 包,可以分析 RTP 流;host 1.2.3.4
过滤 IP 地址;与用 and,或用 or。
originate 命令详解
originate # 使用 FS 发起呼叫(默认主叫号码是 000000000)
-USAGE: <call url> <exten>|&<application_name>(<app_args>) [<dialplan>] [<context>] [<cid_name>] [<cid_num>] [<timeout_sec>]
- 同振,同时呼叫多个用户,某个接听另一个自动挂断:
originate user/1000,user/1001 &echo
- 顺振,呼叫某一个号码,如果失败呼叫下一个:
originate user/1000|user/1001 &echo
- call url 即呼叫字符串,格式
类型/参数/参数
,如user/1000
,类型表示 Channel 的类型,不存在的类型会报错ERR CHAN_NOT_IMPLEMENTED
- 第二个参数是分机号(exten)或者 &app。若是分机号时,会转入 Dialplan 去路由,路由的目的是查找到 enten
-
<dialplan>
第三个参数是 Dialplan 的类型,如果不设置默认是 XML -
<context>
是 Dialplan 的 Context,对于 inlineDialplan 可忽略 -
<cid_name> <cid_num>
是主叫名称 和 主叫号码(CallID Number),用于界面及 FROM 头中显示 -
<timeout_sec>
是不回 100 Trying 的超时时间。 - originate 命令是阻塞的,可以在前方加上使用
bgapi
转为后台执行。若已经发生阻塞:- 可以用 fs_cli 执行
show channels
查到 uuid,然后用uuid_kill <uuid>
结束此呼叫。 - 或用
hupup
挂断所有电话。
- 可以用 fs_cli 执行
- 命令中使用通道变量
originate {var1=1}{var2=2}user/1000 &echo
,细节略。 - 忽略早期媒体的影响用
originate {ignore_early_media=true}sofia/gateway/gw/13800000000 &playback(/a.wav)
,因为 originate 命令是受到媒体指令就返回,如 183 或 200。由于软电话会回复 180 而不是 183,183 相当于携带媒体的 180,而在 PSTN 场景下一般都是回复 183 的。加入此参数后可以忽略 Early Media 对我们呼叫的影响。 -
originate user/1000 &bridge(user/1001)
流程:建立 channel 然后呼叫 user/1000,1000 接听后执行 bridge,bridge 再建立一个 channel 并呼叫 user/1001。此时双方在信令上建立了桥接关系;在 1001 接听后,媒体也会被桥接起来,进入正常通话。
实际上,bridge 和 oiginate 底层用的同一个函数实现,伪代码是
originate(session, new_session, dial_str)
差别在于 originate 调用函数是 session 字段为 null。
- 上方用 bridge 的逻辑中,是先拨通 a-leg 后再建立 b-leg 的,如果 b-leg 回的是 183 则媒体流正常转发到 a-leg 中,若为 180 则由于无媒体流 a-leg 听不到任何声音,为了解决这个问题,可以让 FS 回收一个假的回铃音,方法:1)设置
{transfer_ringback=local_stream://moh}
变量,此变量控制在 b-leg 回复 180 时开始播放声音。2)在 1 的基础上再加{instant_ringback=true}
变量,可以让 bridge 立即播放回铃音,而不等待 180。
呼叫的完整逻辑
假设 1000 呼叫 1001
- 1000 发送 INVITE 到达 mod_sofia 的 inernal Profile(
conf/sip_profiles/internal.xml
,通过 5060 都是先到这里) - FS 收到后立即返回 100,由于 internal.xml 配置了
auth-calls=true
所以会进行鉴权(使用 Digest Auth),一般首次会鉴权失败,所以回复 401 - UAC(即1000)重新发送带鉴权信息的 INVITE 到 internal Profile(UAS)
- UAS 收到后,将鉴权信息提交到上层 FS 代码,FS 通过 Directory(用户目录)找到相应用户信息,并根据配置的密码鉴权(失败 403)
- 鉴权通过,FS 通过 user_context 配置项找到应该进入哪个路由。如 1000.xml 中配的 user_context 为 default,则进入
conf/dialplan/default.xml
。(已经进入路由阶段) - Dialplan 会查到 1001 用户,找到匹配的 Extension 执行里面的 action,action 有 bridge 命令及 data,所以执行
bridge <data>
,此时会再次查 Directory(用户目录)找到 1001 的配置信息 - 找到 1001 的 dial-string 配置项,此项会配置在
conf/directory/default.xml
中(由于此域下所有用户的规则一样,所以放在这里),其中 sofia_contact 这个 API 会查找数据库,找到 1001 的实际注册 Contact 地址,返回真正的呼叫字符串。(如通过sofia_contact 1000
可快速查看sofia/internal/sip:1000@10.0.2.15:63757;ob
) - 当找到 dial-string 后,FS 会另外启动一个会话给 1001 发送 INVITE 请求
- 如果 1001 摘机(接听),则 1001 向 FS 回送 200 OK,FS 再向 1000 回送 200 OK,通话开始。
总结主流程:1000 Invite -> sofia profile -> FS context -> dialplan -> action (bridge 1001) -> invite 1001。
external.xml 配置(5080 会走这里)
auth-calls 为 false,所以不进行鉴权
context=public 其中也没有每一个 user 上配置的 user_context(internal 中也有 context=public 但走 user_context)
FS GUI
图形化界面实现一般有两种方式:
- 通过界面提供的操作方式修改 FS XML 配置文件,及 reloadxml 等操作使之生效
- 通过 http 服务器给 FS 提供 XML
如:
- FusionPBX
- blue.box
- FreeSWITCH Portal