1. select是什么先分清它和伪变量的关系做Kamailio配置脚本写了几年$rU、$fU、$du这些伪变量用得很顺手但有一类语法我身边很多人用得很少甚至看到别人代码里的$(fU{uri.user})会愣一下——这就是Kamailio的select。说起来它不算新功能从OpenSER时代就已经存在只是官方文档总把它放在伪变量那一章的角落里写着写着就被很多人跳过了。数据库领域有SQL SELECT网络编程里有select()多路复用不少人在搜索引擎里找Kamailio select时还会被各种同名词带偏。这篇文章专门聊聊Kamailio配置脚本里的select到底能干什么从SIP消息中按名称取出某个字段并且可以当场做字符串变换。适合谁看凡是写Kamailio路由脚本、做号码规整、Contact地址提取、日志脱敏、按头字段分流的人这篇文章都会对你有用。不夸张地讲把select用好了你的路由配置能少写一半正则。1.1 一个让新手困惑的写法先看一段最直观的示例if ($(rU{s.tolower}) alice) { xlog(L_INFO, alice的请求到了\n); }这里$(...)包裹的部分就是select。rU表示Request-URI里的user部分后面{s.tolower}是变换transform意思是在取到用户名后顺手转成小写。整个过程在一条表达式里完成不需要先赋值、再清洗、再比较。这就是select最大的存在价值把“取值”和“加工”合并成一步。很多第一次接触的人困惑在于$rU不是也能取Request-URI的user吗为什么还要写$(rU)区别在于$rU是一个普通的伪变量Pseudo-Variable它的职责就是“直接返回一个固定字段”而$(rU)是select除了返回字段之外它还可以继续接任意多个变换。简单理解伪变量给的是“已经做好的菜”select给的是“可以按你的口味再加工的食材”。有人可能会问那我是不是应该所有地方都用select不是。伪变量短、快、清晰常见字段直接用伪变量是最省事的只有当你要做大小写转换、截取、正则替换、URI参数提取这些操作时select才真正派上用场。1.2 伪变量和select的对照Kamailio从早期OpenSER开始就一直同时保留两套取值机制。伪变量以$加短名组成比如$ru、$fU、$du设计目标是用最少字符访问最常用字段所以在配置里出现频率很高。select采用$(名字)的形态名字本身可以表达更完整的属性维度并且天然支持变换链。写法类型返回内容是否能继续变换$fU伪变量From头部URI中的user部分否$(fU)selectFrom头部URI中的user部分是$(fU{s.tolower})select加变换转小写后的From用户名是$ru伪变量完整Request-URI否$(ru)select完整Request-URI是我实际用下来的体会是select并不是伪变量的高级替代品两者是互补关系。如果只是判断$rU是否等于某个号码直接用伪变量代码更短如果要做$(rU{s.substr,0,3})这样的前缀截取那就只能交给select。还有一点要注意select里的字段名大小写有严格含义$(rU)和$(ru)返回的是完全不同的两个东西前者是R-URI的user后者是整个R-URI。这个坑我后面会专门讲。1.3 select的字段家族select能取的字段覆盖面很广但配置脚本里真正高频的其实就集中在几组R-URI组、From组、To组、Contact组和消息体组。我整理成一张速查表select写法返回内容$(ru)完整Request-URI字符串$(rU)Request-URI中的user部分$(rd)Request-URI中的domain部分$(fu)From头部的完整URI$(fU)From头部URI中的user部分$(fd)From头部URI中的domain部分$(tu)To头部的完整URI$(tU)To头部URI中的user部分$(td)To头部URI中的domain部分$(cu)Contact头部的URI部分$(cU)Contact头部URI中的user部分$(ct)Contact头的完整字符串$(du)当前下一跳的destination URI$(rb)SIP消息的原始body如果你的路由脚本主要处理INVITE、REGISTER、OPTIONS这三类消息把上面这张表里R-URI、From、To、Contact四组记住就够用了。剩下那些更冷门的属性用到的时候再查官方文档也不迟。记住一个原则select的名字是小写全称还是大小写混合直接影响返回值写之前先想清楚你到底要整个URI还是要user。2. 常用select字段路由脚本里的取号、取头、取body2.1 按主叫或被叫号码段分流运营商中继接入时最常见的一个需求根据号码前缀把请求路由到不同网关。比如北京号码走北京网关上海号码走上海网关。用select可以这样写if ($(rU{s.substr,0,3}) 010) { route(BEIJING_GW); } else if ($(rU{s.substr,0,3}) 021) { route(SHANGHAI_GW); }这里$(rU)先取出被叫号码{s.substr,0,3}表示从第0位开始截取3个字符也就是号码前三位。注意substr的offset是从0开始数的和我早期理解的不太一样后来用xlog打出来才确认。如果上游送来的号码带了前缀可以先把前缀剃掉再截取$var(caller) $(rU{re.subst,/^\//}{s.substr,0,3}); if ($var(caller) 010) { route(BEIJING_GW); }这个例子里变换链做了两件事先用正则^\把开头的加号替换成空串再截取前三位。这种前缀路由场景我建议把结果先赋值给$var()因为后续可能还有日志、计费、DB查询都要用这个号码避免同一个select被反复执行。2.2 从From和Contact里提取真实身份REGISTER请求的处理是select发挥价值的好地方。很多UA在注册时From头里的URI和Contact头里的URI并不完全一致尤其是企业网关转发过来的注册Contact里往往才是设备的真实地址。要取设备标识直接用$var(contact_user) $(cU{s.tolower}); xlog(L_INFO, contact user: $var(contact_user)\n);$(cU)返回Contact URI中的user部分后面接{s.tolower}转小写避免设备ID里大小写不一致导致后续比对失败。这里要注意的是$(cu)和$(cU)的区别$(cu)返回整个Contact URI比如sip:device123192.168.1.50而$(cU)只返回device123。我见过有人把$(cU)写成$(cu)然后在if判断里怎么都比不相等最后查了半天发现是把完整URI拿去跟用户名比了。同样道理From头的处理也有对称的三件套$(fu)整个URI、$(fU)用户名、$(fd)域名。我习惯在脚本里像下面这样把三个值都打出来观察一次xlog(L_INFO, from uri: $(fu) | from user: $(fU) | from domain: $(fd)\n);这行日志在排障时会救你的命因为它能让你一眼看出当前SIP消息里From头到底长什么样字段结构是否和你预判的一致。2.3 用body内容判断消息性质有些场景下光看头部信息不够还要看消息体。select里对应的是$(rb)它返回SIP消息的原始body。比如某些终端会在INFO请求的body里放DTMF或早期媒体标记需要根据body内容决定是否放行if ($(rb) ~ early-media) { xlog(L_WARN, 检测到early-media标记\n); }这里用到了Kamailio的正则匹配操作符~。需要注意$(rb)返回的是完整body如果body特别大每次都拿它做正则匹配会有性能开销。如果有必要可以先判断长度再匹配if ($(rb{s.len}) 0) { if ($(rb) ~ early-media) { xlog(L_WARN, 检测到early-media标记\n); } }s.len变换返回字符串长度提前做一次空判断可以避免正则引擎去处理空串。实际生产里我还遇到过一种情况某些UA在OPTIONS请求的body里带了XML格式的能力集直接对完整body做子串匹配是最省事的方案不需要引入额外的XML解析模块前提是你只想判断“有没有”而不是“具体是什么”。2.4 字段不存在和空值判断select实际执行时如果对应字段不存在返回的是空字符串。比如请求的R-URI只有域名没有user那么$(rU)就是空的。这时候如果你直接拿它去做比较会得到一个永远为false的条件等于这段路由逻辑被静默跳过。处理空值有两种常见模式。一种是明确判断if ($(rU) $null) { xlog(L_ERR, R-URI缺少user部分\n); sl_send_reply(404, Not Found); exit; }另一种是把空值先归一化成默认值$var(user) $(rU); if ($var(user) $null) { $var(user) unknown; }这里特别提醒一句$null这个关键字在Kamailio配置里用来比较伪变量是否为空它是区分“空字符串”和“未定义变量”的常用手段。写判断之前最好先用xlog把select的实际返回值打出来确认字段真的存在再开始写业务逻辑。不要凭经验猜字段结构因为不同UA厂商实现的SIP头千奇百怪。3. 变换transform用select一行完成清洗与格式转换3.1 变换就是“取字段加加工”一条龙select真正的杀手锏是变换。所谓变换可以理解成在select取出字段值之后再套上一个加工管道。原料还是那个原料但经过管道处理后的输出才是你真正想要的值。比如主叫号码是8613800138000你想要的是13800138000不需要先substr再判断再拼接直接在select后面挂变换即可$var(caller) $(fU{re.subst,/^\86//}{s.trim}{s.tolower});这条表达式从左到右依次做了三件事用正则^\86把开头的加号和86国家码替换成空串去掉首尾空白再统一转小写。变换可以一个接一个地串联顺序就是执行顺序。这种写法在配置里特别适合做号码规整不管上游送来的是8613800138000、13800138000还是带空格的脏号码一条select全搞定。3.2 常用变换速查表我把自己在项目里真正用过、没出过问题的变换整理了一下变换写法作用示例{uri.user}提取URI中的user部分$(ru{uri.user}){uri.host}提取URI中的domain部分$(fu{uri.host}){uri.port}提取URI中的port部分$(ru{uri.port}){uri.params}提取URI中的参数串$(ru{uri.params}){uri.param,名称}提取URI中指定参数的值$(ru{uri.param,user}){s.len}返回字符串长度$(rb{s.len}){s.substr,起点,长度}截取子串起点从0开始$(fU{s.substr,0,3}){s.tolower}转小写$(fd{s.tolower}){s.toupper}转大写$(fU{s.toupper}){s.trim}去掉首尾空白$(fU{s.trim}){re.subst,/正则/替换/}正则替换第一个匹配$(fU{re.subst,/^\//}){re.substall,/正则/替换/}正则替换全部匹配$(fU{re.substall,/a/b/})使用时要特别留意uri.*系列变换的输入要求它会尝试把select字段当作一个合法的SIP URI来解析如果传入的字符串根本不是一个URI返回结果就是空。比如对$(ct{uri.user})如果Contact头里带了显示名格式是Alice sip:aliceexample.com直接套uri.user很可能解析不到预期值。这种场景我建议直接用$(cu)或$(cU)它们是专门针对Contact头URI做了解析的比拿整个Contact字符串再去套URI变换要可靠得多。3.3 实战案例日志脱敏与号码规整日志脱敏是select变换特别实用的场景。SIP日志里经常需要打印主叫号码但全量打出来又有隐私和安全风险。一行select就能做到只显示前三位xlog(L_INFO, 来电号码: $(fU{s.substr,0,3})****\n);假设From里的用户名是13800138000那这条日志打出来的就是138****既保留了排障时需要的前缀信息又不用把完整号码写进日志文件。我在多个项目里都是这么做的操作简单效果立竿见影。号码规整则是另一个高频场景。不同上游送来的号码格式各不相同有的带86有的带0086有的带空格。规整逻辑我一般集中写成一段函数式路由入口统一收口$var(caller) $(fU{re.subst,/^\86//}{re.subst,/^0086//}{s.trim}{s.tolower});这里用了两个re.subst串联依次处理86和0086两种前缀。实际效果是8613800138000变成13800138000008613800138000也变成13800138000。需要注意正则里的^是锚定开头如果不加可能会把号码中间出现的86也替换掉那就麻烦了。3.4 变换里的转义与引用坑变换虽然方便但有几个细节容易踩坑。第一re.subst的正则分隔符是/如果替换字符串里本身带了/比如要替换成ab/cd写法会非常别扭甚至导致截断。遇到这种场景我通常会把正则简化或者干脆不用re.subst改用{s.substr}配合字符串拼接来完成。第二变换参数里的逗号是分隔符如果URI参数值里本身包含逗号直接写在{uri.param,user}这种语法里会出问题。稳妥的做法是先用select把整个参数串取出来再在脚本里用其他方式处理。第三变换链的顺序很关键$(fU{s.tolower}{s.trim})和$(fU{s.trim}{s.tolower})大多数时候结果一样但遇到极端字符时顺序不同结果可能不同建议固定成先trim再tolower的习惯。还有一个我特别想提醒的坑变换返回空字符串时不会报错。也就是说$(rU{uri.user})解析失败时你得到的不是异常而是一个静默的空串。这个特性让bug变得非常隐蔽。我的排查手段是在写判断之前先临时加一行xlog把变换结果打出来确认是正确的再继续往下写。4. 把select放进变量、日志和数据库查询4.1 先赋值给变量再复用select每执行一次Kamailio就要对当前的SIP消息做一次字段提取和字符串处理。如果一个select在同一个请求里被反复调用几十次CPU开销是可感知的。我自己做过一次粗略对比高压信令场景下在同一个热点路径里把同一个$(rU{...})写10遍比先赋值给$var()再引用$var()的版本CPU占用明显更高。所以我的习惯是同一段逻辑里需要两次以上使用同一个select结果时就先接住再复用$var(caller) $(fU{re.subst,/^\86//}{s.trim}{s.tolower}); if ($var(caller) 13800138000) { xlog(L_INFO, 呼叫来自测试号码\n); } if ($var(caller) ~ ^138) { route(LOCAL_MOBILE); }这里$var(caller)在第一个if里用一次在第二个if里又用一次select表达式只写了一遍。代码可读性也更好因为变量名本身就是对这串变换逻辑的注释。4.2 数据库查询别直接嵌套select我在项目里见过的另一个常见问题是把select直接写进数据库查询SQL里。比如这样# 不推荐 sql_query(cb, SELECT status FROM subscriber WHERE caller $(fU{s.tolower}), ra);这种写法不是不能跑但问题是配置脚本里的字符串在变量替换时层层嵌套一旦select里再带上正则、引号、分号解析顺序很容易出错排查起来又极其痛苦。而且数据库查询语句本来就该保持结构清晰里面混入一大串变换逻辑三个月后回来看配置你自己都会懵。我推荐的做法是先算好再查$var(caller) $(fU{s.trim}{s.tolower}); # 后续再把这个 var 传给数据库操作SQL 里只写 $var(caller)把select和SQL彻底解耦。这样做的另一个好处是如果后面要对查询参数做调整只需要改上面那一行而不需要在SQL字符串里翻来覆去找那个被包了三层的表达式。4.3 调试日志的最佳落点select的调试最直接的手段就是xlog。我一般会在关键决策点之前打一行日志把输入输出都记录下来xlog(L_INFO, select-debug: ru$(ru) rU$(rU) fU$(fU) fd$(fd)\n);注意这里$(ru)、$(rU)、$(fU)都是selectxlog能直接解析并替换成实际值。打完日志后去syslog里找到对应行就能看到当前消息里R-URI和From头的真实结构。我就靠这个方法抓出过好几次“字段名大小写写错”的低级错误。另外改完配置一定要养成跑一遍kamailio -c的习惯。这个命令会检查配置语法包括select字段名和变换格式是否合法。虽然它不能保证运行时的全部问题但至少能帮你在reload之前拦截掉一大半语法层面的错误。4.4 配合AVP做一次性的结果传递select的结果除了存进$var()还可以直接放进AVP或XAVP里传给后续路由段。比如在route[PREPROCESS]里完成号码规整把结果放到AVP里后面无论走到哪个子路由都能读取$avp(s:caller_clean) $(fU{re.subst,/^\86//}{s.trim}{s.tolower});之后在任意位置用$avp(s:caller_clean)引用即可。这种方式特别适合多级路由结构预处理阶段把所有需要清洗的字段统一处理好业务路由阶段只读结果不再重复解析SIP消息。AVP和select搭配使用能把繁琐的清洗逻辑集中管理让主路由看起来干净很多。5. 常见问题与避坑实录5.1 问题速查表把我在生产环境里实际遇到过的问题做一个速查表方便你对照排查症状原因解法select打日志结果是空的字段本身不存在或URI没有user部分先用xlog打印完整的$(fu)确认消息结构配置检查提示select语法错误字段名写错或变换参数少写对照官方select属性列表检查比较判断永远不成立select结果里混入了大小写、空格、前缀加{s.trim}{s.tolower}或用re.subst去掉前缀$(fU)和$(fu)搞混一个返回用户名一个返回完整URI先打日志看实际值确认字段名再写逻辑变换返回空但不报错uri.*变换要求输入是合法URI换用专门的select字段如$(cU)5.2 高压场景下的性能取舍select不是银弹。在实际的高并发信令场景下我对select的使用有一条非常明确的原则高频热点路径里尽量少用能用伪变量就用伪变量需要变换时先赋值给$var()再用。举个例子$rU能完成的事就不要写成$(rU)虽然两者结果一致但select的通用性意味着它要处理更复杂的解析路径。反过来在低频率的预处理、管理类请求、日志脱敏、号码规整这些场景里select怎么用都不心疼。另外要小心xlog里大量打印select结果。调试阶段无所谓上线前一定要把这些调试日志删掉或降级否则每个请求都触发一整条select解析链CPU会被白白烧掉。5.3 迁移旧配置时的坑如果你手头有从Kamailio 3.x或OpenSER时代迁移过来的配置select这块建议专门做一次全量审查。早期版本的select语法、部分字段名和现在的5.x不完全一致有些旧写法在新版本里已经不推荐了。我的做法是grep配置里所有$(开头的表达式逐条和当前版本的官方select列表核对。特别是uri.param、re.subst这一类涉及参数和正则的写法确认在当前版本里语法是否仍然兼容。表面上看都是$(...)但里面字段名的变化往往特别隐蔽。5.4 一条我踩了无数次的经验最后分享一个我踩了无数次才总结出来的经验select写完之后一定要在真实SIP消息上验证一次而不是只靠语法检查。我遇到过一次很诡异的路由错误最后定位原因就是$(fU)和$(fu)混用了——一个返回用户名一个返回整个From URI。当时if判断怎么都是false路由死活不匹配日志一打出来立刻露馅。所以从那天起我每写一条比较复杂的select都会先加一行xlog把实际返回值打印出来确认没问题之后才继续往下写路由逻辑。再补一个小技巧项目里我会在每个select表达式的注释里写明“输入示例”和“输出示例”比如# $(fU{re.subst,/^\86//}) 从 8613800138000 得到 13800138000 $var(caller) $(fU{re.subst,/^\86//}{s.trim}{s.tolower});三个月后回来看配置注释里的输入输出示例能帮你节省大量回忆时间。这个习惯我保持了很久凡是有select的地方必有注释算是我做Kamailio配置管理的一个小坚持。 SEO 优化官网定制响应式建站教育培训建站