选择框架
(作者:米哈尔·马蒂斯卡)
你可能会问,选择框架究竟为何会被引入?最主要的原因是,以往每当你需要检查入站消息的任意部分(如消息头)时,都必须编写新的模块或新的函数来实现该功能。此外,受历史局限性的影响,这类函数最多只能接收两个参数。若需要传入更多参数,就必须采用变通方案:先设置好相关属性,再由该函数对属性进行校验。因此,为了让路由脚本更具可读性和易懂性,一种全新的功能组件——选择框架,应运而生并得到了支持。
在脚本中,每个选择项都通过其唯一名称来标识。设计这种标识符的初衷,是让它的含义尽可能直观易懂。例如,当你看到@to.uri.user时,就能立刻明白它的作用是从To消息头所包含的URI中提取用户名。在脚本里,无论你在何处看到选择项标识符,它所代表的实际上都是对应的值,这个值会在路由脚本的执行过程中被调用。
为了保证选择框架的执行效率,选择项标识符会在SER启动阶段完成解析和固化。如果标识符无效,系统会输出错误信息,且SER将无法启动。而在请求路由脚本的执行阶段,仅需发起一次函数调用——如果该调用需要用到选择项标识符,就会将解析后的标识符作为参数传入。从性能角度来看,调用某个模块中的专用函数,与调用代表选择项标识符的函数,二者几乎没有差异。
工作原理概述
我们所说的选择项,本质上是一种标准化调用方式,用于调用那些在SER内核或各类模块中被定义为“选择函数”的方法。选择项标识符以@符号开头,由若干文本元素组成,元素之间用英文句点.分隔;部分场景下,还可以在方括号[]内添加单个整数或字符串作为参数。
选择项可在路由脚本的以下场景中使用:
- 表达式求值过程中;
- 作为属性赋值操作的右值;
- 作为部分函数的调用参数(此用法仅适用于支持该特性的函数,且参数必须是包含选择项标识符的文本,因此需要用双引号括起来)。
此外,在Xlog日志格式化功能中,选择项也可作为组成最终日志内容的元素,其格式为%@select.identifier.as.usual。
选择函数的返回值为文本字符串,但需要注意的是,在以下两种情况下,返回值也可能是长度为0的空字符串:一是该空值本身是合法的(例如某些URI参数允许为空);二是选择函数在消息头中未找到目标值,或在解析消息时遇到语法错误。这类异常情况会对条件表达式产生影响——只要选择项的求值过程出现错误,无论其结果是否为空字符串,条件表达式的最终判断结果都会为false。
选择项标识符规则
选择项标识符以@符号开头,后接至少1个、最多30个文本元素,元素之间用英文句点.分隔。标识符的元素大小写不敏感,你可以通过大小写区分来突出某些元素,但建议优先使用小写形式。
部分选择函数支持传入参数(例如重复消息头的索引、消息头名称或鉴权域),这类参数需要放在方括号[]内,嵌入到标识符中。参数索引从1开始计数(程序员需特别注意这一点),这种设计更符合直观认知;同时,你也可以使用负数索引表示反向计数,例如用-1表示获取最后一个消息头。
合法的标识符示例
@the.simplest @another.with!["parameter"] @yet.another![1].parameter![2] @you.can.also.mix.string!["like-this"].and.integer![1].parameters嵌套选择项
你可能会发现SIP请求中存在一些重复的模式,例如许多SIP消息头中都包含URI元素。选择框架支持复用内核中已有的URI解析器和选择函数(也可以是参数值解析器,包括消息头参数或URI参数解析器,或是其他你能想到的解析器),并将其作用于选择项的中间结果,以此简化开发流程。
这种用法被称为嵌套选择项(如果感兴趣,可参考开发者文档),它会将一次选择项调用拆分为两次(或多次)独立的函数调用:第一次调用会返回URI文本作为临时结果,随后该临时结果会被传入内置的URI选择处理机制,最终生成目标结果。
例如,@hf_value!["X-any-header"].nameaddr.uri.user的执行逻辑为:
- 提取
X-any-header消息头的值; - 假设该值符合RFC规范中对
name-addr格式的定义,将其传入name-addr选择解析器; - 解析器从URI中提取用户名,并将其作为该选择项的最终返回值。
所有返回URI的选择项结果,都可以传入专门的URI选择函数,从而访问URI的各个组成部分。
以下选择项均可返回对应的URI:@request_uri、@dst_uri、@next_hop、@from.uri、@to.uri、@refer_to.uri、@rpid.uri、@contact.uri、@record_route.uri,以及@hf_value!["any-header"].nameaddr.uri。
若只需获取用户名,只需在选择项标识符后追加.user即可。无论第一步获取的是哪一个URI,.user这个标识符后缀的含义和用法都是完全相同的。
URI嵌套选择项标识符列表
| 标识符 | 功能说明 |
|---|---|
type | 返回标准化(小写)的URI类型,例如sip、sips、tel、tels |
user | 返回URI中的用户名 |
pwd | 返回URI中的密码 |
host | 返回URI中的主机地址 |
port | 返回URI中的端口号(以上标识符的功能均通俗易懂) |
transport | 返回URI中transport参数的值;若该参数不存在,则返回基于URI类型的默认传输协议 |
hostport | 返回URI中的主机:端口组合值;若端口未指定,则使用基于URI类型的默认端口 |
params | 返回URI中的完整参数部分,例如user=phone;phone-context=+1234 |
params!["parameter-name"] | 返回URI中指定参数的值 |
选择项在表达式中的应用
选择项可用于条件表达式,常见用法如下:
非空判断
if (@select.value) {...}当选择项返回非空字符串时,条件判断结果为
true。注意:这种用法已被废弃,SER启动时会输出警告信息。在Sip-router中,该写法的逻辑变为:仅当返回值为非空且非零的数字字符串(如
"123")时,条件才为true。
推荐使用标准写法:if (@select.value!="")或
if (!strempty(@select.value))等值判断
if (@select.value=="string") {...} if ("string"==@select.value) {...}当选择项求值过程无错误,且返回值与
"string"完全相等时,条件为true。表达式右侧可以是任意值(包括AVP变量、PV变量或其他选择项),而非仅限于常量。不等值判断
if (@select.value!="string") {...} if ("string"!=@select.value) {...}用于判断两个字符串是否不相等。若选择项求值过程出错,条件判断结果也会为
false。正则匹配
if (@select.value=~"reg.*expr.?") {...}当选择函数返回的字符串匹配指定正则表达式时,条件为
true。注意:该运算符的左右操作数顺序不可随意调换,例如:if ("string"=~@select.value) {...}其逻辑是判断固定字符串
"string"是否匹配选择项返回值作为正则表达式的规则,这通常不符合实际需求。
选择项可用于任意类型的表达式中,包括赋值操作的右值、if条件判断、while循环条件或switch分支判断等。
示例:
$attribute=@the.select.you.want选择项在函数参数中的应用
部分函数支持将选择项的返回值作为调用参数,但该特性仅适用于明确支持此功能的函数。这类函数的参数会存储选择项标识符,函数会在SER启动阶段负责解析并固化该标识符,在运行阶段调用对应的选择函数以获取参数值。
若选择项求值过程出错,函数会返回false,且函数后续的执行逻辑通常会被跳过。
系统选择项
随着选择框架的优势逐渐显现,开发者开始探索更多可通过选择项访问的功能,系统选择项便是其中之一。这类选择项的标识符均以@sys开头,例如:
@sys.pid:返回当前进程ID的字符串形式@sys.now:返回Unix时间戳@sys.utc:返回UTC时间戳(适用于多时区场景)@sys.unique:生成并返回一个随机的UUID(版本2)
模块中的选择项
选择框架是一种功能强大的工具,且可通过SER的模块机制轻松扩展。以TLS模块为例:脚本中需要获取或校验的大量TLS相关信息,都需要依赖TLS模块才能实现。因此,所有与TLS相关的选择项都隶属于TLS模块——当你加载该模块后,这些选择项即可使用;若未加载模块,SER会因检测到无效的选择项标识符而拒绝启动。
从使用角度来看,模块提供的选择项与内核原生的选择项并无差异,你无需关心选择项的具体来源,只需确保已加载所有必需的模块即可。
选择项完整列表
关于Sip-router开发版本(主分支)中已定义的所有选择项,可参考官方文档:http://sip-router.org/docbook/sip-router/branch/master/select_list/select_list.html。
追踪:• 选择项