使用 Asterisk 和 Ajax 的来电显示
我使用 Asterisk 服务器处理我们的所有电话服务已经大约一年了。在此期间,我发现了很多可以用 Asterisk、VoIP 和各种其他技术完成的非常巧妙的事情。我做过的比较花哨的事情之一是将来自来电的来电显示信息实时发送到我浏览器上的网页。为了做到这一点,我必须使用 Asterisk、Perl、CGI、HTML、CSS、SQL、XML 和异步 JavaScript 或 Ajax。有很多不同的部分需要组合在一起,但有时这正是使项目有趣的原因。
以下是它的工作原理的简述。当有人打电话到我们家时,Asterisk 服务器会等待来电显示信息被发送。然后,服务器将此信息以及其他一些信息放入 /tmp 下子目录中的一个文件中。这一切都在 Asterisk 拨号计划中完成。然后,我在浏览器中打开一个网页,该网页每秒运行一个 JavaScript 程序。这个 JavaScript 程序使用 XMLHttpRequest 对象向服务器查询新的来电显示信息。服务器上的 CGI 脚本返回一个包含来电信息的 XML 文件。JavaScript 程序解析返回的 XML 并显示内容。我创建了一个层叠样式表 (CSS),使来电信息看起来像贴在网页上的便签。当来电完成时,Asterisk 服务器会创建一个呼叫详细记录,或 CDR,它驻留在 SQL 数据库中。
每次 JavaScript 联系服务器时,CGI 脚本都会查找 CDR。如果它存在,程序就知道呼叫结束了,并删除 /tmp 中的来电信息文件。这具有在呼叫完成时使便签消失的效果。
作为额外的奖励,该程序最多支持四个并发呼叫,并且也可以用于指示外拨呼叫。能够看到谁在打电话,而无需打断正在通话的人来询问,无论该人是呼叫者还是被呼叫者,这有点好。当我的孩子们长大后,这可能会成为一个更重要的功能。
为了使此系统工作,您必须配置您的 Asterisk 服务器将 CDR 放入 SQL 数据库中。默认情况下,Asterisk 将 CDR 放入逗号分隔的文件中。问题在于平面文件 CDR 不包含呼叫的唯一 ID,此系统使用该 ID 来检测呼叫何时完成。放入 SQL 数据库的 CDR 包含此字段。但这不应该是一个苛刻的要求。我记得,配置 Asterisk 以将 CDR 存储在 Postgres 数据库中相当简单,并且在 cdr_pgsql.conf 文件中得到了很好的文档记录。如果您愿意,您也可以使用 MySQL 或 ODBC 数据库。
该项目的第一部分也是最容易的部分是修改 Asterisk 拨号计划,以便在进行呼入或呼出呼叫时创建平面文件。一旦您确定了在哪里进行更改,它就是一个简单的单行添加,如下所示(全部在一行中)
exten => s, n, system(echo "IN#${CALLERID(name)} ↪#${CALLERID(number)}#${UNIQUEID}" > ↪/tmp/panels/cid/${UNIQUEID})
此行在 /tmp/panels/cid 中创建一个文件,其中包含四个字段,用 # 字符分隔。当然,您需要创建 /tmp/panels/cid 并赋予其适当的权限,以便 Asterisk 服务器可以在其中创建文件,并且 CGI 脚本可以读取和删除这些文件。第一个字段是 IN 或 OUT,表示呼叫是呼入还是呼出。接下来的两个字段调用 CALLERID() 函数来检索呼叫者的姓名和电话号码。最后一个字段是呼叫的唯一标识符。您需要将此行放置在您的拨号计划中,以便服务器已经接收到来电显示信息,但在将呼叫移交给拨号命令之前。如果您想接收有关外拨呼叫的信息,您可以将类似这样的行添加到您的拨号计划中
exten => s, n, system(echo "OUT##${EXTEN}#${UNIQUEID}" ↪> /tmp/panels/cid/${UNIQUEID})
对于外拨呼叫的情况,我们没有任何来电显示信息要显示,因此第二个字段留空。我们知道拨打的号码,该号码通过第三个字段中的 ${EXTEN} 变量检索。
在呼入和呼出的情况下,您都需要确保更新扩展字段和优先级字段(在本例中为 s 和 n)。
为了演示的目的,我已将网页剥离到其最基本的要求,如清单 1 所示。
清单 1. 示例网页
<html> <head> <title>CID Test</title> <script language=javascript src=http://hostname/cid.js> </script> <style type="text/css"> @import "cid.css"; </style> </head> <body> <div id="phone1"></div> <div id="phone2"></div> <div id="phone3"></div> <div id="phone4"></div> <script> start_cid(); </script> Your Content Would Go Here. </body> </html>
这段看似简单的 HTML 代码做了很多事情。首先,它加载了 cid.js JavaScript 代码。然后,它导入了一个名为 cid.css 的样式表。此样式表将为您提供很大的灵活性来自定义便签的外观。然后,HTML 代码创建了四个 div 部分,分别称为 phone1 到 phone4。这些部分稍后将被设置为可见,并将填充来电信息。最后,HTML 代码通过调用 start_cid() 函数来启动定期轮询。我们稍后将讨论该函数。
即使我的 CSS 技能不是世界一流的,我也包含了一个示例 cid.css 文件供您入门(清单 2)。
清单 2. 示例 cid.css 文件
div#phone1{ background: #FFFFCC; display: none; position: absolute; border-top: thin solid black; border-left: thin solid black; border-right: 6px solid black; border-bottom: 6px solid black; top: 85%; left: 2%; width: 20%; height: 5em; } div#phone2{ background: #FFFFCC; display: none; position: absolute; border-top: thin solid black; border-left: thin solid black; border-right: 6px solid black; border-bottom: 6px solid black; top: 85%; left: 27%; width: 20%; height: 5em; } div#phone3{ background: #FFFFCC; display: none; position: absolute; border-top: thin solid black; border-left: thin solid black; border-right: 6px solid black; border-bottom: 6px solid black; top: 85%; left: 52%; width: 20%; height: 5em; } div#phone4{ background: #FFFFCC; display: none; position: absolute; border-top: thin solid black; border-left: thin solid black; border-right: 6px solid black; border-bottom: 6px solid black; top: 85%; left: 77%; width: 20%; height: 5em; }
通过将所有常用格式放在一个公共类中,可以使此 CSS 文件更加简洁;我将其留给读者作为练习。此样式表在屏幕底部创建四个均匀间隔的便签。便签是黄色的,带有整洁的 3-D 阴影效果(图 1)。
现在,是时候看看 CGI 脚本了(清单 3)。
清单 3. CGI 脚本
#!/usr/bin/perl use DBI; $dbh = DBI->connect("dbi:Pg:dbname=database", "postgres", "password") || die "Can't connect to database.\n"; print "Content-type: text/xml\n\n\n"; print "<panels>\n"; check_cid("/tmp/panels/cid"); print "</panels>\n"; exit; sub check_cid { my($dir) = @_; my(@a, $a, $file, $count, $top); local(*FILE, *DIR); opendir DIR, "/tmp/panels/cid"; while ($file = readdir(DIR)) { if ($file eq ".") { next; } if ($file eq "..") { next; } open FILE, "/tmp/panels/cid/$file"; chomp($line = <FILE>); close FILE; ($dir, $name, $number, $uid) = split("#", $line); $count++; if ($dir eq "IN") { $html = "Incoming call from $name ($number)"; } else { $html = "Outgoing call from $name ($number)"; } expire_call($uid); print <<EOF <panel> <name>phone$count</name> <content>$html</content> </panel> EOF ; } } sub expire_call { my($id) = @_; my($sth, $count); $sth = $dbh->prepare("select count(*) from cdr where uniqueid=\'$id\'"); $sth->execute(); ($count) = $sth->fetchrow_array(); if ($count) { unlink("/tmp/panels/cid/$id"); } }
这个 Perl 脚本扫描 /tmp/panels/cid 目录中的文件,跳过 . 和 .. 条目。它找到的每个文件都会被打开和读取。最终结果是一个 XML 文件,如清单 4 所示。
清单 4. 生成的 XML 文件
<panels> <panel> <name>phone1</name> <content>Incoming call from Mike Diehl (15055558592)</content> </panel> </panels>
当然,XML 文件最多可以包含四个与 phone1 到 phone4 对应的 <panel> 块。<content> 块包含放入每个便签中的文本。我发现因为这是一个 XML 文件,所以很难在 <content> 块中嵌入 HTML,所以我没有对这段文本进行太多格式化。很容易看出如何分别处理呼入和呼出呼叫。
当为每个电话呼叫生成 XML 并发送到客户端时,将调用 expire_call()。此函数只是搜索 CDR 数据库以查看电话呼叫是否已完成。Asterisk 仅在呼叫结束时添加 CDR 记录,因此如果记录在数据库中,则呼叫已结束,并且可以删除 /tmp/panels/cid 中的文件。
JavaScript 组件既是系统的主力,也是最难理解的部分(清单 5)。
清单 5. JavaScript 组件
function start_cid () { setInterval("update_cid()", 1000); } function update_cid () { var req; var xml; var panels; var count; var name; var div; req = get_from_server(); clear_panels(); xml = req.responseXML.getElementsByTagName("panels")[0]; panels = xml.getElementsByTagName("panel"); for (count=0 ; count < panels.length ; count++) { panel = panels[count]; name = panel.getElementsByTagName("name")[0]; name = name.firstChild.nodeValue; content = panel.getElementsByTagName("content")[0]; content = content.firstChild.nodeValue; div = document.getElementById(name); div.style.display="block"; div.innerHTML = "<b>" + name + ": </b>" + content; if (div.innerHTML == "") { div.style.display="none"; } } } function get_from_server () { var req; if (window.XMLHttpRequest) { req = new XMLHttpRequest(); } else if (window.ActiveXObject) { req = new ActiveXObject("Microsoft.XMLHTTP"); } req.open("GET", "/cgi-bin/cid.pl", false); req.send(null); return req; } function clear_panels () { for (count=1 ; count < 5 ; count++) { document.getElementById("phone" + count).innerHTML = ""; document.getElementById("phone" + count).style.display="none"; } return; }
如前所述,整个系统由最初调用 start_cid() 启动。此函数所做的只是安排每秒调用一次 update_cid() 函数。update_cid() 函数调用 get_from_server() 以浏览器独立的方式获取 XMLHttpRequest 对象。此请求对象被返回以供以后使用。
接下来,update_cid 函数调用 clear_panels(),它只是安排每个便签最初都是空的且不可见的。当我们将内容放入其中时,便签将变为可见。
程序的其余部分有点难以理解。使用前面提到的请求对象和 getElementsByTagName() 函数,我们获得了一个包含完整的 <panels> 块的 XML 对象。getElementsByTagName() 的另一个应用应用于此 XML 对象,为我们提供了一个单独的 <panel> 块数组。
然后,我们开始循环遍历数组中的每个 <panel> 块,并理解每次循环都将对应一个正在进行的电话呼叫;我们将为每个呼叫创建一个新的便签。每个 <panel> 块都包含一个 <name> 和一个 <content> 块,我们将其值提取到适当的变量中。然后,通过使用 getElementById() 文档方法,我们在 HTML 文档中找到了 ID 与面板名称相同的 <div> 元素。现在我们拥有了关于便签的所有必要信息:名称、内容和在网页中的位置。因此,我们将 <div> 块设置为可见,然后通过 innerHTML 属性为其分配一些内容。最后,我们回到循环的顶部并再次继续。
这个“轮询服务器并显示结果”的过程每秒运行一次,无需用户任何干预,也无需重新加载网页。这给用户一种印象,即便签只是在电话铃响时弹出,并在电话挂断时消失。
正如您所看到的,JavaScript 是一种非常强大的语言。不幸的是,浏览器支持和 JavaScript 开发工具非常差甚至不存在。在开发此程序期间,我不得不与浏览器崩溃、意外缓存的信息和神秘的运行时错误消息作斗争。一旦我使其工作,我就必须确保它在我经常使用的每个浏览器 Konqueror 和 Firefox 上都能工作。我怀疑它也会在“另一个浏览器”上运行,但我没有测试过。因为我大部分软件开发都是用 vi 完成的,所以我对集成开发环境 (IDE) 不是很感兴趣,但是如果您知道哪个 IDE 非常适合 JavaScript,我很乐意听取您的意见。
现在程序已经可以工作了,是时候考虑改进和扩展它的方法了。我想对此程序做的第一个明显的更改是使其显示一个超链接,该链接将允许我调出有关呼叫者的更多信息。它可以从我的联系人列表甚至从额外的数据库中获取此信息。也许它可以显示呼叫者的照片,尽管拍摄我所有的朋友、家人和熟人的照片可能需要很多时间。对于来电,如果有一个按钮显示,让我可以拒绝来电并使其直接转到语音邮件,那也很不错。我还可以扩展相同的方法,让网页显示来电显示以外的其他信息。扩展此系统以让我知道我何时有未读语音邮件等待,或者我的朋友何时可以通过 IM 进行聊天,这并不难。
所以这就是您所看到的——一个有趣的小玩具,它汇集了许多不同的工具和技术。回想起 Qwest 过去每月向我们收取 6 美元的来电显示费用,我想知道他们会收取多少费用使其可以从网络访问?
Mike Diehl 在新墨西哥州阿尔伯克基的 Sandia 国家实验室为 SAIC 工作,他在那里编写网络管理软件。Mike 与他的妻子和两个年幼的儿子住在一起,可以通过电子邮件 mdiehl@diehlnet.com 与他联系。