开源应用程序架构(卷 1)
Asterisk

罗素·布莱恩特

Asterisk1 是一个开源电话应用程序平台,根据 GPLv2 协议发布。简而言之,它是一个用于拨打电话、接听电话以及执行自定义电话呼叫处理的服务器应用程序。

该项目由马克·斯宾塞在 1999 年发起。马克有一家名为 Linux Support Services 的公司,他需要一个电话系统来帮助运营他的业务。他没有太多资金购买,所以他只是自己制作了一个。随着 Asterisk 的普及,Linux Support Services 将重点转向 Asterisk,并更名为 Digium, Inc。

Asterisk 的名称来自 Unix 通配符字符 *。Asterisk 项目的目标是完成所有电话功能。通过追求这一目标,Asterisk 现在支持大量技术,用于拨打电话和接听电话。这包括许多 VoIP(互联网电话)协议,以及与传统电话网络或 PSTN(公共交换电话网络)的模拟和数字连接。这种将许多不同类型的电话呼叫接入和呼出系统的能力是 Asterisk 的主要优势之一。

一旦电话呼叫拨打到 Asterisk 系统,并从 Asterisk 系统拨出,就可以使用许多其他功能来自定义电话呼叫处理。一些功能是更大、预先构建的通用应用程序,例如语音邮件。还有一些更小的功能可以组合在一起以创建自定义语音应用程序,例如播放声音文件、读取数字或语音识别。

1.1. 关键架构概念

本节将讨论一些对 Asterisk 的所有部分都至关重要的架构概念。这些想法是 Asterisk 架构的基础。

1.1.1. 通道

Asterisk 中的通道代表 Asterisk 系统与某些电话终端之间的一种连接(图 1.1)。最常见的例子是当电话拨打到 Asterisk 系统时。此连接由单个通道表示。在 Asterisk 代码中,通道以 ast_channel 数据结构的实例形式存在。例如,此呼叫场景可能是呼叫者与语音邮件进行交互。

[A Single Call Leg, Represented by a Single Channel]

图 1.1:单个呼叫路径,由单个通道表示

1.1.2. 通道桥接

也许更熟悉的呼叫场景是两部电话之间的连接,其中使用电话 A 的人已呼叫电话 B 上的人。在此呼叫场景中,有两个电话终端连接到 Asterisk 系统,因此此呼叫存在两个通道(图 1.2)。

[Two Call Legs Represented by Two Channels]

图 1.2:两个呼叫路径由两个通道表示

当 Asterisk 通道像这样连接时,称为通道桥接。通道桥接是指将通道连接在一起,以便在它们之间传递媒体。媒体流最常见的是音频流。但是,呼叫中也可能存在视频流或文本流。即使在存在多个媒体流(例如音频和视频)的情况下,Asterisk 中的每个呼叫端仍然由一个通道处理。在 图 1.2 中,对于电话 A 和 B,有两个通道,桥接负责将来自电话 A 的媒体传递到电话 B,类似地,将来自电话 B 的媒体传递到电话 A。所有媒体流都通过 Asterisk 进行协商。Asterisk 不理解或无法完全控制的任何内容都不允许。这意味着 Asterisk 可以进行录音、音频处理以及不同技术之间的转换。

当两个通道桥接在一起时,可以使用两种方法来实现:通用桥接和原生桥接。通用桥接适用于任何使用的通道技术。它将所有音频和信令通过 Asterisk 抽象通道接口传递。虽然这是最灵活的桥接方法,但由于完成任务所需的抽象级别,它也是效率最低的。图 1.2 说明了通用桥接。

原生桥接是一种特定于技术的将通道连接在一起的方法。如果两个通道使用相同的媒体传输技术连接到 Asterisk,则可能有一种比通过 Asterisk 中连接不同技术所需的抽象层更有效的方法来连接它们。例如,如果正在使用专门的硬件来连接到电话网络,则可能能够在硬件上桥接通道,以便媒体不必完全流经应用程序。在某些 VoIP 协议的情况下,可以使终端直接将媒体流发送到彼此,这样只有呼叫信令信息继续流经服务器。

通用桥接和原生桥接之间的决策是在桥接通道时通过比较两个通道来完成的。如果两个通道都表明它们支持相同的原生桥接方法,则将使用该方法。否则,将使用通用桥接方法。要确定两个通道是否支持相同的原生桥接方法,可以使用一个简单的 C 函数指针比较。当然,这并不是最优雅的方法,但我们还没有遇到任何此方法无法满足我们需求的情况。为通道提供原生桥接函数将在 第 1.2 节 中更详细地讨论。图 1.3 说明了原生桥接的示例。

[Example of a Native Bridge]

图 1.3:原生桥接的示例

1.1.3. 帧

在呼叫期间,Asterisk 代码内部的通信是通过使用帧来完成的,帧是 ast_frame 数据结构的实例。帧可以是媒体帧或信令帧。在基本的电话呼叫期间,包含音频的媒体帧流会通过系统传递。信令帧用于发送有关呼叫信令事件的消息,例如按下数字、将呼叫置于保持状态或挂断电话。

可用帧类型的列表是静态定义的。帧标记有数字编码的类型和子类型。完整列表可以在 include/asterisk/frame.h 中的源代码中找到;一些示例包括

1.2. Asterisk 组件抽象

Asterisk 是一个高度模块化的应用程序。存在一个核心应用程序,该应用程序由源代码树的 main/ 目录中的源代码构建。但是,它本身并不是很有用。核心应用程序主要充当模块注册表。它还具有了解如何将所有抽象接口连接在一起以使电话呼叫正常工作的代码。这些接口的具体实现由可加载模块在运行时注册。

默认情况下,在文件系统上预定义的 Asterisk 模块目录中找到的所有模块都将在启动主应用程序时加载。这种方法之所以选择是因为它简单。但是,存在一个配置文件,可以更新该配置文件来指定要加载哪些模块以及加载它们的顺序。这使得配置稍微复杂一些,但提供了指定不需要的模块不应加载的能力。主要好处是减少了应用程序的内存占用。但是,也有一些安全优势。最好不要加载一个接受网络连接的模块,除非它确实需要。

当模块加载时,它会将所有组件抽象的实现注册到 Asterisk 核心应用程序。模块可以实现和注册到 Asterisk 核心中的接口类型有很多。允许模块注册尽可能多的不同接口。通常,相关功能会分组到单个模块中。

1.2.1. 通道驱动程序

Asterisk 通道驱动程序接口是最复杂也是最重要的可用接口。Asterisk 通道 API 提供了电话协议抽象,这使得所有其他 Asterisk 功能能够独立于使用的电话协议工作。此组件负责在 Asterisk 通道抽象与其实现的电话技术细节之间进行转换。

Asterisk 通道驱动程序接口的定义称为 ast_channel_tech 接口。它定义了一组必须由通道驱动程序实现的方法。通道驱动程序必须实现的第一个方法是 ast_channel 工厂方法,即 ast_channel_tech 中的 requester 方法。当创建 Asterisk 通道(对于传入或传出电话呼叫)时,与所需通道类型关联的 ast_channel_tech 的实现负责为该呼叫实例化和初始化 ast_channel

一旦创建了 ast_channel,它就会引用创建它的 ast_channel_tech。必须以特定于技术的方式处理许多其他操作。当必须对 ast_channel 执行这些操作时,操作的处理将延迟到 ast_channel_tech 中的适当方法。图 1.2 显示了 Asterisk 中的两个通道。图 1.4 扩展了这一点,以显示两个桥接通道以及通道技术实现如何融入图景。

[Channel Technology and Abstract Channel Layers]

图 1.4:通道技术和抽象通道层

ast_channel_tech 中最重要的方法是

呼叫结束时,驻留在 Asterisk 核心的抽象信道处理代码将调用 ast_channel_tech hangup 回调函数,然后销毁 ast_channel 对象。

1.2.2. 拨号计划应用程序

Asterisk 管理员使用驻留在 /etc/asterisk/extensions.conf 文件中的 Asterisk 拨号计划来设置呼叫路由。拨号计划由一系列称为扩展的呼叫规则组成。当电话呼叫进入系统时,拨打的号码将用于在拨号计划中查找应用于处理呼叫的扩展。扩展包含将对信道执行的拨号计划应用程序列表。拨号计划中可执行的应用程序维护在一个应用程序注册表中。此注册表在运行时随着模块的加载而填充。

Asterisk 包含近 200 个应用程序。应用程序的定义非常宽松。应用程序可以使用任何 Asterisk 内部 API 与信道交互。有些应用程序执行一项单一的任务,例如 Playback,它将音频文件播放给呼叫者。其他应用程序则更为复杂,执行大量操作,例如 Voicemail 应用程序。

使用 Asterisk 拨号计划,可以将多个应用程序组合使用来定制呼叫处理。如果提供的拨号计划语言无法满足更广泛的定制需求,则可以使用脚本接口,允许使用任何编程语言定制呼叫处理。即使使用这些脚本接口与其他编程语言进行交互,也会调用拨号计划应用程序来与信道进行交互。

在我们深入示例之前,让我们先看一下处理对号码 1234 的呼叫的 Asterisk 拨号计划语法。请注意,这里选择 1234 是任意的。它调用了三个拨号计划应用程序。首先,它应答呼叫。接下来,它播放一个音频文件。最后,它挂断呼叫。

; Define the rules for what happens when someone dials 1234.
;
exten => 1234,1,Answer()
    same => n,Playback(demo-congrats)
    same => n,Hangup()

exten 关键字用于定义扩展。在 exten 行的右侧,1234 表示我们正在定义当有人拨打 1234 时的规则。下一个 1 表示这是拨打该号码时执行的第一步。最后,Answer 指示系统应答呼叫。接下来两行以 same 关键字开头,是为最后指定的扩展(在本例中为 1234)定义的规则。n 代表下一步要采取的行动。这些行的最后一项指定要采取的操作。

以下是使用 Asterisk 拨号计划的另一个示例。在这种情况下,入站呼叫被应答。呼叫者会听到一个蜂鸣声,然后从呼叫者那里读取最多 4 位数字并存储到 DIGITS 变量中。然后,将这些数字读回给呼叫者。最后,结束呼叫。

exten => 5678,1,Answer()
    same => n,Read(DIGITS,beep,4)
    same => n,SayDigits(${DIGITS})
    same => n,Hangup()

如前所述,应用程序的定义非常宽松——注册的函数原型非常简单

int (*execute)(struct ast_channel *chan, const char *args);

但是,应用程序实现使用了 include/asterisk/ 中几乎所有 API。

1.2.3. 拨号计划函数

大多数拨号计划应用程序都接受一系列参数。虽然某些值可能被硬编码,但变量用于需要更动态行为的地方。以下示例显示了设置变量,然后使用 Verbose 应用程序将变量的值打印到 Asterisk 命令行界面中的拨号计划片段。

exten => 1234,1,Set(MY_VARIABLE=foo)
    same => n,Verbose(MY_VARIABLE is ${MY_VARIABLE})

拨号计划函数的调用方式与前面示例相同。Asterisk 模块能够注册拨号计划函数,这些函数可以检索某些信息并将其返回给拨号计划。或者,这些拨号计划函数可以接收来自拨号计划的数据并对其进行操作。一般而言,虽然拨号计划函数可以设置或检索信道元数据,但它们不执行任何信令或媒体处理。这些任务留给拨号计划应用程序来处理。

以下示例演示了拨号计划函数的用法。首先,它将当前信道的呼叫者 ID 打印到 Asterisk 命令行界面。然后,它使用 Set 应用程序更改呼叫者 ID。在本例中,VerboseSet 是应用程序,CALLERID 是一个函数。

exten => 1234,1,Verbose(The current CallerID is ${CALLERID(num)})
    same => n,Set(CALLERID(num)=<256>555-1212)

这里需要使用拨号计划函数,而不仅仅是一个简单的变量,因为呼叫者 ID 信息存储在 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 也可以被配置为进行呼叫记录。如果为记录配置的格式与呼叫的格式不同,则需要进行转码。

编解码器协商

用于协商将用于媒体流的编解码器的方法是特定于用于将呼叫连接到 Asterisk 的技术的。在某些情况下,例如传统电话网络 (PSTN) 上的呼叫,可能没有任何协商要进行。但是,在其他情况下,特别是使用 IP 协议时,存在一个协商机制,其中会表达能力和偏好,并会协商一个共同的编解码器。

例如,在 SIP(最常用的 VOIP 协议)的情况下,这是将呼叫发送到 Asterisk 时如何执行编解码器协商的高级视图。

  1. 终端设备将一个新的呼叫请求发送到 Asterisk,其中包含它愿意使用的编解码器列表。
  2. Asterisk 会参考管理员提供的配置,其中包含一个按优先级排序的允许编解码器列表。Asterisk 将通过选择最优先的编解码器(基于其自身配置的优先级)来响应,该编解码器被列为 Asterisk 配置中允许的编解码器,并且也列为传入请求中支持的编解码器。

Asterisk 处理得不是很好的一个领域是更复杂的编解码器,尤其是视频。编解码器协商需求在过去十年中变得更加复杂。我们还需要做更多工作才能更好地处理最新的音频编解码器,并能够比今天更好地支持视频。这是下一个主要版本的 Asterisk 新开发的最高优先级之一。

编解码器转换器模块提供了 ast_translator 接口的一个或多个实现。转换器具有源格式和目标格式属性。它还提供了一个回调函数,该回调函数将用于将媒体块从源格式转换为目标格式。它不知道电话呼叫的概念。它只知道如何将媒体从一种格式转换为另一种格式。

有关转换器 API 的更多详细信息,请参阅 include/asterisk/translate.hmain/translate.c。转换器抽象的实现可以在 codecs 目录中找到。

1.3. 线程

Asterisk 是一个高度多线程的应用程序。它使用 POSIX 线程 API 来管理线程和相关的服务,例如锁定。所有与线程交互的 Asterisk 代码都通过一组用于调试目的的包装器来执行。Asterisk 中的大多数线程都可以归类为网络监控线程或信道线程(有时也称为 PBX 线程,因为它的主要目的是为信道运行 PBX)。

1.3.1. 网络监控线程

网络监控线程存在于 Asterisk 中的每个主要信道驱动程序中。它们负责监控它们连接到的任何网络(无论是 IP 网络、PSTN 等),并监控入站呼叫或其他类型的入站请求。它们处理初始连接设置步骤,例如身份验证和拨号号码验证。完成呼叫设置后,监控线程将创建一个 Asterisk 信道实例 (ast_channel),并启动一个信道线程来处理呼叫,直到呼叫结束。

1.3.2. 信道线程

如前所述,信道是 Asterisk 中的一个基本概念。信道可以是入站的,也可以是出站的。当呼叫进入 Asterisk 系统时,就会创建入站信道。这些信道是执行 Asterisk 拨号计划的信道。每个执行拨号计划的入站信道都会创建一个线程。这些线程被称为信道线程。

拨号计划应用程序始终在信道线程的上下文中执行。拨号计划函数几乎总是这样做。从异步接口(例如 Asterisk CLI)读取和写入拨号计划函数是可能的。但是,始终是信道线程拥有 ast_channel 数据结构并控制对象生命周期。

1.4. 呼叫场景

前两个章节介绍了 Asterisk 组件的重要接口,以及线程执行模型。在本节中,我们将分解一些常见的通话场景,以演示 Asterisk 组件如何协同工作来处理电话呼叫。

1.4.1. 检查语音信箱

一个示例通话场景是当有人呼叫电话系统以检查他们的语音信箱时。参与此场景的第一个主要组件是通道驱动程序。通道驱动程序负责处理来自电话的传入呼叫请求,这将在通道驱动程序的监控线程中发生。根据用于将呼叫传递到系统的电话技术,可能需要进行某种协商以建立呼叫。建立呼叫的另一个步骤是确定呼叫的目标。这通常由呼叫者拨打的号码指定。但是,在某些情况下,由于用于传递呼叫的技术不支持指定拨打的号码,因此没有可用的特定号码。例如,在模拟电话线上的传入呼叫。

如果通道驱动程序验证 Asterisk 配置在拨号计划(呼叫路由配置)中为拨打的号码定义了分机,则它将分配一个 Asterisk 通道对象(ast_channel)并创建一个通道线程。通道线程主要负责处理其余的呼叫(图 1.5)。

[Call Setup Sequence Diagram]

图 1.5:呼叫建立序列图

通道线程的主循环处理拨号计划执行。它转到为拨打的扩展定义的规则,并执行已定义的步骤。以下是使用 extensions.conf 拨号计划语法表达的示例扩展。此扩展接听呼叫并在有人拨打 *123 时执行 VoicemailMain 应用程序。用户可以通过此应用程序检查留在其邮箱中的信息。

exten => *123,1,Answer()
    same => n,VoicemailMain()

当通道线程执行 Answer 应用程序时,Asterisk 将接听传入呼叫。接听呼叫需要特定于技术的处理,因此除了某些通用的接听处理之外,还会调用相关 ast_channel_tech 结构中的 answer 回调以处理接听呼叫。这可能涉及通过 IP 网络发送特殊数据包,将模拟线路摘机等。

下一步是通道线程执行 VoicemailMain图 1.6)。此应用程序由 app_voicemail 模块提供。需要注意的是,虽然语音信箱代码处理许多通话交互,但它对用于将呼叫传递到 Asterisk 系统的技术一无所知。Asterisk 通道抽象隐藏了语音信箱实现的这些细节。

为呼叫者提供访问其语音信箱的功能需要许多功能。但是,它们全部主要实现为根据呼叫者的输入(主要是按键形式)读取和写入声音文件。DTMF 数字可以通过多种方式传递到 Asterisk。同样,这些细节由通道驱动程序处理。一旦按键到达 Asterisk,它就会被转换为通用的按键事件并传递给语音信箱代码。

Asterisk 中已经讨论过的重要接口之一是编解码器转换器。这些编解码器实现对于此通话场景非常重要。当语音信箱代码想要将声音文件回放给呼叫者时,声音文件中的音频格式可能与 Asterisk 系统与呼叫者之间通信中使用的音频格式不同。如果必须对音频进行转码,它将构建一个或多个编解码器转换器的转换路径,以从源格式转换到目标格式。

[A Call to VoicemailMain]

图 1.6:呼叫 VoicemailMain

在某个时刻,呼叫者将完成与语音信箱系统的交互并挂断电话。通道驱动程序将检测到这种情况,并将其转换为通用的 Asterisk 通道信令事件。语音信箱代码将接收此信令事件并退出,因为呼叫者挂断电话后,没有其他操作可执行。控制将返回到通道线程中的主循环以继续执行拨号计划。由于在此示例中,没有其他拨号计划处理需要完成,因此通道驱动程序将有机会处理特定于技术的挂断处理,然后 ast_channel 对象将被销毁。

1.4.2. 桥接呼叫

Asterisk 中另一个非常常见的通话场景是两个通道之间的桥接呼叫。当一个电话通过系统呼叫另一个电话时,就会出现这种情况。初始呼叫设置过程与前面的示例相同。处理的差异从呼叫建立并通道线程开始执行拨号计划时开始。

以下拨号计划是一个简单的示例,它会导致桥接呼叫。使用此扩展,当电话拨打 1234 时,拨号计划将执行 Dial 应用程序,它是用于发起外呼的主要应用程序。

exten => 1234,1,Dial(SIP/bob)

指定给 Dial 应用程序的参数表明系统应该向称为 SIP/bob 的设备发起外呼。此参数中的 SIP 部分指定应使用 SIP 协议来传递呼叫。bob 将由实现 SIP 协议的通道驱动程序 chan_sip 解释。假设通道驱动程序已使用名为 bob 的帐户正确配置,它将知道如何到达 Bob 的电话。

Dial 应用程序将要求 Asterisk 核心使用 SIP/bob 标识符分配一个新的 Asterisk 通道。核心将请求 SIP 通道驱动程序执行特定于技术的初始化。通道驱动程序还将启动向电话发起呼叫的过程。随着请求的进行,它将事件传递回 Asterisk 核心,这些事件将被 Dial 应用程序接收。这些事件可能包括呼叫已接听、目的地忙、网络拥塞、呼叫因某些原因被拒绝或许多其他可能的响应。在理想情况下,呼叫将被接听。呼叫已接听的事实将传播回传入通道。在拨出呼叫接听之前,Asterisk 不会接听传入系统的一部分呼叫。一旦两个通道都接听,通道桥接将开始(图 1.7)。

[Block Diagram of a Bridged Call in a Generic Bridge]

图 1.7:通用桥接中桥接呼叫的框图

在通道桥接期间,来自一个通道的音频和信令事件将传递到另一个通道,直到发生导致桥接结束的事件,例如呼叫的一方挂断电话。图 1.8 中的序列图演示了在桥接呼叫期间对音频帧执行的关键操作。

[Sequence Diagram for Audio Frame Processing During a Bridge]

图 1.8:桥接期间音频帧处理的序列图

呼叫结束后,挂断过程与前面的示例非常类似。这里的主要区别在于涉及两个通道。在通道线程停止运行之前,将针对两个通道执行特定于通道技术的挂断处理。

1.5. 总结

Asterisk 的架构已经超过十年了。然而,使用 Asterisk 拨号计划的通道和灵活的呼叫处理的基本概念仍然支持在不断发展的行业中开发复杂的电话系统。Asterisk 架构没有很好地解决的一个领域是在多个服务器之间扩展系统。Asterisk 开发社区目前正在开发一个名为 Asterisk SCF(可扩展通信框架)的配套项目,旨在解决这些可扩展性问题。在未来几年中,我们预计 Asterisk 以及 Asterisk SCF 将继续占据电话市场的重要部分,包括更大的安装。

脚注

  1. http://www.asterisk.org/
  2. DTMF 代表双音多频。这是当有人按下电话上的按键时,在电话呼叫的音频中发送的音调。