freeswitch 权威指南 --- 高级篇

官网文档:https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/

关于 freeswitch 的公开教程:https://zhuanlan.zhihu.com/p/451981734

内容来自 《FreeSWITCH 权威指南》:目录:https://juejin.cn/post/7020580794829635591

代码下载:https://book.dujinfang.com/download.html

1、客户端和开发接口

基于 FreeSWITCH 的开发一般有四种方式:后两种方法需要熟悉FreeSWITCH的源代码

    1. 使用嵌入式脚本在 FreeSWITCH 内部灵活地控制呼叫流程,以及通过共享数据库或简单API 等与现有业务系统集成;
    1. 使用 Event Socket 在外部程序中控制呼叫流程,控制更灵活,与其他系统更容易集成;
    1. 直接在FreeSWITCH中修改现有代码,或通过添加新模块以扩展FreeSWITCH的现有功能,并能结合前两种方式创建更强大的应用;
    1. 将FreeSWITCH的底层库 libfreeswitch 嵌入到其他系统中,这样被嵌入的用系统中就瞬间增加了 FreeSWITCH 的全部功能,这种方法用得比较少。

FreeSWITCH 默认使用 XML Dialplan 配置呼叫流程。XML 文件描述性很强,因而也可以描述比较复杂的呼叫流程。但在一些比较高级的 IVR 应用和呼叫交互流程中,仅靠简单的 XML 的配置很难满足要求。因而还需要更灵活、更强大的解决方案。除 XML Dialplan外,FreeSWITCH 支持使用嵌入式的脚本语言控制呼叫流程。不仅可以用它们写出灵活多样的 IVR,给用户带来更好的体验,

在内部,FreeSWITCH 通过使用 swig 工具来支持多种开发语言。简单来讲,swig 是一个包装工具(Wrapper),它可以将 FreeSWITCH 用C语言实现的一些功能包装成各种其他语言的接口、类或者方法,这样就可以在使用其他语言时以原生的方式调用。现在已知支持的语言有 C、Perl、PHP、Python、Ruby、Lua、Java、Tcl 以及由 Managed 支持的 .Net 平台语言,如 C#、VB.NET等。FreeSWITCH 源代码中的 swig 脚本和程序已被转换成各种语言的接口了,因而开发者不需要安装 swig 工具就可以使用。不过 JavaScript 语言比较特殊一些,对它的支持是基于Google 的V8库,在 mod_v8 模块中实现的。

理论上讲,可以使用任何语言,只要该语言支持 TCP Socket 就行。

SWIG 官网 :http://www.swig.org

SWIG 简介、安装、使用方法:https://blog.csdn.net/qq_41185868/article/details/103558686

SWIG:Python 调用 C++:https://zhuanlan.zhihu.com/p/462193340

XML Dialplan 已经体现了其非凡的配置能力,它配合 FreeSWITCH 提供的各种 App 使用时,也可以认为是一种脚本。当然,毕竟 XML 是一种描述语言,功能还比较有限,为了扩展其功能,FreeSWITCH 通过嵌入其他语言的解析器支持很多流行的编程语言。这些语言一般都能提供if...else 判断及等循环跳转控制等,因而控制呼叫流程更加灵活。

Lua ELS 开发

FreeSWITCH Lua脚本:https://www.cnblogs.com/garvenc/p/freeswitch_learning_lua.html

Lua 因其优雅的语法及小巧的身段受到很多开发者的青睐,尤其是游戏开发人员。FreeSWITCH 中 Lua 模块是默认加载的。在所有嵌入式脚本语言中,Lua 是最值得推荐的语言。首先它非常轻量级,mod_lua.so 经过减肥(Strip)后只有272KB;另外,它的语法相对的简单。有人做过对比,在嵌入式的脚本语言里,如果Python得2分,Perl得4分,JavaScript得5,则Lua语言可得10分。另外,Lua模块的文档也是最全的。

Lua 官网:https://www.lua.org/

Lua 下载:https://www.lua.org/download.html

Lua 教程:https://www.runoob.com/lua/lua-tutorial.html

Lua与JS(JavaScript的缩写,下同)有很多相似的地方,简述如下。

  • 变量无需声明。 Lua与JS都是"弱"类型的语言(不像C),不需要事先声明变量的类型。
  • 区分大小写。 Lua和JS都是区分大小写的。true和false分别代表布尔类型的真和假,
  • 函数可以接受个数不定的参数。 与JS类似,在Lua中,与已经声明的函数参数个数相比,实际传递的参数个数可多可少
  • 哈希可以用方括号或点方式引用。
  • 数字区别不大。 在JS和Lua中,整数和浮点数是没有区别是的。它们在内部都是以浮点数表示。在Lua中,所有的数字类型都是number类型。
  • 分号是可选的。
  • 默认全局变量。 在JS中,如果用var声明一个变量并赋值,则它是本地变量;如果不用var声明,默认就是全局的
  • 使用双引号和单引号表示字符串。
  • 函数是一等公民。 在JS和Lua中,函数是一等公民,这意味着,你可以将它赋值给一个变量,将它作为参数传递,或者直接加上括号进行调用。
  • 函数都是闭包。 在JS和Lua中,函数都是闭包。简单来说,这意味着函数可以随时访问该函数在定义时可以访问的本地变量,尽管在以后调用时这些本地变量逻辑上已经"失效"了。

将电话路由到Lua脚本:originate user/1000 &lua(test.lua)

lua是一个App,它的参数就是脚本的名字,脚本的默认路径在安装路径的scripts目录下,当然你也可以指定一个绝对路径,如/tmp/test.lua。在Dialplan XML中,使用下列配置便可将进入Dialplan的电话(Channel)交给Lua脚本接管。<action application="lua" data="test.lua"/>

除此之外,也可以直接使用uuid_transfer命令直接配合inline Dialplan将一个Channel路由到Lua脚本,如:uuid_transfer <uuid> lua:/tmp/test.lua inline

总之,这里的Lua是一个标准的App,在任何可以使用App的地方都可以使用它(如上面介绍的各种场景,以及后面要介绍的Event Socket等。甚至在Lua脚本中也可以再次使用lua App来调用下一个Lua脚本)。

Session 相关函数

在Lua环境中,FreeSWITCH会自动生成一个session对象(实际上是一个Table),因而可以在Lua脚本中使用Lua类似面向对象的语法特性编程,如以下脚本放播放欢迎声音

Lua 复制代码
--
session:answer()
--
session:sleep(1000)
--
session:streamFile("/tmp/hello-lua.wav")
--
session:hangup()

大部分与Session有关的函数都是跟FreeSWITCH中的App是一一对应的,如上面的answer、hangup等。有一点要特别说明:streamFile对应playback这个App。如果在Lua中没有对应的函数,也可以通过session:execute()函数来执行相关的App,如session:execute("playback","/tmp/sound.wav") 与 session:streamFile("/tmp/sound.wav") 是等价的

需要注意,Lua脚本执行完毕后默认会挂断电话,所以上面的Hello Lua例子中不需要明确的session:hangup()。如果想在Lua脚本执行完毕后继续执行Dialplan中的后续流程,则需要在脚本开始处先设置不要自动挂机,语法如下:session:setAutoHangup(false)

例如下列场景,test.lua执行完毕后(假设没有session:hangup(),主叫也没有挂机),如果没有setAutoHangup(false),则后续的playback动作得不到执行。

XML 复制代码
<extension name="test-lua">
	<condition field="destination_number" expression="^1234$">
		<action application="answer"/>
		<action application="lua" data="test.lua"/>
		<action application="playback" data="lua-script-complete.wav"/>
	</condition>
</extension>

与 Ssession 相关的几个常用的函数:

  • getVariable。 取得变量的值
  • getUUID。 取得当前Session的UUID
    local uuid = session:get_uuid(]); 等价于 local uuid = session:getVariable("uuid")
  • setVariable。 设置通道变量,等价于Dialplan App里的set:session:setVariable("varname", "varval")
  • hangup。 挂断当前通话。session:hangup(); 或者 session:hangup("USER_BUSY")
  • ready。 检查Session是否可正常使用,如果已经挂机就会返回false。在写脚本时,如果有循环,一定需要经常检测session:ready()是否为true,否则Session挂机后Lua脚本可能仍然在死循环地运行。
  • streamFile: 放音,相当于Dialplan App里的playback。session:streamFile("/tmp/test.wav")
  • recordFile: 录音,相当于Dialplan App里的record,参数是:file_name [,max_len_secs] [,silence_threshold] [,silence_secs]
    其中,各参数含义如下:
    file_name: 录音文件名。
    max_len_secs: 录音最长的秒数。
    silence_threashold: 一个声音阈值,如果声音小于该值,就认为是静音。
    silence_secs: 如果静音时长大于一定秒数,则停止录音。
    例如,以下函数将对当前的Channel录音,并存放到/tmp/test_record.wav中:
    session:recordFile("/tmp/test_record.wav")
  • read。 类似于Dialplan App中的read,用于播放一个声音并获取DTMF。它的5个参数与read含义相同:<min digits><max digits><file to play><inter-digit timeout><terminators>
    digits = session:read(15, 18, "/tmp/input-id-card.wav", "5000", "#");
    session:("log", "INFO ID Card Number: ".. digits .."\n"); 可以发现Lua中的read比Dialplan App中的read少了一个参数。由于session:read()能返回值,因此那个参数就不需要了,实际收到的 DTMF 会返回到本例的 digits 变量中。
  • playAndGetDigits。 与Dialplan App中的play_and_get_digits类似,它的参数格式是:<min_digits>, <max_digits>, <max_attempts>, <timeout>, <terminators>,<prompt_audio_files>,<input_error_audio_files>,<digit_regex>, [variable_name], [digit_timeout], [transfer_on_failure]) 其中,大部分参数都很直观,也跟play_and_get_digits中类似。其中timeout是收齐所有号的超时值,而digit_timeout是允许的两次按键之音的时间间隔最大值,最后transfer_on_failure指明如果失败后是否转到Dialplan中的一个Extension上去,它的格式应该是一个Dialplan三要素的格式串,如"failed XML dialplan"。
    重写如下:
    digits = session:playAndGetDigits(15, 18, 3, 10000, "#",
    "/tmp/input-id-card.wav", "/tmp/invalid_num.wav",
    "^\\d{15}|\\d{17}[0-9\\*]$")
    session:execute("log", "INFO ID Card Number: ".. digits .."\n");
  • setInputCallback。 在放音或录音时,用户按下的DTMF可以用于触发一些功能。所以在这些状态下,Lua支持如果收到DTMF等外部输入时,则调用相关的回调函数。setInputCallback的作用就是设置(安装)一个回调函数。

更多与Session相关的函数可以参考相关的wiki文档:http://wiki.freeswitch.org/wiki/Mod_lua

非Session函数、独立的Lua脚本

Lua脚本中也可以使用跟Session不相关的函数,最典型的是freeswitch.consoleLog(),其用于输出日志,如:freeswitch.consoleLog("NOTICE", "Hello lua log!\n")

另外一个是freeswitch.API(),允许你在Lua中执行任意API,如:a.lua

api = freeswitch.API()

reply = api:execute("version", "")

freeswitch.consoleLog("INFO", "Got reply:\n\n" .. reply .. "\n")

上面 Lua 脚本可以直接在 FreeSWITCH 控制台上执行:freeswitch> lua /tmp/a.lua

除此之外,其他的非 Session 函数还有 freeswitch.bridge()、freeswitch.email() 等,

非Session函数一般运行在独立的Lua脚本中。独立的Lua脚本可以直接在控制台终端上执行(使用luarun),这种脚本大部分可用于执行一些非Session相关的功能(因为这里面没有Session)。读到这里读者已经了解到了,Lua是一个App,而luarun是一个API。

上面的 a.lua 就是一个典型的可独立运行的Lua脚本。独立运行的Lua脚本跟在Dialplan中用Lua App运行的不同,前者不会自动获得一个session对象(Table)。当然,独立运行的脚本也可以自行创建session对象。

Event 相关函数

FreeSWITCH使用事件机制进行异步通信。在Lua脚本中,可以"生产"事件,也可以"消费"事件,

FreeSWITCH的事件也跟一个SIP消息类似,它包含一些事件头(Header)和可选的事件正文(Body)。在FreeSWITCH内部使用C语言结构体表示,可以序列化成类似SIP消息的简单文本格式(Plain)、JSON或XML。

下面我们来看一下与事件相关的函数。

  • freeswitch.Event。 初始化一个事件,该事件类型需要在switch_event_types_t枚举类型中有定义,它是在switch_types.h中定义的。如果使用了未定义过的名字,则统一为MESSAGE。下面的例子初始化一个主事件:event = freeswitch.Event("MESSAGE_WAITING") 也可以初始化一个CUSTOM事件,其中第二个参数可以是任意字符串,它将作为事件中的EventSubclass,如:event = freeswitch.Event("CUSTOM", "freeswitch:book")

  • event:addHeader。 给事件增加一个事件头,如: event:addHeader("MWI-Messages-Waiting", "no")

    event:addHeader("MWI-Message-Account", "sip:[email protected]")

    event:addHeader("Sofia-Profile", "internal")

  • event:fire。 产生(生产)事件,如:event:fire()

  • event:addBody。 给事件增加一个可选的正文,并使用Content-Type头标志正文的类型,如:

    event:addHeader("Content-Type", "text/plain")

    event:addBody("Hello FreeSWITCH")

  • event:delHeader。 从事件中删除一个头域,下面的例子可以替换from头:

    event:delHeader("from")

    event:addHeader("from", "[email protected]")

  • event:getHeader。 在收到一个事件后,可以取得其头域的值,如:

    event:getHeader("from")

    event:getHeader("Caller-Caller-ID-Name")

  • event:getBody。 取得Body的值(如果有的话),如:event:getBody()

  • event:getType。 取得事件的类型(名字),以下两种方法是等价的:

    event:getType()

    event:getHeader("Event-Name")

  • event:serialize。 将事件序列化成可读的形式(字符串),支持plain text、JSON、XML三种类型,如:

    event:serialize()

    event:serialize("json")

    event:serialize("xml")

完整示例

Lua 复制代码
function log(k, v)
    if not v then v = "[NIL]" end
    freeswitch.consoleLog("INFO", k .. ": " .. v .. "\n")
end
event = freeswitch.Event("CUSTOM", "freeswitch:book")
event:addHeader("Author", "Seven Du")
event:addHeader("Content-Type", "text/plain")
event:addBody("FreeSWITCH: The Definitive Guide")
type = event:getType()
author = event:getHeader("Author")
text=event:serialize()
json=event:serialize("json")
xml=event:serialize("XML")
log("type", type)
log("author", author)
log("text", text)
log("json", json)
log("xml", xml)
event:fire()
log("MSG", "Event Fired")

将上述内容保存到/tmp/event.lua中,执行结果如下

freeswitch>lua /tmp/event.lua

INFO\] switch_cpp.cpp:1288 type: CUSTOM \[INFO\] switch_cpp.cpp:1288 author: Seven Du \[INFO\] switch_cpp.cpp:1288 text: 'Event-Name: CUSTOM ... Event-Subclass: freeswitch%3Abook Author: Seven%20Du Content-Type: text/plainContent-Length: 32 FreeSWITCH: The Definitive Guide' \[INFO\] switch_cpp.cpp:1288 json: { "Event-Name": "CUSTOM", "Core-UUID": "bc647e68-47de-407f-b32a-d9bdf5c25786", "Event-Sequence": "5000", "Event-Subclass": "freeswitch:book", "Author": "Seven Du", "Content-Type": "text/plain", "Content-Length": "32", "_body": "FreeSWITCH: The Definitive Guide" } \[INFO\] switch_cpp.cpp:1288 xml: \ \ \CUSTOM\ \bc647e68-47de-407f-b32a-d9bdf5c25786\ \5000\ \freeswitch%3Abook\ \Seven%20Du\ \text/plain\ \ \32\ \FreeSWITCH: The Definitive Guide\ \ \[INFO\] switch_cpp.cpp:1288 MSG: Event Fired #### Chat 相关函数 FreeSWITCH通过mod_sms支持文本消息。一个文本消息与一个Session类似,FreeSWITCH收到文本消息后将执行Chatplan,然后在Chatplan中可以执行Lua脚本。在Chatplan中的Lua脚本会自动获得一个message对象,该对象的内部表示跟event是一样的。因而与Event相关的函数,如addHeader、delHeader、addBody、serialize等,都是可以用的。除此之外,还有一个chat_execute函数,它可以执行mod_sms中支持的以下动作。 * fire: 产生一个MESSAGE事件。 * send: 发送消息。 * reply: 回复消息。 * set: 设置变量。 * info: 显示信息。 * stop: 停止消息路由。 * system: 调用system函数执行系统调用。 下面的 Lua 脚本可以在 Chatplan 中执行,收到消息后先打印出来,然后修改目的号码和主机,并发送出去。 ```Lua area_code = "010" to_host = "192.168.0.2" function log(k, v) if not v then v = "[NIL]" end freeswitch.consoleLog("INFO", k .. ": " .. v .. "\n") end log("Message", message:serialize()) to_user = message:getHeader("to") message:delHeader("to") message:addHeader("to", "internal/sip:" .. area_code .. to_user .. "@" .. to_host) message:delHeader("to_host") message:addHeader("to_host", to_host) log("New Message", message:serialize()) message:chat_execute("send") ``` 与在 Dialplan 中类似,在 Chatplan 中可以用以下方法调用 Lua 脚本,如:\ #### LUA 拨号计划 拨号计划除XML外还有很多种,其中一种就是LUA拨号计划,即可以通过Lua脚本提供Lua风格的Dialplan。 下面的脚本入进路由阶段时将查询并生成一个Dialplan,FreeSWITCH接下来执行该Dialplan,打印一些Log,并执行answer和playback。 ```Lua function log(k, v) if not v then v = "[NIL]" end freeswitch.consoleLog("INFO", k .. ": " .. v .. "\n") end cid = session:getVariable("caller_id_number") dest = session:getVariable("destination_number") log("From Lua DP: cid: ", cid) log("From Lua DP: dest: ", dest) -- Some Bussinuss logic here ACTIONS = { {"log", "INFO I'm From Lua Dialplan"}, {"log", "INFO Hello FreeSWITCH, Playing MOH ..."}, "answer", {"playback", "local_stream://moh"} } ``` 首先,跟在Dialplan中执行Lua脚本类似,这里也有一个session对象,可以执行所有与Session相关的函数,如获取主被叫号码(cid、dest)等。获取到相关信息后可以通过Lua相关的函数,如判断日期时间、连接数据库检查主被叫号码合法性及黑白名单等(这里我们省略了跟逻辑相关的操作)。最后,生成一个Lua Table。该Table的名字必须是ACTIONS。其成员可以是一个字符串或一个子Table。FreeSWITCH在ROUTING阶段获得该Table后,便可以进入EXECUTING阶段执行ACTIONS中定义的一系列动作(Action)。 Dialplan 有三个要素:Extension、Context和Dialplan的名字,在Lua Dialplan中,Dialplan的名字当然是LUA,其 Context 就是 Lua 脚本的路径。把上面的脚本存为/tmp/db.lua,使用originate测试:freeswitch\> originate user/1000 test LUA /tmp/dp.lua originate命令首先呼叫user/1000,当它接听后,即转入LUA Dialplan中的/tmp/dp.lua这一Context进行路由,其对应的extension是test。其执行结果也很直观。用originate回呼是一种快速的测试方法,除此之外也可以尝试在 XML Dialplan 中转入 LUA Dialplan,如: > \ 或直接修改 Profile,让电话在呼入时直接进入 LUA Dialplan。如将 internal.xml 中的: > \ > > \ > > 修改为: > > \ > > \ 执行 sofia profile internal rescan 使之生效。如果是注册用户拨打的话,还需要修改User Directory中的user_context(因为它的优先级比Profile中的context要高),如在1000.xml中: > \ #### 连接数据库 ### JavaScript ELS 开发 JavaScript 是 Web 浏览器上最主流的编程语言,它最早用于配合HTML渲染页面,由于node.js 的发展使它在服务器端的应用也发扬光大。它遵循EMCAScript标准。FreeSWITCH 通过加载 mod_v8 模块可以使用 JavaScript 解析器,该模块基于Google的V8 JavaScript库。 在FreeSWITCH中,二者除了语法不同外,其用法类似。如使用JavaScript(它是一个App)执行一个Session相关的脚本,或jsrun(它是一个API)执行一个非Session相关的脚本。 上面的 Lua 脚本可以用 JavaScript 重写如下: ```javascript session.answer(); session.sleep(1000); session.streamFile("/tmp/hello-js.wav"); session.hangup(); ``` 在XML Dialplan中使用如下配置将来话交给上述脚本处理(假设文件名为test.js): > \ 在源代码目录中的 scripts/javascript目录下有几个JavaScript应用的例子,可自行研究。 官网示例: [Javascript Examples \| FreeSWITCH Documentation (signalwire.com)](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/JavaScript/Javascript-Examples/ "Javascript Examples | FreeSWITCH Documentation (signalwire.com)") 示例: * [Javascript Example - DISA (direct inward system access)](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/JavaScript/Javascript-Examples/9634479 "Javascript Example - DISA (direct inward system access)") * [JavaScript Example - Say IVR Menu](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/JavaScript/Javascript-Examples/JavaScript-Example---Say-IVR-Menu/ "JavaScript Example - Say IVR Menu") * [JavaScript Example - Session in Hangup Hook](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/JavaScript/Javascript-Examples/JavaScript-Example---Session-in-Hangup-Hook_7766254 "JavaScript Example - Session in Hangup Hook") * [JavaScript Example - Test Tones](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/JavaScript/Javascript-Examples/JavaScript-Example---Test-Tones_7144682 "JavaScript Example - Test Tones") * [JavaScript Example - cidspoof](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/JavaScript/Javascript-Examples/JavaScript-Example---cidspoof_7144661 "JavaScript Example - cidspoof") * [JavaScript Example - cnam](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/JavaScript/Javascript-Examples/JavaScript-Example---cnam_7144664 "JavaScript Example - cnam") * [JavaScript Example - dbIVRmenu](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/JavaScript/Javascript-Examples/JavaScript-Example---dbIVRmenu_7144656 "JavaScript Example - dbIVRmenu") * [JavaScript example - XML](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/JavaScript/Javascript-Examples/JavaScript-example---XML_7144136 "JavaScript example - XML") * [Javascript Example - AfterHoursIVR](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/JavaScript/Javascript-Examples/Javascript-Example---AfterHoursIVR_7144104 "Javascript Example - AfterHoursIVR") * [Javascript Example - Answering Machine](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/JavaScript/Javascript-Examples/Javascript-Example---Answering-Machine_7143487 "Javascript Example - Answering Machine") * [Javascript Example - Collect Account Number](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/JavaScript/Javascript-Examples/Javascript-Example---Collect-Account-Number_7144593 "Javascript Example - Collect Account Number") * [Javascript Example - DTMF Callback](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/JavaScript/Javascript-Examples/Javascript-Example---DTMF-Callback_7144571 "Javascript Example - DTMF Callback") * [Javascript Example - FollowMe](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/JavaScript/Javascript-Examples/Javascript-Example---FollowMe_9634482 "Javascript Example - FollowMe") * [Javascript Example - HelloWorld](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/JavaScript/Javascript-Examples/Javascript-Example---HelloWorld_7144139 "Javascript Example - HelloWorld") * [Javascript Example - Intercom](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/JavaScript/Javascript-Examples/Javascript-Example---Intercom_7144108 "Javascript Example - Intercom") * [Javascript Example - Prompt For Digits](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/JavaScript/Javascript-Examples/Javascript-Example---Prompt-For-Digits_7144118 "Javascript Example - Prompt For Digits") * [Javascript Example - set hook](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/JavaScript/Javascript-Examples/Javascript-Example---set-hook_9634690 "Javascript Example - set hook") * [Sched hangup javascript example](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/JavaScript/Javascript-Examples/Sched-hangup-javascript-example_13174181 "Sched hangup javascript example") * [Session getVariable](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/JavaScript/Javascript-Examples/Session-getVariable_13174000 "Session getVariable") ### Python ELS 开发 freeswitch 在使用 python 做业务开发时,有2种接入方式, * 一种是 mod_python 模块。freeswitch 源码安装时,默认不安装 mod_python 模块,需要进入源代码目录中安装 Python 模块。freeswitch python模块:[https://zhuanlan.zhihu.com/p/410634433](https://zhuanlan.zhihu.com/p/410634433 "https://zhuanlan.zhihu.com/p/410634433") * 一种是 ESL 接口。通过 socket 套接字与 freeswitch 进行命令交互,包括发送命令、命令响应和事件回调等,类似于在外部增加一个第三方模块控制 fs 行为。**pip install python-ESL** #### python-ESL 库 官网文档:[https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Python-ESL/](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Python-ESL/ "https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Python-ESL/") > 在 FreeSWITCH 源目录中,更改为 libs/esl 并运行: > > make pymod > > make pymod-install > > 这会将 ESL 模块安装到 python site-packages 文件夹中。如果想手动安装它或将其保留在本地,你仍然必须运行 make pymod 命令来编译它,随后可以将 libs/esl/_ESL.so 和 libs/esl/ESL.py 复制到你选择的文件夹中。 示例 1: ```python #!/usr/bin/env python ''' events.py - subscribe to all events and print them to stdout ''' import ESL con = ESL.ESLconnection('localhost', '8021', 'ClueCon') if con.connected(): con.events('plain', 'all') while 1: e = con.recvEvent() if e: print e.serialize() ``` 示例 2: ```python #!/usr/bin/env python ''' server.py ''' import SocketServer import ESL class ESLRequestHandler(SocketServer.BaseRequestHandler): def setup(self): print self.client_address, 'connected!' fd = self.request.fileno() print fd con = ESL.ESLconnection(fd) print 'Connected: ', con.connected() if con.connected(): info = con.getInfo() uuid = info.getHeader("unique-id") print uuid con.execute("answer", "", uuid) con.execute("playback", "/ram/swimp.raw", uuid); # server host is a tuple ('host', port) server = SocketServer.ThreadingTCPServer(('', 8040), ESLRequestHandler) server.serve_forever() ``` 示例 3: ```python #!/usr/bin/env python ''' single_command.py - execute a single command over ESL ''' from optparse import OptionParser import sys import ESL def main(argv): parser = OptionParser() parser.add_option('-a', '--auth', dest='auth', default='ClueCon', help='ESL password') parser.add_option('-s', '--server', dest='server', default='127.0.0.1', help='FreeSWITCH server IP address') parser.add_option('-p', '--port', dest='port', default='8021', help='FreeSWITCH server event socket port') parser.add_option('-c', '--command', dest='command', default='status', help='command to run, surround multi-word commands in ""s') (options, args) = parser.parse_args() con = ESL.ESLconnection(options.server, options.port, options.auth) if not con.connected(): print 'Not Connected' sys.exit(2) # Run command e = con.api(options.command) if e: print e.getBody() if __name__ == '__main__': main(sys.argv[1:]) ``` #### greenswitch 库 > python-ESL 好久没更新,可以使用 greenswitch:[https://github.com/EvoluxBR/greenswitch](https://github.com/EvoluxBR/greenswitch "https://github.com/EvoluxBR/greenswitch") > > greenswitch 是基于 Gevent 开发,并且是经过实战验证的 FreeSWITCH Event Socket Protocol 客户端。 完全支持 Python3! > > 安装:**pip install greenswitch** **入站套接字模式** ```python import greenswitch fs = greenswitch.InboundESL(host='127.0.0.1', port=8021, password='ClueCon') fs.connect() r = fs.send('api list_users') print(r.data) ``` **出站套接字模式** 出站是通过同步和异步支持实现的。主要思想是创建一个应用程序,该应用程序将被调用,将 OutboundSession 作为参数传递。此 OutboundSession 表示由 ESL 连接处理的调用。基本功能已经实现: * playback 回放 * play_and_get_digits * hangup 挂断 * park 公园 * uuid_kill * answer 答 * sleep 睡 使用当前的 api,很容易混合同步和异步操作,例如: play_and_get_digits方法将在块模式下返回按下的 DTMF 数字,这意味着只要您在 Python 代码中调用该方法,执行流就会阻塞并等待应用程序结束,只有在结束应用程序后才能返回下一行。但是在获取数字后,如果您需要使用外部系统,例如将其发布到外部 API,您可以在 API 调用完成时让调用者听到 MOH,您可以使用 block=False、playback('my_moh.wav', block=False) 调用 playback 方法,在您的 API 结束后,我们需要告诉 FreeSWITCH 停止播放文件并返回调用控制权, 为此,我们可以使用uuid_kill方法。 ```python ''' Add a extension on your dialplan to bound the outbound socket on FS channel as example below Or see the complete doc on https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket ''' import gevent import greenswitch import logging logging.basicConfig(level=logging.DEBUG) class MyApplication(object): def __init__(self, session): self.session = session def run(self): """ Main function that is called when a call comes in. """ try: self.handle_call() except: logging.exception('Exception raised when handling call') self.session.stop() def handle_call(self): # We want to receive events related to this call # They are also needed to know when an application is done running # for example playback self.session.myevents() print("myevents") # Send me all the events related to this call even if the call is already # hangup self.session.linger() print("linger") self.session.answer() print("answer") gevent.sleep(1) print("sleep") # Now block until the end of the file. pass block=False to # return immediately. self.session.playback('ivr/ivr-welcome') print("welcome") # blocks until the caller presses a digit, see response_timeout and take # the audio length in consideration when choosing this number digit = self.session.play_and_get_digits('1', '1', '3', '5000', '#', 'conference/conf-pin.wav', 'invalid.wav', 'test', '\d', '1000', "''", block=True, response_timeout=5) print("User typed: %s" % digit) # Start music on hold in background without blocking code execution # block=False makes the playback function return immediately. self.session.playback('local_stream://default', block=False) print("moh") # Now we can do a long task, for example, processing a payment, # consuming an APIs or even some database query to find our customer :) gevent.sleep(5) print("sleep 5") # We finished processing, stop the music on hold and do whatever you want # Note uuid_break is a general API and requires full permission self.session.uuid_break() print("break") # Bye caller self.session.hangup() print("hangup") # Close the socket so freeswitch can leave us alone self.session.stop() server = greenswitch.OutboundESLServer( bind_address='0.0.0.0', bind_port=5000, application=MyApplication, max_connections=5 ) server.listen() ``` ## 2、嵌入式(Lua) 及 HTTP开发 ### 官网 lua 示例 [Lua examples \| FreeSWITCH Documentation (signalwire.com)](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Lua-API-Reference/Lua-examples/ "Lua examples | FreeSWITCH Documentation (signalwire.com)") 示例: * [Lua: send SMS via Flowroute when voicemail is received](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Lua-API-Reference/Lua-examples/10683223 "Lua: send SMS via Flowroute when voicemail is received") * [Lua ASR TTS Directory example](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Lua-API-Reference/Lua-examples/Lua-ASR-TTS-Directory-example_1049011 "Lua ASR TTS Directory example") * [Lua DISA Example](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Lua-API-Reference/Lua-examples/Lua-DISA-Example_7144309 "Lua DISA Example") * [Lua Database agent login example](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Lua-API-Reference/Lua-examples/Lua-Database-agent-login-example_3965212 "Lua Database agent login example") * [Lua Directory example](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Lua-API-Reference/Lua-examples/Lua-Directory-example_1049009 "Lua Directory example") * [Lua Fakecall responder example](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Lua-API-Reference/Lua-examples/Lua-Fakecall-responder-example_3965229 "Lua Fakecall responder example") * [Lua Group Pickup example](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Lua-API-Reference/Lua-examples/Lua-Group-Pickup-example_3965238 "Lua Group Pickup example") * [Lua IVR Menu Example](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Lua-API-Reference/Lua-examples/Lua-IVR-Menu-Example_16352016 "Lua IVR Menu Example") * [Lua Intercom example](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Lua-API-Reference/Lua-examples/Lua-Intercom-example_3965150 "Lua Intercom example") * [Lua Mail Call example](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Lua-API-Reference/Lua-examples/Lua-Mail-Call-example_7144597 "Lua Mail Call example") * [Lua Mail on NoAnswer example](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Lua-API-Reference/Lua-examples/Lua-Mail-on-NoAnswer-example_7144600 "Lua Mail on NoAnswer example") * [Lua MythTV alert example](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Lua-API-Reference/Lua-examples/Lua-MythTV-alert-example_3965153 "Lua MythTV alert example") * [Lua Numeric Paging Example](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Lua-API-Reference/Lua-examples/Lua-Numeric-Paging-Example_16354769 "Lua Numeric Paging Example") * [Lua TeleCaptcha example](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Lua-API-Reference/Lua-examples/Lua-TeleCaptcha-example_3965261 "Lua TeleCaptcha example") * [Lua Welcome IVR example](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Lua-API-Reference/Lua-examples/Lua-Welcome-IVR-example_3965157 "Lua Welcome IVR example") * [Lua arguments calling functions](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Lua-API-Reference/Lua-examples/Lua-arguments-calling-functions_16354984 "Lua arguments calling functions") * [Lua example Bridging two calls with retry](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Lua-API-Reference/Lua-examples/Lua-example-Bridging-two-calls-with-retry_3965224 "Lua example Bridging two calls with retry") * [Lua example Send mail when no answer](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Lua-API-Reference/Lua-examples/Lua-example-Send-mail-when-no-answer_3965242 "Lua example Send mail when no answer") ### 交互 小游戏 装个软电话,拨"1"就会进入FreeSWITCH上的一个Lua程序,该程序会提示输入一个数字,并使用TTS读出这个数字。如果输入的是"\*",就将数字减 1,如果按的是"#",就将数字加 1。 ```Lua local x = 1 function onInput(s, type, obj, arg) if (type == "dtmf") then freeswitch.consoleLog("INFO","DTMF: " .. obj.digit .. " Duration: " .. obj.duration .. "\n") if (obj.digit == "*") then x = x - 1 if (x < 0) then x = 0 end n = x elseif (obj.digit == "#") then x = x + 1 n = x else n = obj.digit end s:execute("system", "banner -w 40 " .. n) s:speak(n) end return '' end session:set_tts_params("tts_commandline", "Ting-Ting") session:answer() session:speak("请按一个数字") session:setInputCallback('onInput', '') session:streamFile("local_stream://moh") ``` 程序的代码很简单。在第2行定义了一个onInput函数,当有按键输入时,系统会调用该回调函数,它用一个简单的算法计算一个变量值n,然后在第 15 行调用banner在控制台上输出n(其中的s变量就是传入的当前的"session"),并在第16行使用TTS技术"说"出n的值。 真正脚本的执行是从第20行开始的。该脚本在执行时会自动获得一个session变量,它唯一标志了当前的通话。在第20行,首先设置了将要使用的TTS的参数;然后在第21行进行应答;第22行播放一个提示音;第23行安装一个回调函数,当该session上有输入时,它将回调该函数;第24行播放保持音乐。 这里只是简单地按"1"就呼叫到该脚本,Dialplan 如下: ```XML ``` 按"2"时来一阵视频通话: ```XML ``` ### 用 Lua 实现 IVR :[Lua Welcome IVR example \| FreeSWITCH Documentation (signalwire.com)](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Lua-API-Reference/Lua-examples/Lua-Welcome-IVR-example_3965157/#about "Lua Welcome IVR example | FreeSWITCH Documentation (signalwire.com)") IVR (Interactive Voice Response)交互式语言应答,是呼叫中心的1个经典应用场景,FreeSwitch官方有一个利用 lua 实现的简单示例,大致原理是利用 lua 脚本 + TTS实现 步骤1:安装TTS FreeSwitch自带了1个TTS引擎(发音效果比较生硬,仅支持英文,不过用来学习足够了),找到安装目录下的 freeswitch/conf/modules.conf.xml > \ ``` 然后,打个电话呼入名称为3000的会议,在会议中就可以通过按\*号键在听到拨号音后输入一个号码进行外呼了。如果只想会议管理员才能使用上述功能,也可以将上述功能键的映射关系放到单独的group中(如group name="modrator")并通过会议Profile中的moderator-controls指定该组以确保只有管理员才可以使用这些按键来进行控制。 ### 在FreeSWITCH中外呼的脚本 能否实现在FreeSWITCH中外呼,然后放一段录音?当然能!写个简单的脚本就行。实现思路是:将待呼号码放到一个文件中,每个号码一行,然后用Lua依次读取每一行,并进行呼叫。呼通后播放一个声音文件,并将呼叫(通话)结果写到一个日志文件中。但如果要求还要知道呼叫是否成功,那实现就要复杂一点了。 ```Lua prefix = "{ignore_early_media=true}sofia/gateway/gw1/" number_file_name = "/usr/local/freeswitch/scripts/number.txt" file_to_play = "/usr/local/freeswitch/sounds/ad.wav" log_file_name = "/usr/local/freeswitch/log/dialer_log.txt" function debug(s) freeswitch.consoleLog("notice", s .. "\n") end function call_number(number) dial_string = prefix .. tostring(number); debug("calling " .. dial_string); session = freeswitch.Session(dial_string); if session:ready() then session:sleep(1000) session:streamFile(file_to_play) session:hangup() end -- waiting for hangup while session:ready() do debug("waiting for hangup " .. number) session:sleep(1000) end return session:hangupCause() end number_file = io.open(number_file_name, "r") log_file = io.open(log_file_name, "a+") while true do line = number_file:read("*line") if line == "" or line == nil then break end hangup_cause = call_number(line) log_file:write(os.date("%H:%M:%S ") .. line .. " " .. hangup_cause .. "\n") end ``` 将上述脚本保存到FreeSWITCH的scripts目录中(通常是/usr/local/freeswitch/scripts/),命名为dialer.lua,然后在FreeSWITCH控制台上执行如下命令便可以开始呼叫了: > freeswitch\> luarun dialer.lua 除此之外,还有一个batch_dialer,用于批量外呼,感兴趣的可以研究下:http://fisheye.freeswitch.org/browse/freeswitch-contrib.git/seven/lua/batch_dialer.lua?hb=true ### 使用 Lua 通过多个网关循环外呼 有时候,外呼需要通过多个网关。除了可以使用 mod_distributor 将呼叫分配到多个网关。也可以使用 Lua 脚本来实现功能。 关键位置 ```Lua retries = 0 bridge_hangup_cause = "" gateways = {"gw1", "gw2", "gw3", "gw4"} dest = argv[1] function call_retry() freeswitch.consoleLog("notice", "Calling [" .. dest .. "] From Lua\n"); retries = retries + 1 if not session.ready() then return; end dial_string = "sofia/gateway/" .. gateways[retries] .. "/" .. dest; freeswitch.consoleLog("notice", "Dialing [" .. dial_string .. "]\n"); session:execute("bridge", dial_string); bridge_hangup_cause = session:getVariable("bridge_hangup_cause") or session:getVariable("originate_disposition"); if (retries < 4 and (bridge_hangup_cause == "NORMAL_TEMPORARY_FAILURE" or bridge_hangup_cause == "NO_ROUTE_DESTINATION" or bridge_hangup_cause == "CALL_REJECTED" or bridge_hangup_cause == "INVALID_GATEWAY") ) then freeswitch.consoleLog("notice", "On calling [" .. dest .. "] hangup. Cause: [" .. bridge_hangup_cause .. "]. Retry: " .. retries .. " \n"); session:sleep(1000); call_retry(); else freeswitch.consoleLog("notice", "Retry Exceed, Now hangup!\n"); end end session:preAnswer(); session:setVariable("hangup_after_bridge", "true"); session:setVariable("continue_on_fail", "true"); call_retry(); ``` Dialplan调用该脚本: ```XML ``` 其中,如果匹配到以0开头的被叫号码,我们"吃掉"0,把剩余的部分作为参数传给Lua脚本,然后在Lua脚本中就可以从argv\[1\]中获得这些被叫号码的值了 ### 在FreeSWITCH中执行长期运行的嵌入式脚本 上面的Lua脚本都是"短暂"运行的------它们或者存在于一路通话的会话期内,或者是在命令行上执行一个短暂的命令。但在有些情况下,我们可能希望脚本能永远不停地运行,下面来看一个例子。 写一个Lua脚本,用于监控网关的状态。实现的思路是:如果接收到挂机事件,就判断该通话是否是经过一个网关出去的;如果是,就判断通话是否成功;然后记录统计结果,并将统计结果以几种方式呈现: * 在FreeSWITCH中触发一个事件,由其他程序进行处理; * 发送到一个远端的HTTP服务器上; * 直接写入数据库; * 其他方式,如写入一个文件等。 其他的程序在收到这些统计结果后再使用Web方式呈现,进而我们可以知道哪些网关(运营商提供的SIP中继)比较好,哪些网关总是出问题。 既然是长期运行的脚本,那为什么要停止呢?大部分时间是不需要停止的,但是都是开发人员,如果在开发过程中你需要调试和修改脚本,总不能每次都重启FreeSWITCH吧。 * 通过使用事件机制构造另一个循环,然后就可以在检测到一个特殊事件后停止该循环。 * 在循环体内通过检测一个FreeSWITCH全局变量的值来终止循环 ### 使用 Lua 提供 XML Binding 上面用 Lua 实现了动态拨号计划,下面再看下 Lua 能提供的另外一个功能:XML绑定(Binding) 前面学习过的 XML 配置文件都是静态的,在很多时候编辑静态的XML很不方便。**FreeSWITCH 提供了一种机制可以在 XML 配置节点上绑定一些回调(函数或方法)**,然后当FreeSWITCH用到一段XML时,就可以调用该回调功能来取得XML。我们可以使用Lua绑定一个回调,并通过Lua脚本来产动态的XML内容。 ### 语音识别 语音识别与TTS技术可以说是一对孪生兄弟,但"长相"却迥然不同。 TTS是把文字转成语音,而语音识别则是把声音转换成文字 。 * TTS技术是比较容易实现的,最简单的实现仅需要用查表法将与文字对应的录音逐个查找到并读出,高级一些的借助一些语法和词法分析并借助语音合成技术能读出抑扬顿挫的声调; * 语音识别就不同了,它不仅需要语法和词法分析,还需要"理解"声音的内容,以转换成合适的文字。语音识别分为基于关键词的识别和自然语音识别。基于关键词的识别比较成熟,因为词汇数量有限,比较容易做到精确。这类应用一般用于声控场合,如发出打开、关闭(设备或程序)、呼叫(某人)等命令。基于自然语言的识别则比较难 通过商业语音识别软件进行识别 ### 使用 mod_xml_curl 提供动态用户管理 可以使用 Lua 来绑定一个回调为FreeSWITCH提供XML Dialplan,但Lua脚本的灵活性还是稍微差一点。因此,这里再来看一个用外部的脚本来提供XML用户目录的例子。 FreeSWITCH默认使用静态的XML文件配置用户,但如果要动态认证,就需要跟数据库相关联。FreeSWITCH通过使用mod_xml_curl模块完美解决了这个问题。它的实现思路是你自己提供一个HTTP服务器,当有用户有注册请求时(或INVITE或其他,总之需要XML的请求时),FreeSWITCH向你的HTTP服务器发送请求,你查询数据库生成一个标准的XML文件,FreeSWITCH进而通过这一文件所描述的用户信息对用户进行认证。 FreeSWITCH会将每次请求得到的XML文件存放到文件名类似/tmp/xxx.xml的文件中 ### 使用 mod_xml_cdr 模块处理话单 与mod_xml_curl模块类似,mod_xml_cdr会在每次生成话单后请求一个HTTP服务器,然后HTTP服务器就可以进行一些逻辑处理和写入数据库等操作。在HTTP服务器端,用户可以使用任何熟悉的语言(如Java、PHP、Ruby、Python、C#等)来开发。 ## 3、Event Socket 参见:[http://wiki.freeswitch.org/wiki/Event_Socket](http://wiki.freeswitch.org/wiki/Event_Socket "http://wiki.freeswitch.org/wiki/Event_Socket") 与Lua之类的嵌入式语言不同,通过 Event Socket 方式,可以使用运行在FreeSWITCH外部的程序控制FreeSWITCH。Event Socket是操控FreeSWITCH的"瑞士军刀"。它可以通过Socket方式使用FreeSWITCH提供的所有的App程序和API命令。由于绝大多数程序语言都支持Socket,因而它几乎可以跟任何语言开发的程序通信,也就是说,它几乎可以跟任何系统进行集成。 FreeSWITCH 使用 SWIG 来支持多语言。简单来讲,FreeSWITCH 用C语言写了一些库函数,通过 SWIG 包封装成其他语言接口。现在已知 FreeSWITCH 支持的语言有 C、Perl、PHP、Python、Ruby、Lua、Java、Tcl, 以及由 Managed 支持的 .Net平台语言如 C#、VB.NET 等。 > Simplified Wrapper and Interface Generator,即简单包装及接口生成器,用于帮助使用C或C++语言写的程序生成其他高级语言的接口。参见:[http://www.swig.org/](http://www.swig.org/ "http://www.swig.org/") Event Socket 其实并没有提供什么新的功能,只是提供了一个开发接口,所有的通道处理及媒体控制都是由FreeSWITCH内部提供的App和API来完成的。 ### Event Socket 架构 Event Socket 有两种模式 ( **内和外都是针对FreeSWITCH而言** ): * 内连( Inbound )模式 * 外连( Outbound )模式 初学者往往比较容易理解 Event Socket 的 Inbound模式 和 Outbound模式,但对于何时该使用哪种模式不是很清楚。一般来说, * Outbound 比较适合控制单腿的呼叫,**实现复杂的 IVR 应用;**Outbound 模式的 Socket 是由 FreeSWITCH 建立的,它是建立在 Channel 的基础上的,每一个Channel 均会为外部的 TCP Server 建立一个连接,在 Channel 挂机时释放。因此,Outbound 的连接要考虑 Channel 的生命周期(即 Socket 的生命周期)。在Outbound 模式中,又分为同步模式和异步模式,同步模式控制比较简单,但自由度较小;异步模式需要更多的编程技巧,但会更强大。 * Inbound 更适合接收所有的事件,与多条腿进行交互,**进行更复杂的呼叫控制**。Inbound 的连接由客户端主动向FreeSWITCH 发起连接,只需要考虑断线重连等问题。 当然,上述说法不是绝对的,Inbound 和 Outbound两种模式都能完成所有的控制功能。在实际开发应用中,具体使用哪种模式需要具体问题具体分析,解决问题需要自己动手进行实验,而不能盲目迷信别人说的。如果你使用C语言进行ESL开发,可以参考一下源代码目录中的**fs_cli.c**(在libs/esl目录中),里面有各种函数的真实使用方法。 #### 外连 ( Inbound ) 模式 如图所示,FreeSWITCH作为一个TCP客户端连接到一个TCP Server上。 ![](https://file.jishuzhan.net/article/1761625559219048449/a97c28e16fa3b4e83aa82999b81be657.webp) TCP Server 就是用户自己需要开发的部分,用户可以实现自己的业务逻辑,以及连接数据库获取数据帮助决策等。 怎么让 FreeSWITCH 去连接 用户的TCP Server呢?FreeSWITCH是一个B2BUA,当Bob呼叫Alice时,首先电话会到达FreeSWITCH(通过SIP),建立一个单腿的Channel(a-leg),然后电话进入路由状态,FreeSWITCH查找Dialplan,然后可以通过以下动作建立一个到TCP Server的连接:\ 到此为止,还是只有一个Channel。其中socket是一个App,它会先把这个Channel置为Park状态,然后FreeSWITCH作为一个TCP客户端连接到TCP Server上,把当前呼叫的相关信息告诉它,并询问下一步该怎么做。当然,这里FreeSWITCH跟TCP Server说的语言称为ESL,该语言只有它们两个人懂,与SIP及RTP没有任何关系。也就是说,**TCP Server只是发布控制指令,并不实际处理语音数据。** 接下来,TCP Server 收到 FreeSWITCH 的连接请求后,进行决策,如果它认为Bob想要呼叫Alice(根据来话信息和主被叫号码判断),它就给FreeSWITCH发一个执行bridge App的消息,告诉它应该继续呼叫Alice(给Alice发SIP INVITE消息)。 在Bob挂机之前,FreeSWITCH会一直向TCP Server汇报Channel的相关信息,所以这个TCP Server就可掌握这路电话所有的详细信息,也可以在任何时间对它们发号施令。 Bob挂机后,与TCP Server的连接也会断开,并释放资源。读到这里,读者应该明白了。之所以叫做TCP Server,是因为它应该是一个服务器应用,永远在监听一个端口(在上面的例子中是8040)等待有人连接。如果需要支持多个连接,这个服务器就应该使用Socket的Select机制或做成多线程(多进程)的。 > 所谓 Inbound 和 Outbound 都是针对 FreeSWITCH 而言的。由于在这种模式下,FreeSWITCH 要向外连接到 TCP Server,因此称为 外连模式。 **示例**:使用如下 Dialplan 进行测试 ```XML ``` 当电话呼叫1234时,FreeSWITCH便会使用Outbound模式,使用socket App启动Socket连接。注意这里的两个参数 async 和 full。 * async 表示异步执行。默认是同步的,比如在同步状态下,如果FreeSWITCH正在执行playback操作,而playback是阻塞的,因而在playback完成之前向它发送任何消息都是不起作用的,但异步状态可以进行更灵活的控制。当然,异步状态增加了灵活性的同时也增加了开发的复杂度,在实际的开发过程中可以对比一下它们的异同。 * full 指明可以在外部程序中使用全部的API,默认只有少量的API是有效的。至于哪些API跟这个参数相关,可以自行练习。总之一句话,如果你不确定,那么加上full是没有错的。 好了,电话来了,由于还没有准备好TCP Server,因此连接会失败,电话就断掉了。下面我们需要实现一个TCP Server,在这里使用 netcat 这个工具来讲解。 netcat 是一般 Linux 系统自带的一个工具,它可以启动一个 Socket,做服务器或客户端。如果作为客户端,你可以认为它类似于你更熟悉的 telnet 命令。虽然它叫 netcat,但程序的名字是 nc。 打开一个终端A,启动一个ServerA,监听8040端口(其中-l表示监听,即listen;-k表示客户端断开后继续保持监听。注意,有些版本的netcat参数稍有不同,使用时请查看相关的man文档)。 > nc -l -k localhost 8040 打开另一个终端(Terminal)B,启动一个Client B连上它: > nc localhost 8040 > > 然后在这个客户端中打些字,回车,在终端A中就应该能看到你输入的文字了。 好了,接下来按Ctrl+C退出终端B,到这里就该让FreeSWITCH上场了,即我们用FreeSWITCH来替换终端B。 拿起电话拨打1234,电话路由到Socket,FreeSWITCH就会连到ServerA上。这时候你听不到任何声音,因为Channel处于Park状态。但是你在ServerA里也看不到任何连接的迹象。不过,如果你打开另一个终端,使用如下命令可以显示8040端口已处于连接(ESTABLISHED)状态。 > $ **netstat -an\|grep 8040** > > tcp4 0 0 127.0.0.1.8040 127.0.0.1.60588 ESTABLISHED > > tcp4 0 0 127.0.0.1.60588 127.0.0.1.8040 ESTABLISHED > > tcp4 0 0 127.0.0.1.8040 \*.\* LISTEN 现在回到终端A,输入 connect 然后按两下回车,奇迹出现了吧?会看到类似下面的输出: > Event-Name: CHANNEL_DATA > > Core-UUID: 4bfcc9bd-6844-4b45-96a7-4feb2a4f9795 > > ... 这便是 FreeSWITCH 发给 ServerA 的第一个事件消息,里面包含了该Channel所有的信息。下面该Channel何去何从就完全看你的了。比如,发送如下消息给它放段音乐(local_stream://moh): > sendmsg > > call-command: execute > > execute-app-name: playback > > execute-app-arg: local_stream://moh 建议你直接粘贴上面的命令到ServerA的窗口里,记得完成后按两下回车。sendmsg的作用就是发送一个App命令(这里是playback)给FreeSWITCH,然后FreeSWITCH就乖乖地照着做(执行该App)了。如果你玩够了,就把上面的playback换成hangup再发一次,电话就挂断了。这些命令就跟直接写到Dialplan里一样,不同的是现在是由你来控制,以后你可以用自己的程序控制什么时候该发什么命令。 > 1. nc -l -k localhost 8040 启动监听 > > 2. nc localhost 8040 开启终端监听 > > 3. ctrl+c退出B终端,电话拨打1234,链接到8040, > > 4. 回到A终端输入connect然后打两下回车,会出现 > > Event-Name: CHANNEL_DATA > > Core-UUID: 5ed01200-5c09-11e9-8ae3-6733192b29d4 > > ... > > 5. 测试播放一段音乐 > > sendmsg > > call-command:execute > > execute-app-name:playback > > execute-app-arg:local_stream://moh > > 6. 结束,把上面得playback改为hangup再发一遍,电话就挂断了 #### 内连 ( Outbound ) 模式 内边模式如图所示。 ![](https://file.jishuzhan.net/article/1761625559219048449/4d21ecd0d88d25591e4bfadb4ad4c6ff.webp) 在内连模式下,FreeSWITCH作为一个服务器,而用户的程序可以作为一个TCP Client主动连接到FreeSWITCH上。同样,FreeSWITCH允许多个客户端连接。每个客户端连接上来以后,可以订阅FreeSWITCH的一些内部事件。上面说过,**FreeSWITCH 通过 EventSocket 向外部发送信息,这些信息就是以事件的形式体现的**。同样,在内部好多功能也是事件驱动的。用户的TCP Client收到这些事件后,可以通过执行App和API来控制FreeSWITCH的行为。 对于外连模式来讲,由于Socket来自一个App,而且它所连接的TCP Server也像是这个App功能的一部分,它们在Alice这个Channel的内部工作,与之相连的TCP Server发布的命令通常也是让FreeSWITCH执行一些App。只是在使用bridge APP桥接到Bob后这个Socket Server又好像是一个中间人或第三者。 对于内连模式,很明显外部的TCP Client是一个第三者,它通常不是Channel的一部分,而是监听到一个感兴趣的事件以后,通过API(uuid_一族的API)来对Channel进行操作。 **示例:**FreeSWITCH 启动后,会启动一个EventSocket TCP Server,IP、端口号和密码均可以在conf/autoload_configs/event_socket.conf.xml文件里配置。 还使用nc作为客户端,使用以下命令连接FreeSWITCH:nc localhost 8021 连接上以后,你会看到如下消息:Content-Type: auth/request 这表示已经连接上FreeSWITCH的Socket了,并且它告诉你,应该输入密码进行验证。这时我们输入"auth ClueCon",记得按两下回车。FreeSWITCH默认监听在8021端口上,默认的密码是ClueCon,因此我们在上面使用了这些默认值,当然需要的话也可以根据情况在conf/autoload_configs/event_socket.conf.xml 中修改。 如果一切顺利的话,我们就已经作为一个客户端连接到FreeSWITCH上了。可以输入下列命令试一试(记得每个命令后面都按两下回车): > api version > > api status > > api sofia status > > api uptime 你肯定经常在fs_cli中使用这些命令,只不过在此我们在每个命令前面多加了个"api"。其实fs_cli作为一个客户端也是使用ESL与FreeSWITCH通信的,只是它帮你做了好多事,你不用手工敲一些协议细节了。但在这里,我们手工输入各种命令,更有助于理解这些细节,例如: > api status > > Content-Type: api/response > > Content-Length: 327 > > UP 0 years, 0 days, 17 hours, 13 minutes, 19 seconds, 959 milliseconds, 304 microseconds > > FreeSWITCH (Version 1.2.11 git b9c9d34 2013-07-20 19:06:40Z) is ready > > 27 session(s) since startup > > 0 session(s) - 0 out of max 30 per sec peak 2 (0 last 5min) > > 1000 session(s) max > > min idle cpu 0.00/100.00 > > Current Stack Size/Max 240K/8192K 键入如下命令接收事件:**event plain ALL** 可以订阅所有的事件,当然如果你看不过来可以少订一些。比如命令仅订阅CHANNEL_CREATE事件:**even plain CHANNEL_CREATE** 它等效于在fs_cli中输入以下命令:**/event plain CHANNEL_CREATE** > 1. 使用nc localhost 8021链接freeswitch,会出现 > > Content-Type: auth/request > > 2. 输入 "auth ClueCon"按两下回车,显示以下表示成功 > > Content-Type: command/reply > > Reply-Text: +OK accepted > > 3. 可以使用命令控制freeswitch > > 1. api version 查看freeswitch版本 > > 2. api status 查看状态 > > 3. api sofia status 查看sofia状态 > > 4. event plain ALL订阅所有事件 > > 5. 订阅某一个事件 > > event plain \<事件名称\> > > event plain CHANNEL_CREATE ### Event Socket 命令 ESL 还提供了更多的命令,用于各种控制。下面是部分在 Event Socket 中可以使用的命令。 * auth:对于 Inbound 连接来说,auth 是第一个需要发送的命令,用于向FreeSWITCH认证,格式如下:auth \ 例如:auth ClueCon 实际的密码(这里是Cluecon)是在conf/autoload_configs/event_socket.conf.xml中定义的。 * api:用于执行FreeSWITCH的API,语法如下:api \ \ 其中,command和args分别是FreeSWITCH实际的命令和参数 * bgapi:命令是阻塞执行的,因此,对于执行时间比较长的API命令(如originate),会有一段时间得不到响应结果。因此,可以使用bgapi将这些命令放到后面执行,语法是:bgapi \ \ 命令会立即执行,在后台建立一个任务(Job)并返回一个Job-UUID,当真正需要执行的API命令返回后,FreeSWITCH会产生一个BACKGROUND_JOB事件,该事件带了原先的Job-UUID以及命令执行的结果。因而客户端可以通过匹配Job-UUID知道上一次命令的执行结果。FreeSWITCH也允许客户端提供自己的Job-UUID,这样匹配起来就更容易一些(但客户端需要保证产生的Job-UUID要全局唯一),命令格式是: bgapi \ \ Job-UUID: \![](https://file.jishuzhan.net/article/1761625559219048449/4a2292203e81664820b311c8166a9d9c.webp) #### linger 和 nolinger 在外连模式下,当一个Channel挂断时,FreeSWITCH会断开与 TCP Server 的 Socket 的连接。这时,可能还有与该Channel相关的事件没有来得及发送到TCP Server上,因而会"丢失"事件。为避免这种情况发生,TCP Server可以明确告诉FreeSWITCH希望它能在断开之前 "逗留、徘徊"(linger)一段时间,以等待把所有事件都发完。格式如下:**linger \** > 例如:linger 10 如果开启 linger 又后悔了,可以再用 nolinger 命令撤销,该命令没有参数。 #### event (事件) event 用于订阅事件。让 FreeSWITCH 把相关的事件发送过来。格式是:**event \[type\] \** 其中,type(即事件类型)有 **plain、json 和 xml**三种,默认为 plain,即纯文本;events 参数可以指定事件的名字,或者使用 ALL 表示订阅全部事件。 > 订阅全部事件的命令如下:**event plain ALL** > > 仅订阅部分事件,事件名字之间以空格隔开:**event plain CHANNEL_CREATE CHANNEL_ANSWER CHANNEL_HANGUP_COMPLETE** 要想订阅 CUSTOM 事件该怎么做呢?CUSTOM 事件即自定义事件,它是一类特殊的事件,它主要是为了扩充 FreeSWITCH 的事件类型,它具体的类型是在 Subclass 中指定。 > 指定订阅 Subclass 为 "sofia::register" 的事件:event plain CUSTOM sofia::register > 可以一次订阅多个CUSTOM事件:event plain CUSTOM sofia::register sofia::unregister sofia::expire > > 也可以使用多个event命令混合订阅: > > event plain CHANNEL_ANSWER CHANNEL_HANGUP > > event plain CHANNEL_BRIDGE CUSTOM sofia::register sofia::unregister 但参数中一旦出现了 CUSTOM,后面就不能有普通的事件类型了。 如下面的订阅方法是达不到预期的效果的(FreeSWITCH会把 CHANNEL_ANSWER 当成 CUSTOM 事件的 Subclass 对待,因而不是你想要的):**event plain CHANNEL_CREATE CUSTOM sofia::register CHANNEL_ANSER** 另外CUSTOM事件必须逐一明确订阅,这种订阅是不对的:**event plain CUSTOM ALL** 其他的例子还有: > event json CHANNEL_CREATE > > event xml CHANNEL_CREATE > > event plain DTMF > > event plain ALL 最后,值得一提的是,HEARTBEAT 是一个特殊的事件,它每20秒就产生一次,用于汇报FreeSWITCH 的当前状态。有时候可以用它做心跳,如果超过20秒没收到事件,就可以认为网络或FreeSWITCH异常。下面是HEARTBEAT事件的三种不同输出格式: json格式: > Content-Length: 939 > > Content-Type: text/event-json > > { > > "Event-Name": "HEARTBEAT", > > ... > > "Event-Info": "System Ready", > > "Up-Time": "0 years, 0 days, 22 hours, 19 minutes, 14 seconds, ... > > ... > > } XML格式: > Content-Length: 1432 > > Content-Type: text/event-xml > > \\ > > \HEARTBEAT\ > > ... > > \System%20Ready\ > > \0%20years,%200%20days,%2022%20hours,%2019%20minutes,%2034%20seconds,...\ > > ... > > \ > > \ PLAIN(纯文本)格式: > Content-Length: 848 > > Content-Type: text/event-plain > > Event-Name: HEARTBEAT > > ... > > Event-Info: System%20Ready > > Up-Time: 0%20years,%200%20days,%2022%20hours,%2019%20minutes,%2054%20seconds,... > > ... #### myevents myevents是event的一种特殊情况,它主要用于Outbound模式中。在Outbound模式中,对于每一个呼叫(对应一个Channel),FreeSWITCH都会向外部的TCP Server请求以建立一个新的连接。外部的TCP Server就可以通过myevents订阅与该Channel(UUID)相关的所有事件。使用的格式为:**myevents \\** 。当然,myevents也支持json及XML形式,如: > myevents > > myevents json > > myevents xml 当然,在 Inbound 模式中也可以调用 myevents,这样它看起来类似于一个Outbound模式的连接,但它要指定 UUID,原因是显而易见的,如:myevents 289fe829-af62-47be-9a59-7519a77d0d40 #### divert_events 还有一类特殊的事件,它们是作为InputCallback产生的。那么什么时候会产生Input-Callback呢?当Channel上通过setInputCallback()函数安装了相关的回调函数并遇到某些事件,如收到用户按键(DTMF)或收到语音识别的结果(DETECTED_SPEECH),就会产生InputCallback这样的事件,并回调指定的回调函数,但这些InputCallback默认只能在嵌入式脚本的回调函数中捕获。通过使用divert_events,就能将这些事件转发到Event Socket连接上来,进而在通过Event Socket连接的外部程序中也能收到相关事件。使用格式是: > divert_events on #开启 > > divert_events off #关闭 #### filter 过滤器 filter 用于安装一个过滤器。这里的过滤不是 "滤出",而是"滤入",即把符合过滤条件的过滤进来,也就是要收到它们。可以同时使用多个过滤器。 使用格式是:filter \ \ 例如,下面的例子与 myevent\的作用是相同的: > event plain all > > filter Unique-ID \ 又如,下面的例子会订阅所有事件,但只接收匹配主叫号码是1001的事件: > event plain all > > event filter Caller-Caller-ID-Name 1001 为了理解滤入的概念,可以看下面的例子以加深印象,它可以接收3个 Channel 事件: > event plain all > > filter Unique-id uuid1 > > filter Unique-ID uuid2 > > filter Unique-ID uuid3 如果过滤器写错了,或不想使用某些过滤器了,则可以将其取消掉,如: > filter delete # 取消所有过滤器 > > filter delete Unique-ID uuid2 # 只取消 uuid2 相关的过滤器 #### nixevent 与 noevent nixevent是event的反义词,与event的语法一样,只是取消某些已订阅的事件。如: > nixevent CHANNEL_CREATE > > nixevent all 另外,还有一个 noevent 命令用于简单取消使用event订阅的所有事件,相当于"nixevent all"。 #### log、nolog 跟使用event订阅事件类似,log用来订阅日志,它的使用格式是:log \ > log info > > log 6 一个完整的例子。这个例是在开启了info级别的日志以后打了一个电话,此时会收到很多日志信息,下面是两条信息:$ nc 127.0.0.1 8022 nolog 关闭使用 log 命令订阅的日志。 这里的level是整数值,对应关系如下: * 0 为 EMERG * 1 为 ALERT * 2 为 CRIT * 3 为 ERROR * 4 为 WARNING * 5 为 NOTICE * 6 为 INFO * 7 为 DEBUG #### exit 告诉 FreeSWITCH 关闭 Socket 连接。FreeSWITCH 收到该命令后会主动关闭Socket连接。 #### sendevent 向FreeSWITCH的事件系统发送事件。使用格式是:**sendevent \** 比如,你可以发送一个NOTIFY消息: > sendevent NOTIFY > > profile: internal > > event-string: check-sync > > user: 1002 > > host: 192.168.7.5 > > content-type: application/xml > > content-length: 29 > > \FreeSWITCH IS COOL\ FreeSWITCH收到NOTIFY消息后,将启用内部处理机制,最后它可能会生成一个SIP NOTIFY消息,如: > NOTIFY sip:[email protected]:32278;rinstance=3db08ce44e5166a4 SIP/2.0 > > Via: SIP/2.0/UDP 192.168.7.5;rport;branch=z9hG4bKXDtD32g0N9atg > > Max-Forwards: 70 > > From: \;tag=ZF9SFyaUHeZ4p > > To: \ > > ... > > Event: check-sync > > Subscription-State: terminated;reason=noresource > > Content-Type: application/xml > > Content-Length: 29 > > \FreeSWITCH IS COOL\ 当然,也可以使用它发送MESSAGE消息,如: > sendevent SEND_MESSAGE > > profile: internal > > user: 1002 > > host: 192.168.7.5 > > content-type: text/plain > > content-length: 10 > > Hello 1002 上述命令将会产生如下的SIP消息: > send 623 bytes to udp/\[192.168.7.5\]:32278 at 16:01:23.686473: > > ------------------------------------------------------------------------ > > MESSAGE sip:[email protected]:32278;rinstance=3db08ce44e5166a4 SIP/2.0 > > Via: SIP/2.0/UDP 192.168.7.5;rport;branch=z9hG4bK085Q8K3aD4DjK > > Max-Forwards: 70 > > From: \;tag=11UBKmc2B0Bae > > To: \ > > ... > > Content-Type: text/plain > > Content-Length: 10 > > Hello 1002 当然,在实际使用中更多的是产生CUSTOM的事件,可以自定义一些事件类型。使用这种方式甚至可以把FreeSWITCH当成一个消息队列(Message Queue)来用,如你可以启动一个客户端订阅以下消息:event plain CUSTOM freeswitch:book 我们可以在另外的客户端上发送一条消息,如: > sendevent CUSTOM > > Event-Subclass: freeswitch::book > > content-type: text/plain > > content-length: 44 > > This Message comes from the FreeSWITCH Book! 我们可以在订阅该消息的客户端上收到如下信息: > Content-Length: 688 > > Content-Type: text/event-plain > > Event-Subclass: freeswitch%3A%3Abook > > Command: sendevent%20CUSTOM > > ... > > content-type: text/plain > > Content-Length: 44 > > This Message comes from the FreeSWITCH Book! #### 总 结 1. auth <密码> 第一个需要发送得命令,用于向freeswitch认证,例: auth ClueCon 2. api 其中command和args分别是freeswitch实际得命令和参数 3. bgapi api执行时间比较长,有一段时间会得不到响应,可以使用bgapi将命令放到后面执行; 1. 会建立一个任务(job),并返回job-UUID; 2. 执行完成后fs会产生一个BACKGROUND_JOB事件,事件中带了job-UUID和命令执行结果 3. fs允许自己提供job-UUID(要保证全局唯一) 4. linger和nolinger 1. 当channel挂断时,fs会断开与socket得连接,可能有一些channel相关得事件还没有发送过去避免这种"丢失"事件,tcp server告诉fs在断开之后逗留("linger")一段时间,等待把所有事件发完 linger 延时10秒 linger 10 2. linger开启后悔了,使用nolinger命令撤销,没有参数 5. event [type] 1. event用于订阅事件,type(时间类型)有plain、json、xml三种,默认plain(纯文本) 2. events可以指定事件得名字,ALL表示订阅全部事件,事件之间用空格隔开 3. 订阅CUSTOM事件(自定义事件),它具体的类型是在Subclass中指定得,如指定订阅Subclass为"sofia::register"事件: 1. event plain CUSTOM sofia::register 2. 可以一次订阅多个 event plain CUSTOM sofia::register sofia::unregister 3. 使用多个event命令混合订阅 event plain CHANNEL_ANSWER CUSTOM sofia::register sofia::unregister 4. 参数中一旦出现CUSTOM后面就不能跟普通得事件类型了 5. CUSTOM事件只能逐一订阅,不能使用ALL 6. HEARTBEAT是一个特殊事件,每20秒产生一次,用于回报fs得当前状态,当20秒没有收到fs事件,可以认为网络或fs异常 6. myevents 1. 主要用于outbound模式,在outbound模式中,外部得TCP server可以通过myevents订阅与该channel相关得所有事件 2. 使用格式myevents myevents myevents json myevents xml myevents 289dwd-af35-47de-9a58-754191454d0d2 7. divert_events 1. 作为InputCallback产生的,当channel通过setInputCallback()函数安装了相关的回调函数并遇到某些事件,如收到用户按键(DTMF)或语音识别的结果(DETECTED_SPEECH),产生InputCallback时间,默认是在嵌入式脚本的回调函数捕捉,通过使用diver_events,可以将这些时间转发到Event Socket,外部程序中也能收到相关事件 8. filter 1. filter用于安装一个过滤器,只做"虑入",把符合条件的过滤进来,可以同时使用多个过滤器 2. 格式:filter 3. 例如:订阅所有事件,只接受匹配主叫号码1001事件 event plain all event filter Caller-Caller-ID-Name 1001 4. 取消过滤器 先接受三个过滤器 event plain all filter Unique-id uuid1 filter Unique-id uuid2 filter Unique-id uuid3 filter delete 取消所有的过滤器 filter delete Unique-ID uuid2 取消与uuid2相关的过滤器liru 9. nixevent与noevent 1. nixevent与event相反,是取消订阅事件 nixevent CHANNEL_CREATE nixevent all 2. noevent相当于"nixevent all"取消所有event订阅 10. log订阅日志 1. 格式:log 1. level包含(数目越高越详细): 0-CONSOLE 1-ALERT 2-CRIT 3-ERR 4-WARNING 5-NOTICE 6-INFO 7-DEBUG 2. log info 或 log 6 11. nolog log的反义词,关闭使用log命令订阅的日志 12. exit 告诉fs关闭socket连接 13. sendevent 1. 通过sendevent可以向fs的事件系统发送事件 2. 格式:sendevent 例如发送MESSAGE消息 sendevent SEND_MESSAGE profile: internal user: 1002 host: 192.168.0.126 content-type: text/plain content-length: 10 hello 1002 ### Event Socket 库 官网文档:[FreeSWITCH 文档 --- Event Socket Library](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/ "FreeSWITCH 文档 --- Event Socket Library") 通过 ESL( Event Socket Library,即 Event Socket库的缩写)可以让 FreeSWITCH 跟外部的程序 "交流"。ESL协议是纯文本的协议。它的设计思想来自于大家熟悉的 HTTP 协议及 SIP 协议 ESL提供了一些库函数,通过这些库函数可以很方便地使用ESL协议与FreeSWITCH交互,进而控制FreeSWITCH的各种功能。 * [0. About](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#0-about "0. About") * [1. Prerequisites](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#1-prerequisites "1. Prerequisites") * [Installation](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#installation "Installation") * [Reference](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-reference- "Reference") * [Quoting and Escaping](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#quoting-and-escaping- "Quoting and Escaping") * [ESL Object](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-esl-object- "ESL Object") * [eslSetLogLevel](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#eslsetloglevel- "eslSetLogLevel") * [ESLevent Object](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-eslevent-object- "ESLevent Object") * [new](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-events- "new") * [serialize](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-serialize- "serialize") * [setPriority](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-setpriority- "setPriority") * [getHeader](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-getheader- "getHeader") * [getBody](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-getbody- "getBody") * [getType](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-gettype- "getType") * [addBody](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-addbody- "addBody") * [addHeader](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-addheader- "addHeader") * [delHeader](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-delheader- "delHeader") * [firstHeader](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-firstheader- "firstHeader") * [nextHeader](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-nextheader- "nextHeader") * [ESLconnection Object](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-eslconnection-object- "ESLconnection Object") * [new](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-events- "new") * [socketDescriptor](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-socketdescriptor- "socketDescriptor") * [connected](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-connected- "connected") * [getInfo](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-getinfo- "getInfo") * [send](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-events- "send") * [sendRecv](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-sendrecv- "sendRecv") * [api](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-events- "api") * [bgapi](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-events- "bgapi") * [sendEvent](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-sendevent- "sendEvent") * [recvEvent](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-recvevent- "recvEvent") * [recvEventTimed](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-recveventtimed- "recvEventTimed") * [filter](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-filter- "filter") * [events](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-events- "events") * [execute](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-execute- "execute") * [executeAsync](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-executeasync- "executeAsync") * [setAsyncExecute](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-setasyncexecute- "setAsyncExecute") * [setEventLock](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-seteventlock- "setEventLock") * [disconnect](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-disconnect- "disconnect") * [Examples](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#c-example "Examples") * [Getting a uuid](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-getting-a-uuid- "Getting a uuid") * [Simple Perl Example](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-simple-perl-example- "Simple Perl Example") * [Ruby Example](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-ruby-example- "Ruby Example") * [Java Example](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-java-example- "Java Example") * [C Example](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#c-example "C Example") * [See Also](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#-events- "See Also") #### Event Socket 示例 (**Ruby**) **Ruby客户端:**下面是一个使用Ruby语言通过ESL控制FreeSWITCH的例子。脚本内容如下: ```ruby require 'ESL' con = ESL::ESLconnection.new('127.0.0.1', '8021', 'ClueCon') esl = con.sendRecv('api sofia status') puts esl.getBody ``` 上述脚本只有短短的4行代码,首先它加载了ESL库,然后连接到FreeSWITCH,接着执行sofia status命令,最后将结果输出到控制台。 #### Event Socket 示例 (**C**) 在源代码目录 **lib/esl** 中有 **入站(** testclient.c**) 和 出站(** testserver.c**)** 的C示例。 **入站 (testclient.c)** :[https://github.com/signalwire/freeswitch/blob/master/libs/esl/testclient.c](https://github.com/signalwire/freeswitch/blob/master/libs/esl/testclient.c "https://github.com/signalwire/freeswitch/blob/master/libs/esl/testclient.c") ```cpp #include #include #include int main(void) { // 初始化一个handle,用于标志到FreeSwitch的Socket连接 esl_handle_t handle = {{0}}; // 连接服务器。如果成功,handle 就代表连接成功 esl_connect(&handle, "localhost", 8021, NULL, "ClueCon"); // 发送一个命令,并接收返回值 esl_send_recv(&handle, "api status\n\n"); // last_sr_event 应该是 last server response event,即针对上面命令的响应 if (handle.last_sr_event && handle.last_sr_event->body) { // 打印返回结果 printf("%s\n", handle.last_sr_event->body); } else { // 这在API或bgapi(上面硬编码的)中不太可能发生,但对于其他命令可能会执行到这里 printf("%s\n", handle.last_sr_reply); } // 断开连接 esl_disconnect(&handle); return 0; } ``` 可以看出,该程序很简单,它运行之后向FreeSWITCH建立一个连接,运行一个API命令,然后输出命令的执行结果。 **出站 (** testserver.c**)** :[https://github.com/signalwire/freeswitch/blob/master/libs/esl/testserver.c](https://github.com/signalwire/freeswitch/blob/master/libs/esl/testserver.c "https://github.com/signalwire/freeswitch/blob/master/libs/esl/testserver.c") ```cpp #include #include #include static void mycallback(esl_socket_t server_sock, esl_socket_t client_sock, struct sockaddr_in *addr, void *user_data) { esl_handle_t handle = {{0}}; int done = 0; esl_status_t status; time_t exp = 0; // 将 handle 与 socket 绑定 esl_attach_handle(&handle, client_sock, addr); // 打印一条日志 esl_log(ESL_LOG_INFO, "Connected! %d\n", handle.sock); // 添加一个过滤器(filter),只收取与本次连接的Channel相关的事件(与本次连接的Channel UUID相同的事件) esl_filter(&handle, "unique-id", esl_event_get_header(handle.info_event, "caller-unique-id")); // 订阅各种类型的事件 esl_events(&handle, ESL_EVENT_TYPE_PLAIN, "SESSION_HEARTBEAT CHANNEL_ANSWER CHANNEL_ORIGINATE CHANNEL_PROGRESS CHANNEL_HANGUP " "CHANNEL_BRIDGE CHANNEL_UNBRIDGE CHANNEL_OUTGOING CHANNEL_EXECUTE CHANNEL_EXECUTE_COMPLETE DTMF CUSTOM conference::maintenance"); // 发送一个linger命令开启逗留模式。逗留模式的目的就是告诉FreeSWITCH晚一些断开这个Socket。 //如果不开启该模式,则主叫挂机后,FreeSWITCH会立即断开它主动建立的Socket,就会导致一些后续的事件收不到 esl_send_recv(&handle, "linger"); // 执行answer App对来话进行应答(跟在Dialplan中类似) esl_execute(&handle, "answer", NULL, NULL); // 将来话送入一个会议 esl_execute(&handle, "conference", "3000@default", NULL); // 无限循环,用于不断地接收事件 // 接收事件函数 esl_recv_timed 是非阻塞的,如果在1秒内没有收到任何事件,它就会返回,然后程序进入循环体。 while((status = esl_recv_timed(&handle, 1000)) != ESL_FAIL) { // 检测done变量,它是个结束标志,用于结束循环 if (done) { if (time(NULL) >= exp) { break; } } else if (status == ESL_SUCCESS) { const char *type = esl_event_get_header(handle.last_event, "content-type"); if (type && !strcasecmp(type, "text/disconnect-notice")) { const char *dispo = esl_event_get_header(handle.last_event, "content-disposition"); esl_log(ESL_LOG_INFO, "Got a disconnection notice dispostion: [%s]\n", dispo ? dispo : ""); // 由于使用了逗留模式,因此FreeSWITCH返回的消息中将包含linger字符(可以从打印出的日志中看到) if (dispo && !strcmp(dispo, "linger")) { done = 1; esl_log(ESL_LOG_INFO, "Waiting 5 seconds for any remaining events.\n"); exp = time(NULL) + 5; } } } } esl_log(ESL_LOG_INFO, "Disconnected! %d\n", handle.sock); esl_disconnect(&handle); } int main(void) { esl_global_set_default_logger(7); esl_listen_threaded("localhost", 8040, mycallback, NULL, 100000); return 0; } ``` 该程序也很简单,其运行于 Outbound 模式,是多线程的。它启动后监听一个端口,每次有电话进来时,通过 Dialplan 路由到 socket App。该 App 便会从 FreeSWITCH 中向testserver 发起一个TCP连接。大致意思就是:当FreeSWITCH中有来话路由到它时便启动一个新线程为新的Channel进行服务(具体的服务就是应答),并将来话送入一个会议。由于它是多线程的,因而可以同时为很多来话服务。 **main 函数** > 首先它调用 esl_global_set_default_logger 函数设置日志级别(代表DEBUG级别,即最大级别,这样能看到详细的日志,包括所有协议的细节)。这里的 level 是整数值,对应关系如下: > > * 0 为 EMERG > * 1 为 ALERT > * 2 为 CRIT > * 3 为 ERROR > * 4 为 WARNING > * 5 为 NOTICE > * 6 为 INFO > * 7 为 DEBUG > > 然后它通过 esl_listen_threaded 启动一个 Socket 监听本地回环地址(localhost)的8040端口。如果有连接(从FreeSWITCH)到来,它便回调 mycallback 函数为该连接服务。其中,NULL是一个空指针,该参数的位置是一个无类型(void\*)的指针,即允许你传入任何类型的指针,该指针将作为 mycallback 函数的参数(user_data)在回调中携带 回调函数,它有4个参数, * 服务器端的Socket标志、 * 客户端的Socket标志、 * 连接地址 * 用户私有数据 **使用 ESL 发送 SIP MESSAGE 消息** 执行下面程序后,在1000这个SIP电话上将会收到一个Hello消息(如果该用户正常注册的话)。 ```cpp #include #include #include int main(void) { esl_handle_t handle = {{ 0 }}; struct esl_event *event; struct esl_event_header header; esl_event_create_subclass(&event, ESL_EVENT_CUSTOM, "SMS::SEND_MESSAGE"); esl_event_add_header_string(event, ESL_STACK_BOTTOM, "to", "[email protected]"); esl_event_add_header_string(event, ESL_STACK_BOTTOM, "from", "[email protected]"); esl_event_add_header_string(event, ESL_STACK_BOTTOM, "sip_profile", "internal"); esl_event_add_header_string(event, ESL_STACK_BOTTOM, "dest_proto", "sip"); esl_event_add_header_string(event, ESL_STACK_BOTTOM, "type", "text/plain"); esl_event_add_body(event, "Hello"); esl_connect(&handle, "localhost", 8021, NULL, "ClueCon"); esl_send_recv(&handle, "api version\n\n"); if (handle.last_sr_event&& handle.last_sr_event->body) { printf("%s\n", handle.last_sr_event->body); printf("sending event....\n"); esl_sendevent(&handle, event); esl_event_destroy(&event); } else { printf("%s\n", handle.last_sr_reply); } esl_disconnect(&handle); return 0; } ``` #### ESL :ESLevent 对象 FreeSWITCH 通过ESL库包装了大量的易于使用的函数。ESL本身与FreeSWITCH没有任何依赖关系,可以单独编译和使用。它在底层是使用C语言实现的,并通过swig包装成了其他程序语言惯用的格式。 使用 C语言的 ESL连接 FreeSwitch:[https://blog.csdn.net/xxm524/article/details/125840597](https://blog.csdn.net/xxm524/article/details/125840597 "https://blog.csdn.net/xxm524/article/details/125840597") * ELS github 源码 ( C语言 ):[https://github.com/signalwire/freeswitch/tree/master/libs/esl/src](https://github.com/signalwire/freeswitch/tree/master/libs/esl/src "https://github.com/signalwire/freeswitch/tree/master/libs/esl/src") * ESL Object (**官网文档是 Perl 语言形式的接口,可以对照 github 上 C语言接口一块查看** ):[Event Socket Library \| esl-object](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Client-and-Developer-Interfaces/Event-Socket-Library/#esl-object "Event Socket Library | esl-object") ESL Event 是一个事件对象,在C语言中的定义如下(在esl_event.h中定义) ![](https://file.jishuzhan.net/article/1761625559219048449/14662dfba31350aacfc6ab8c32c387cf.webp) 当从FreeSWITCH中收到一个事件后,你就得到一个事件对象。ESL定义了一些函数用于从该对象中获取信息或构造新的对象。 * new($event_type \[, $event_subclass\]) 实例化一个新的 event对象,方法的新事件对象。 * serialize(\[$format\]) 可以将事件序列化成可读的形式。$format 可以是:"xml"、"json"、"plain" (default) * setPriority(\[$number\]) 设置事件的级别。 * getHeader($header_name) 从事件中获取头域的值。 * getBody() 从事件中获取正文 * getType() 获取事件的类型。 * addBody($value) 向事件中增加正文,可以调用多次。 * setBody($value) 设置事件的正文,可以多次调用,但后者将覆盖前者。 * addHeader($header_name, $value) 向事件中增加一个头域。 * delHeader($header_name) 从Event中删除头域。 * firstHeader() 将指针指向Event的第一个头域,并返回它的Key值。C语言中没有明确的定义,靠使用ESL Event的headers结构体成员实现。 * nextHeader() 移动指针指向下一个header,在调用该函数前必须先调用firstHeader(),同样在C语言中没有明确定义,靠访问esl_event_header结构体的next成员实现。 #### ESL :ESLconnection 对象 ESLConnection对象维护与FreeSWITCH之间的连接,以发送命令并进行事件处理。在C语言中使用如下定义(在esl.h中) ![](https://file.jishuzhan.net/article/1761625559219048449/6d99404eeba58f4db340a8eee5ec4f24.webp) 相关函数 * new($host, $port, $password) 该函数初始化一个新的连接,仅用于inbound模式。 * new($fd) 根据已存在的Socket句柄建立一个ESLconnection对象。仅用于outbound模式。 * socketDescriptor() 该函数返回连接的UNIX文件句柄。 * connected() 判断是否已连接,连接返回1,否则返回0。 * getInfo() 当FreeSWITCH使用outbound模式连接时,它将首先发出一个CHANNEL_DATA事件,getInfo会返回该事件。在inbound模式中它返回NULL。 * send($command) 向FreeSWITCH发送一个ESL命令,它不会等待接收事件,而需要明确地使用recvEvent或recvEventTimed以接收返回的事件。返回事件的类型为api/response或command/reply。使用sendRecv()可以自动获取返回结果。 * sendRecv($command) 在ESL内部,sendRecv(command)首先调用send(command),然后调用recvEvent()并最终返回一个ESLevent对象。recvEvent()会在一个循环中调用并一直阻塞直到收到头域为api/response或command/reply的事件为止。在此期间所有收到的其他的将保存到一个内部队列中,这些队列中的事件会能在后续的recvEvent()中取到。 * api($command\[, $arguments\]) 向FreeSWITCH发API命令,它是阻塞执行的。它与sendRecv("api$command$args")是等价的。 * bgapi($command\[, $arguments\]\[,$custom_job_uuid\]) 后台执行API,要执行的API将在新的线程中执行,因而不会阻塞。该函数与sendRecv("bgapi$command$args")也是完全等价的。它执行后也返回一个Job-UUID,与我们上面讨论的bgapi使用场景类似。 * sendEvent($send_me) 向FreeSWITCH发送一个事件。 * recvEvent() 从FreeSWITCH中接收事件,如果此时没有事件,它将一直阻塞直到有新事件到来。如果在调用它之前曾经调用了sendRecv(),并且sendRecv()曾经将收到的事件放到队列中,则该函数会返回队列中的第一个事件,否则,它会一直等待。 * recvEventTimed($milliseconds) 该函数与recvEvent类似,不同的是它不会永远阻塞,而是将在参数指定的毫秒数会返回。recvEventTimed(0)可以立即返回,可以用于事件轮循。 * filter($header, $value) 类似上面提到的filter命令,用于过滤事件。 * events($event_type,$value) 订阅事件,类似上面的event命令。 * execute($app\[, $arg\]\[, $uuid\]) 执行DialplanApp,并阻塞等待返回。它将最终返回一个ESLevent对象,通过getHeader("Reply-Text")方法可以获取返回值,一般来说"+OK\[成功的信息\]"表示执行成功,"-ERR\[错误信息\]"表示执行失败。 * executeAsync($app\[, $arg\]\[, $uuid\]) 与execute()相同,但非阻塞。两者实际上都调用了上面的execute命令,只是executeAsync带了"async:true"头。 * setAsyncExecute($value) 强制将Socket连接设为异步模式。value为1为异步,0为同步。调用该函数后,所有使用execute()执行的App都将带有"async:true"头域,因而执行是异步的。除此之外本函数对其他函数没有影响。在C语言中可以通过设置esl_handle结构体成员async_execute=1来实现。 * setEventLock($value) 设置所有后续的execute()调用都将带有"event-lock:true"头域。 * disconnect() 主动中断与FreeSWITCH的Socket连接。 ### FreeSWITCH 的 事件系统 FreeSWITCH 的 事件系统:[Event System](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Introduction/Event-System/ "Event System") * [Debugging Event Socket Message](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Introduction/Event-System/Debugging-Event-Socket-Message_9634071 "Debugging Event Socket Message") * [ESL Example Clients](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Introduction/Event-System/ESL-Example-Clients_27591923 "ESL Example Clients") * [Event Handlers](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Introduction/Event-System/Event-Handlers_32178263 "Event Handlers") * [Event List](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Introduction/Event-System/Event-List_7143557 "Event List") * [Event headers](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Introduction/Event-System/Event-headers_32178341 "Event headers") * [Events](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Introduction/Event-System/Events_32178330 "Events") * [List of CUSTOM Events](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Introduction/Event-System/List-of-CUSTOM-Events_15139388 "List of CUSTOM Events") * [Making Event Socket behave like the console](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Introduction/Event-System/Making-Event-Socket-behave-like-the-console_10682474 "Making Event Socket behave like the console") FreeSWITCH都会产生哪些事件啊?这些事件都是在什么情况下产生的?FreeSWITCH的事件那么"长",里面的都是什么意思?首先,FreeSWITCH事件的机制就是在特定的情况下产生特定的事件。这些事件都是在源代码的**switch_types.h** 文件中定义的,从定义一般能很直观地看到事件的含义。只要了解了这些事件的含义,那么可能在什么情况下产生什么样的事件就能大体猜出,剩下的只需要去实践中验证。比如,跟踪所有的事件,然后打个电话看一下产生的事件是否跟你想的一样。 FreeSWITCH中的事件分为 主事件 和 CUSTOM事件,但其实FreeSWITCH中也没有严格的分类方法。下面就以CUSTOM事件、CHANNEL事件、CHANNEL相关的事件、系统事件和其他事件这种分类来进行说明。 #### CUSTOM 事件 * CUSTOM 事件:主要是用于跟系统内部核心逻辑关系不大的一些事件,一般用于模块内部,且便于扩展。它的 Event-Name 永远是 CUSTOM,不同事件的类型使用 Event-Subclass 区分。Event-Subclass 可以是任意的字符串,可以任意定义,但 FreeSWITCH 中也有约定俗成的命名空间,如 mod_sofia 模块中常用的 sofia::register、sofia::unregister 等,都使用以"::"隔开、以 sofia 开头的命名空间。类似的还有,会议模块(mod_conference)中的conference::maintenaince、FIFO 模块(mod_fifo)中的 fifo:info 等。 #### Channel 事件 * Channel 事件:有一部分事件是以Channel开头的,它主要跟Channel的状态(状态机)有关。下从以Channel的生命周期来大体讲一下。 首先,系统中有来话或去话时,将生成一个新的Channel,并产生一个CHANNEL_CREATE事件。该事件中会包含该Channel的UUID(其对应的字段名字是Unique-ID)、主叫号码、被叫号码、Channel的创建时间等。 接下来,如果Channel正常继续进行,则会产生CHANNEL_PROGRESS事件(如在SIP中收到对方的100或180消息)。如果在应答之前能收到Early Media(如在SIP中收到对方的)183消息,则会产生CHANNEL_PROGRESS_MEDIA事件。如果一个Channel被应答,就会产生Channel Answer事件。如果一个Channel与另外一个Channel bridge成功,则会产生CHANNEL_BRIGE事件。注意,bridge是由两个Channel参与的,其中两个Channel分别称为a-leg和b-leg。在SIP中,如果使用bridge这个App等待bridge的Channel收到对端Channel发来的Early Media消息(如SIP中的183消息)即bridge成功,可以在Channel创建时使用ignore_early_media通道变量延迟bridge的返回(直到应答,如收到SIP中的200消息。但结果是a-leg,则听不到b-leg发来的Early Media(回铃音)。另外,CHANNEL_BRIDGE事件只在一个Channel上发生,即只发生在主动的那个leg上。例如,终端A通过FreeSWITCH呼叫B,A端的呼叫到达FreeSWITCH后产生一个新的Channel,FreeSWITCH使用bridge App去呼叫B,又产生一个新的Channel。如果B应答,则b-leg上会产生一个CHANNEL_ANSWER消息,同时,该应答信号通过bridge App传递到A上,a-leg上也会产生一个CHANNEL_ANSWER消息。bridge成功后(假设没有Early Media参与,因此会直到应答bridge才完成),a-leg上会产生CHANNEL_BRIGE事件,而b-leg上不会。CHANNEL_BRIGE事件与上面单腿的事件不同的地方在于它里面包含了b-leg的信息,如Other-Leg-Unique-ID是b-leg的Channel UUID,Other-Channel-Caller-ID-Number是b-leg上的主叫号码等。挂机后将产生CHANNEL_HANGUP事件和CHANNEL_HANGUP_COMPLETE事件,其中,后者比前者内容丰富一些,比如后者带有variable_duration(通话时长,从Channel创建开始计时)及variable_billsec(计费时长,从应答后开始计时)。因此,一般使用该事件取计费信息。最后还将产生CHANNEL_DESTROY事件,表示该Channel已经完全释放(销毁)了。 在 Channel 生存期间,在执行 App 的时候,将产生 CHANNEL_EXECUTE 事件,表示一个App已开始执行。有的App执行非常快,如set;有的则可能比较慢,如 playback(要等声音文件播放完毕)。在App执行完毕后,会产生 CHANNEL_EXECUTE_COMPLETE 事件。在使用异步方式进行 ESL 编程时可以在收到该事件后执行下一个 App。其中, 这两个事件都会包含一个 Application字段,标志当前正在执行或已完成的App的名字。对于大部分的 Channel 事件,与Channel相关的Channel Variable都会附加在Channel相关的事件上。与系统内部的字段名字不同(大写字母开头加中横线方式命名,如Unique-ID),这些Channel Variable都是以"variable_"开头的,如variable_effective_caller_id_name。某些 Channel 事件默认没有 variable_ 开头的字段,如CHANNEL_CALLSTATE。这主要是为了减少消息量。如果希望这些消息中也包含所有的variable,则可以在Dialplan中使用verbose_events App来打开,如: \ #### Channel 相关事件 有一部分事件虽然与Channel相关的,但是它们的名字不是以CHANNEL_开头的,例如PLAYBACK_START(放音开始)、PLAYBACK_STOP(放音结束)、RECORD_START(录音开始)、RECORD_STOP(录音结束)、DTMF(双音多频按键信息)等。 这类事件也有Channel UUID(Unique-ID),但它们一般与Channel的状态无关。 #### 系统 事件 系统事件包含STARTUP(系统启动)、SHUTDOWN(系统关闭)、MODULE_LOAD(模块加载)、MODULE_UNLOAD(模块卸载)等。另外,系统每隔20秒会产生一个HEARTBEAT(心跳)事件,可以用于检测FreeSWITCH是否正常运行。 其他还有许多事件,如API(执行API时产生)、BACKGROUND_JOB(使用bgapi后台执行API时产生)等,在此我们就不多介绍了。 ## 4、使用 ESL 开发 案例 ESL是一个客户端库,它主要用于对 FreeSWITCH 进行逻辑控制,因此,很多实际的功能还得靠freeswitch 的 App 和 API 来完成。 ### 创建独立的 ESL 应用 :[https://blog.csdn.net/MMsmileNN/article/details/118147532](https://blog.csdn.net/MMsmileNN/article/details/118147532 "https://blog.csdn.net/MMsmileNN/article/details/118147532") 创建目录和分离 esl 源文件 > 1. 创建一个 myesl 目录,然后创建一个 myesl.c 的文件。 > > 2. 直接把fs源码目录下的testclient.c中的内容原样复制过来。 > > 3. 将/usr/local/freeswitch/lib 下的 libesl.a 拷贝到 /media/sf_share/freeswitch/libs/esl 目录下 > > 4. 创建一个MakeFile文件,保存在myesl.c相同的目录,内容如下 > > ESLPATH = /media/sf_share/freeswitch-1.6.20/libs/esl > > CFLAGS = -I$(ESLPATH)/src/include > > LIBESL = $(ESLPATH)/.libs/libesl.a > > #LIBESL = $(ESLPATH)/libesl.a > > > all: myesl charge acd > > myesl: myesl.c > > gcc $(CFLAGS) -o myesl myesl.c $(LIBESL) -ldl -lm -lpthread > > 5. 然后 make 编译生成 myesl 运行文件,然后执行 > > 1. make myesl.c 编译 > > 2. ./myesl 执行 myesl.c:[https://github.com/seven1240/myesl/blob/master/myesl.c](https://github.com/seven1240/myesl/blob/master/myesl.c "https://github.com/seven1240/myesl/blob/master/myesl.c") ```cpp #include #include #include int main(void) { esl_handle_t handle = {{0}}; esl_connect(&handle, "127.0.0.1", 8022, NULL, "ClueCon"); esl_send_recv(&handle, "api status\n\n"); if (handle.last_sr_event && handle.last_sr_event->body) { printf("%s\n", handle.last_sr_event->body); } else { // this is unlikely to happen with api or bgapi (which is hardcoded above) but prefix but may be true for other commands printf("%s\n", handle.last_sr_reply); } esl_disconnect(&handle); return 0; } ``` ### 用 ESL 重写空中充值服务 ### 用 ESL 实现 呼叫中心 ### inbuound 模式实现 IVR ### 使用 Erlang 控制呼叫流程 > 在 [https://github.com/seven1240/myesl](https://github.com/seven1240/myesl "https://github.com/seven1240/myesl") 中有 ESL 书写的案例 > > * ESL空中充值服务 charge.c > * acd 呼叫中心 acd.c > * inbuound 模式实现 IVR icharge.c > * 使用 Erlang 控制呼叫流程 echarge.erl > > ![](https://file.jishuzhan.net/article/1761625559219048449/d5425d3ea3c2aac44b23023f7b23311e.webp) 步骤: > 1. 编辑 charge.c 等案例文件,在main函数中设置连接的地址为:127.0.0.1,端口号为8021。 > > 2. 修改 Makefeile 如下 > > ESLPATH = /media/sf_share/freeswitch-1.6.20/libs/esl > > CFLAGS = -I$(ESLPATH)/src/include > > LIBESL = $(ESLPATH)/.libs/libesl.a > > > all: myesl charge acd > > myesl: myesl.c > > gcc $(CFLAGS) -o myesl myesl.c $(LIBESL) -ldl -lm -lpthread > > charge: charge.c > > gcc $(CFLAGS) -o charge charge.c $(LIBESL) -ldl -lm -lpthread > > acd: acd.c > > gcc $(CFLAGS) -o acd acd.c $(LIBESL) -ldl -lm -lpthread > > 3. 在myesl目录下make编译,会执行all后面的所有的.c文件,生成对应的可执行文件 > > 4. 配置freeswitch设置连接 > > 1. vi ../autoload_configs/event_socket.conf.xml > > 2. 默认的监听地址配置 > > \ > > 3. 去掉下面的注释 > > \

相关推荐
MarsYjZ1 天前
电脑网络重置,找不到原先自家的WIFI,手机还能正常连接并上网
智能手机·信息与通信
追寻4 天前
知识拓展卡————————关于Access、Trunk、Hybrid端口
网络·网络协议·计算机网络·信息与通信
长流小哥6 天前
STM32:CAN总线精髓:特性、电路、帧格式与波形分析详解
网络·stm32·单片机·嵌入式硬件·信息与通信
C66668886 天前
TCP/IP协议
开发语言·tcp/ip·计算机视觉·信息与通信
半青年8 天前
IEC61850规约客户端软件开发实战(第二章)
java·c++·qt·网络协议·c#·信息与通信·iec61850
爱研究的小梁10 天前
乾元通渠道商中标青海省自然灾害应急能力提升工程基层防灾项目
网络·智能路由器·信息与通信
AORO_BEIDOU10 天前
如何区分防爆手机与普通手机?
科技·5g·安全·智能手机·信息与通信
tan1314587643510 天前
KEYSIGHT N9320B是德科技N9320B频谱分析仪
信息与通信
AORO_BEIDOU11 天前
三防平板科普:有什么特殊功能?应用在什么场景?
网络·5g·安全·电脑·信息与通信
geneculture11 天前
亚当·斯密思想精髓的数学建模与形式化表征
人工智能·数学建模·课程设计·信息与通信·融智学的重要应用·富国论·道德情操论