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