GNU Bayonne 用于电话通信
三年前,我意识到我们在自由软件方面存在严重的需求。尽管自由软件已经扩展到几乎填补了企业基础设施中的每一个空白,但我们尚未解决电信的需求。电信不仅是每个企业基础设施的一部分,而且它们常常是被桌面用户体验所忽视的一部分。与此同时,创建公共电话网络所需的硬件在商品 PC 平台和操作系统(包括 GNU/Linux)下变得更加广泛可用。
在选择使用自由软件解决电信问题时,我和其他几个人决定创建一个框架,描述所有这些服务可能是什么,范围从桌面用户和应用程序程序员的需求到最大的商业运营商的需求。当该项目正式并入 GNU 项目工作组时,后来被称为 GNUCOMM。
我们选择定义的一个领域是电话应用程序服务器的概念。这样的服务器应该使创建和部署新的电话应用程序服务成为可能且容易。这些将是专门编写的应用程序,用于与通过普通电话线呼叫服务器的真人互动,并通过语音和电话键盘与应用程序互动。
这种性质的应用程序通常包括语音邮件系统或预付费(借记卡)呼叫平台之类的东西。所有这些系统都是复杂的,有时是可编程的系统,并且需要专门的计算机电话硬件来提供 PC 平台和公共电话网络之间的接口。这可以是与单个模拟电话线通信的硬件,甚至是提供对 ISDN 和 T1 数字语音电路的多端口语音控制的硬件,大型企业可以直接从本地运营商的中心办公室获得这些电路。
充分考虑到过去这样的系统通常非常昂贵、始终是专有的且通常难以编程,我选择通过在当时可用的最佳支持的自由软件平台 GNU/Linux 下编写服务器来一次性解决所有这些问题。
当我们启动该项目时,很少有公司在 GNU/Linux 下提供电话硬件,因此我们使用了可用的硬件。即使现在,每张电话卡都与其他卡不同,并且往往包含自己的 API。由于硬件和 API 都没有以任何方式标准化,因此大多数生产电话应用程序的人员都只为一个供应商的卡系列这样做,并且他们完全使用供应商提供的 API 来这样做。这种做法也意味着计算机电话业务中的任何供应商都必须提供非常广泛的硬件系列,因为人们无法轻易地替换其他产品来填补产品供应中的空白。所有这些都使得新的电话卡供应商难以出现,并且使得有限的现有供应商能够维持其市场而无需太多变化。
这并不是说没有努力标准化 API。毕竟,有 ECTF(欧洲共同体远程工作/远程信息论坛)。作为一个专有供应商的行业联盟,他们必须通过委员会提出一套复杂的标准和提案,说明专有供应商如何开发和维护计算机电话解决方案。此外,他们需要以扩大对专业知识的需求的方式这样做,从而加强其现有成员对计算机电话市场的控制。
另一个受欢迎的组织是 ITU(国际电信联盟),最出名的是其任命通常由国家政府处理。例如,在美国,这是由国务院进行的政治任命,而不是从最优秀和最聪明的人才中选拔。
我们的目标不仅是生产一个作为自由软件的电话服务器,而且我们还希望使电话应用程序服务像创建和管理网站一样容易接近。我们还希望抽象电话驱动程序和 API,使其在应用程序服务的开发中既不相关又不可见。这样做意味着任何人都可以根据自己的意愿替换硬件,而不是被锁定在一个供应商的产品中。
由于我们希望在服务器内部的低级别抽象一切,因此我们首先需要用 C++ 编写的可移植类基础。我想使用 C++ 有几个原因。首先,由于驱动程序接口的抽象性质,使用类封装似乎很自然。其次,我发现我可以比编写 C 代码更快地编写无错误的 C++ 代码。事实上,这将成为我的第一个大型 C++ 项目。
我们为什么选择不使用现有框架也很容易解释。我们知道我们需要线程、套接字支持和其他一些元素。除了少数更大且比我们需要的更复杂的框架之外,没有一个现有框架可以完成所有这些事情。例如,我们希望电话服务器的占用空间很小。当时最适应性强的框架是 ACE(自适应通信环境),它通常为运行时库添加几个 MB 的核心映像。由于我们正在考虑在内存只有 8-12MB 的机器上运行,这似乎是不可接受的开销。
创建 GNU Common C++(最初是 APE)是为了为线程、套接字、信号量、异常等提供易于理解和可移植的类抽象。APE 后来不断发展,现在除了作为 GNU 的一部分外,还被用作许多项目的基础。
至于创建服务本身,我们意识到我们需要一种新的方法来创建电话应用程序——一种使普通系统管理员可以轻松上手的方法。为了简单起见,我们选择使用一种通用的脚本语言,后来被称为 GNU ccScript。通过编写脚本和录制音频样本来创建电话应用程序服务,几乎任何人都可以参与,而无需专门的知识或对 ECTF 推广的那些极其复杂的 API 的深刻理解。由于底层的电话硬件既不可见又从应用程序脚本语言中抽象出来,因此对使用单个卡系列的依赖周期也被打破了。
但是这种新的脚本语言应该采取什么形式呢?许多扩展语言假定每个解释器实例都有单独的执行实例(线程或进程),这使得它们不适合我们的项目。许多扩展语言假定表达式解析具有不确定的运行时。例如,表达式可以调用递归函数或整个子程序。同样,我们不希望每个解释器实例都有单独的执行实例,并且我们不希望每个实例在步进状态机时响应来自电话驱动程序的事件回调的前沿,因此 Tcl、Perl、Guile 等现有通用解决方案都无法立即为我们工作。相反,我们为我们的第一个服务器创建了一个全新的非阻塞和确定性脚本引擎。
我们的脚本语言在几个方面是独一无二的。首先,它是步进执行且非阻塞的。语句可以立即执行并返回,也可以安排稍后由执行器完成。这允许单个线程调用和管理多个解释器实例。虽然电话服务器有可能在高密度运营商规模的硬件上支持与数百个同时呼叫者的交互,但我们不需要在服务器中运行数百个本地“线程”实例,并且我们的 CPU 负载非常适中。
我们的脚本的另一个独特之处在于对内存加载脚本的支持。为了避免加载脚本时的延迟或阻塞,所有脚本都会被加载并解析到内存中的虚拟机 (VM) 结构中。当我们希望更改脚本时,会创建一个全新的 VM 实例来包含这些脚本。当前正在进行的呼叫在旧 VM 下继续,新的呼叫者将获得新的 VM。当最后一个旧呼叫终止时,整个旧 VM 将被处置。这允许 100% 的正常运行时间,即使在修改服务时也是如此。
最后,由于我们正在构建一个 C++ 脚本系统,因此我们允许直接类扩展脚本解释器,作为添加新脚本功能的一种手段。这允许人们创建特定于给定应用程序或如果需要,特定于给定电话驱动程序的派生方言,只需通过标准 C++ 类扩展从核心语言派生即可。
虽然服务器脚本语言可以支持完整电话应用程序的创建,但它并非旨在成为通用编程语言或像传统语言那样与外部库集成。非阻塞要求为服务器创建的任何模块扩展都必须高度定制。相反,我们想要一种通用的方式来创建可以与数据库或其他系统资源交互的脚本扩展。为此,我们选择了一种本质上类似于 Web 服务器在创建 ACS(辅助通信服务器)项目时所做的方式的模型。
我们服务器的 TGI 模型类似于 Web 服务器的 CGI 工作原理。在 TGI 中,启动一个单独的进程,然后通过环境变量将有关电话呼叫者的信息传递给它。使用环境变量而不是命令行参数是为了防止窥探可能包含信用卡信息等事务,这些信息可以通过简单的 ps 命令看到。
TGI 进程通过 stdout 绑定到服务器,并且 TGI 应用程序生成的任何输出都用于调用服务器命令。这些命令可以执行诸如设置返回值之类的操作,例如数据库查找的结果,或者它们可以执行诸如调用新会话以执行出站拨号之类的操作。与其为每个并发呼叫会话创建一个网关,不如为 TGI 网关维护一个可用进程池,以便可以将其视为受限资源。假设网关执行时间占总呼叫时间的一小部分,因此维护一个始终可用于快速 TGI 启动的小进程池是高效的。这有助于防止出现蜂拥而至的情况,例如,如果所有呼叫者在同一时刻访问 TGI。
有了这些基本工具,就可以创建交互式语音应答应用程序。一旦它开始运行,我们的第一个电话服务器就被 Open Source Telecom 和其他公司商业化使用。这种广泛采用部分归因于创建新应用程序服务以及将此服务器下的电话应用程序与商业企业的其他方面集成是多么简单。如前所述,唯一的要求是一些构建服务器端脚本的技能、播放和录制音频的能力以及一些通用工具(如 Perl)的知识。
我们的服务器的典型应用程序可能类似于清单 1 [可在 ftp://ftp.linuxjournal.com/pub/lj/listings/issue100/6077.tgz 获取] 中显示的 playrec 脚本。此脚本演示了当前脚本语言中的不同概念,包括符号范围和事件捕获,这些概念在命名的脚本引用下使用,形成了处理交互式电话应用程序的逻辑链。在清单 2 [可在 ftp://ftp.linuxjournal.com/pub/lj/listings/issue100/6077.tgz 获取] 中,我们有一个服务器使用 Perl 以及 TGI.pm 模块和 tgigetdbval.pl Perl 脚本的示例。
如前所述,大约两年前,我们使用第一个电话应用程序服务器实现了所有这些目标,如前所述,该服务器简称为辅助通信服务器或 ACS。不幸的是,ACS 遇到了名称问题,我收到了许多不同的人的来信,指出 ACS 被其他几个项目使用,包括 Al 的电路模拟器。显然这是一个问题。

Bayonne 内部结构
与此同时,ACS 架构正在显示其局限性。首先,它基于将服务器直接绑定到电话卡的想法,很像 XFree86 3 将 X 服务器绑定到给定的视频硬件系列。这意味着必须为每个卡系列编译单独的服务器,并且正在不必要地重复大量代码。
我选择从头开始重写整个核心服务器,这在几周内完成。我做的第一件事是创建一个支持插件的概念,使用与大多数人过去完成插件的方式略有不同的想法。
通常,插件是一个小的对象文件,动态加载,其中包含已知的符号名称或结构,一旦检查加载的文件就可以轻松找到。因此,可以使用 dlopen 打开插件,使用 dlsym 查找已知符号,从而调用插件中的函数。
我想出了一个不同的方法:我让新服务器导出自己的动态符号。然后,服务器有一堆基类,其构造函数将初始化注册结构。插件被编写为 C++ 派生类,其中基类在服务器中定义,并且这些派生类具有静态对象。当使用 dlopen 加载插件时,将自动调用这些静态对象的构造函数,并且基类对服务器映像的引用将自动解析。基类保存在服务器映像内部将从构造函数调用,并且它将注册插件对象。因此,单个 dlopen 将同时加载插件并作为单个操作执行所有初始化。
此外,作为 ACS 一部分的东西被分拆为单独的软件包。正是在这时,GNU ccScript 和 GNU ccAudio 成为了单独的类库,因为这些代表了 ACS 中已经很有用的脚本引擎和音频处理功能。特别是,我们正在考虑在将成为 GNUCOMM 一部分的其他服务器中使用脚本语言。
GNU ccAudio 已被证明是一个有用的通用音频处理库。它可以用于预生成单频和双频音调,这些音调稍后可以从内存中播放,并且它可以将来自多个输入文件的音频组装成打包的、固定大小的帧,并在末尾填充静音,这通常是馈送 DSP(数字信号处理)所需要的。此功能使其有点独特,因为其他音频处理库通常不关心这些问题。理想情况下,我希望将 GNU ccAudio 扩展为一个完整的、通用的音频处理框架,该框架也可以用于提供基于主机的类 DSP 处理。
所以我们有了一个新服务器,只是缺少一个名字。由于我们想要一些独特且不太可能被其他人使用的东西,我们决定不再使用另一个首字母缩写词。相反,由于服务器本质上是计算机和电话通信世界之间的桥梁,因此选择桥梁作为隐喻似乎很自然。但是什么桥梁呢?
当然有布鲁克林大桥。但由于过度使用且具有不良含义,这似乎是一个糟糕的选择。同样,金门大桥也被过度使用,并且在任何情况下都与 IBM 的 Java 倡议相关联。塔科马海峡大桥是一个可能性,但考虑到它因自我毁灭而闻名,我们认为我们会让它保持原样,也许留给华盛顿州的专有供应商。
在我们新泽西州不远处有一座桥:巴约讷大桥。几乎没有人听说过它,而且在任何情况下,这个名字都很少使用。
2002 年夏季标志着 GNU Bayonne 1.0 版本的推出。目前,GNU Bayonne 不仅是 GNU 项目的一部分,而且已被打包并作为几个 GNU/Linux 发行版的标准部分分发,包括 GNU/Debian 和 Mandrake。在我们希望使电话应用程序服务普遍可用于自由软件开发人员的情况下,这是一个积极的发展。
GNU Bayonne 已在世界各地广泛使用。用户范围从俄罗斯的商业运营商到美国的州和联邦政府机构,他们包括许多正在寻找专门的电话通信支持的 Web 服务或企业应用程序平台(如语音消息传递)的企业。
GNU Bayonne 并非孤立存在,而是更大的元项目 GNUCOMM 的一部分。GNUCOMM 的目标是使用自由许可的软件为当前和下一代电话网络提供电话通信服务。这些服务可以定义为:1) 与桌面用户交互的服务,例如可以拨打电话的地址簿和软电话应用程序;2) 用于电话交换的服务,例如 IPSwitch GNU 软交换机项目和 GNU oSIP 代理服务器;3) 当前和下一代电话网络之间的网关服务,例如 troll,以及防火墙电话网络之间的代理,例如 Ogre;4) 实时数据库事务系统,例如 preViking Infotel 和 BayonneDB;以及 5) 语音应用程序服务,例如通过 GNU Bayonne 提供的服务。
甚至在 GNU Bayonne 1.0 定稿之前,2001 年底就开始了 GNU Bayonne 后继者的工作。这个后继者试图简化该项目早期做出的许多架构选择,希望使其更容易以新的方式适应和集成 GNU Bayonne。设计选择和大部分初步计划发生在 2001 年底为期两天的时间内,当时我在伦敦会见了开发 preViking 电话服务器的人员。其中一些更改涉及将 preViking 项目直接纳入 GNU Bayonne 开发。
当前 GNU Bayonne 服务器中最大的挑战之一是创建电话卡插件。这些插件通常涉及为每个驱动程序实现一个完整状态机,并且代码经常重复。GNU Bayonne 2 通过将状态机推送到核心服务器并通过 C++ 类扩展使其完全抽象来解决此问题。这允许简化驱动程序,但它也使我们能够从单个代码库构建多个服务器。
GNU Bayonne 2 的另一个主要区别是对运营商级 Linux 解决方案的更直接支持。特别是,与其前身不同,这个新服务器可以在实时服务器上启用和禁用端口,从而允许热插拔或热更换卡。在运营商级平台上,内核将提供切换事件通知,应用程序服务可以侦听并响应这些事件。GNU Bayonne 2 旨在支持这种用于管理其控制的资源的概念通知。
最后,GNU Bayonne 2 从头开始设计,以各种方式利用 XML。它使用自定义 XML 方言作为配置语言。它还充当 Web 服务,既能够请求描述 GNU Bayonne 服务运行状态的 XML 内容,又能够支持 XMLRPC。这符合我们使电话服务器与 Web 服务集成的愿景,代表了我们设想项目向前发展的一部分。

David Sugar 在过去 20 年中一直参与开发自由软件,并且是 GNU 项目中许多软件包(包括 GNU Bayonne)的主要作者。David Sugar 是 Open Source Telecom 的创始人,并且是 DotGNU 指导委员会的主席。