Chinaunix首页 | 论坛 | 博客
  • 博客访问: 430124
  • 博文数量: 168
  • 博客积分: 320
  • 博客等级: 二等列兵
  • 技术积分: 955
  • 用 户 组: 普通用户
  • 注册时间: 2012-06-21 08:40
个人简介

知足却不乏追求

文章分类

全部博文(168)

文章存档

2017年(1)

2016年(6)

2015年(36)

2014年(5)

2013年(6)

2012年(114)

分类: C/C++

2014-11-29 18:14:17

原文地址:asterisk代码框架 作者:shaohui973

英文出处:

 

作者:

RussellBryant <>

注意:

这篇文档所描述的内容,可能已经过时。为了保证您所获取的信息是最新的,请您确保您使用的文档是从Asterisktrunk上生成的。

 

1    引言

本文档从一个开发者的角度出发,概要描述Asterisk的体系架构。至于详细的API讨论,请参考公开API头文件所关联的文档。本文档假定您了解Asterisk的一些知识,并知道如何使用它。

本文的意图是:从一个高的层次开始了解Asterisk,并逐步深入。它从Asterisk的组件差异开始,最终讨论这些组件在不同应用场景里的协作关系。

文中,提供了很多交叉引用链接,指向相关API的一些引用参考,也可能指向相关的源码链接。

欢迎对本文档的反馈和贡献。请将您的真知灼见发给asterisk开发组的邮件组:

 

谢谢,并预祝您享受Asterisk!

2      模块构架

Asterisk是一个高度模块化的应用。在源码的main/目录下,建立了内核应用。然而,它(内核)本身的用处并不是很大。

运行时,Asterisk加载了许多模块。Asterisk的模块都有具体的名称,以标识模块所提供的功能,但是,这些名称没有任何技术意义上的特殊。Asterisk加载一个模块时,模块向内核注册它所提供的功能。整个流程看起来是这样的:

1.   启动Asterisk

2.   Asterisk加载模块

3.   模块跟内核说:嗨,Asterisk,我是一个模块,我能提供XYZ三种功能,用得着的时候要记得我哦。

3    抽象接口类型

Asterisk提供了许多不同类型的接口,具体的模块可以实现这些接口并注册给内核调用。任何模块,都可以注册任意多种的接口。通常,一个模块内整合了某些相关的功能。

本节讨论接口的类型,后续将讨论各种场景下不同组件间的协作关系。

3.1   编码解释器CodecInterpreter

编码解释器接口的实现,提供了两种编码间的转换能力。Asterisk当前只有音频编码转换的能力。

这些模块不了解话务相关的任何信息,也不知为什么要调用它们进行音频转换。它们仅需要知道音频采样率、音频的输入格式、期待的输出格式这些信息。

如果注册了多个编码解释器,那么编码A转换为编码B的过程,就可能有多种不同的转换路径。在(编码)模块加载之后,Asterisk建立一张转换表,表中包含不同的转换开销评估值,因此,Asterisk能够找出A转换B的最佳路径。

在源码树中,编码模块通常在codecs/目录下。

已有编码解释器的实现列表,请参考:

更多编码解释器API的信息,请参考接口定义文件:.

内核关于编码解释器的相关实现,参考源码:

3.2   文件格式处理器File Format Handler

文件格式处理器接口的实现,为Asterisk提供了读写文件的能力。文件格式处理器可以提供音频、视频或图像文件的处理能力。

文件处理器的接口是相当原始的。模块简单地告诉Asterisk内核:它能处理某种具有待定扩展名的文件,比如说".wav"。同时,它还说明读取文件之后,将以编码X的形式提供音频。如果它还提供写文件的能力,那么它还必须说明用它写文件的音频编码要求(即说明它能把什么编码格式的音频编码写成带什么扩展名的文件)

源码树中,文件格式模块通存放常在formats/目录下。

现有的实现,请参考:

文件格式处理器的API定义信息,请参考头文件:

内核中,与文件格式相关的实现,请参考:

3.3     C API Providers

Asterisk有一些可选的C API。内核API主应用内置的,始终可用。可选CAPI则是由某个模块提供的,只有在相应模块加载后,才可用。某些API的提供方,也提供了它自身的接口,供其它模块实现和注册(接口)。

提供C API的模块,通常存放在res/目录下。

一些提供C API的模块有:

·        

·        

o   提供一种日历技术接口

·        

·        

·        

·        res_curl.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的其它特性,才能不依赖于具体的电话协议。

通道驱动实现的具体接口是所封装的接口。一个通道驱动,必须实现执行各种呼叫信令任务的回调函数。比如说,必须实现一个初始化呼叫的方法,实现一个挂断呼叫的方法,等等。数据结构是抽象通道数据结构。每个实例,有一个关联的以标识通道类型。一个实例,描述了呼叫中的一条腿(call leg的概念,也就是Asterisk与终端设备间的连接概念)

源码中,通道驱动通常在channels/目录里。

当前实现的通道驱动列表,请参考:

需要进一步了解通道API,请参考头文件

内核中,关于 API的实现,则在中。

3.7     桥接技术

桥接,是把两个或多个通道连接在一起的操作。通道,AB的呼叫,用的是简单的双通道桥接,而在三方通话或会议中,用的就是多方桥接技术。

桥接API允许其它模块注册桥接技术。桥接技术的实现,知道如何选择两个(或多个)通道,并将它们连接在一起。具体是怎样发生的,取决于实现。

这些接口的代码,需要在两个(或多个)通道间交换音频,却又不需要知道交换的实现细节。在底层,会议可能由操作系统内核实现(通过DAHDI);也可能由Asterisk的内部方法实现;如果有人实现了硬件扩展模块,还可能用硬件实现。

写这篇文档时,桥接API相对来说还比较新,所以执行桥接应用的操作,还没有全部使用这些API。在拨号计划应用实现里,ConfBridge是在桥接API之上实现的一个会议应用。

桥接技术实现模块,存放在bridges/目录下。

桥接技术的实现列表,请参考:

桥接API的更多信息,请参考头文件:.

内核关于桥接技术的实现细节,请参考:

3.8     CDR处理器

Asterisk内核实现了保留通话记录的功能。这些记录在呼叫处理过程中建立,并缓存在数据结构里。在通话结束时,这些数据结构将被释放。在记录丢弃之前,这些数据会传给已注册的CDR处理器。而处理器则会把记录写入文件或存入DB

通常,CDR模块的代码存放在cdr/目录下。

CDR处理器的实现列表,请参考:

CDR API相关的更多信息,请参考头文件

内核中,与CDR相关的实现,请参考

3.9     CEL处理器

Asterisk内核实现了一个通用的事件系统,这个系统允许Asterisk组件报告事件,订阅事件。呼叫事件记录(CEL)就是建立在事件系统之上的一个应用。

CELCDR有点类似,它们都跟踪通话历史记录。通常CDR记录和呼叫是一一对应的关系;而CEL事件和通话则是多对一的关系。CEL模块和CDR模块看起来很相似。

通常CEL模块存放在cel/目录下。

CEL API相关的更多信息,请参考头文件

内核关于CEL API的实现细节,请参考

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的实现列表,请参考:

Asterisk内核注册APP相关的API定义信息,请参考头文件:

3.11   拨号计划功能(FUN)

顾名思义,FUNAPP相同,是提供给Asterisk拨号方案用的。FUN在拨号方案中的使用方式,大部分和方案中的变量相同。它们提供读/写接口,还有可选参数。虽然它们行为上和变量类似,但比起简单的文本值,APP的存储和检索要复杂得多了。

比方说:CHANNEL()这个FUN能让您访问当前通道上的数据。

exten=> 123,1,NoOp(This channel has the name: ${CHANNEL(name)})

通常,FUN的实现代码存放在funcs/目录下。

FUN的实现列表,请参考:

Asterisk内核注册FUN相关的API定义信息,请参考头文件:

3.12RTP引擎

Asterisk内核提供处理RTP流的API。但是,实际上处理这些流的是实现RTP引擎接口的模块。

RTP引擎的实现代码,存放在/res目录下,通常以res_rtp_为文件名前缀。

3.13   定时接口

Asterisk内核实现了定时API,供需要定时服务的组件调用。比如说,在向主叫方播放语音文件时,插入一个定时器来限定播放时间长度。这些API依赖定时接口的实现来提供稳定可靠的计时源。

通常,这些接口实现的代码可以在res/目录中找到。

定时接口实现列表,请参考:

与定时API的定义信息,请参考头文件

内核的定时API实现代码,请参考

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也是如此。虽然可以通过AMICLI之类的异步接口读写FUN,但无论如何,通道线程始终是数据结构的执行主体。

4.2     网络监视线程

Asterisk中,几乎所有主要通道驱动都有网络监视线程。这些线程负责监视网络连接(无论是IP网络还是PSTN)、入呼和其它请求。它们处理呼叫连接建立的前期步骤,如权鉴和拨号验证。最后,当呼叫建立之后,监视线程创建一个Asterisk通道 (),并启动一个通道线程来处理余下的呼叫时间。

4.3     服务连接线程

有许多基于TCP的服务也使用线程。比如SIPAMI。在这些场景下,用线程来处理每个 TCP连接。

AsteriskCLI也以同样的方式操作。然而,它用的不是TCP,而是UNIX socket连接。

4.4     其它线程

系统里,存在着各种执行某项待定任务的线程。比如说:事件API()使用一个内部线程()来处理异步事件分发。又如devicestateAPI (include/asterisk/devicestate.h)使用一个内部线程(main/devicestate.c)来处理异步的设备状态变化信息。

5      其它架构概念

本节涵盖了其它一些重要的Asterisk架构概念。

5.1     通道桥接

正如前面讨论通道技术接口时所提及的,桥接动作把一个或多个通道连接在一起,使它们之间能够彼此交换音频包。然而,前面也提到,现在的Asterisk代码中,很多地方还没有使用新的桥接架构设计。因为,本节讨论传统的桥接功能,它在Dial()Queue()这些APP里还在使用。

当调用这些APP,决定把两个通道桥接在一起时,它执行API调用。从这里开始,有可能出现两种不同的桥接:

1.   通用桥接Generic Bridge:通用桥接()是一种与具体使用的通道技术无关的桥接方法。它通过Asterisk抽象的通道和帧接口交换音频数和信令,因此,它可以在任意两种通道驱动间通信。虽然这是最灵活的桥接方式,但同时它也是最低效的方式,因此它需要抽象层参与。

2.   本地桥接Native Bridge:通道驱动可以选择实现自己的桥接功能函数。具体说来,这意味着要实现结构中的bridge回调函数。如果被桥接双方的驱动类型相同,并且驱动程序实现了本地桥接方法,那么Asterisk没理由迫使呼叫驻留在内核处理,这时它会调用本地桥接函数。这使得通道驱动能够利用类型相同的优势,优化桥接处理。在使用DAHDI的场合中,这意味着通道在硬件层面直接桥接了。在使用SIP时,这意味着Asterisk可以让音频流直接在终端间交互,而只要求信令流经过Asterisk

6      代码流程实例

现在,我们已经讨论了Asterisk的各种组件,本节通过实例来说明这些组件是如何协同工作,向外提供强大的功能的。

6.1     SIP呼叫到Playback

这个例子假设通过SIP协议呼入AsteriskAsterisk接受这通呼叫,然后向呼叫方播放一个语音文件,最后挂机。

实例拨号规则:

exten => 5551212,1,Answer()
exten => 5551212,n,Playback(demo-congrats)
exten => 5551212,n,Hangup()

 

1.   呼叫建立:从一个SIP INVITE开始这个场景。SIP通道驱动()收到这条消息。具体地说,是chan_sip的监听线程接收并处理这条请求消息。进一步,监听消息负责完成呼叫建立的握手过程(SIP权鉴)。

2.   接受呼叫:一旦SIP通道驱动完成呼叫建立流程,它接受呼叫并启动Asterisk处理流程。为了完成这一任务,它必须先调用API分配一个抽象通道的实例()。这个通道实例暂且称之为SIP通道。SIP通道驱动负责完成SIP通道的初始化。SIP通道创建并初始化之后,创建一个通道线程来处理后续的呼叫流程()

3.   执行拨号方案::在通道线程的主循环中,查找对应的并执行。这些实现代码在ast_pbx_run()函数里。

4.   接听电话:一旦开始执行拨号方案,第一个执行的APPAnswer()。这个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规范的挂机流程。在这个点上,即使拨号方案中还有其它步骤没处理,处理也必须停止,因为通道已经被挂断了。紧接着,通道线程将退出拨号计划处理循环,并销毁数据结构。

6.2     SIP IAX2 的呼叫桥接

这个例子假设外部通过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()

    1. 创建一个Outbound通道: The Dial()需要创建一个outbound。它首先调用API请求分配一个名为IAX2/mypeer的通道。这个API是内核通道API()的一部分。它会查找类型为IAX2的通道驱动,然后调用接口中的requeste回调函数。在这里,回调指向实现的函数。这个函数请求IAX2通道驱动分配一个IAX2类型的通道,并初始化它。然后Dial()为新的通道调用 API。这个API,调用接口里的call()回调函数,请求IAX2通道驱动初始化outbound呼叫。在IAX2的实现代码()里,call回调指向iax2_call()函数。

b.   等待应答:这时候Dial()开始等待outbound通道应答呼叫。与此同时,它必须持续地为inboundoutbound两个通道所接收的音频包提供服务。完成这项工作的循环体,和Asterisk的其它通道服务循环体相似。通道服务循环的核心功能就是调用等待通道帧的到来,然后调用读取帧。

c.   处理应答:一旦远端用户接听电话,Dial()将会把这个信息反馈给inbound通道。它是通过这个内核通道API调用实现这个功能的。

d.   通道协调:在连接两个呼叫终端之前,Asterisk必须先协调两个通道,才能保证他们间的通话。具体地说,两个通道收发的音频编码格式可能不同。必要时,调用 API来为设置每个通道的编码转换路径。

e.   桥接通道:现在,inboundoutbound通道都已经完整建立,可以连接在一块了。这个连接是建立在两个通道之间的,这样它们间可以来回地交换音频和信令,我们称之为桥接。处理桥接的API。在这个例子中,桥接的处理过程是一个通用桥接,调用的是,通用桥接是与通道类型无关的桥接过程。如果两个桥接通道的类型不一样,那么只能用通用桥接了。桥接的核心功能是调用等待两个通道的数据。然后,如果某个通道有数据到达,则调用读取数据帧,然后调用,把数据帧写给另一个通道。

f.    打破桥接:桥接状态会一直持续下去,直到某个打破桥接的事件触发,跳出桥接循环体,控制权返回给Dial()应用。比如说,呼叫双方之一挂机,桥接就停止了。

5.   挂机::桥接停止之后,控制权返回给Dial()应用。因为是Dial()创建了outbound通道,所以这个通道隶属于Dial()。因此,outboundIAX2通道将在Dial()结束之前被销毁。销毁通道是通过调用这个API实现的。Dial()执行结束之后,控制权返回到拨号方案执行循环体。这时,它会发现拨号方案已经执行到头了,因此,它会挂断inbound通道,同样,调用的API执行一系列与通道类型无关的任务,也调用接接口里的hangup调函数来执行与通道类型相关的任务,在本例中,调用的chan_sip模块的函数。最后,通道线程自然退出。

7      Asterisk数据结构

Asterisk提供了一些数据结构的通用实现。

7.1     Astobj2

Astobj2代表Asterisk对象模型,第二版。它的API定义在头文件中。在文件里有的实现细节。在源码树中,还保留着第一版的代码,然而我们不赞成继续使用它。

Astobj2提供引用计数对象处理。同时它还为对象提供了一套容器接口。容器提供的是一个哈希表。

关于astobj API的更多使用细节,请参考。在源码中,到处可以看到它的使用实例。

7.2     链表

Asterisk提供了一套宏,用于链表的处理。这些宏定义在头文件.中。

7.3     双端链表

同样的,Asterisk提供了一套宏,用于处理双端链表。这些宏在头文件.中定义。

7.4     Heap

Asterisk提供了一个最大堆数据结构的实现。堆相关API定义,可以在头文件中看到。堆的实现代码则在文件中。

8    Asterisk调试工具

Asterisk提供了一些内置的调试工具,以帮助诊断一些常见的问题。

8.1   线程调试

Asterisk保持跟踪系统中的所有活跃线程。通过AsteriskCLI,执行core showthreads命令,可以看到系统中的线程列表。

Asterisk有一个叫DEBUG_THREADS的编译选项。这个编译开关打开后,Asterisk封装的pthread API就会保持记录与线程和锁相关的一些附加信息,以帮助调试。除了线程列表之外,Asterisk还维护了系统中每个线程锁的信息。它也知道一个线程因为尝试获取一个锁资源而堵塞的信息。在调试死锁时,所有这些信息都非常有用。这些数据,可以通过Asterisk CLI,执行core show locks命令获取。

这些封装的定义信息,可以在头文件中找到。大部分实现代码都在

8.2   内存调试

Asterisk的动态内存管理,是通过一套封装的接口处理的。这些封装在头文件中定义。缺省情况下,这些封装使用标准C库函数里的,等。如果编译时打开MALLOC_DEBUG编译开关,则会加入一些内存调试信息。

Asterisk内存调试系统提供以下几种功能:

·        跟踪当前分配的所有内存块,包括内存初始化时的大小、文件、函数和行号。

·        内存释放时,做一些基本的防御检查,检查内存块的写入情况。

·        释放非法内存时,给出通知

Asterisk提供了一些CLI命令,用于查询当前内存分配状况:

·        memory show summary

·        memory show allocations

实现内存调试系统的代码文件是
阅读(1081) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~