From e7c15769849f89eb6c1ca2b199f88d2325c5d981 Mon Sep 17 00:00:00 2001
From: mnbw5433 <6501847+mnbw5433@user.noreply.gitee.com>
Date: Thu, 22 Jul 2021 10:20:39 +0000
Subject: [PATCH] =?UTF-8?q?add=20=E5=8D=B7=E4=B8=80=20=E7=AC=AC=E4=B8=80?=
=?UTF-8?q?=E7=AB=A0/Asterisk.md.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../Asterisk.md" | 166 ++++++++++++++++++
1 file changed, 166 insertions(+)
create mode 100644 "\345\215\267\344\270\200 \347\254\254\344\270\200\347\253\240/Asterisk.md"
diff --git "a/\345\215\267\344\270\200 \347\254\254\344\270\200\347\253\240/Asterisk.md" "b/\345\215\267\344\270\200 \347\254\254\344\270\200\347\253\240/Asterisk.md"
new file mode 100644
index 0000000..3e9cd26
--- /dev/null
+++ "b/\345\215\267\344\270\200 \347\254\254\344\270\200\347\253\240/Asterisk.md"
@@ -0,0 +1,166 @@
+[Asterisk](http://www.aosabook.org/en/asterisk.html)
+[Russell Bryant](http://www.aosabook.org/en/asterisk.html)
+
+Asterisk 1是基于GPLv2协议发布的一款开源电话应用平台。简单地说,这是一个服务端程序,用于处理电话的拨出、接入以及自定义流程。
+
+ 此项目由Mark Spencer于1999年创始。当时Mark有一个自己的公司,叫做Linux支持服务公司,他需要一个电话系统来帮助自己操作业务。但他没有那么多钱去买这样一个系统,因此他决定自己做。随着Asterisk知名度的提升,Linux支持服务公司的业务重点也转向Asterisk,公司也更名为Digium。
+
+ Asterisk得名于Unix通配符:*,该项目的宗旨是能做所有与电话相关的事情。通过对自己宗旨的不懈追求,如今的Asterisk已经支持一系列用于接拨电话的技术。这些技术包括诸多VoIP(Voice over IP,语音IP)协议,与传统电话网络的模/数连接性,以及PSTN(Public Swithed Telephone Network,公共交换电话网络)。对多种不同类型电话的拨出与接入能力是Asterisk的拿手好戏之一。
+ 当Asterisk系统有电话接入或拨出时,系统有很多附加特性可用于电话的自定义处理。有些特性是较大型的预置常用应用,如语音信箱(voicemail);另外还有一些小特性,可配合使用,用于创建自定义应用,如回放音频文件、读数字按键、语音识别等。
+
+## 1.1 关键架构概念
+
+ 本节讨论一些跟Asterisk每一部分息息相关的概念。这些思想是Asterisk架构的基础。
+
+### 1.1.1 通道
+
+ 在Asterisk中,通道表示Asterisk系统与某电话端点的一条连接(如图1)。一个最常见的例子是,一路电话呼叫接入了Asterisk系统,就用通道表示这一连接。在Asterisk代码中,通道是数据结构ast_channel的实例。图中这个呼叫场景可以视为呼叫方与某一系统应用(比如语音信箱)的交互。
+
+![img](C:\Users\yiwenfeng\typora\01RK9EIWxG3f)
+
+ 图1.1 一个通道表示一条呼叫线路
+1.1.2 通道桥接
+ 可能大家更熟悉的一个呼叫场景是两个电话之间的连接:一个人使用电话A呼叫另一个使用电话B的人。在此场景下,连接到Asterisk系统的有两个电话终端,因而分配了两个通道(如图1.2)。
+
+图1.2 两个通道表示两条呼叫线路
+ 如上图连接的Asterisk通道称之为通道桥接。为了视线在通道间传输媒体的目的而把通道连接起来,称为通道桥接。然而,在电话呼叫过程中也可能有视频流或文本流。即使有多种类型的媒体流,也是由Asterisk系统中负责连接两端的通道独立处理。在图1.2中,两个通道分别对应电话A和电话B,桥接的作用是将媒体从电话A传输到电话B,同理也可从电话B传输到电话A。所有的媒体流都是通过Asterisk系统传输的。Asterisk不允许传输无法识别或不能完全控制的媒体流。
+ 有两种方法可以完成两个通道的桥接:通用桥接和本地桥接。通用桥接时,无论通道使用什么技术都能够工作,它上层通过Asterisk抽象通道接口传输所有的音频和信号。尽管这是一种最灵活的桥接方式,却是最低效的,因为完成桥接必须有多层抽象。图1.2描述的就是通用桥接。
+ 本地桥接是面向特定技术的通道连接方式。如果连接到Asterisk的两个通道使用相同的媒体传输技术,则势必有一种比通过抽象层更为高效的连接方式,因为抽象层是为使用不同技术的通道之间连接而准备的。例如,如果使用相同专用硬件连接的电话网络,则可以在硬件上直接桥接,这样根本不必通过应用程序向上流动,只有呼叫信号流经Asterisk,通话的媒体流直接连接,高效的多。
+ 在桥接两个通道的时候,系统通过比较两通道的连接技术来决定使用通用桥接还是本地桥接。如果两通道都标识出支持相同的连接方式,那么就用本地桥接;反之使用通用方式。判决两通道是否支持同一种本地桥接方式,通过简单的比较C函数指针即可做到。此法固然绝非上策,但我们还没有遇到不适用此法的情况。1.2节还要讨论更多有关本地桥接功能的细节。图1.3描述的是本地桥接的一个实例,呼叫信号流经Asterisk传递,媒体流建立直接连接
+
+图1.3 本地桥接实例
+1.1.3 帧
+ 在呼叫过程中,Asterisk帧来通信使用帧,帧是数据结构ast_frame的实例。帧可以是媒体帧,也可以是信号帧。在一个基本的呼叫过程中,媒体帧的流包含音频,是通过系统传输的。信号帧则用于发送呼叫事件相关的消息,如按下数字键,挂起电话,挂断电话等。
+可用的帧类型是静态定义的。帧由一个编码类型和一个子类型表示。完整列表可在源码文件include.asterisk/frame.h中找到。下面举几个例子:
+VOICE:这类帧携带一部分音频流。
+VIDEO:这类帧携带一部分视频流。
+MODEM:这类帧数据部分使用的编码,如用于通过IP协议发送传真的T.38编码;其主要用途就是处理传真。对于这类帧的处理,保证原始数据完好无损是很重要的,这样另一端才能被成功解码。AUDIO帧不一样,因为转码到其他音频编解码器的时候,牺牲音频质量以节省带宽是可接受的做法。
+CONTROL:这类帧表示呼叫信号,用于指示呼叫信号事件,包括电话接通,挂断、挂起等等。
+DTMF_BEGIN:开始的数字键。当呼叫者按下电话上的DTMF键时,发送此帧(DTMF 代表双音多频,当按下电话上的某个键时在电话音频中发送的音调)。
+DTMF_END:结束的数字键。当呼叫者停止按电话上的DTMF键时,发送此帧。
+1.2 Asterisk组件抽象
+ Asterisk是一款高度模块化的软件。其内核程序可由源码树上的main/目录的源码构建而成。但是内核程序本身作用不大,因为其主要作用是注册模块。系统还有一些代码负责连接所有抽象接口,使电话呼叫工作起来。这些接口的具体实现是由一些可载入模块在运行时完成注册的。
+ 默认状态下,当主程序启动时,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中进行拓展,表示两个桥接通道,以及图中对应的通道技术实现。
+
+图1.4 通道技术层和抽象通道层
+ast_channel_tech中最重要的方法有:
+requester:回调函数,用于请求某个通道驱动实例化一个ast_channel对象,并针对其类型进行初始化。
+call:回调函数,用于从端点(由ast_channel表示)发起一个拨出呼叫。
+answer:Asterisk决定对接入的呼叫进行应答时调用。
+hangup:系统决定应该挂断呼叫时调用。调用后,通道驱动以基于某种协议的方式通知端点:呼叫结束。
+indicate:呼叫接通后,有可能产生许多其它的事件,需要给端点发信号。例如,如果电话被挂起,此回调函数将被调用,以通知此事件。通知呼叫挂起事件的方法可以是基于协议的,也可能只是由通道驱动简单发起播放挂起音乐的操作。
+send_digit_begin:调用此函数的作用是指示数字按键(DTMF)的开始发送至设备。
+send_digit_end:调用此函数的作用是指示数字按键(DTMF)的结束发送至设备。
+read:此函数由Asterisk内核调用,用于从端点读回*ast_frame*。*ast_frame*是1.1.3节中提到Asterisk的一个用于封装媒体(如音频或视频)以及触发事件的帧。
+write:此函数用于向设备发送*ast_frame*。通道驱动取得数据,按照其实现的电话协议做适当的打包,并传送至端点。
+bridge:针对通道类型的本地桥接回调函数。如前所述,通道驱动使用本地桥接,可以为两个同类型通道实现更高效的桥接方法,而没必要让这两个通道的信号和媒体流都通过额外的抽象层。从性能原因考虑,这是极为重要的。
+呼叫结束后,Asterisk内核中负责抽象通道处理的代码调用*ast_channel_tech*的hangup回调函数,销毁*ast_channel*对象。
+1.2.2 拨号计划
+ Asterisk管理员使用Asterisk拨号计划(存于/etc/asterisk/extensions.conf文件)来设置呼叫路由表。拨号计划是由一系列被称为扩展规则的呼叫规则组成的。当有一个电话呼叫接入,系统用被叫号码在拨号计划中查找扩展规则,用以处理本次呼叫。扩展规则包括一组拨号计划应用,由通道执行。拨号计划中可用于执行的应用由一个应用注册表维护,在运行期间,模块被加载填充至注册表。
+ Asterisk内置近200个应用。应用定义非常松散,并可任意使用系统内部API与通道交互。有些应用执行单个任务,如回放(用于向呼叫方回放一个音频文件);还有一些应用则复杂得多,要执行大量操作,如语音信箱。
+ 你可以集成诸多使用Asterisk拨号计划的应用,用于自定义呼叫处理。如果你需要对内置拨号计划语言的能力做些自定义扩展,系统也有脚本接口,允许使用任意编程语言做自定义呼叫处理。即使通过另一编程语言使用这些脚本接口,也需要调用拨号计划应用来实现与通道交互。
+ 举例说明之前,我们先看一个Asterisk拨号计划的语法,此拨号计划用于处理对号码1234的呼叫。注意,这里1234这个号码只是举例。共有3个拨号程序被调用:首先,应答呼叫;其次,回放音频文件;最后,挂断呼叫。
+; 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是下一步(next)的简写;该行的最后一项指定了采取的动作。
+ 下面是一个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, constchar *args);
+应用的实现可以使用/asterisk/目录下几乎所有的API。
+1.2.3 拨号计划函数
+ 大多数拨号计划应用接受字符串参数。其中有些值是硬编码,但在某些地方的行为需要有更多的动态处理,这时应使用变量。下面这个例子是一个拨号计划的代码片段,其作用是设置一个变量,并使用Verbose应用在Asterisk命令行界面上打印这个变量值。
+exten => 1234,1,Set(MY_VARIABLE=foo)
+same => n,Verbose(MY_VARIABLE is ${MY_VARIABLE})
+ 调用拨号计划函数的语法与应用相同。Asterisk模块可注册拨号计划函数,取得一些信息并返回给拨号计划;反之,函数也可以从拨号计划中取数据并有所动作。一个通用规则是:拨号计划可设置或获取通道的元数据,但不发任何信号,也不做任何媒体处理,这些工作留给拨号计划应用来做。
+下面这个例子展示了拨号计划函数的用法。此函数首先向Asterisk命令行界面打印当前通道的CallerID,然后调用Set应用更改CallerID。此例中,Verbose和Set是应用,CALLERID是函数。
+exten => 1234,1,Verbose(The current CallerID is ${CALLERID(num)})
+same => n,Set(CALLERID(num)=<256>555-1212)
+CallerID信息存于数据结构*ast_channel*的实例中,不仅仅是一个变量,更多是一个用于信息存储的数据结构。拨号计划函数中的代码能够从这些数据结构中存取数据。
+还有一个拨号计划函数的用法示例--在呼叫日志中添加自定义信息,即CDR(呼叫详细记录)。CDR函数允许获取呼叫详细记录信息以及添加自定义信息。
+exten => 555,1,Verbose(Time thiscallstarted: ${CDR(start)})
+same => n,Set(CDR(mycustomfield)=snickerdoodle)
+1.2.4 编解码转换器
+ 在VOIP领域有许多编解码器用于媒体编码及跨网络发送。多种技术选择为媒体质量、CPU消耗、带宽需求等方面提供了折中方案。Asterisk支持多种不同的编解码器,必要时能够在它们之间进行转码。
+ Asterisk设置完呼叫后,将会尝试使用公共媒体编解码器来沟通两个端点,这样就不需要转码。然而实际上这种情况不太可能发生。即使使用公共编解码器,也可能需要转码。比如,如果通过配置使Asterisk对流经系统的音频做信号处理(如增大或减小音量),就需要将音频转换为未压缩形式之后,才能执行信号处理。也可以通过配置使Asterisk做呼叫录音。如果配置的录音格式与呼叫的音频格式不同,也需要转码。
+编解码的协商
+
+--------------------------------------------------------------------------------
+
+ 用于协商媒体流将使用哪个编解码器的方法和连接到asterisk的通信技术密切相关。在某些情况下,例如在传统电话网络 (PSTN) 上的呼叫,可能不需要进行任何协商。然而,在其他情况下,尤其是使用 IP 协议时,会使用一种协商机制来根据能力和偏好需求就编解码器达成一致。
+
+例如,对于SIP(最常用的VOIP协议),从高层视角来看,当呼叫送达Asterisk系统时,编解码器的协调执行如下:
+端点向Asterisk发送呼叫请求中包含其优先使用的编解码器列表。
+Asterisk查询管理员提供的配置文件,配置文件中包含一个支持编解码器的列表,按优先级排序。随后Asterisk从配置文件的列表中选择优先级最高(基于配置文件中的优先级设置)、同时也包含在请求方所支持的列表中的编解码器,供应答使用。
+ 对于更复杂的编解码,尤其是视频方面,Asterisk对此领域处理得还不够好。在过去十年里,编解码器协商需求变得更加复杂。我们还有更多的工作要做,才能更好的处理最新的视频编解码,才能使系统对视频的支持比现在更好。在Asterisk下一个主要发布版的诸多新开发任务中,这项工作是重中之重。
+
+--------------------------------------------------------------------------------
+
+ 编解码转换器的模块提供了*ast_translator*接口的一种或多种实现。转码器有原格式和目标格式两种属性,还提供了一个回调函数,用于将媒体数据块从原格式转换为目标格式。转码器不涉及电话呼叫的概念,它只知道如何将一种媒体格式转换为另一种媒体格式。
+
+转码器API更多细节信息,参见include/asterisk/translate.h和main/translate.c文件。转码器抽象类的实现存于编解码器目录。
+1.3 线程
+ Asterisk是多线程应用程序,使用POSIX线程API来管理线程,并使用了相关服务,如加锁。所有与线程相关的调用的 Asterisk 代码都会被包装一层,这样调试会更方便。Asterisk的大多数线程可归类为网络监视线程或通道线程(有时亦称为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 创建呼叫的时序图
+通道线程的主循环用于处理拨号计划的执行,按照拨号扩展定义的规则执行。下面是一个扩展示例,用拨号计划的语法编写,存于extension.conf文件。有人拨叫**123*时,此扩展应答呼叫,并执行应用VoicemailMain。用户调用此应用就能检查邮箱里的信息。
+exten => *123,1,Answer()
+same => n,VoicemailMain()
+当通道线程执行应用Answer时,Asterisk就会应答接入的呼叫。应答呼叫除了一些通用的接听处理之外,还需调用关联ast_channel_tech结构中的回调来处理电话接听,这可能会涉及通过 IP 网络发送特殊数据包、将模拟线路连通等操作。
+下一步就是由通道线程执行应用VoicemailMain(如图1.6)。此应用是由*app_voicemail*模块提供的。需要注意:虽然Voicemail代码处理大量呼叫交互,但它对用于将呼叫传送到 Asterisk 系统的技术一无所知,它只负责语音信箱相关的处理。Asterisk通道的抽象对语音邮件的实现隐藏了这些细节。
+为呼叫方提供对语音信箱的访问涉及很多系统功能。然而,所有这些功能主要都实现为读写音频文件响应呼叫方的输入(主要是以数字按键的形式输入)。DTMF数字可以通过多种不同的方式发送给Asterisk系统。同样,这些实现细节都由通道驱动处理。当读入一个按键输入时,Asterisk将其转换为一个通用按键事件,传递给语音信箱代码。
+我们讨论过,Asterisk重要接口之一是编解码转换器接口。编解码的实现对于这类呼叫场景而言非常重要。语音信箱代码向呼叫方回放一个音频文件时,Asterisk系统与呼叫方通信使用的音频格式不一定和该音频格式相同。如果需要音频转码,系统会生成一个转码路径,从源格式经一个或多个编解码转换器到目标格式。
+
+图1.6 VoicemailMain的调用
+在某一时刻,呼叫者完成与语音信箱的交互,挂断了呼叫。这时通道驱动检测到此动作的发生,并将其转换为Asterisk通道的一个通用信号事件。语音信箱代码接收到这一信号事件后退出,因为呼叫方挂断后就没有什么可执行的了。然后通道线程中的控制流程将返回到主循环,继续执行拨号计划。
+1.4.2 呼叫桥接
+Asterisk中还有一个很常用的呼叫场景叫做两通道间的呼叫桥接。此场景即一方电话通过系统呼叫另一方电话。呼叫的初始设置过程与前例相同。呼叫设置完毕,通道线程开始执行呼叫计划,之后的处理流程是不同的。
+下面这个拨号计划是呼叫桥接的一个简单示例。如果系统使用了此扩展,当一方电话拨叫1234时,拨号计划执行应用Dial,这正是发起出站呼叫的主应用。
+exten => 1234,1,Dial(SIP/bob)
+Dial应用的参数SIP/bob的含义是,系统应发起一个出站呼叫,发送到设备SIP/bob。此参数的SIP部分指定了传送呼叫应使用SIP协议,bob部分由实现SIP协议的通道驱动*chan_sip*负责解释。假设此通道驱动有一个叫做bob的账户已经配置正确,那么它就知道如何将呼叫送达Bob的电话。
+首先,应用Dial要求Asterisk内核根据SIP/bob标识符分配一个新的Asterisk通道。然后,内核请求SIP通道驱动执行针对所用技术的初始化操作。通道驱动也会发起出站呼叫过程。随着请求操作的继续执行,将会有事件传回给Asterisk内核,并由Dial应用接收。这些事件包括呼叫响应、目标忙、网络拥塞、呼叫被拒,或者其它很多可能的响应。理想情况下,呼叫会被应答。当两个通道都有应答时,通道桥接就开始了(如图1.7)。
+
+图1.7 普通呼叫桥接的组件图
+通道桥接过程中,音频和信号事件由一个通道不断传送至另一通道,直到发生某些导致桥接终止的事件,如一方呼叫挂断。图1.8所示的顺序图阐释了通过呼叫桥接传输音频帧的执行过程。
+
+图1.8 桥接中处理音频帧的顺序图
+呼叫完成时,挂断流程与前例很相似。主要不同之处在于此处涉及两个通道。通道线程结束之前,两个通道都要执行对应技术的挂断处理操作。
+1.5 结论
+迄今为止,Asterisk的架构已有十年以上的历史。然而,尽管这个行业在不断发展,Asterisk的一些东西,如通道的基本概念、使用拨号计划进行灵活的呼叫处理,仍然支持着复杂电话系统的开发。有一个领域Asterisk的架构还没有处理的太好,即如何使系统在多服务器间可伸缩。Asterisk开发社区正在开发一个叫做Asterisk SCF(可伸缩通信框架)的伙伴项目,目的就是解决可伸缩性的课题。未来几年,我们期待看到Asterisk以及Asterisk SCF继续称雄电话市场,包括更大型的系统项目。
+脚注
+http://www.asterisk.org/
+DTMF表示多频双音,即按下一个电话键发送的呼叫音。
+
+
+
+
+
+
+
+
+
+--------------------------------------------------------------------------------
+
+--------------------------------------------------------------------------------
\ No newline at end of file
--
Gitee