Asterisk 的时区处理,第二部分
上个月,我撰写了一篇关于使用 Asterisk 处理电话呼叫的系统,该系统可以根据远程位置的当天时间自动处理呼叫。然而,该系统的使用依赖于用户执行关键任务,即通过电话呼叫设置远程位置的时间。与其依赖用户手动发起电话呼叫,不如让呼叫自动发生会更容易。
如果设置呼叫自动发生,用户就不会忘记执行它。初始 SIP 注册可能发生在本地位置的非常奇怪的时间,但 SIP 注册的发生是因为用户插入了某些设备。因此,我们知道用户是清醒的,可以接听短时间的呼叫。Asterisk 提供了一个管理界面,可以报告 SIP 注册何时发生,并可以用于根据它采取行动。通过一些额外的处理,与管理器对话的脚本可以仅在 SIP 注册为“新”时发起呼叫。
Asterisk 管理器报告 Asterisk 处理的事件,并通过接口接受命令。接口的形式是一种基于文本的协议,它将事件报告和命令分隔成具有键:值格式的行簇。例如,使用 SIP 协议注册分机 300 看起来像这样
Event: PeerStatus PeerStatus: Registered Peer: SIP/300
在获得对 Asterisk 管理器的访问权限之前,客户端必须根据存储在 /etc/asterisk/manager.conf 中的管理用户列表进行身份验证。登录后,客户端可以使用一组以 Action: 开头的行,后跟命令来发出命令或查询变量的值。对命令的响应通常以 Response: Success 开头。
由于该协议是基于文本的,因此可以使用像 Expect 这样的语言进行脚本编写。Perl 对象环境 (POE) 也提供了一个组件,POE 是一个在 Perl 中构建事件驱动程序的框架。这个免费提供的组件提供了需要在 Expect 中编写的基础级别响应解析,因此它是控制 Asterisk 管理器的程序更具可扩展性的基础。
程序的主要代码很简单。POE 设置了一个系统,其中状态处理程序被调用以响应程序状态。状态可以由程序员或外部事件定义。程序中的典型流程是注意到 SIP 注册,检查它是否具有活动的时区注册,如果没有,则发起配置呼叫。
为了响应事件执行代码,POE 框架使用一个名为 CallBacks 的哈希。CallBacks 中的每个条目都根据从管理器接收的事件定义一个状态。当事件与回调匹配时,将触发为该状态定义的处理程序。要使用 CallBacks 子句设置触发器,请识别事件中的每一行,并设置一个哈希,以便事件每一行的左侧是哈希行键值。例如,考虑前面 SIP 注册事件的回调定义
Event: PeerStatus register => { 'Event' => 'PeerStatus', PeerStatus: Registered 'PeerStatus' => 'Registered', } Peer: SIP/300
为了将回调链接到处理程序,inline_states 哈希具有状态列表和对要调用的相应代码的引用。虽然可以将事件处理程序代码内联,但为了提高可读性,我已将代码分离到外部过程中。响应 CallBack 调用的代码无法传递参数
123456789*123456789*123456789*123456789*123456789*12 inline_states => { register => \®ister_state_handler, },
根据程序的流程,有三个事件值得关注。首先,SIP 注册事件用于启动整个过程。SIP 注册通常每小时发生一次,因此仅在注册为“首次”注册时才发起呼叫非常重要。为了防止发起重复的电话呼叫,程序将从 Asterisk 内部数据库 AstDB 以及 SIP 对等信息请求数据。第二个和第三个事件将分别处理对命令和数据库查询的响应。第四个事件将在收到查询返回的数据后处理发起电话呼叫。我目前正在使用的代码还为注销事件定义了一个状态,尽管它是我目前未使用事件的存根。
程序的核心只有 35 行,其中大部分定义了程序事件状态,并显示了将使用哪些代码来响应这些状态。请注意,呼叫状态由程序而不是回调定义,因此呼叫状态只能由程序本身输入,而不是响应来自管理器的事件。(程序的完整列表可在 Linux Journal FTP 站点上找到;请参阅“资源”部分。)
POE::Component::Client::Asterisk::Manager->new( Alias => 'monitor', RemoteHost => 'localhost', RemotePort => 5038, Username => 'autotzcaller', Password => 'secretpassword', CallBacks => { input => ':all', response => { 'Response' => 'Success', }, dbresponse => { 'Event' => 'DBGetResponse', }, register => { 'Event' => 'PeerStatus', 'PeerStatus' => 'Registered', }, unregister => { 'Event' => 'PeerStatus', 'PeerStatus' => 'Unregistered', }, }, inline_states => { input => \&input_state_handler, response => \&response_state_handler, dbresponse => \&db_response_state_handler, register => \®ister_state_handler, call => \&call_state_handler, unregister => \&unregister_state_handler, }, ); POE::Kernel->run(); exit(0);
其中两个状态处理程序只是存根。如果设置了调试标志,输入状态处理程序会打印出它获得的任何内容,它在那里是为了开发目的。它捕获来自管理器的任何无法识别的事件,并且在测试回调是否捕获重要事件时很有用。注销状态处理程序目前不执行任何操作,但它在那里作为一个钩子,以便将来如果我选择根据它采取任何操作时进行扩展。
在程序核心就位后,让我们按照它们在典型程序执行流程中被调用的顺序查看每个状态。
每当从新分机收到 SIP 注册事件时,都会调用注册状态处理程序。其主要目的是获取设置配置电话呼叫所需的数据,以便在新分机弹出时进行设置。是否进行呼叫取决于分机的状态(就时区处理而言),因此该例程请求信息以确定分机是否已注册、其 IP 地址和其他组件。要获取分机,我们必须获取通道名称,该名称以技术和斜杠(例如 SIP/)开头,并剥离前导部分。
事件处理程序的一个问题是 POE 处理程序运行到完成。在处理程序运行时,无法中断处理程序。子过程 getTZChannelVars 将请求有关时区偏移量和 IP 地址的信息,但该信息在注册处理程序完成且响应通过管理器返回之前不会变为可用。在该过程结束时,注册处理程序使用 delay_set POE 方法将呼叫状态排队,以便在将来延迟,以便请求将在那时返回其信息。延迟由程序中的全局变量设置。我发现对于只有一个待处理分机需要设置的单用户 PBX 来说,一秒钟以上的时间已经足够了,但为了安全起见,延迟设置为三秒。
状态处理程序之间的通信与过程驱动程序中的通信略有不同。POE 状态处理程序将引用传递给 POE 内核(用于调度)以及 POE 堆(用于向 Asterisk 管理器发出命令)。POE 定义了常量,因此堆和内核可以很容易地被事件处理程序访问,分别为 $_[HEAP] 和 $_[KERNEL]。任何其他可用信息都位于 $_[ARG0],这是一个常量,其定义方式使其成为第一个参数。
事件中定义状态的任何行都将作为哈希 $_[ARG0] 传递,并且可以通过询问所需行左侧出现的哈希键来访问。在注册响应中,可以通过引用以下内容来访问对等分机$_[ARG0]->{Peer},这将返回SIP/300:
Event: PeerStatus PeerStatus: Registered Peer: SIP/300
在 SIP 注册时,程序需要识别分机,请求有关它的信息,然后在延迟后设置分机数据的进一步处理。当通过 delay_set 方法调用事件时,可以将分机传递给状态处理程序,例如此处使用的分机号
sub register_state_handler { my $kernel = $_[KERNEL]; # Split peer extension off from technology my $peer = $_[ARG0]->{Peer}; debug_print("\tExtension is $peer; "); my @exten_parts = split('/',$peer); my $ext = @exten_parts[1]; debug_print("extension number is $ext\n"); getTZChannelVars($_[HEAP], $ext); debug_print("Queuing call event for "); debug_print("$REG_CALL_DELAY seconds\n"); $kernel->delay_set("call", $REG_CALL_DELAY, $ext); } # register_state_handler
作为分机注册过程的一部分,我们在 getTZChannelVars 过程中收集有关通道状态的变量。作为第一个参数传递的 POE 堆可用于向管理器发出命令。例如,服务器的 put 参数可用于发出命令。要获取 SIP 对等数据(包括对等方的当前 IP 地址),命令如下所示
$heap->{server}->put({'Action' => 'SIPShowPeer', 'Peer' => $ext });
要获取数据库变量,put 命令中的 action 是 DBGet。时区数据作为 tz 系列中的键存储,因此有必要指定系列并组装正确的键名,其形式为 300-TIMESKEW 或类似形式
$heap->{server}->put({'Action' => 'DBGet', 'Family' => 'tz', 'Key' => $ext . '-TIMESKEW'});
getTZChannelVars 请求了四个数据库请求和 SIP 对等数据。由于此函数由事件处理程序调用,因此它也是不可中断的。因此,它向管理器发送四个数据库查询事件,但它不直接处理响应。(完整过程中五个请求的完整代码可在 Linux Journal FTP 站点上找到。)
在发出请求和调度呼叫状态的时间间隔内,响应从 SIP 数据请求和数据库查询中流入。从 SIP 数据请求中,我们需要挑选出对等 IP 地址,该地址出现在管理器响应的行中,内容为Address-IP: 192.168.1.5。方便的是,POE 模块解析了响应中的行,因此我们所需要做的就是通过获取传递给处理程序的参数之一中 Address-IP 哈希元素的值来查找 Address-IP 行。POE 堆在事件之间是可访问的,因此将 SIP 对等 IP 地址的值添加到堆中使其可以被其他事件处理程序访问
sub response_state_handler { my $peer_ip = $_[ARG0]->{'Address-IP'}; if (defined($peer_ip)) { debug_print("SIP context found; Peer IP address" debug_print("is $peer_ip\n"); $_[HEAP]->{'SIP-Peer-IP'}=$peer_ip; } } # response_state_handler
在 SIP 数据响应返回后,四个数据库查询应返回响应。对查询的响应如下所示
DBGetResponse: Success Family: tz Key: 300-TIMESKEW Val: -8
每当有DBGetResponse: Success来自管理器的事件,其参数是一个哈希,其中包含数据包中的每一行。我们感兴趣的是键和值行,可以从传递给状态处理程序的参数中检索到。与之前的处理程序一样,响应存储在 POE 任务堆中,以便其他处理程序可以使用它
sub db_response_state_handler { my $family = $_[ARG0]->{'Family'}; my $key = $_[ARG0]->{'Key'}; my $value = $_[ARG0]->{'Val'}; if (defined($family)) { debug_print("Key $key in DB family $family"); debug_print("has value = $value\n"); # Store in heap $_[HEAP]->{$key} = $value; } } # db_response_state_handler
每个注册事件都会触发一个“呼叫”事件在延迟后发生。延迟用于收集用于确定是否发起呼叫的信息。只有当时间区设置已过期或 SIP 设备已更改其 IP 地址且记录不再有效时,才应触发设置电话呼叫。
由于呼叫状态处理程序由注册处理程序放置在队列中以供执行,因此它确实有一个参数,即所讨论的呼叫的分机号。分机可以作为 $_[ARG0] 检索。通过处理数据库响应和 SIP 数据请求添加到堆中的所有数据也随时可用
sub call_state_handler { # Get extension out of arguments to function my $exten = $_[ARG0]; my $hp = $_[HEAP]; # Variables we use to determine if the call is required my $skew = $hp->{$exten.'-TIMESKEW'}; my $skew_addr = $hp->{$exten.'-TIMESKEW_ADDR'}; my $skew_start = $hp->{$exten.'-TIMESKEW_START'}; my $skew_end = $hp->{$exten.'-TIMESKEW_END'}; my $sip_peer_ip = $hp->{'SIP-Peer-IP'}; my $now = time();
为了确定是否需要呼叫,处理程序将当前时间与时区偏移记录的到期时间以及 SIP 设备的 IP 地址与存储在时区偏移记录中的 IP 地址进行比较。如果 IP 地址匹配且偏移量未过期,则不需要呼叫。否则,需要呼叫并使用 makeTZSetupCall 函数进行呼叫
if ($now > $skew_end) { debug_print("Make call - offset has expired.\n"); makeTZSetupCall($_[HEAP], $exten); } elsif (!($skew_addr eq $sip_peer_ip)) { debug_print("Make call - SIP IP addr changed\n"); makeTZSetupCall($_[HEAP], $exten); } else { debug_print("No call -- record OK & same IP\n"); }
作为最后一步,处理程序需要删除放置在堆上的变量。堆仅用于在状态处理程序之间传递变量,并且一旦该函数完成,就不再需要这些变量。可以使用 undef 函数取消定义每个变量
# Need to clean up heap undef $_[HEAP]->{$exten.'-TIMESKEW'}; undef $_[HEAP]->{$exten.'-TIMESKEW_ADDR'}; undef $_[HEAP]->{$exten.'-TIMESKEW_START'}; undef $_[HEAP]->{$exten.'-TIMESKEW_END'}; undef $_[HEAP]->{'SIP-Peer-IP'}; } # call_state_handler
发起设置呼叫使用 Asterisk 管理器的 Originate 命令,但它受到最后一个检查的保护。我定义了一组分机作为远程通道列表。只有远程通道列表上的分机才会进行时区设置呼叫。最初,该列表包含我的软电话和模拟电话适配器,但我将来可能需要添加更多。在发起呼叫之前,我确保该号码在远程通道列表上,该列表在全局数组 REMOTE_CHANNEL_LIST 中定义。Originate 命令也可以接受多个参数。分机、优先级和上下文必须引用设置菜单的定义位置。在我的情况下,这些值是分机 *89(对于 *-T-Z)、优先级 1 和上下文 from-internal。我还可以向我正在呼叫的电话提供“时区设置”的来电显示文本
sub makeTZSetupCall { my $heap = $_[0]; my $exten = $_[1]; my $callOK = 0; # Check that extension to call is a remote channel foreach $number (@REMOTE_CHANNEL_LIST) { if ($number == $exten) { $callOK = 1; } } if ($callOK) { $heap->{server}->put({ 'Action' => 'Originate', 'Channel' => 'SIP/'.$exten, 'Context' => $TZ_CONTEXT, 'Exten' => $TZ_EXTEN, 'Priority' => $TZ_PRIORITY, 'Callerid' => $CALLERID, }); } } # makeTZSetupCall
如果 Originate 命令被触发,新注册的电话会响铃,我将通过上个月文章中描述的语音菜单。
资源
Perl 脚本完整源代码列表:ftp.linuxjournal.com/pub/lj/listings/issue156/9284.tgz
关于 Asterisk Manager API 的信息:www.voip-info.org/wiki-Asterisk+manager+API
Perl POE 框架:poe.perl.org
Perl Asterisk Manager 组件:search.cpan.org/~xantus/POE-Component-Client-Asterisk-Manager-0.06/Manager.pm
Matthew Gast 是关于无线 LAN 的领先技术书籍 802.11 无线网络:权威指南(O'Reilly Media 出版)的作者。可以通过 matthew.gast@gmail.com 与他联系,但仅当他靠近海平面时。