Asterisk的体系架构
1 引言
本文档从一个开发者的角度出发,概要描述Asterisk的体系架构。至于详细的API讨论,请参考公开API头文件所关联的文档。
本文档假定您了解Asterisk的一些知识,并知道如何使用它。
本文的意图是:从一个高的层次开始了解Asterisk,并逐步深入。它从Asterisk的组件差异开始,最终讨论这些组件在不同应用场景里
的协作关系。
文中,提供了很多交叉引用链接,指向相关API的一些引用参考,也可能指向相关的源码链接。
欢迎对本文档的反馈和贡献。请将您的真知灼见发给asterisk开发组的邮件组:http://lists.digium.com/.
谢谢,并预祝您享受Asterisk!
2 模块构架
Asterisk是一个高度模块化的应用。在源码的main/目录下,建立了内核应用。然而,它(内核)本身的用处并不是很大。
运行时,Asterisk加载了许多模块。Asterisk的模块都有具体的名称,以标识模块所提供的功能,但是,这些名称没有任何技术意义上
的特殊。Asterisk加载一个模块时,模块向内核注册它所提供的功能。整个流程看起来是这样的:
1. 启动Asterisk
2. Asterisk加载模块
3. 模块跟内核说:嗨,Asterisk,我是一个模块,我能提供X、Y、Z三种功能,用得着的时候要记得我哦。
3 抽象接口类型
Asterisk提供了许多不同类型的接口,具体的模块可以实现这些接口并注册给内核调用。任何模块,都可以注册任意多种的接口。通常,一个模块内整合了某些相关的功能。
本节讨论接口的类型,后续将讨论各种场景下不同组件间的协作关系。
3.1 编码解释器CodecInterpreter
编码解释器接口的实现,提供了两种编码间的转换能力。Asterisk当前只有音频编码转换的能力。
这些模块不了解话务相关的任何信息,也不知为什么要调用它们进行音频转换。它们仅需要知道音频采样率、音频的输入格式、期待的
输出格式这些信息。
如果注册了多个编码解释器,那么编码A转换为编码B的过程,就可能有多种不同的转换路径。在(编码)模块加载之后,Asterisk建立一
张转换表,表中包含不同translator的转换开销评估值,因此,Asterisk能够找出A转换B的最佳路径。
在源码树中,编码模块通常在codecs/目录下。
已有编码解释器的实现列表,请参考:Module:Codecs
更多编码解释器API的信息,请参考接口定义文件:include/asterisk/translate.h.
内核关于编码解释器的相关实现,参考源码:main/translate.c
3.2 文件格式处理器File Format Handler
文件格式处理器接口的实现,为Asterisk提供了读写文件的能力。文件格式处理器可以提供音频、视频或图像文件的处理能力。
文件处理器的接口是相当原始的。模块简单地告诉Asterisk内核:它能处理某种具有待定扩展名的文件,比如说".wav"。同时,
它还说明读取文件之后,将以编码X的形式提供音频。如果它还提供写文件的能力,那么它还必须说明用它写文件的音频编码要求
(即说明它能把什么编码格式的音频编码写成带什么扩展名的文件)。
源码树中,文件格式模块通存放常在formats/目录下。
现有的实现,请参考:Module:Media File Formats。
文件格式处理器的API定义信息,请参考头文件:include/asterisk/file.h。
内核中,与文件格式相关的实现,请参考:main/file.c。
3.3 C API Providers
Asterisk有一些可选的C API。内核API是主应用内置的,始终可用。可选CAPI则是由某个模块提供的,只有在相应模块加载后,才可用。
某些API的提供方,也提供了它自身的接口,供其它模块实现和注册(接口)。
提供C API的模块,通常存放在res/目录下。
一些提供C API的模块有:
· res_musiconhold.c
· res_calendar.c
o 提供一种日历技术接口
· res_odbc.c
· res_ael_share.c
· res_crypto.c
· res_curl.c
· res_jabber.c
· res_monitor.c
· res_smdi.c
· res_speech.c
o 提供一种语音识别引擎接口
3.4 Manager Interface (AMI) Actions
Asterisk管理接口是一个socket接口,用于监视和控制Asterisk。它是建立于主应用的核心功能。继而,其它模块可以向AMI注册自己的
action供客户端调用。
向AMI注册action的模块,通常提供了某种辅助功能以补充扩展某项主要的功能(这个功能不一定是内核功能,可能是模块自身的功能)。
比方说:一个提供电话会议功能的模块,提供了一个管理action接口,用于返回与会者列表。
3.5 CLI Commands
Asterisk CLI是主应用实现的命令行管理功能。外围模块可以注册附加的CLI命令。
3.6 Channel Drivers
Asterisk通道接口是最复杂、最重要的接口。Asterisk通道API提供了电话协议的抽象,这样,所有Asterisk的其它特性,才能不依赖于
具体的电话协议。
通道驱动实现的具体接口是ast_channel_tech所封装的接口。一个通道驱动,必须实现执行各种呼叫信令任务的回调函数。比如说,必须
实现一个初始化呼叫的方法,实现一个挂断呼叫的方法,等等。数据结构ast_channel是抽象通道数据结构。每个ast_channel实例,有一
个关联的ast_channel_tech以标识通道类型。一个ast_channel实例,描述了呼叫中的一条腿(call leg的概念,也就是Asterisk与终端设
备间的连接概念)。
源码中,通道驱动通常在channels/目录里。
当前实现的通道驱动列表,请参考:Module:Asterisk Channel Drivers。
需要进一步了解通道API,请参考头文件include/asterisk/channel.h。
内核中,关于ast_channel API的实现,则在main/channel.c中。
3.7 桥接技术
桥接,是把两个或多个通道连接在一起的操作。通道,A到B的呼叫,用的是简单的双通道桥接,而在三方通话或会议中,用的就是多方桥
接技术。
桥接API允许其它模块注册桥接技术。桥接技术的实现,知道如何选择两个(或多个)通道,并将它们连接在一起。具体是怎样发生的,取决
于实现。
这些接口的代码,需要在两个(或多个)通道间交换音频,却又不需要知道交换的实现细节。在底层,会议可能由操作系统内核实现
(通过DAHDI);也可能由Asterisk的内部方法实现;如果有人实现了硬件扩展模块,还可能用硬件实现。
写这篇文档时,桥接API相对来说还比较新,所以执行桥接应用的操作,还没有全部使用这些API。在拨号计划应用实现里,ConfBridge
是在桥接API之上实现的一个会议应用。
桥接技术实现模块,存放在bridges/目录下。
桥接技术的实现列表,请参考:bridges。
桥接API的更多信息,请参考头文件:include/asterisk/bridging.h和include/asterisk/bridging_technology.h.。
内核关于桥接技术的实现细节,请参考:main/bridging.c。
3.8 CDR处理器
Asterisk内核实现了保留通话记录的功能。这些记录在呼叫处理过程中建立,并缓存在数据结构里。在通话结束时,这些数据结构将被释
放。在记录丢弃之前,这些数据会传给已注册的CDR处理器。而处理器则会把记录写入文件或存入DB。
通常,CDR模块的代码存放在cdr/目录下。
CDR处理器的实现列表,请参考:Module:CDR Drivers。
CDR API相关的更多信息,请参考头文件include/asterisk/cdr.h。
内核中,与CDR相关的实现,请参考main/cdr.c。
3.9 CEL处理器
Asterisk内核实现了一个通用的事件系统,这个系统允许Asterisk组件报告事件,订阅事件。呼叫事件记录(CEL)就是建立在事件系统之上的一个应用。
CEL和CDR有点类似,它们都跟踪通话历史记录。通常CDR记录和呼叫是一一对应的关系;而CEL事件和通话则是多对一的关系。CEL模块和
CDR模块看起来很相似。
通常CEL模块存放在cel/目录下。
CEL API相关的更多信息,请参考头文件include/asterisk/cel.h。
内核关于CEL API的实现细节,请参考main/cel.c。
3.10拨号计划应用(APP)
app实现Asterisk拨号方案中可以与呼叫交互的功能。比如说:在extensions.conf文件中:
exten=> 123,1,NoOp()
在上例中,NoOp是一个APP。当然,实际上NoOp什么事也没做。
这些app使用Asterisk提供的一系列API与通道进行交互。App最重要的任务之一,是源源不
断地从通道里读取音频,同时向通道回写音频。完成这一任务的细节,通常隐藏在一个API调用的后面,比如说播放文件或等待用户按键
输入。
除了与原先执行应用的通道交互之外,APP有时还能创建额外的通道。比如说:Dial()这个APP会创建一个外呼通道,并将它与入呼通道
桥接在一块。有关APP功能的进一步讨论,将在场景细节中展开。
源码中,APP的实现代码通常存放在apps/目录下。
APP的实现列表,请参考:Module:Dial plan applications。
Asterisk内核注册APP相关的API定义信息,请参考头文件:include/asterisk/pbx.h。
3.11 拨号计划功能(FUN)
顾名思义,FUN和APP相同,是提供给Asterisk拨号方案用的。FUN在拨号方案中的使用方式,大部分和方案中的变量相同。它们提供读/写
接口,还有可选参数。虽然它们行为上和变量类似,但比起简单的文本值,APP的存储和检索要复杂得多了。
比方说:CHANNEL()这个FUN能让您访问当前通道上的数据。
exten=> 123,1,NoOp(This channel has the name: ${CHANNEL(name)})
通常,FUN的实现代码存放在funcs/目录下。
FUN的实现列表,请参考:Module:Dial plan functions。
Asterisk内核注册FUN相关的API定义信息,请参考头文件:include/asterisk/pbx.h。
3.12RTP引擎
Asterisk内核提供处理RTP流的API。但是,实际上处理这些流的是实现RTP引擎接口的模块。
RTP引擎的实现代码,存放在/res目录下,通常以res_rtp_为文件名前缀。
3.13 定时接口
Asterisk内核实现了定时API,供需要定时服务的组件调用。比如说,在向主叫方播放语音文件时,插入一个定时器来限定播放时间长度。
这些API依赖定时接口的实现来提供稳定可靠的计时源。
通常,这些接口实现的代码可以在res/目录中找到。
定时接口实现列表,请参考:timing_interfaces。
与定时API的定义信息,请参考头文件include/asterisk/timing.h。
内核的定时API实现代码,请参考main/timing.c。
4 Asterisk线程模型
Asterisk是一个多线程应用程序。它用POSIX线程API来管理线程和相碰的服务,比如说锁。Asterisk中,几乎所有与pthread交互相关的代
码,都通过一套统一的封装实现,这样可以减少调试和代码量。
Asterisk里的线程,可以划分为以下几种类型“
· 通道线程(有时也称为PBX线程)
· 网络监视线程
· 服务连接线程
· 其它线程
4.1 通道线程
通道是Asterisk的一个基本概念。通道不是inbound的,就是outbound的。呼叫到达Asterisk系统时,创建一个inbound通道。这些通道是
Asterisk拨号方案的执行方。每个执行拨号方案的通道,都建立一个线程。这些线程称为通道线程。因为这些线程的主要任务是为
inbound呼叫执行Asterisk的拨号方案,所以有时也称它们为PBX线程。
一个通道线程开始只负责一个Asterisk通道。然而,有时一个通道线程里也会有第二个通道的存在。当inbound通道执行了诸如Dial()的
APP之后,就在inbound线程里创建了一个outbound通道,并在对方应答之后将两个通道桥接在一起。
拨号方案的APP始终在一个通道线程的上下文里执行。FUN也是如此。虽然可以通过AMI或CLI之类的异步接口读写FUN,但无论如何,通道
线程始终是ast_channel数据结构的执行主体。
4.2 网络监视线程
Asterisk中,几乎所有主要通道驱动都有网络监视线程。这些线程负责监视网络连接(无论是IP网络还是PSTN等)、入呼和其它请求。
它们处理呼叫连接建立的前期步骤,如权鉴和拨号验证。最后,当呼叫建立之后,监视线程创建一个Asterisk通道 (ast_channel),
并启动一个通道线程来处理余下的呼叫时间。
4.3 服务连接线程
有许多基于TCP的服务也使用线程。比如SIP和AMI。在这些场景下,用线程来处理每个 TCP连接。
Asterisk的CLI也以同样的方式操作。然而,它用的不是TCP,而是UNIX socket连接。
4.4 其它线程
系统里,存在着各种执行某项待定任务的线程。比如说:事件API(include/asterisk/event.h)使用一个内部线程(main/event.c)来
处理异步事件分发。又如devicestateAPI (include/asterisk/devicestate.h)使用一个内部线程(main/devicestate.c)来处理异步
的设备状态变化信息。
5 其它架构概念
本节涵盖了其它一些重要的Asterisk架构概念。
5.1 通道桥接
正如前面讨论通道技术接口时所提及的,桥接动作把一个或多个通道连接在一起,使它们之间能够彼此交换音频包。然而,前面也提到,
现在的Asterisk代码中,很多地方还没有使用新的桥接架构设计。因为,本节讨论传统的桥接功能,它在Dial()和Queue()这些APP里还
在使用。
当调用这些APP,决定把两个通道桥接在一起时,它执行ast_channel_bridge()API调用。从这里开始,有可能出现两种不同的桥接:
1. 通用桥接Generic Bridge:通用桥接(ast_generic_bridge())是一种与具体使用的通道技术无关的桥接方法。它通过Asterisk抽象
的通道和帧接口交换音频数和信令,因此,它可以在任意两种通道驱动间通信。虽然这是最灵活的桥接方式,但同时它也是最低效的方
式,因此它需要抽象层参与。
2. 本地桥接Native Bridge:通道驱动可以选择实现自己的桥接功能函数。具体说来,这意味着要实现ast_channel_tech结构中的bridge回调函数。如果被桥接双方的驱动类型相同,并且驱动程序实现了本地桥接方法,那么Asterisk没理由迫使呼叫驻留在内核处理,这时它会调用本地桥接函数。这使得通道驱动能够利用类型相同的优势,优化桥接处理。在使用DAHDI的场合中,这意味着通道在硬件层面直接桥接了。在使用SIP时,这意味着Asterisk可以让音频流直接在终端间交互,而只要求信令流经过Asterisk。
6 代码流程实例
现在,我们已经讨论了Asterisk的各种组件,本节通过实例来说明这些组件是如何协同工作,向外提供强大的功能的。
6.1 SIP呼叫到Playback
这个例子假设通过SIP协议呼入Asterisk。Asterisk接受这通呼叫,然后向呼叫方播放一个语音文件,最后挂机。
实例拨号规则:
exten => 5551212,1,Answer()
exten => 5551212,n,Playback(demo-congrats)
exten => 5551212,n,Hangup()
1. 呼叫建立:从一个SIP INVITE开始这个场景。SIP通道驱动(chan_sip.c)收到这条消息。具体地说,是chan_sip的监听线程接收并
处理这条请求消息。进一步,监听消息负责完成呼叫建立的握手过程(SIP权鉴)。
2. 接受呼叫:一旦SIP通道驱动完成呼叫建立流程,它接受呼叫并启动Asterisk处理流程。为了完成这一任务,它必须先调用
ast_channel_alloc()API分配一个抽象通道的实例(ast_channel)。这个通道实例暂且称之为SIP通道。SIP通道驱动负责完成
SIP通道的初始化。SIP通道创建并初始化之后,创建一个通道线程来处理后续的呼叫流程(ast_pbx_start())。
3. 执行拨号方案::在通道线程的主循环中,查找对应extension的并执行。这些实现代码在main/pbx.c的ast_pbx_run()函数里。
4. 接听电话:一旦开始执行拨号方案,第一个执行的APP是Answer()。这个APP是一个内置APP,在main/pbx.c中实现的。
Answer()的实现代码简单地调用了ast_answer()API。这个API调用直接操作ast_channel。它可以处理通常的ast_channel挂机,
最终执行answer回调函数,这个回调函数关联在活跃通道的ast_channel_tech实例中。在这个场景中,最终执行的是实现
chan_sip.c的sip_answer()函数,这个函数将按SIP规范回应一个接听信令。
5. 播放语音文件:拨号方案的下一步动作是向呼叫方播放一个语音文件。执行的是Playback()这个APP。这个APP是在
apps/app_playback.c实现的。这个APP的实现代码是非常简单的。它先作参数处理,然后调用API来播放语音文件:
ast_streamfile()、ast_waitstream()和ast_stopstream()分别对应设置文件,等待文件播放完成和释放资源这三个动作。
这些API调用的一些重要操作步骤描述如下:
a. 打开文件:文件格式API负责打开语音文件的操作。它首先查找是否有以通道期待格式编码存储的文件。如果没有,它会找一个能
转换成通道期待编码的文件。一旦找到,调用恰当的文件格式接口来读取文件,并将文件内容转换为Asterisk音频帧。
b. 设置转换:如果文件里的音频编码格式和通道预期格式不匹配,那么文件API将通过编码转换API来设置转换路径。转换API将
调用对应的编码转换接口,以最小的开销将码流从源格式转换为目标格式。
c. 把音频发送给呼叫方:文件API将调用定时器API,以适时地将文件转换为音频帧并发送出去。与此同时,Asterisk会持续地从
通道中读取处理音频包,音频包是持续实时抵达的。然而,在本例场景中,它仅是将这些包丢弃而已。
6. 挂机:Playback()这个APP执行结束之后,拨号方案继续执行下一个APP,本例中就是Hangup()。这个操作和Answer()非常相似,
它处理与通道类型无关的挂机操作,然后调用SIP通道的回调接口来处理SIP规范的挂机流程。在这个点上,即使拨号方案中还
有其它步骤没处理,处理也必须停止,因为通道已经被挂断了。紧接着,通道线程将退出拨号计划处理循环,并销毁ast_channel
数据结构。
6.2 SIP到 IAX2 的呼叫桥接
这个例子假设外部通过SIP协议入呼到Asterisk系统,然后Asterisk通过IAX2协议发起一个outbound呼叫,对端通过IAX2应答之后,建立
桥接。
实例拨号方案:
exten => 5551212,n,Dial(IAX2/mypeer)
1. 呼叫建立:从一个SIP INVITE开始这个场景。SIP通道驱动(chan_sip.c)收到这条消息。具体地说,是chan_sip的监听线程接收并
处理这条请求消息。进一步,监听消息负责完成呼叫建立的握手过程(SIP权鉴)。
2. 接受呼叫:一旦SIP通道驱动完成呼叫建立流程,它接受呼叫并启动Asterisk处理流程。为了完成这一任务,它必须先调用
ast_channel_alloc()API分配一个抽象通道的实例(ast_channel)。这个通道实例暂且称之为SIP通道。SIP通道驱动负责完成
SIP通道的初始化。SIP通道创建并初始化之后,创建一个通道线程来处理后续的呼叫流程(ast_pbx_start())。
3. 执行拨号方案:在通道线程的主循环中,查找对应extension的并执行。这些实现代码在main/pbx.c的ast_pbx_run()函数里。
4. 执行 Dial():本例中,拨号方案里执行的唯一APP就是Dial():
创建一个Outbound通道: The Dial()需要创建一个outbound的ast_channel。它首先调用ast_request()API请求分配一个名为
IAX2/mypeer的通道。这个API是内核通道API(include/asterisk/channel.h)的一部分。它会查找类型为IAX2的通道驱动,然后
调用ast_channel_tech接口中的requeste回调函数。在这里,回调指向channels/chan_iax2.c实现的iax2_request()函数。
这个函数请求IAX2通道驱动分配一个IAX2类型的ast_channel通道,并初始化它。然后Dial()为新的通道调用ast_call() API。
这个API,调用ast_channel_tech接口里的call()回调函数,请求IAX2通道驱动初始化outbound呼叫。在IAX2的实现代码
(channels/chan_iax2.c)里,call回调指向iax2_call()函数。
b. 等待应答:这时候Dial()开始等待outbound通道应答呼叫。与此同时,它必须持续地为inbound和outbound两个通道所接收的音频
包提供服务。完成这项工作的循环体,和Asterisk的其它通道服务循环体相似。通道服务循环的核心功能就是调用ast_waitfor()
等待通道帧的到来,然后调用ast_read()读取帧。
c. 处理应答:一旦远端用户接听电话,Dial()将会把这个信息反馈给inbound通道。它时通过ast_answer()这个内核通道API调用实现
这个功能的。
d. 通道协调:在连接两个呼叫终端之前,Asterisk必须先协调两个通道,才能保证他们间的通话。具体地说,两个通道收发的音频编
码格式可能不同。必要时,调用ast_channel_make_compatible() API来为设置每个通道的编码转换路径。
e. 桥接通道:现在,inbound和outbound通道都已经完整建立,可以连接在一块了。这个连接是建立在两个通道之间的,这样它们间
可以来回地交换音频和信令,我们称之为桥接。处理桥接的API是ast_channel_bridge()。在这个例子中,桥接的处理过程是一个
通用桥接,调用的是ast_generic_bridge(),通用桥接是与通道类型无关的桥接过程。如果两个桥接通道的类型不一样,那么只能
用通用桥接了。桥接的核心功能是调用ast_waitfor()等待两个通道的数据。然后,如果某个通道有数据到达,则调用ast_read()
读取数据帧,然后调用ast_write(),把数据帧写给另一个通道。
f. 打破桥接:桥接状态会一直持续下去,直到某个打破桥接的事件触发,跳出桥接循环体,控制权返回给Dial()应用。比如说,呼叫
双方之一挂机,桥接就停止了。
5. 挂机::桥接停止之后,控制权返回给Dial()应用。因为是Dial()创建了outbound通道,所以这个通道隶属于Dial()。因此,
outboundIAX2通道将在Dial()结束之前被销毁。销毁通道是通过调用ast_hangup()这个API实现的。Dial()执行结束之后,控制权
返回到拨号方案执行循环体。这时,它会发现拨号方案已经执行到头了,因此,它会挂断inbound通道,同样,调用的API是
ast_hangup()。ast_hangup()执行一系列与通道类型无关的任务,也调用接ast_channel_tech接口里的hangup回调函数来执行与
通道类型相关的任务,在本例中,调用的chan_sip模块的sip_hangup()函数。最后,通道线程自然退出。
7 Asterisk数据结构
Asterisk提供了一些数据结构的通用实现。
7.1 Astobj2
Astobj2代表Asterisk对象模型,第二版。它的API定义在头文件include/asterisk/astobj2.h中。在main/astobj2.c文件里有astobj2的
实现细节。在源码树中,还保留着第一版的代码,然而我们不赞成继续使用它。
Astobj2提供引用计数对象处理。同时它还为astobj2对象提供了一套容器接口。容器提供的是一个哈希表。
关于astobj API的更多使用细节,请参考astobj2。在源码中,到处可以看到它的使用实例。
7.2 链表
Asterisk提供了一套宏,用于链表的处理。这些宏定义在头文件include/asterisk/linkedlists.h.中。
7.3 双端链表
同样的,Asterisk提供了一套宏,用于处理双端链表。这些宏在头文件include/asterisk/dlinkedlists.h.中定义。
7.4 堆Heap
Asterisk提供了一个最大堆数据结构的实现。堆相关的API定义,可以在include/asterisk/heap.h头文件中看到。堆的实现代码则在
main/heap.c文件中。
8 Asterisk 调试工具
Asterisk提供了一些内置的调试工具,以帮助诊断一些常见的问题。
8.1 线程调试
Asterisk保持跟踪系统中的所有活跃线程。通过AsteriskCLI,执行core showthreads命令,可以看到系统中的线程列表。
Asterisk有一个叫DEBUG_THREADS的编译选项。这个编译开关打开后,Asterisk封装的pthread API就会保持记录与线程和锁相关的一些
附加信息,以帮助调试。除了线程列表之外,Asterisk还维护了系统中每个线程锁的信息。它也知道一个线程因为尝试获取一个锁资源
而堵塞的信息。在调试死锁时,所有这些信息都非常有用。这些数据,可以通过Asterisk CLI,执行core show locks命令获取。
这些封装的定义信息,可以在头文件include/asterisk/lock.h和include/asterisk/utils.h中找到。大部分实现代码都在main/utils.c
里。
8.2 内存调试
Asterisk的动态内存管理,是通过一套封装的接口处理的。这些封装在头文件include/asterisk/utils.h中定义。缺省情况下,这些封装
使用标准C库函数里的malloc()、free(),等。如果编译时打开MALLOC_DEBUG编译开关,则会加入一些内存调试信息。
Asterisk内存调试系统提供以下几种功能:
· 跟踪当前分配的所有内存块,包括内存初始化时的大小、文件、函数和行号。
· 内存释放时,做一些基本的防御检查,检查内存块的写入情况。
· 释放非法内存时,给出通知
Asterisk提供了一些CLI命令,用于查询当前内存分配状况:
· memory show summary
· memory show allocations
实现内存调试系统的代码文件是main/astmm.c。