本文译自《FreeSWITCH 1.2》(PACK出版社,2013),附录C:The History of FreeSWITCH。翻译得到作者授权。 ——译者注
Anthony Minessale/文 杜金房/译
为了恰当地介绍FreeSWITCH的起源,我们必须回到从前,那时,我们甚至还没想到要实现它。VoIP革命真正的开始与成形是在世纪之交,以开源的Asterisk PBX和OpenH323项目为主要标志。这两个软件的革命先驱使得很多开发者得以访问VoIP的资源而无需付出高昂的商业解决方案的费用。这两个项目后来又导致了很多创新,真正的可能的IP电话通信是真实存在的被迅速传播开来。
我在2002年第一次进入这一行业。当时我们公司的业务是对外技术支持外包,我们需要一种方式管理电话呼叫,并把呼叫送到一个线下的地方(原文是an off-site location)。当时我们用的是的解决方案,但是那种方案不仅部署费用太贵,而且我们还得支付不菲的每座席的费用。作为一个Web平台的架构师,我在过去的工作中曾经基于开源项目如Apache和MySQL做过很多开发,所以我决定研究一下在电话方面有没有相关的开源解决方案。很自然地我找到了Asterisk。
当我第一次下载了Asterisk的时候,我惊呆了。为了使它工作,我找来了好多模块电话板卡,然后中,在我家里,装在我的Linux PC机上,当在后面插上电话线并在话机上听到第一声拨号音之后,我叫到:哇塞!简直是帅呆了!我很快就深入代码中,试着研究出它是怎么工作的。我很快的学到,类似Apache,该软件竟然也可以以可加载模块的方式扩展它的功能以做其它的有用的事情。这比以前任何的东西都要好。现在,我不仅可以使我自己的电话可以与计算机通信,我还可以让它在的拨打特定号码的情况下执行我自己写的代码。
我尝试并验证了几种想法之后,忽然,我产生了一个新想法这些想法:嘿,我非常喜欢Perl[^perl],并且这些电话系统非常的酷,如果我把它们结合起来会怎么样?我研究了如何把Perl嵌入C语言程序的文档,很快我就有了一个app_perl.so[^app-perl-so],它是一个Asterisk的可加载模块,通过该模块可以在电话路由到我的模块以后执行我的Perl代码。当时它并不完美,然后我开始很快地学习将Perl嵌入一个多线程的程序中的各种挑战。但至少是对我的想法的可行性的一种概念验证,在经过几天的修修补补之后能够得以运行也算是一个不小的成熟了。
[^perl]: 一种编程语言。译者注。
随后,我深入了Asterisk在线社区。在这些代码上玩了几周的之后,我用Asterisk作为电话引擎开始做一个呼叫中心解决方案,以及一个简单的Web前端程序。在实现的过程中,我遇到了Asterisk中的几个Bug,然后我就把它们提交到了Asterisk开发分支的缺陷跟踪系统上。该过程重复地越多,我参与该项目的成长和发展不越深。除了零散的Bug修复之外,我也开始写代码对它进行改进。到2004年的时候,我除了修复我报告的Bug之外,也在修复其它人报告的Bug了。这是我感觉在我找到我自己的问题的免费解决方案以后,我所能做到的回报方式。如果我的问题解决了,大家也都能看到。
事件升级
当我在测度我的程序的时候,我会往系统中打很多电话并看着一个Web页面在更新、控制呼叫队列,以及看着各种状态统计信息。然而,我没有注意到的一件事就是并发的呼叫数以及呼叫量本身。确实我在测试的时候也就一般同时打一两个电话,而没有全面的测试我的程序。当我第一次将程序放到生产系统上的时候,也是我第一次看到一个多线程的软件遇到无法解决的锁冲突的时候,这种锁冲突就是众所周知的死锁。我非常熟悉段错误[^segfault],因为我在开发自己的模块的时候遇到过无数次。但是,使我不解的是,我有时看到在某些非常无法解释的情况下也出现这种错误。
[^segfault]: 即Segmentation Falult,是程序运行期间由于共享的内存遭到破坏而引起的程序崩溃。——译者注。
段错误是由于一个程序在运行时进行了不正当的内存访问,如多次释放同一段内存或者试图越界访问内存地址或者访问根本不存在的内存。由于你可以直接访问底层的操作系统,并且除非你非常自律,系统无法防止你犯错误,所以,在C语言编写的程序中这种错误是很常见的。但我不是轻言放弃的人,那样的话你会认为是一个诅咒抑或是恩赐。所以,当我遇到问题时,我已经准备好了奋斗到底。我花了无数的时间研究GNU调试器的输出,并尝试模拟出大量的呼叫以便重现我遇到的问题。经过一些试错(原文是trail-and-error),我成功了。我终于通过一个呼叫发生器的帮助找到了导致系统崩溃方法。当时的感觉非常好,只是好的感觉太短暂。就在那天下午,我又在代码的另一处发现了另外一个类似的新问题。
我尝试小心的去掉我的程序中的一些可能导致死锁或崩溃的功能,便是我无法去掉全部。最终我发现导致我的不幸的是app_queue模块,这对我来说可不是个好消息,因为在我的呼叫中心程序中我主要就用了那个模块。我修复了那个模块中的问题,但一些修改对原有的模块影响太大,因而无法包含到主流的发布代码中。最后,我还是自己维护了我的模块代码,并继续更新Asterisk中其它的部分。这总算令它稳定下来了,但这种稳定只是在找到另一种解决方案前相对稳定的方案。
到那时为止,我往Asterisk里加入了很多的新特性,并对开发一些性功能有了很好的想法。我创建了一个新的概念,叫做功能变量(function variables),它允许模块可以对外提供一个接口,通过该接口,能从拨号计划(Dialplan)中扩展模块的功能(如果你读了本书,你会感觉本书中同样实现了类似的想法)。与此同时,我仍然在纠结那个队列死锁问题。所以后来我与Asterisk社区中的另一个成员开始计划一个新的队列模块——mod_icd。
ICD的代表智能呼叫分配(Intelligent Call Distribution),与首字母缩写的自动电话分配(ACD,Automatic Call Distribution)相对。我们找出了app_queue模块所有的问题,我们对做一些新的稳定的,再也不会导致无休止的死锁和崩溃的模块同样感兴趣。我们使用状机以及更高级的基于内存池的内存管理抽象以及其它一些在标准的Asterisk中不存在的创造性地概念。但问题是,我觉得我们在那个模块上做过了头,看起来几乎是我们把Asterisk核心的一些功能也边缘化了。当然,那只是其中的一个可载模块,完全边缘化Asterisk的核心是不可能的。
我们始终没有完全完成mod_icd。在2004年底,我参与呼叫中心解决方案的机会被那些不可饶恕的段错误和死锁的深海击碎、涤荡殆尽。我们开始关注另外的与队列无关的电话服务。我使用了我添加到主流的Asterisk中的几个新特性以及我的几个未被批准的不太流行的小模块开发了一个新的被叫付费业务(类似中国的800电话)以及传真转电子邮件服务。我建立了一个由7台Asterisk主机组成的集群,将它们与电信部分提供的线路对接。这种部署Asterisk的方式并不是不会出问题,而比较美好的一点是,如果某台机器崩溃,就会有另一台顶上去,然后我们就有机会重启那台崩溃的机器。
新点子和新项目
在这一点上我积累了一些新想法——有的测试过、有的没有,还有一些需要对Asterisk做一些大的改动。我跟我的队友Brain West以及Michael Jerris在Asterisk项目上贡献了很多时间。我们帮助管理缺陷跟踪系统(Issue Tracker),我们修复了很多Bug,并且我们每周都主办开发者的电话会议,甚至我们还在我们的服务器上做了一个代码库镜像站点。我们参与的太多以至于我们的一些新想法在Astersisk社区中引起了一些政治骚乱,起因在于一场不同的开发者之间的一场没必要的竞争——每一个Asterisk的贡献者必须签署一个表格以声明他们写的所有的日后可能用于Asterisk的代码都自动对Digium公司(,Asterisk的拥有者)有一个免版税的授权,以便他们可以用你的代码做任何他们喜欢的事。如,通过这种方式,他们可以将这种无限的许可证以高的多的价格卖给他们的潜在客户。这完全背离了开源精神,但这就是另一个故事了。我认为这种差异化引起了一些跟我一样的志愿开发者与Digium雇佣的一些Asterisk开发者之间的一些冲突。
即使在这么紧张的环境下,我们还是全力以赴地支持该项目真正希望它能成功。我们继续好召开每周的电话会议,他们也确实采取一些措施开始帮助开发者们增加动力。我们觉得我们应该有一场现场的见面会,以便我们所有人都可以聚到一起分享我们的电话技术知识,并一起玩几天。我们不知道我们要干什么,但我们决定要做,并把该聚会称为ClueCon。有一个Clue[^clue]意味着你知道你要做什么,所以ClueCon的意思就是帮每一个人找到一个“Clue”。我承认,即我刚刚说过我们也不知道我们要做什么。看起来非常有意思,一群没有Clue的人开了一个有Clue的会——ClueCon。不过,事实证明非常幸运,这好像根本不是个问题。前面我们所指的Clue都是指电话技术,而不是做会议。
[^clue]: 线索,后面的Con指会议(Conference)。——译者注。
因此,在第一届ClueCon之前的几个月中,即2005年春天,我们在一次例行的电话会议中开始专门深入讨论Asterisk中的几个缺点。这非常正常,因为我们主要的目标就是找出这些问题并找到解决方案。在那个时期,有一大群不守规矩的、受够了他们在Asterisk上遇到的无穷无尽的问题的人。那群人中的很多人都参加了那次周会,希望能说服我们帮忙看一看他们遇到的问题。我想得越多,越觉得解开折磨我们的核心架构问题越是任重道远。Asterisk中的很多核心都具有单一的性质而无法扩展(Scale),其中的很多我发都有很多用户依赖于它们,任何企图改进他们的动作都有可能会引发功能上的退化。有些问题看起来是无解的,除非用一把大锤把那些旧代码砸个稀巴烂,并从核心的代码深入重写。但这种方法看似不可行,因为它将会使得Asterisk在几个月内甚至一年或更长的时间内都不可用。也就在那时候,我有了一个想法——我们做一个2.0版吧。
从一个2.0版并不是一个最坏的想法。我知道它将有很多挑战,但是,我想我们可以与旧的代码并行启动一个新的代码库,那样我们就可以删除那些有问题引起问题最多的部分旧代码并替换成新的,并仍然维护用户依赖的一些仍然可用的代码。有了这个想法后我感到很兴奋,同时也在我提出该想法后看到该项目的领导人的反映时得到了同样地震惊。他似乎也对我竟然提出这么一个想法同样震惊。总之,简单来讲,我们没有做2.0版。那时,我有无数的想法,我也非常清楚地知道我喜欢以及不喜欢Asterisk的地方,但没地方写。
我盯着那个在一个空目录中打开的空的编辑器缓冲区[^empty-text-editor],盯了足足一个小时。我知道我想要做什么,但不知道怎么写出来。在我在编辑顺中增加了一些奇怪的单词以及一些标点符号之后,我才知道该如何开始。那些单词并不是你常用的单词,还是一些符号以及变量声明——我是在写C语言代码。几天后,我用C写了一个基本的程序,试验了几个我在过去编程中喜欢用的一些编程工具。我有Apache可移植性运行库(或称APR),有Perl编程语言,以及一些其它的程序包。我建立了一个核心,以及一个可加载模块的结构,一些助手函数用于内存池管理,并且我有了一个简单的命令提示符,你可以在命令行上键入help,如果你愿意看到命令行上显示一个尖刻的提示,提示你根据没有帮助信息的话,或者也可以键入exit以终止程序。我还写了一个示例模块,允许你使用telnet连接到一个特定的TCP端口上,你输入的任何字符都将原来的回显回来。另外我还实现了一个简单的状态机。我把这个程序叫做Choir。因为我希望我的一系统的想法都可以像教堂里的唱诗班一样发出和谐的声音。在那些最初的代码之后,我放了一段时间。因为ClueCon快要来了,我还有许多东西要准备,而不想到时候太仓促。
[^empty-text-editor]: 用于编写程序代码的编辑器。作者使用Emacs编辑器,在Emacs中,每一个文件(甚至Shell等)都称为一个缓冲区(Buffer)。——译者注。
第一届ClueCon
2005年8月的ClueCon是第一届。当时参加的有几个VoIP项目的领军人物,包括OpenH323的作者之一Craig Southeren以及Asterisk的创建者Mark Spencer,当然,不喜欢我的Asterisk 2.0的想法的也是他(Mark),但无论如何,将这些人聚到同一间屋子里还是一件非常酷的事情。我们整天都有演示,以及反复地交流,并且,我们真正使得每都人都开始想问题。那一届ClueCon非常成功,会议圆满结束以后我动力十足,并准备好继续写我的Choir代码。但事实是,我并没有立即开始行动,而是在我们的电话会议上讨论了几个月,同时挣扎在时时刻都有倾覆危险的Asterisk平台上。秋天马上就到了,Asterisk社区的混乱最终导致了一场苦命——社区中占相当比例的人Fork[^fork]了Asterisk,新的项目叫做OpenPBX。
[^fork]: 专业术语,指在原来代码库的基础上重新建了一个分支,然后两个分支分别独立发展。
我完全理解他们为什么那么做,并尽我所能地支持他们。我贡献了我所有为Asterisk所写的代码,他们可以根据自己的喜好随时取用。如果有闲暇时间,我也会帮助他们,但我最终还是没有完全融入他们。因为我仍然有同样的问题没有解决——我认为有些问题必须从最底层解决,而这一新项目(指OpenPBX)的创始人更倾向于解决那些Asterisk团队没有能够及时解决的现实的问题。我们仍然开电话会议,但大多数情况下Asterisk项目的人都不再参加了,因为我们为Asterisk社区的革命欢呼使用他们不高兴。由于在不将Asterisk核心完全推倒重来的情况下我无法修复任何问题,有一天我为此事道了歉。也就在那时候有人(如Tootisie Pop commercials的Owl先生)问我:“你觉得让你的新代码能打电话需要多称时间呢?” 我不知道,所以我决定试一下——不管是一周、两周还是三周。
我写的第一个能够发出声音的模块是mod_woomera,该模块是一个Endpoint模块,它使用了Craig Southeren(与我在ClueCon上遇见的是同一个人)写的Woomera协议。我也为Asterisk写了一个类似的模块。Woomera协议非常简单,它并不需要编解码或其它复杂的东西。它的实现思想是它屏蔽了H323协议的复杂性并允许应用程序使用该简单的协议与它通信以便更容易地集成到VoIP程序中。所以,从它开始好像是一个正确的选择。当我开始工作的时候,我意识到在我的核心代码中需要更多的元素,然后我就慢慢的添加,并最终将这些代码融合到一处,我终于可以给以Woomera协议武装的H323监听进程打电话了,同时,我也可以在我的Pandora代码里获取通话的状态。是的,我把我的项目名称改成了“Pandora”,因为大家都不喜欢Choir这个名字。我非常愉快的听着Alan Parsons Project hit Sirius stream 第一次从我的扬声器里流出来。这一次比我第一次使用Asterisk打电话时更加兴奋,因为我白手起家从头开始写的代码现在开始工作了。
现在,我已经有所进展了。我研究出了如何让两个Channel桥接到一起、如何支持更多的其它协议以及做一些其它的基础的事情,而不是仅仅打印一条尖刻的帮助信息并退出。有人建议把这些代码叫做OpenPBX 2,有人也建议其它名字。在经历了足够的命名争论后,我知道了(并永远决定了)我应该叫它什么:FreeSWITCH。我终于有了一个我将为之坚持不懈的名字、一些可以工作的代码,以及很多野心。我埋下头继续工作。所有地方都有工作要做,多得甚至你都没时间去想。所以我就不停地写代码。时间很快到了2006年1月,那时我有足够的代码可以与公众分享了。我们向开发者们开放了我们的代码库。通过让他们注册一个开发者账号才能获取代码的访问权限,我们确保只有非常严肃的开发者才愿意完成整个注册过程。有一些人下载了[^checking-out]源代码并提供了一些反馈,我们当时真觉得我们有了一个真正的项目。
[^checking-out]: 原文是Check out,即从SVN仓库检出代码。
我们有了一个可以桥接电话的模块、一个可以放音的模块、一些编解码模块、一些作为例子的拨号计划(Dialplan)模块,以及一些其它的模块。哦,我有没有说过它在Windows上也可以运行[^freeswitch-windows]?
[^freeswitch-windows]: FreeSWITCH可以在Windows上原生的运行,而Asterisk不能,或者只能通过Cygwin等模拟环境运行。
虽然页面上所有的链接都失效了,但我们原来的站点仍然保留着:<http://www.freeswitch.org/old_index.html> 。
Our original site is still preserved, though none of the links are active:
<http://www.freeswitch.org/old_index.html> 。
FreeSWITCH诞生
在实现我们计划的过程中,我们确实写出了可以运行于Windows、Linux以及Mac OSX上的代码。我早期团队的伙伴——Michael和Brian,从一开始就跟我在一起。Mike[^mike]在Windows平台上很有经验,他确保了我们的代码能在MSVC里编译和运行。最初这确实是一个痛苦的过程,但在修复了无数情况下无数的编译错误之后,我第一次开始学习如何编写跨平台的代码。光阴似箭,下一次ClueCon的时间到了。在那一年,我进行了我的第一次FreeSWITCH演讲,演示了在本书开篇[^arch-of-fs]中所描述的核心设计和基础架构。我们看到了非常令人兴奋的模块,如一个可以与Google Talk通信的模块。在演讲过程中,我也通过mod_exosip模块现场演示了在有几千个并发呼叫的情况下呼叫的实时建立和释放。那是一个很好的演示,但我们并不满足。
[^mike]: Michael的简称。——译者注。
[^arch-of-fs]: 该书第一章是Architecture of FreeSWITCH,即FreeSWITCH的架构与设计。——译者注。
Exosip仅仅是在Osip基础上进行了一些上层的封装,而原来的Osip库则是一个开源的SIP协议栈,它提供了大多数的SIP功能。Exosip使得开发一个Endpoint模块更简单一些,所以,我们决定基础它来开发我们的SIP模块。但后来,我们还是遇到了几个灾难性的问题,使得我开始感觉我们陷入了与当时让Asterisk正常工作一样的境地。因而,我们开始寻找一个Exosip的替代者。我们寻找替代者的另一个原因是Exosip选择了GPL许可证,而不顾原始的Osip库本来是LGPL(就我个人感觉,应该继续选择LGPL更合理一些),这就导致了潜在的许可证冲突。由于我们在我们的项目中使用的是MPL许可,而GPL协议不允许使用GPL许可的代码嵌入MPL许可的程序中。有关许可证的争论很有趣,也经常能令人兴奋,但当时我们没有时间参与这些。
由于在FreeSWITCH中对SIP功能有很高的要求,我们上天入地,找遍了开源界的每一寸土地,希望能找到一个可以用的新的SIP协议栈以及RTP协议栈。我们针对这两点评估了几个库,最终几乎每种协议都试用了至少5个库,但还是没有找到一个令我满意的RTP协议栈,所以最终我决定自己实现。当然,我并不至于傻到也自己去实现SIP。在我看到Asterish曾试图从头到始写一个SIP协议栈并最终失败而转投Exosip之后,我继续在开源领域中寻找SIP协议栈,直到最后发现了诺基亚(Nokia)开发的Sofia-SIP。我们写了一个可用的mod_sofia模块来进行测试,效果非常不错。我们继续打磨该模块直到它可以完全替代mod_exosip,然后mod_sofia就成了我们系统中首要的SIP模块。不过,那仅仅是开始。因为到后来,即使是现在我还经常要往mod_sofia中添加代码。SIP是一个非常复杂并且令信恐惧的协议,它带来了许多令人不愉快的思想,而不像它的名字看起来那样简洁。但现在不是讨论这个的时候。
我们在ClueCon 2007上再一次演示了FreeSWITCH,那一次,有了一个新的SIP模块以及更多的代码。另外,还有OpenZAP,它使用一个TDM库将FreeSWITCH连接到电话硬件上。OpenZAP后来初FreeTDM替代,现在由Sangoma公司负责维护。我经历了使用同样的板卡,很久以前让它在Asterisk上工作,现在又让它在FreeSWITCH上工作的愉快过程。我们曾经很快地宣布我们将推出FreeSWITCH 1.0.0版。很多看过我们原来的主页的人可能会注意到当时我人宣布了一个官方的新版本将“很快发布(oming soon)”,条消息的发布时间是2006年1月,当时我们拼命的想让所有事情按我们希望的那种方式工作。我们非常希望专注于在添加任何其它功能前做一个稳定的核心,并且我们也有了很大的进展,但是我们还是没有准备好发布1.0版。
2008年春天我们有了稳定的SIP、有了Event Socket用于远程控制FreeSWITCH、有了一个模块可以通过HTTP执行FreeSWITCH命令、有了XML curl等等一系列的新功能和特性。我们最终觉得可以发布一个版本了。所以我们就一举发布了FreeSWITCH 1.0凤凰版(Phoenix)。我之所以将该版本取名为凤凰,是因为感觉到我们所有的辛苦的工作成果都是从先前的失败的骨灰中来的,并且这个名字也被很多其它人使用,包括NASA在同一时刻将“凤凰号”送上了火星。总之我认为那是一个很合适的名字。
在ClueCon 2008上,我们又一次宣布了曾于当年5月份发布的1.0版。当时还有另外一些与FreeSWITCH有关的演讲,以及与Asterisk有关的演讲,因为当前也发布了Asterisk 1.6。在当前接下来的时间里,我们用了所有的时间专注于宽带(高清)语音的支持以及其它的一些功能,例如即时的进行采样率转换。此外,我们还增加了一些新的SIP功能,如状态呈现(SIP Presence)以及其它的一些简单通话以外的功能,并于后来发布了1.0.1版。
2009年,我们发布了1.0.2至1.0.4并于第5届ClueCon年度会议上又一次演示了FreeSWITCH。到那时候,我们早期的一些创新变成了现实。因为我们可以演示使用Polycom话机新的Siren编码进行高清语音的通话,并且我们还支持了与Skype互通。当年的FreeSWITCH演示概括了一些你可能根本就意识不到你你可以做的事情,除非你具有四维空间的想象力。而这,正是Emmett Brown博士(来自电影《回到未来(Back To The Future)》)都想做的。FreeSWITCH有一些与Asterisk类似的行为,但是我们同时也有一个新的词汇表,该词汇表新像为你打开一个通向无限可能的空间的大门,使你可以通过一个PC和一个电话就可以做任何无法想象的事情。
2012年的ClueCon是在Trump Tower[^trump-tower]举行的。它是一届有史以来最值得怀念的ClueCon。当年我们出版了本书的第一版,我们还发了几本做为奖品。我们详细演示了FreeSWITCH以及它的性能。当年我们发布了1.0.5和1.0.6。那年的主题好像是Erlang,好像每个人都赶上的事件驱动(Event Driven)架构的时尚。所以,我们绝对是在命令的时候处在了合适的位置。
[^trump-tower]: 一个国际性酒店。——译者注。
2011年是该项目大跨跃的一年。受我们一年前自己关于性能的演讲的启发,我们对Sofia SIP模块进行了大刀阔斧地修改,将原来串行化的消息处理改为并行的处理,每个路电话的消息都会被推到它自己的线程中处理。这次改变产生了很多并行的操作,避免了由于单一的Channel发生问题时阻塞整个SIP协议栈的可能。那年的ClueCon是在壮丽的Sofitel酒店举行的。我们演示了很多新特性,包括一些用于帮助开发者进行开发的特性,如变量数组的概念以及范围变量——你可以使用它来设置一个通道变量,该变量仅在某一特定App执行的时候才有效。
2012年我们宣布了一个新的计划,在FreeSWITCH代码库中支持稳定版的分支(stable branch)。这是一个令人望而却步的任务,因为你必须将成熟的代码与新的代码分离,并且在每次修改时都需要在不同的分支上做额外的检查以确保所有东西都平稳运行。我们为此非常努力地工作。并且本次新版的书将包含FreeSWITCH 1.2稳定版中最初版本的内容。我们在Hyatt酒店举行了一次很成功的ClueCon,并演示了一些新的特性如支持Hylafax的软件模拟器以及一个新的mod_httapi模块,该模块也在本书前面的章节中讲到了。
在写本书的时候,已是临近2013年了。在活过了玛雅人的预言[^2012]之后,我们开始研究消除传统的电话与HTML5以及WebRTC之间的间隙以及第一个FreeSWITCH 1.4 Alpha版本。ClueCon将继续在Hyatt酒店举行[^cluecon-2013],他们为给我们打造更温馨的环境重新进行了装修。我们希望在那儿见到你们所有人,并希望你通过本书多学到了一丁点儿的FreeSWITCH的知识以及了解为我为什么决定在那个空白的文本编辑器上开始打上那几个字符[^type-on-blank]并最终变成FreeSWITCH核心组件的近50万行代码的。
[^2012]: 传说玛雅人的历法中预言2012年世界将灭绝。——译者注。
[^type-on-blank]: 指作者最初写FreeSWITCH代码的时候。——译者注。
[^cluecon-2013]: 本文写于ClueCon 2013之前,后来,ClueCon 2013如期在Hyatt举行,当年的主题好像是WebRTC。译者曾在现场。——译者注