本文档从一个开发者的角度出发,概要描述Asterisk的体系架构。至于详细的API讨论,请参考公开API头文件所关联的文档。
本文档假定您了解Asterisk的一些知识,并知道如何使用它。
本文的意图是:从一个高的层次开始了解Asterisk,并逐步深入。它从Asterisk的组件差异开始,最终讨论这些组件在不同应用场景里
的协作关系。
文中,提供了很多交叉引用链接,指向相关API的一些引用参考,也可能指向相关的源码链接。
欢迎对本文档的反馈和贡献。请将您的真知灼见发给asterisk开发组的邮件组:http://lists.digium.com/.
谢谢,并预祝您享受Asterisk!
引言说得很清楚了,这篇文章讲的是asterisk的体系架构,会从一个高的层次讲asterisk的知识,所以我们的目标不是读懂代码数据结构是什么,而是读懂asterisk是做什么的,模块与模块间的关系,模块的作用。
Asterisk是一个高度模块化的应用。在源码的main/目录下,建立了内核应用。然而,它(内核)本身的用处并不是很大。所谓内核我们一般理解为操作硬件的部分,而我们经常听到的内核,通常指的是系统内核,操作CPU网卡等硬件的部分,而asterisk的内核同样可以理解为操作硬件的部分,准确的说是驱动,那这里的硬件指的是什么呢,那就是通信硬件与协议栈,例如模拟话机于IP话机通信,那么其模拟信号必须转化为IP话机的信号,至于是如何转换的,我们就不具体理解了,这些asterisk都会帮你完成。
运行时,Asterisk加载了许多模块。Asterisk的模块都有具体的名称,以标识模块所提供的功能,但是,这些名称没有任何技术意义上的特殊。Asterisk加载一个模块时,模块向内核注册它所提供的功能。
这里几乎体现了asterisk强大功能的核心原理,asterisk实现功能的本质是通过加载模块的方式,asterisk加载一个模块时,模块向内核注册它所提供的功能,只要以此为遵循,我们想要扩展功能,只需要新增模块即可,每个模块之间是独立的关系,就好比windows是我们的操作系统,他提供我们底层调用接口,而我们想要什么功能比如说QQ,微信,还是英雄联盟,直接下载基于windows系统的app就行了。这么说你可能会更能理解main目录下asterisk内核的作用,这就是为什么要分成模块功能代码和内核代码的区别了。
因此整个流程看起来是这样的:
启动Asterisk
Asterisk加载模块
模块跟内核说:嗨,Asterisk,我是一个模块,我能提供X、Y、Z三种功能,用得着的时候要记得我哦。
记住这三点,asterisk是美女,模块是备胎,备胎对美女说,女神你快来,我是备胎,你可利用我,因为我有这些功能,利用完你就把我丢掉就好,但是我仍然爱你,因此美女做事找备胎即可,要知道美女的备胎是很多的噢。
Asterisk提供了许多不同类型的接口,具体的模块可以实现这些接口并注册给内核调用。任何模块,都可以注册任意多种的接口。通常,一个模块内整合了某些相关的功能。这里比如说你要使用pjsip协议栈进行通信,你就可以编写实现pjsip功能的接口,然后注册给内核调用,那么这部分的通信就是按照由pjsip协议进行的。
本节讨论接口的类型,后续将讨论各种场景下不同组件间的协作关系。
编码解释器接口的实现,提供了两种编码间的转换能力。Asterisk当前只有音频编码转换的能力。
注意,注意,这是我们第一个抽象接口类型,编码解释器接口,提供两种编码之间的转换能力,如不同音频之间的编码方式各有不同,如何翻译一种编码,并转化成另一种编码,都需要此类编码解释器。我妈在此处可以假设asterisk内核中所支持的编码类型A,而传入的音频编码方式B,为什么两种编码方式不一样?这我也不不得而知,但可以猜测,很有可能是对原始的音频进行了加密处理,或者是采集音频的格式或是传入音频的格式asterisk是无法正常识别的。
这些模块不了解话务相关的任何信息,也不知为什么要调用它们进行音频转换。它们仅需要知道音频采样率、音频的输入格式、期待的输出格式这些信息。你看,这些模块虽然提供了音频转化的功能,辛辛苦苦的整理音频采样率。输入格式,并输出,但对通话却是一无所知,唉只能是个妥妥的备胎了。
如果注册了多个编码解释器,那么编码A转换为编码B的过程,就可能有多种不同的转换路径。在(编码)模块加载之后,Asterisk建立一张转换表,表中包含不同translator的转换开销评估值,因此,Asterisk能够找出A转换B的最佳路径。这就更过分了,因为备胎多了,美女还需要评估各个备胎哪个条件好再进行选择,比如你钱多不多,大不大,帅不帅,这么看asterisk就是一个妥妥的渣女。
我们可以看看其源码目录:各种各样的编解码算法!
在源码树中,编码模块通常在codecs/目录下。
已有编码解释器的实现列表,请参考:Module:Codecs
更多编码解释器API的信息,请参考接口定义文件:include/asterisk/translate.h.
具体的API声明放在了上面的地方。
内核关于编码解释器的相关实现,参考源码:main/translate.c
记住内核实现的都在main目录里面
文件格式处理器接口的实现,为Asterisk提供了读写文件的能力。文件格式处理器可以提供音频、视频或图像文件的处理能力。
这里注意了噢,文件格式处理器其实也是一个抽象接口类型,但你要具体实现这个接口的功能,必须在外部模块来实现,然后注册到asterisk里面,asterisk后期会直接调用这个接口。
文件处理器的接口是相当原始的。模块简单地告诉Asterisk内核:它能处理某种具有待定扩展名的文件,比如说".wav"。同时,它还说明读取文件之后,将以编码X的形式提供音频,编码X就是最终传给asterisk的格式。如果它还提供写文件的能力,那么它还必须说明用它写文件的音频编码要求(即说明它能把什么编码格式的音频编码写成带什么扩展名的文件)。
文件处理器也算是我们常用的模块了,就像图片有png,jpg等等,音频同样有多种,如.wav,.mp3,.cda等等。
源码树中,文件格式模块通存放常在formats/目录下。
现有的实现,请参考:Module:Media File Formats。
文件格式处理器的API定义信息,请参考头文件:include/asterisk/file.h。
注意,模块API定义信息一般都存在include/asterisk/file.h的路径下。
内核中,与文件格式相关的实现,请参考:main/file.c。
Asterisk有一些可选的C API。内核API是主应用内置的,始终可用。可选CAPI则是由某个模块提供的,只有在相应模块加载后,才可用。
注意这里提到的API与上面所说的模块不能等同理解,我们在写dialplan拨号脚本的时候,会使用到设置变量,播放等待音等等的函数功能,而这里的API所提供的正是此类函数功能。
某些API的提供方,也提供了它自身的接口,供其它模块实现和注册(接口)。
提供C API的模块,通常存放在res/目录下。
一些提供C API的模块有:
· res_musiconhold.c
· res_calendar.c
提供一种日历技术接口
· 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
提供一种语音识别引擎接口
Asterisk管理接口是一个socket接口,用于监视和控制Asterisk。它是建立于主应用的核心功能。继而,其它模块可以向AMI注册自己的action供客户端调用。所谓AMI就是Asterisk Management Interface。这里我们理解一下socket,其实大家都明白socket的名字是套接字,做通信用的,但是我们往往使用的都是基于传输层的套接字,参数为域、端口号、协议类型此类的,但其实还有基于网络层和链路层的scoket,通过socket我们将走OSI七层模型的数据报文抓取,并进行解包分析,封包,再发送完成两者之间的通信。
向AMI注册action的模块,通常提供了某种辅助功能以补充扩展某项主要的功能(这个功能不一定是内核功能,可能是模块自身的功能)。
比方说:一个提供电话会议功能的模块,提供了一个管理action接口,用于返回与会者列表。
Asterisk CLI是主应用实现的命令行管理功能。外围模块可以注册附加的CLI命令。我们通过AMI与asterisk建立连接,并通过执行CLI指令,获取我们想得到的资源。
Asterisk通道接口是最复杂、最重要的接口。Asterisk通道API提供了电话协议的抽象,这样,所有Asterisk的其它特性,才能不依赖于具体的电话协议。通道驱动实现的具体接口是ast_channel_tech所封装的接口。一个通道驱动,必须实现执行各种呼叫信令任务的回调函数。比如说,必须实现一个初始化呼叫的方法,实现一个挂断呼叫的方法,等等。数据结构ast_channel是抽象通道数据结构。每个ast_channel实例,有一个关联的ast_channel_tech以标识通道类型。一个ast_channel实例,描述了呼叫中的一条腿(call leg的概念,也就是Asterisk与终端设备间的连接概念)。
这个通道驱动是极为重要的概念,所谓通道就是asterisk和电话建立的连接,两台电话想要相互拨打,必须分别与asterisk建立连接,即形成l两个通道,然后再将两个通道桥接在一起才能通话。就比如模拟话机走的是FXS而SIP话机走的是网线,我们是必须通过对应的协议将其进行转化的。
源码中,通道驱动通常在channels/目录里。
当前实现的通道驱动列表,请参考:Module:Asterisk Channel Drivers。
需要进一步了解通道API,请参考头文件include/asterisk/channel.h。
内核中,关于ast_channel API的实现,则在main/channel.c中。
桥接,是把两个或多个通道连接在一起的操作。通道,A到B的呼叫,用的是简单的双通道桥接,而在三方通话或会议中,用的就是多方桥接技术。
桥接API允许其它模块注册桥接技术。桥接技术的实现,知道如何选择两个(或多个)通道,并将它们连接在一起。具体是怎样发生的,取决
于实现。
这些接口的代码,需要在两个(或多个)通道间交换音频,却又不需要知道交换的实现细节。在底层,会议可能由操作系统内核实现(通过DAHDI);也可能由Asterisk的内部方法实现;如果有人实现了硬件扩展模块,还可能用硬件实现。
写这篇文档时,桥接API相对来说还比较新,所以执行桥接应用的操作,还没有全部使用这些API。在拨号计划应用实现里,ConfBridge是在桥接API之上实现的一个会议应用。看到这里大家心里应该会松一口气,asterisk告诉了我妈一件很简单的事情,桥接是怎么桥接的你不用懂,是astersik帮你做的,同时他给你提供了使用桥接技术的接口即API,如ConfBridge,我们只需要按照函数给的参数去传参,就能实现两端通话了,难道不香吗,当然,如果你有能力,当然建议你深入看下去,尤其是asterisk的内核代码。
桥接技术实现模块,存放在bridges/目录下。
桥接技术的实现列表,请参考:bridges。
桥接API的更多信息,请参考头文件:include/asterisk/bridging.h和include/asterisk/bridging_technology.h.。
内核关于桥接技术的实现细节,请参考:main/bridging.c。
Asterisk内核实现了保留通话记录的功能。这些记录在呼叫处理过程中建立,并缓存在数据结构里。在通话结束时,这些数据结构将被释
放。在记录丢弃之前,这些数据会传给已注册的CDR处理器。而处理器则会把记录写入文件或存入DB。记住这个逻辑流程,就好像把大象放进冰箱里,需要走三步,第一步在发生通话时astersik内核实现保留通话记录的功能,并缓存在数据结构里,第二步通话结束时,把记录传给CDR处理器,并释放通话数据。第三步,CDR处理器将收到的记录写入文件或者是数据库,永久保存。真是细思即恐,astersisk真是好一个渣女啊,玩弄被胎还要记录他被玩弄的样子。
通常,CDR模块的代码存放在cdr/目录下。
CDR处理器的实现列表,请参考:Module:CDR Drivers。
CDR API相关的更多信息,请参考头文件include/asterisk/cdr.h。
内核中,与CDR相关的实现,请参考main/cdr.c。
Asterisk内核实现了一个通用的事件系统,这个系统允许Asterisk组件报告事件,订阅事件。呼叫事件记录(CEL)就是建立在事件系统之上的一个应用。
CEL和CDR有点类似,它们都跟踪通话历史记录。通常CDR记录和呼叫是一一对应的关系;而CEL事件和通话则是多对一的关系。CEL模块和CDR模块看起来很相似。啧啧,多个CEL对一个通话,发生了什么事件立即响应,还有团伙作案。
通常CEL模块存放在cel/目录下。
CEL API相关的更多信息,请参考头文件include/asterisk/cel.h。
内核关于CEL API的实现细节,请参考main/cel.c。
app实现Asterisk拨号方案中可以与呼叫交互的功能。注意,好戏开场了,我之前提到C API并不是直接在Dialplan中调用的,而是通过APP的形式,APP将API封装在里面,比如说:在extensions.conf文件中:
exten=> 123,1,NoOp()
在上例中,NoOp是一个APP。当然,实际上NoOp什么事也没做(他只是一个普普通通的打印函数)。
这些app使用Asterisk提供的一系列API与通道进行交互。App最重要的任务之一,是源源不断地从通道里读取音频,同时向通道回写音频。完成这一任务的细节,通常隐藏在一个API调用的后面,比如说播放文件或等待用户按键
输入。注意,这个通道指的是channel,之前已经提到过了,一台分机想拨通另一台分机,必须分别与asterisk建立通道连接,因此就有两个通道,两个通道再桥接,才能真正实现语音流的交互。
除了与原先执行应用的通道交互之外,APP有时还能创建额外的通道。比如说:Dial()这个APP会创建一个外呼通道,并将它与入呼通道桥接在一块。有关APP功能的进一步讨论,将在场景细节中展开。
APP的实现列表,请参考:Module:Dial plan applications。
Asterisk内核注册APP相关的API定义信息,请参考头文件:include/asterisk/pbx.h。
顾名思义,FUN和APP相同,是提供给Asterisk拨号方案用的。FUN在拨号方案中的使用方式,大部分和方案中的变量相同。它们提供读/写接口,还有可选参数。虽然它们行为上和变量类似,但比起简单的文本值,APP的存储和检索要复杂得多了。
比方说:CHANNEL()这个FUN能让您访问当前通道上的数据。
exten=> 123,1,NoOp(This channel has the name: ${CHANNEL(name)})
通常,FUN的实现代码存放在funcs/目录下。
但是无论是哪个小伙伴看到这里都会有这个疑惑,都是再拨号脚本里面调用,那么FUN和APP有什么差别呢,为什么不都放到FUN目录下,或者是APP目录下呢?其实我也不太明白,不过我这里有一个思考的角度,大家可以尝试理解,其实在asterisk的源码中,我们是可以看到源码是有在大量调用APP中的函数,却并没有调用FUN中的函数,那么我们是不是可以理解为,APP不仅提供给拨号脚本使用,还提供给asterisk使用,但是FUN只提供给拨号脚本使用。
FUN的实现列表,请参考:Module:Dial plan functions。
Asterisk内核注册FUN相关的API定义信息,请参考头文件:include/asterisk/pbx.h。
Asterisk内核提供处理RTP流的API。但是,实际上处理这些流的是实现RTP引擎接口的模块。
实时传输协议(RTP)为数据提供了具有实时特征的端对端传送服务,如在组播或单播网络服务下的交互式视频音频或模拟数据。应用程序通常在 UDP 上运行 RTP 以便使用其多路结点和校验服务;这两种协议都提供了传输层协议的功能。但是 RTP 可以与其它适合的底层网络或传输协议一起使用。如果底层网络提供组播方式,那么 RTP 可以使用该组播表传输数据到多个目的地。
RTP引擎的实现代码,存放在/res目录下,通常以res_rtp_为文件名前缀。反正这也是个抽象类型接口就是了。
Asterisk内核实现了定时API,供需要定时服务的组件调用。比如说,在向主叫方播放语音文件时,插入一个定时器来限定播放时间长度。
这些API依赖定时接口的实现来提供稳定可靠的计时源。这就是我上面说的,在asterisk的代码中是可以调用API的,但没见过掉FUN的。
通常,这些接口实现的代码可以在res/目录中找到。
定时接口实现列表,请参考:timing_interfaces。
与定时API的定义信息,请参考头文件include/asterisk/timing.h。
内核的定时API实现代码,请参考main/timing.c。
Asterisk是一个多线程应用程序。它用POSIX线程(Portable Operating System Interface of UNIX 可移植操作系统接口)API来管理线程和相碰的服务,比如说锁。Asterisk中,几乎所有与pthread交互相关的代码,都通过一套统一的封装实现,这样可以减少调试和代码量。
Asterisk里的线程,可以划分为以下几种类型“
· 通道线程(有时也称为PBX线程)
· 网络监视线程
· 服务连接线程
· 其它线程
通道是Asterisk的一个基本概念。通道不是inbound的,就是outbound的。所谓inbound是由外向内发送数据信息而outbound是由内向外发送信息,在通话系统里,这就是所谓的呼入与呼出。
例如,当呼叫到达Asterisk系统时,创建一个inbound通道。
呼入-Inbound!
这些通道是Asterisk拨号方案的执行方,意思就是通道会去执行我们所编写的拨号脚本。每个执行拨号方案的通道,都建立一个线程,这是自然的,每个通道线程都需要去处理与解析对应通道的拨号方案所对应的文件中的内容,然后完成相应的操作。我们称这些线程称为通道线程。因为这些线程的主要任务是为inbound呼叫执行Asterisk的拨号方案,所以有时也称它们为PBX线程。
一个通道线程开始只负责一个Asterisk通道。然而,有时一个通道线程里也会有第二个通道的存在。当inbound通道执行了诸如Dial()的APP之后,就在inbound线程里创建了一个outbound通道,并在对方应答之后将两个通道桥接在一起。这里几乎已经简述了,两台话机在asterisk中是如何进行通话的。电话呼入-》asterisk建立inbound通道-》建立通道线程-》通道线程解析拨号脚本并执行-》调用拨号脚本中Dial()-》再建立outbound通道-》启动outbound线程…
拨号方案的APP始终在一个通道线程的上下文里执行。FUN也是如此。虽然可以通过AMI或CLI之类的异步接口读写FUN,但无论如何,通道
线程始终是ast_channel数据结构的执行主体。
struct ast_channel {
const struct ast_channel_tech *tech; /*!< Technology (point to channel driver) */
void *tech_pvt; /*!< Private data used by the technology driver */
void *music_state; /*!< Music State*/
void *generatordata; /*!< Current generator data if there is any */
struct ast_generator *generator; /*!< Current active data generator */
struct ast_channel *_bridge; /*!< Who are we bridged to, if we're bridged.
Who is proxying for us, if we are proxied (i.e. chan_agent).
Do not access directly, use ast_bridged_channel(chan) */
struct ast_channel *masq; /*!< Channel that will masquerade as us */
struct ast_channel *masqr; /*!< Who we are masquerading as */
const char *blockproc; /*!< Procedure causing blocking */
const char *appl; /*!< Current application */
const char *data; /*!< Data passed to current application */
struct sched_context *sched; /*!< Schedule context */
struct ast_filestream *stream; /*!< Stream itself. */
struct ast_filestream *vstream; /*!< Video Stream itself. */
int (*timingfunc)(const void *data);
void *timingdata;
struct ast_pbx *pbx; /*!< PBX private structure for this channel */
struct ast_trans_pvt *writetrans; /*!< Write translation path */
struct ast_trans_pvt *readtrans; /*!< Read translation path */
struct ast_audiohook_list *audiohooks;
struct ast_cdr *cdr; /*!< Call Detail Record */
struct ast_tone_zone *zone; /*!< Tone zone as set in indications.conf or
in the CHANNEL dialplan function */
struct ast_channel_monitor *monitor; /*!< Channel monitoring */
#ifdef HAVE_EPOLL
struct ast_epoll_data *epfd_data[AST_MAX_FDS];
#endif
AST_DECLARE_STRING_FIELDS(
AST_STRING_FIELD(name); /*!< ASCII unique channel name */
AST_STRING_FIELD(language); /*!< Language requested for voice prompts */
AST_STRING_FIELD(musicclass); /*!< Default music class */
AST_STRING_FIELD(accountcode); /*!< Account code for billing */
AST_STRING_FIELD(call_forward); /*!< Where to forward to if asked to dial on this interface */
AST_STRING_FIELD(uniqueid); /*!< Unique Channel Identifier */
AST_STRING_FIELD(parkinglot); /*! Default parking lot, if empty, default parking lot */
AST_STRING_FIELD(dialcontext); /*!< Dial: Extension context that we were called from */
);
struct timeval whentohangup; /*!< Non-zero, set to actual time when channel is to be hung up */
pthread_t blocker; /*!< If anyone is blocking, this is them */
ast_mutex_t lock_dont_use; /*!< Lock a channel for some operations. See ast_channel_lock() */
struct ast_callerid cid; /*!< Caller ID, name, presentation etc */
struct ast_frame dtmff; /*!< DTMF frame */
struct varshead varshead; /*!< A linked list for channel variables. See \ref AstChanVar */
ast_group_t callgroup; /*!< Call group for call pickups */
ast_group_t pickupgroup; /*!< Pickup group - which calls groups can be picked up? */
AST_LIST_HEAD_NOLOCK(, ast_frame) readq;
AST_LIST_ENTRY(ast_channel) chan_list; /*!< For easy linking */
struct ast_jb jb; /*!< The jitterbuffer state */
struct timeval dtmf_tv; /*!< The time that an in process digit began, or the last digit ended */
AST_LIST_HEAD_NOLOCK(datastores, ast_datastore) datastores; /*!< Data stores on the channel */
unsigned long insmpl; /*!< Track the read/written samples for monitor use */
unsigned long outsmpl; /*!< Track the read/written samples for monitor use */
int fds[AST_MAX_FDS]; /*!< File descriptors for channel -- Drivers will poll on
these file descriptors, so at least one must be non -1.
See \arg \ref AstFileDesc */
int cdrflags; /*!< Call Detail Record Flags */
int _softhangup; /*!< Whether or not we have been hung up... Do not set this value
directly, use ast_softhangup() */
int fdno; /*!< Which fd had an event detected on */
int streamid; /*!< For streaming playback, the schedule ID */
int vstreamid; /*!< For streaming video playback, the schedule ID */
int oldwriteformat; /*!< Original writer format */
int timingfd; /*!< Timing fd */
enum ast_channel_state _state; /*!< State of line -- Don't write directly, use ast_setstate() */
int rings; /*!< Number of rings so far */
int priority; /*!< Dialplan: Current extension priority */
int macropriority; /*!< Macro: Current non-macro priority. See app_macro.c */
int amaflags; /*!< Set BEFORE PBX is started to determine AMA flags */
enum ast_channel_adsicpe adsicpe; /*!< Whether or not ADSI is detected on CPE */
unsigned int fin; /*!< Frames in counters. The high bit is a debug mask, so
the counter is only in the remaining bits */
unsigned int fout; /*!< Frames out counters. The high bit is a debug mask, so
the counter is only in the remaining bits */
int hangupcause; /*!< Why is the channel hanged up. See causes.h */
unsigned int flags; /*!< channel flags of AST_FLAG_ type */
int alertpipe[2];
int nativeformats; /*!< Kinds of data this channel can natively handle */
int readformat; /*!< Requested read format */
int writeformat; /*!< Requested write format */
int rawreadformat; /*!< Raw read format */
int rawwriteformat; /*!< Raw write format */
int annexb;//G.729
int annexa;//G.723.1
int mode;//iLBC
unsigned int emulate_dtmf_duration; /*!< Number of ms left to emulate DTMF for */
#ifdef HAVE_EPOLL
int epfd;
#endif
int visible_indication; /*!< Indication currently playing on the channel */
unsigned short transfercapability; /*!< ISDN Transfer Capbility - AST_FLAG_DIGITAL is not enough */
union {
char unused_old_dtmfq[AST_MAX_EXTENSION]; /*!< (deprecated, use readq instead) Any/all queued DTMF characters */
struct {
struct ast_bridge *bridge; /*!< Bridge this channel is participating in */
struct ast_timer *timer; /*!< timer object that provided timingfd */
};
};
char context[AST_MAX_CONTEXT]; /*!< Dialplan: Current extension context */
char exten[AST_MAX_EXTENSION]; /*!< Dialplan: Current extension number */
char macrocontext[AST_MAX_CONTEXT]; /*!< Macro: Current non-macro context. See app_macro.c */
char macroexten[AST_MAX_EXTENSION]; /*!< Macro: Current non-macro extension. See app_macro.c */
char emulate_dtmf_digit; /*!< Digit being emulated */
//2011.0711.yeastar.ckt.for sip rpid.
char sendrpid[16];//
//2011.0722.yeastar.ckt.for blf fxo callout
unsigned int blffxo;
unsigned int lasttime;//
#ifdef OEMVERSION_FOR_UKZAMIR
//2012.0807.yeastar.ckt.for uk zamir
unsigned int skipvoice;
#endif
unsigned int transhangup;
int playfile_flag; //2013.10.25 jv it-nextech ringgroup
};
Asterisk中,几乎所有主要通道驱动都有网络监视线程。这些线程负责监视网络连接(无论是IP网络还是PSTN等)、入呼和其它请求。
它们处理呼叫连接建立的前期步骤,如权鉴和拨号验证。最后,当呼叫建立之后,监视线程创建一个Asterisk通道 (ast_channel),
并启动一个通道线程来处理余下的呼叫时间。
有许多基于TCP的服务也使用线程。比如SIP和AMI。在这些场景下,用线程来处理每个 TCP连接。
Asterisk的CLI也以同样的方式操作。然而,它用的不是TCP,而是UNIX socket连接。UNIX socket用于一台主机的进程间通信,不需要基于网络协议,主要是基于文件系统的.
系统里,存在着各种执行某项待定任务的线程。比如说:事件API(include/asterisk/event.h)使用一个内部线程(main/event.c)来
处理异步事件分发。又如devicestateAPI (include/asterisk/devicestate.h)使用一个内部线程(main/devicestate.c)来处理异步
的设备状态变化信息。
本节涵盖了其它一些重要的Asterisk架构概念。
正如前面讨论通道技术接口时所提及的,桥接动作把一个或多个通道连接在一起,使它们之间能够彼此交换音频包。然而,前面也提到,现在的Asterisk代码中,很多地方还没有使用新的桥接架构设计。因为,本节讨论传统的桥接功能,它在Dial()和Queue()这些APP里还在使用。
当调用这些APP,决定把两个通道桥接在一起时,它执行ast_channel_bridge()API调用。从这里开始,有可能出现两种不同的桥接:
通用桥接Generic Bridge:通用桥接(ast_generic_bridge())是一种与具体使用的通道技术无关的桥接方法。它通过Asterisk抽象的通道和帧接口交换音频数和信令,因此,它可以在任意两种通道驱动间通信。虽然这是最灵活的桥接方式,但同时它也是最低效的方式,因此它需要抽象层参与。
本地桥接Native Bridge:通道驱动可以选择实现自己的桥接功能函数。具体说来,这意味着要实现ast_channel_tech结构中的bridge回调函数。如果被桥接双方的驱动类型相同,并且驱动程序实现了本地桥接方法,那么Asterisk没理由迫使呼叫驻留在内核处理,这时它会调用本地桥接函数。这使得通道驱动能够利用类型相同的优势,优化桥接处理。在使用DAHDI的场合中,这意味着通道在硬件层面直接桥接了。在使用SIP时,这意味着Asterisk可以让音频流直接在终端间交互,而只要求信令流经过Asterisk。
现在,我们已经讨论了Asterisk的各种组件,本节通过实例来说明这些组件是如何协同工作,向外提供强大的功能的。
这个例子假设通过SIP协议呼入Asterisk。Asterisk接受这通呼叫,然后向呼叫方播放一个语音文件,最后挂机。注意这个场景没有所谓通道的说法,因为只建立了与asterisk的连接。
实例拨号规则:
exten => 5551212,1,Answer()
exten => 5551212,n,Playback(demo-congrats)
exten => 5551212,n,Hangup()
呼叫建立:从一个SIP INVITE开始这个场景。SIP通道驱动(chan_sip.c)收到这条消息。具体地说,是chan_sip的监听线程接收并处理这条请求消息。进一步,监听消息负责完成呼叫建立的握手过程(SIP权鉴)。!!!画重点,通话的核心机制开始了,一台IP电话(注意使用的是SIP协议,数据是走网线的,IP话机必须要先注册到PBX才能进行通话),因此话机必须先发起INVITE操作,想办法注册到PBX上,这是第一个流程,处理这个流程的是SIP通道驱动chan_sip.c。
接受呼叫:一旦SIP通道驱动完成呼叫建立流程,它接受呼叫并启动Asterisk处理流程。为了完成这一任务,它必须先调ast_channel_alloc()API分配一个抽象通道的实例(ast_channel)。这个通道实例暂且称之为SIP通道。SIP通道驱动负责完成SIP通道的初始化(也就是chan_sip.c)。SIP通道创建并初始化之后,创建一个通道线程来处理后续的呼叫流程(ast_pbx_start())。再强调一遍,这一步非常关键!!!我们再捋一遍,
chan_sip.c监听处理话机的Invite请求等完成了话机的注册-》话机开始拨号呼叫-》asterisk接受到呼叫请求,并接受同时准备开始建立通道-》先调用ast_channel_alloc()创建ast_channel通道实例-》SIP通道驱动初始化该通道实例-》通道创建一个线程来处理后续呼叫ast_pbc_start();!!!核心代码ast_pbx_start,后面就需要改线程来解析拨号脚本了。
执行拨号方案::在通道线程的主循环中,查找对应extension的并执行。这些实现代码在main/pbx.c的***ast_pbx_run()***函数里。
接听电话:一旦开始执行拨号方案,第一个执行的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规范回应一个接听信令。
播放语音文件:拨号方案的下一步动作是向呼叫方播放一个语音文件。执行的是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会持续地从
通道中读取处理音频包,音频包是持续实时抵达的。然而,在本例场景中,它仅是将这些包丢弃而已。
这个例子假设外部通过SIP协议入呼到Asterisk系统,然后Asterisk通过IAX2协议发起一个outbound呼叫,对端通过IAX2应答之后,建立
桥接。
实例拨号方案:
exten => 5551212,n,Dial(IAX2/mypeer)
呼叫建立:从一个SIP INVITE开始这个场景。SIP通道驱动(chan_sip.c)收到这条消息。具体地说,是chan_sip的监听线程接收并
处理这条请求消息。进一步,监听消息负责完成呼叫建立的握手过程(SIP权鉴)。
接受呼叫:一旦SIP通道驱动完成呼叫建立流程,它接受呼叫并启动Asterisk处理流程。为了完成这一任务,它必须先调用
ast_channel_alloc()API分配一个抽象通道的实例(ast_channel)。这个通道实例暂且称之为SIP通道。SIP通道驱动负责完成
SIP通道的初始化。SIP通道创建并初始化之后,创建一个通道线程来处理后续的呼叫流程(ast_pbx_start())。
执行拨号方案:在通道线程的主循环中,查找对应extension的并执行。这些实现代码在main/pbx.c的ast_pbx_run()函数里。
执行 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()应用。比如说,呼叫
双方之一挂机,桥接就停止了。
Asterisk提供了一些数据结构的通用实现。
Astobj2代表Asterisk对象模型,第二版。它的API定义在头文件include/asterisk/astobj2.h中。在main/astobj2.c文件里有astobj2的
实现细节。在源码树中,还保留着第一版的代码,然而我们不赞成继续使用它。
Astobj2提供引用计数对象处理。同时它还为astobj2对象提供了一套容器接口。容器提供的是一个哈希表。
关于astobj API的更多使用细节,请参考astobj2。在源码中,到处可以看到它的使用实例。
Asterisk提供了一套宏,用于链表的处理。这些宏定义在头文件include/asterisk/linkedlists.h.中。
同样的,Asterisk提供了一套宏,用于处理双端链表。这些宏在头文件include/asterisk/dlinkedlists.h.中定义。
Asterisk提供了一个最大堆数据结构的实现。堆相关的API定义,可以在include/asterisk/heap.h头文件中看到。堆的实现代码则在
main/heap.c文件中。
Asterisk提供了一些内置的调试工具,以帮助诊断一些常见的问题。
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
里。
Asterisk的动态内存管理,是通过一套封装的接口处理的。这些封装在头文件include/asterisk/utils.h中定义。缺省情况下,这些封装
使用标准C库函数里的malloc()、free(),等。如果编译时打开MALLOC_DEBUG编译开关,则会加入一些内存调试信息。
Asterisk内存调试系统提供以下几种功能:
· 跟踪当前分配的所有内存块,包括内存初始化时的大小、文件、函数和行号。
· 内存释放时,做一些基本的防御检查,检查内存块的写入情况。
· 释放非法内存时,给出通知
Asterisk提供了一些CLI命令,用于查询当前内存分配状况:
· memory show summary
· memory show allocations
实现内存调试系统的代码文件是main/astmm.c。