作者:Russell Bryant 翻译:jiazhengfeng
Asterisk[1]是一款GPLv2协议下的开源电话应用平台。简单来说,Asterisk是一个服务器应用,能够完成发起电话呼叫、接受电话呼叫、对电话呼叫进行定制处理。
Asterisk这个项目是由Mark Spencer于1999年开创的。Mark当时有一个名为Linux技术支持服务公司,公司需要一套电话系统来开展业务。由于Mark当时没有足够的钱购买,就决定自己研发一套。随着Asterisk逐渐流行,Linux支持服务公司逐渐将业务重点转移到Asterisk,并将公司的名字改为Digium。
Asterisk的命名是从Unix的通配符*来的,可以看出Asterisk的目标是能够做通讯里面的任何事情。为完成该目标,Asterisk现在已经支持了很多种能够发起和接受电话呼叫的技术,包括多种VoIP协议,与传统电话网络或者公用电话网络的模拟连接和数字连接。Asterisk的一个主要优势是能够让不同类型的电话呼叫可以呼入到Asterisk中,以及从Asterisk呼出到不同类型的电话。
电话呼入到asterisk或者从asterisk呼出,就具有了许多额外的特性,我们可以利用这些额外的特性来对电话进行定制处理。有些特性是经常用到的,诸如语音邮件。还有一些其他的特性可以与别的特性结合在一起用以创建和定制语音应用,诸如播放一个提示音,获取数字输入,或者语音识别等。
1.1 概念本部分讨论对Asterisk各个部分都比较重要的结构概念,这些思想都是Asterisk体系结构的基础。
1.1.1 通道Asterisk中的通道,代表了Asterisk系统与一些电话终端之间的连接(图1.1)。最通常的例子就是电话呼叫到asterisk系统中。这个连接是由一个单侧的通道来表示的。在Asterisk代码里,通道是作为ast_channel这个数据结构的实例存在。这种单侧的呼叫场景比如一个主叫用户使用Asterisk中的语音邮件服务。
图1.1 单侧呼叫leg,用以表示一个单侧通道
1.1.2 通道桥接一个更为熟悉一点的呼叫场景是两个电话间的连接。在这个场景里 ,有两个电话终端与Asterisk系统连接,所以这个通话里存在两个通道。
图1.2 两个呼叫leg,代表了两个通道
当asterisk的通道像上面这样连接在一起的,就称之为一个通道桥接。执行通道桥接后将两个通道桥接在一起,其目的是在这两个通道间可以传递媒体信息。媒体流最常见的是音频流。当然,也可以在呼叫中包括视频流或者文本流。即便是包含多种媒体流,也是由单个通道来处理的。在图2中,有电话A和电话B对应的通道,桥接负责从电话A向电话B传递媒体和从电话B向电话A船体媒体。所有的媒体流都是通过asterisk来协商的。asterisk可以在不同的技术之间进行录音、音频操作、和转码。
两个通道桥接在一起,可以通过如下两个方法来完成:通用桥接和本地桥接。通用桥接是不管使用什么样的通道技术均能正常工作,这种桥接是通过asterisk的抽象通道接口来传递所有的音频和信令数据。这种桥接方法是最复杂的,也是最有效的。
本地桥接是和通话所使用的技术相关的一种桥接。如果两个通道使用相同的媒体传输技术,可以使用一种更高效的方式而不用通过像不同技术那种方式要通过asterisk的抽象层来完成。例如,如果连接到电话网络中硬件是固定的,可以通过在硬件上将两个通道桥接在一起,不用放到应用层来完成。有些VoIP协议,可以让终端之间直接互发媒体信息,只让信令信息通过服务器。
决定使用通用桥接还是本地桥接是在桥接的时候通过两个通道的比较完成的。如果两个通道均支持本地桥接,则采用本地桥接,反之,则使用通用桥接。为了判断两个通道是否支持相同的本地桥接方法,可以简单的通过c函数指针的比较。这种比较方法,并不是最优雅的方法,但是我们还没有遇到不能满足我们需要的情况。关于通道的本地桥接将会在1.2节讨论。图1.3说明的是一个本地桥接的例子。
图3 本地桥接
1.1.3 帧在asterisk代码里一个通话的通信是通过使用帧来完成的。帧是数据结构ast_frame的一个实例。帧可以是媒体帧也可以是信令帧。在一个基本的呼叫中,媒体帧流包括通过服务器的语音数据。信令帧用来发送与呼叫信令事件相关的消息,诸如,按下一个数字键,通话被保持,通话被挂断等。
Asterisk中支持的帧类型列表是静态定义的,每种类型的帧是通过数字编码的类型(type)和子类型(subtype)标识的。完整的帧类型列表中include/asterisk/frame.h文件中,一些例子如下:
· VOICE: 这些帧携带部分语音流
· VIDEO: 这些帧携带部分视频流
· MODEM: 这种帧里面的数据的编码,诸如T.38是通过IP网络来发送传真的。这种帧类型主要是用来处理传真。一定要注意的一点是这种数据帧,一定要连续不能中断,以保证对端能对数据进行正确的解码。这与AUDIO帧不同,音频帧可以通过不同的音频编码进行转码虽然牺牲了音频质量但节省了网络带宽。
· CONTROL: 这种帧中包括的是通话的信令消息。这些帧通常用来说明通话信令时间,包括电话接通,挂断,保持等。
· DTMF_BEGIN: 数字开始处。这种帧一般是通话者在电话机上开始按一个DTMF按键。
· DTMF_END: 数字结束处。这种帧是通话者在结束电话机上的DTMF按键。
1.2 Asterisk组件抽象Asterisk是一个高度模块化的应用程序。包括一个核心的应用,可以通过Asterisk代码树的main目录编译构建。但是,光这个核心通常没有什么用。核心应用主要处理模块注册,也有代码包括如何连接抽象接口来完成电话通话。具体的实现接口是通过可以在运行时刻加载到系统中的模块完成的。
默认情况下,所有的模块均放在asterisk预定义好的模块文件目录中,该目录下的所有模块会由主应用启动后进行加载。之所以这样设计,就是为了保持更加简单。在asterisk中还有一个配置文件,在这个配置文件中可以定义加载的模块和加载模块的顺序。这样会显得配置起来有点麻烦,不过可以让用户指定哪些模块中不需要时,可以不加载。这样最大的好处就是减少应用的内存占用,当然有时候也会有助于提高系统安全。最好的做法是如果不是非常需要,不要加载那些能够接受网络连接的模块。
当模块加载完成后,会向asterisk主应用注册本模块所实现的组件抽象接口。模块可以实现并向asterisk核心注册的接口有多种类型。一般而言,相关联的功能会放在一个模块中。
1.2.1 通道驱动asterisk的通道驱动接口是最复杂也是最重要的可用接口。asteisk的通道API提供了对各种通信协议的抽象,使得asterisk的各种功能特性不必关心具体的通信协议。该组件主要是负责在asterisk通道抽象和具体的通信协议实现中的通信。
asterisk通道驱动接口的定义是ast_channel_tech接口。这个接口中定义了一些通道驱动必须要实现的方法。通道驱动首先要实现的方法是ast_channel工厂方法,即ast_channel_tech中的requester。当一个asterisk通道创建后,无论该通道是incoming方向的还是outgoing方向的,与该通道相关联的ast_channel_tech实现负责实例化和初始化该路通话对应的ast_channel。
ast_channel创建完成后,该结构中有一个创建该通道的ast_channel_tech指针。当然有很多其他的操作需要按照具体技术相关的方式来处理。图1.2中展示了asterisk中的两个通道,图1.4进行了扩展,展示了两个桥接的通道,以及通道技术如何实现的。
图1.4 通道技术和通道抽象层
在ast_channel_tech中最重要的方法包括:
· requester:用于向通道驱动请求并实例化一个ast_channel对象,根据通道类型进行适当的初始化工作。
· call: 用户向ast_channel表示的终端发起一个出局呼叫。
· answer: 当asterisk决定应该对ast_channel关联的入局呼叫进行应答时调用。
· hangup: 当系统决定当前的呼叫应该挂断时调用。通道驱动需要与终端按照一定的协议进行通信。
· indicate: 通话开始后,还会产生一些其他的事件,需要将这些事件通知给终端。例如,如果设备被保持住了,这个函数就会被调用。
· send_digit_begin: 当终端设备开始向asterisk发送按键DTMF的时候,调用该函数。
· send_digit_end: 当终端设备向asterisk发送按键DTMF结束的时候,调用该函数。
· read: 当asterisk核心需要从终端读入一个ast_frame数据帧的时候调用read函数。ast_frame帧是asterisk中用来封装媒体(诸如音频或者视频)和信号的抽象结构。
· write: 使用该函数向终端设备发送一个ast_frame帧。一般是由通道驱动来完成数据的处理(采集等)和打包使得数据包能够适合所采用的通信协议,然后将打包后的数据发送到终端。
· bridge:该通道类型中的本地桥接函数。前面提到了,进行本地桥接是通道驱动为相同类型的两个通道提供了一种更高效的桥接方法,而不是将所有的信令流和媒体流都通过额外的抽象层来完成。这对于提供性能极其重要。
通话结束后,asterisk核心中的抽象通道处理代码会调用ast_channel_tech中的hangup函数,然后销毁ast_channel对象。
1.2.2 拨号应用asterisk管理员通过/etc/asterisk/extensions.conf中的拨号规划来设置呼叫路由。拨号方案是一系列的呼叫路由规则(称为extension)构成。电话呼叫进到系统中后,系统使用被叫号码在该呼叫应该使用的拨号方案中查找对应的extension。extension包括一系列可以在该通道上执行的拨号方案应用。拨号方案中使用的应用是由asterisk中的应用注册机制来维护的。应用的注册是在对应的模块加载的时候就完成。
asterisk提供了将近200个应用。应用的定义非常松散。应用可以使用asterisk的内部API与通道进行交互。有些应用只完成非常单一的工作,如playback应用,只负责给主叫播放一个声音文件。另外一些应用比较负责,执行多个操作,比如voicemail应用。
通过asterisk的拨号方案,多个应用可以一起使用来定制呼叫的处理过程。对于使用提供的拨号方案无法完成的复杂定制,可以使用脚本接口对呼叫进行个性化处理。脚本接口可以使用任意一种编程语言。在使用脚本的时候,拨号方案中的应用仍然可以与通道进行交互。
在我们进入例子之前,让我们一起看看asterisk拨号方案中处理呼叫1234这个号码的语法。注意,1234这个号码是随便选的。呼叫该号码后,调用了3个拨号方案应用,首先接听该通话,然后播放一个声音文件 ,最后挂断该通话。
; Define the rules for what happens when someone dials 1234.;exten = > 1234,1,Answer() same = > n,Playback(demo-congrats) same = > n,Hangup().csharpcode, .csharpcode pre{font-size: small;color: black;font-family: consolas, "Courier New", courier, monospace;background-color: #ffffff;/*white-space: pre;*/}.csharpcode pre { margin: 0em; }.csharpcode .rem { color: #008000; }.csharpcode .kwrd { color: #0000ff; }.csharpcode .str { color: #006080; }.csharpcode .op { color: #0000c0; }.csharpcode .preproc { color: #cc6633; }.csharpcode .asp { background-color: #ffff00; }.csharpcode .html { color: #800000; }.csharpcode .attr { color: #ff0000; }.csharpcode .alt {background-color: #f4f4f4;width: 100%;margin: 0em;}.csharpcode .lnum { color: #606060; }exten关键字是用来定义extension的。在exten行的右侧,1234是为呼叫1234这个号码定义的呼叫规则。下面的1是拨打1234这个号码时执行的第一步操作,Answer是告诉系统接听这个呼叫。下面的两行,是用same这个关键字开头的,是定义的上面的extension接下来的规则,这里就是1234接下来的规则。n是下一步要执行的操作,后面的项指明了拨号方案要执行的具体动作。
接下来是使用asterisk拨号方案的另一个例子。在这个例子中,进来的呼叫首先被接听,给主叫播放一个beep,然后从主叫读取4位的按键输入,并存储到DIGITS变量中,然后将读取到的数字播放给主叫,最后通话结束。
exten = > 5678,1,Answer() same = > n,Read(DIGITS,beep,4) same = > n,SayDigits(${DIGITS}) same = > n,Hangup().csharpcode, .csharpcode pre{font-size: small;color: black;font-family: consolas, "Courier New", courier, monospace;background-color: #ffffff;/*white-space: pre;*/}.csharpcode pre { margin: 0em; }.csharpcode .rem { color: #008000; }.csharpcode .kwrd { color: #0000ff; }.csharpcode .str { color: #006080; }.csharpcode .op { color: #0000c0; }.csharpcode .preproc { color: #cc6633; }.csharpcode .asp { background-color: #ffff00; }.csharpcode .html { color: #800000; }.csharpcode .attr { color: #ff0000; }.csharpcode .alt {background-color: #f4f4f4;width: 100%;margin: 0em;}.csharpcode .lnum { color: #606060; }前面提到过,应用的定义非常松散,注册函数定义非常简单:
int (*execute)(struct ast_channel *chan, const char *args);
应用实现的函数可以使用include/asterisk/.下所有的API函数。
1.2.3 拨号方案函数很多拨号方案应用使用字符串作为参数。由于很多数值是硬编码的,因此在需要更多动态数值的时候,往往需要适时的使用变量。下面的例子中展示了拨号方案中如何设置变量,然后通过asterisk中的verbose应用在asterisk的命令行打印出变量的值。
exten => 1234,1,Set(MY_VARIABLE=foo)
same => n,Verbose(MY_VARIABLE is ${MY_VARIABLE})
拨号方案的函数使用语法与前面提到的应用的语法一样。asterisk模块可以注册为拨号方案的函数,这些函数可以获取信息然后返回到拨号方案中,也可以接受一些信息并对信息进行处理。一般的原则是,拨号方案中的函数可以设置或者获取通道的原数据,但不能做信令或者媒体处理的事情,这些是由拨号方案中的应用来完成的。
下面的例子说明了拨号方案的函数用法。首先,在asterisk的命令行里打印出当前通道的主叫ID,然后通过Set函数修改主叫ID。
在这个例子中Verbose和Set都是应用,而CALLERID是函数。
exten => 1234,1,Verbose(The current CallerID is ${CALLERID(num)})
same => n,Set(CALLERID(num)=<256>555-1212)
由于CallerID信息是存储在ast_channel数据结构中的,所以我们需要使用拨号方案函数而不仅仅是一个简单的变量。拨号方案函数代码知道如何从数据结构中设置和获取值。
另一个例子是使用拨号方案函数为呼叫日志(即呼叫详细记录CDR)中增加额外的信息。CDR函数可以获取呼叫详细信息,也可以添加定制的信息。
exten => 555,1,Verbose(Time this call started: ${CDR(start)})
same => n,Set(CDR(mycustomfield)=snickerdoodle)
1.2.4 编码转换在VOIP世界中,我们使用多种不同的编码来对媒体进行编码,并将编码后的数据发送到网络上。目前存在有很多的编码供选择,但在媒体质量、CPU消耗、带宽需求上会有所牺牲。Asterisk支持多种不同的压缩编码,并且在必要的时候可以进行编码格式间的转换。
在呼叫建立时,Asterisk会尝试让两个终端使用相同的媒体编码格式,这样就可以不用进行转码。但是,这只是理想情况。即便是有共同的编码,转码也仍然需要。例如,如果通过配置让asterisk对经过系统的音频数据进行信号处理诸如增加或者降低音量。Asterisk也可以通过配置进行通话录音,如果配置的录音文件的格式与通话的编码格式不一致,仍然需要编码。
说明:编码协商
协商媒体流使用何种编码的方法对于连接到asterisk所使用的各种技术是确定的。在有些时候,例如通过传统电话网络的呼叫,就不需要进行任何协商。但是,在另外一些情况下,特别是使用IP协议,需要使用一种协商机制,通过描述的终端的能力和优先选择的编码,协商出共同的编码。
以SIP协议为例,这里说明一下呼叫到asterisk后如何进行编码协商的。
1.终端向asterisk发起呼叫请求,在该请求中包含了终端希望采用的编码格式。
2.Asterisk通过查询管理员配置好的语音编码优先顺序,选择一种最优先的编码,该编码既是asterisk优先顺序表的编码,同时终端也可以支持。
Asterisk编码处理不太好的地方是对于较为复杂的编码,特别是视频编码。编码协商的需求在过去的10年内已经非常复杂。为了更好的处理新的音频编码和能更好的支持视频编码,我们还有很多工作需要做。这是asterisk下一发行版本中优先考虑的新需求。
编码转换模块提供了一个或者多个ast_translator接口的实现。编码转换器具有源和目的格式属性,提供了一个回调函数可以完成一段媒体信息从源格式到目的格式的转换。编码转换器本身对通话本身并不知道,所需知道的仅仅是如何完成媒体从一个格式到另一个格式的转换。
关于转换器API的更多信息,可以参考include/asterisk/translate.h和main/translate.c。转换器抽象的实现可以在codecs 目录中找到。
1.3 线程Asterisk是一个典型的多线程应用程序,主要使用POSIX线程API来管理线程和相关的服务例如加锁。在Asterisk中所有和线程相关的代码为便于调试,都进行了一层封装。Asterisk中的大部分线程可以分为两大类网络监控线程和通道线程。通道线程有时候也指的是PBX线程,因为通道线程的主要目的是在一个通道上执行PBX。
1.3.1 网络监控线程网络监控线程主要是在asterisk通道驱动模块中。他们主要负责监听驱动模块连接网络,监听来自网络的呼叫和其他类型的请求信息。监听线程会进行初始连接建立阶段的工作,诸如认证和被叫号码的验证。一旦呼叫建立完成,监听线程会创建一个asterisk通道,然后再创建的通道上开启一个通道线程来负责接下来的事情。
1.3.2 通道线程正如前面讨论的,通道在asterisk中是一个基本概念,通道包含入局通道和出局通道。入局通道是呼叫进入到asterisk系统后创建的,拨号方案的执行就是在入局通道上进行的。对于每一个执行拨号方案的入局通道均会创建一个线程,称为通道线程。
拨号方案应用一般是在通道线程中执行。拨号方案函数基本上也是如此。拨号方案函数可以通过异步接口来进行读和写操作,比如Asterisk的命令行。但是,多数情况下是由ast_channel数据结构的所属通道线程来完成ast_channel生命周期。
1.4 呼叫场景前面两部分介绍了asterisk组件的重要接口,和线程执行模型。本部分主要通过一些常见的呼叫场景来说明asterisk的组件间如何相互操作来完成呼叫处理电话呼叫的。
1.4.1 语音邮件检查第一个呼叫场景是某人呼叫到电话系统中来查看自己的语音邮件。在这个场景中用到的第一个重要组件是通道驱动。通道驱动是负责处理从电话终端进来的呼叫请求,主要是在通道驱动的监控线程中完成的。取决于呼叫到系统中具体所使用的技术,可能会有某些协商来建立通话。建立通话的下一步就是决定呼叫目的号码,这一般是由主叫拨打的号码。但有些时候由于所使用的技术不支持被叫号码的传送,就不存在被叫号码。比如,从模拟线进来的呼叫就是这样。
如果通道驱动验证了asterisk拨号方案的配置文件中有被叫号码对应的extension,就会分配一个asterisk通道对象(ast_channel),并创建一个通道线程。通道线程将负担起呼叫剩下的主要任务,如图1.5所示。
图1.5 呼叫建立顺序流程图
通道线程的大循环来负责拨号方案的执行,首先获取到被叫号码对应extension定义的规则,然后按照预定义好的顺序依次执行。下面是一个按照extensions.conf语法定义的例子。呼叫到*123时,首先接听该呼叫,然后执行VoicemailMain应用。
Exten => *123,1,Answer()
same => n,VoicemailMain()
当通道线程执行Answer应用时,Asterisk将应答入局呼叫。接听呼叫需要与通道使用技术的处理,所以在通用应答处理之外,还要调用ast_channel_tech结构中定义的answer函数来处理接听通话,比如向IP网络发送专门的数据包,或者模拟线摘机等。
通道线程下一步是执行VoicemailMain,该应用是app_voicemail模块提供的。需要注意的一点是Voicemail代码虽然处理了很多和呼叫相关的交互,但应用本身对于呼叫到asterisk系统使用的何种技术并不知情,asterisk通道抽象将这些细节隐藏起来了。
用户通过呼叫访问语音邮件时,提供了很多特性。所有的特性主要是根据主叫输入的数字按键来读或者写音频文件。DTMF数字传送到asterisk中有很多种方式,这是由通道驱动来处理的。一旦某个键按下传送到asterisk中,就会转化成一个键按下事件,然后传递到voicemail代码中。
前面已经讨论过,在asterisk中一个重要的接口是编码转换。在这个场景中非常重要。当voicemail代码想播放一个声音文件给主叫时,音频文件中音频的格式和主叫与asterisk系统通信的音频格式未必一致。如果必须进行音频的转码,就会建立一个由一个或者多个编码转换器构成的转码路径来完成从源格式到目标格式的转换。
图1.6 一个到VoicemailMain的呼叫
在某个时候,主叫会结束与语音邮件系统的交互,然后挂机。通道驱动将检测到这些动作,然后转换成asterisk通道信令事件。语音邮件代码会接收到该信令事件,然后退出。控制权将会返回到通道线程的大循环中继续拨号方案的执行。由于在这个例子里面没有接下来的拨号方案,通道驱动会优先处理通道相关的挂断处理,然后ast_channel对象会被销毁。
1.4.2 桥接呼叫在asterisk中,另外一个比较常见的呼叫场景是在两个通道间桥接呼叫。这种场景下,一个电话呼叫通过asterisk系统。初始呼叫建立过程与前面的语音留言类似,不同之处在于从通话建立开始和通道线程执行拨号方案开始。
下面的拨号方案是一个建立桥接呼叫的简单例子。使用这个规则,当一个电话呼叫1234,拨号方案会执行Dial应用,Dial是发起一个出局呼叫的主要应用。
exten => 1234,1,Dial(SIP/bob)
Dial应用中的参数说明系统呼叫到的终端是SIP/bob。参数的SIP部分说明发起这个呼叫应该使用SIP协议。Bob是有通道驱动来负责解释使用的,实现是在sip协议chan_sip中。假设SIP通道驱动中事先定义好了一个帐号叫bob的,这个呼叫就会到达Bob的电话上。
Dial应用会向asterisk核心请求分配一个标识符为SIP/bob的asterisk通道。核心会请求sip通道驱动,完成技术相关的初始化工作。通道驱动会初始化外呼到终端电话的过程。通道驱动在呼叫进行过程中,会向asterisk核心传送一些事件,进而传送到Dial应用中。这些事件包括呼叫被应答,目标用户正忙,网络阻塞,呼叫被拒绝,以及其他的一些响应。理想的情况是,呼叫被应答。呼叫被接听会传回到入局通道中,asterisk不会对呼叫到asterisk系统中的入局呼叫进行接听除非出局呼叫被接听。两个通道均被接听后,通道间的桥接开始,如图1.7所示。
图1.7 通用桥接中桥接通话的流程块
在一个通道桥接过程中,音频和信令事件从一个通道中传送到另一个通道,直到结束桥接的事件发生,例如一方挂断。图1.8的顺序流程图说明了桥接通话中一个音频帧上的关键操作。
图1.8 桥接中音频帧处理流程图
通话结束后,挂断过程与上面的例子类似,最大不同的地方是这里有两个通道。在通道线程结束之前,通道技术相关的挂断过程需要在两个通道上分别执行。
1.5 最后注释Asterisk现在的体系结构已经超过10年了。但是通道的基本概念和asterisk拨号方案中灵活的呼叫处理仍然支持着复杂电话系统的继续发展。Asterisk没有很好的解决的一个领域是扩大到多个服务器。Asterisk开发社区目前正在开发一个项目叫Asterisk SCF主要就是为了解决伸缩相关的问题。在未来的几年里,我们希望看到Asterisk与Asterisk SCF一起继续在电信市场乃至大型安装上占据较大份额。
脚注 1. http://www.asterisk.org/2.DTMF代表双音-多频。电话通话中当有人按下电话按键时,发送的音频提示音。