OpenSIPS 入门:一台 SIP 服务器,一个可编程的呼叫路由引擎
Posted on 一 14 9月 2026 in Tech
| Abstract | OpenSIPS 入门:一台 SIP 服务器,一个可编程的呼叫路由引擎 |
|---|---|
| Authors | Walter Fan |
| Category | Tech |
| Version | v1.0 |
| Updated | 2026-09-14 |
| License | CC-BY-NC-ND 4.0 |
大纲
展开看看
- 它是什么:一个 GPL 的 SIP 服务器,本质是"可编程的 SIP 信令路由引擎",不碰媒体
- 和 FreeSWITCH 什么关系:一个管信令和路由(前台),一个管媒体和业务(后台)
- 怎么编译:源码 +
make,理解Makefile.conf、模块机制、TLS=1 - 进程与路由模型:多进程 worker + 一套自己的脚本语言,
route/branch_route/onreply_route/failure_route - 典型流程:一次 REGISTER 和一次 INVITE 在
opensips.cfg里到底走了哪几步 - 代码结构:core 干什么、193 个模块怎么分类、
tm/rr/usrloc是骨架 - 最佳实践:事务、record_route、NAT、限流、MI 管理接口
- 常见坑:483 死循环、prefix 不一致、忘了
t_relay、NAT 媒体不通 - 上手清单:从零到打通第一路电话
一、OpenSIPS 是什么
先说结论:OpenSIPS 是一台可编程的 SIP 服务器。它不处理语音数据(RTP),只处理 SIP 信令——也就是那些 REGISTER、INVITE、BYE 消息。它的核心价值是:你可以用一门专门的脚本语言,决定每一条 SIP 消息该怎么处理、往哪里转。
一句话类比:如果 SIP 呼叫是快递,OpenSIPS 就是那个巨大的分拣中心——它自己不送货(不传语音),但每个包裹到底发往哪个网点、要不要验身份、要不要限流、走不走加密通道,全由它说了算。而真正"送货"(收发 RTP 语音流、放音、录音、会议混音)的活,交给 FreeSWITCH、Asterisk 或 rtpproxy/rtpengine 这类媒体服务器。
几个需要先对齐的名词:
- SIP(Session Initiation Protocol):发起和管理通话的信令协议,长得像 HTTP,纯文本。
- 信令 vs 媒体:信令是"喂,我要给你打电话"这套约定;媒体是接通后真正传输的声音。OpenSIPS 只管前者。
- SIP proxy:把 SIP 请求从一方转发到另一方的中间人。OpenSIPS 主要就是干这个,外加注册、鉴权、路由等一堆增强。
OpenSIPS 起源于 SER(SIP Express Router),2005 年从 SER 分叉出来,主打"更开放的项目管理和更快的特性合入"。本文对照的是仓库里的开发版 4.1.0-dev(GPLv2 许可)。
它和 FreeSWITCH 到底谁干什么
写过前面几篇 FreeSWITCH 的读者会问:FreeSWITCH 不也能当 SIP proxy 吗?能,但定位不同。这是最容易混淆的地方,用一张表说清:
| 维度 | OpenSIPS | FreeSWITCH |
|---|---|---|
| 本质 | SIP 信令路由引擎(SIP proxy / registrar) | 媒体服务器 + 呼叫控制平台(B2BUA) |
| 碰不碰媒体 | 不碰 RTP(可借 rtpproxy/rtpengine 中继) | 收发、转码、放音、录音、会议 |
| 扩展性 | 极强,单机可扛几万到几十万并发信令 | 单机媒体并发受 CPU/编解码限制 |
| 编程方式 | 自己的路由脚本(opensips.cfg) |
Dialplan / mod_lua / ESL |
| 典型角色 | 运营商级前端、负载均衡、注册代理 | IVR、会议、录音、转码、业务逻辑 |
生产里常见的搭配是:OpenSIPS 站在最前面当注册中心和负载均衡,把呼叫分发给后面一排 FreeSWITCH 干重活。前台一个人挡住海量请求,后台一群人干细活。
二、怎么编译
OpenSIPS 是 C 写的,构建系统是一套手写的 GNU Makefile,不用 CMake。核心思路:先编 core,再编模块,模块按需选。
依赖
最小依赖不多:gcc、bison(或 yacc)、flex、GNU make(>= 3.79)、sed、tr。可选依赖按模块来:要 TLS 就装 openssl,要 MySQL 就装 libmysqlclient,要 PostgreSQL 就装 libpq,要 XML-RPC 管理接口就装 libxmlrpc-c3,以此类推。
Debian/Ubuntu 上装齐基础依赖:
sudo apt-get install gcc make bison flex libssl-dev \
libmysqlclient-dev libpq-dev
三条命令跑起来
git clone https://github.com/OpenSIPS/opensips.git
cd opensips
# 只编 core(等价于 make opensips)
make
# 编所有默认模块
make modules
# 或者一步到位:core + 模块
make all
# 安装(默认装到 /usr/local/)
sudo make install
关键在于理解模块选择机制,这是新手最容易懵的地方。构建配置存在 Makefile.conf 里(首次 make 会从 Makefile.conf.template 拷一份),几个开关记住就够用:
exclude_modules:默认不编的模块(通常是有外部依赖的)。include_modules:强制编进去,哪怕它在 exclude 列表里。skip_modules:这次构建临时跳过某些模块。
举几个实战例子:
# 只编 textops 一个模块(调试单模块时很有用)
make modules=modules/textops modules
# 默认模块里跳过 textops 和 db_mysql
make skip_modules="textops db_mysql" modules
# 把默认不编的 db_mysql 拉进来编
make include_modules="db_mysql" modules
# 想编 TLS / SCTP 支持,用编译期开关
make TLS=1 all
make SCTP=1 all
两个必须知道的编译期约定
第一,prefix 要前后一致。 默认安装前缀是 /usr/local/,而默认配置文件路径是编译期硬编码进二进制的。如果你 make all 用默认 prefix,却 make prefix=/ install 装到别处,OpenSIPS 启动时会去 /usr/local/etc/opensips/opensips.cfg 找配置,结果文件明明在 /etc/opensips/ ——直接找不到。正解是所有 make 命令都带同一个 prefix,或者干脆写进 Makefile.conf。
第二,改了 DEFS、编译器/链接器 flag 之后要 make proper 再重编。 否则依赖没重新生成,你会遇到各种玄学错误。
除了源码,官方也提供 Debian/RPM 包和 Docker 镜像(仓库 docker/ 下有 CI 用的 Dockerfile),不想折腾编译可以直接用包管理器装。
三、进程与路由模型:OpenSIPS 的灵魂
要真正会用 OpenSIPS,得理解两件事:它怎么跑(进程模型),以及你怎么指挥它(路由脚本)。
多进程 worker 模型
OpenSIPS 启动后 fork 出一组常驻进程。核心是一批 worker 进程(比如 UDP worker、TCP worker),每个 worker 独立完整地处理一条 SIP 消息——收包、跑脚本、转发,一气呵成。配置里那句 udp_workers=4 就是说开 4 个 UDP worker。除此之外还有 timer 进程、管理接口进程等。
进程间共享状态靠共享内存(shm),这是 OpenSIPS 高性能的关键,也是踩坑的高发区:worker 之间的私有内存(pkg)彼此看不见,只有放进 shm 的数据(比如注册信息、事务状态)才是全局可见的。
一门专门的路由脚本语言
OpenSIPS 最独特的地方:配置文件 opensips.cfg 不是 key-value,而是一段用专门语言写的程序。它由词法/语法分析器(cfg.lex / cfg.y)解析,每来一条 SIP 消息,就执行对应的"路由块"。
路由块分几种类型(定义在 route.h 里),各自在不同时机被触发:
| 路由块 | 触发时机 |
|---|---|
route{} |
收到一条 SIP 请求(主入口) |
branch_route[] |
请求即将往某个分支发出前(每个分支各触发一次) |
onreply_route[] |
收到一条 SIP 应答时 |
failure_route[] |
某个事务收到失败应答(4xx/5xx/6xx)时 |
local_route[] |
OpenSIPS 自己生成请求时 |
startup_route[] |
进程启动时执行一次(适合初始化) |
timer_route[] |
按设定周期定时执行 |
event_route[] |
订阅的内部事件发生时 |
理解这套"事件 → 路由块"的映射,是读懂任何一份 OpenSIPS 配置的钥匙。下面我们就顺着官方自带的 residential 配置(etc/opensips.cfg),把两个最典型的流程走一遍。
四、典型用例与流程
流程一:一次注册(REGISTER)
用户的 SIP 话机开机,要告诉服务器"我现在这个 IP/端口能被找到",这就是注册。在脚本里,它落到主 route{} 的这几行:
if (is_method("REGISTER")) {
# 保存注册信息到 usrloc(location 表)
if (!save("location"))
xlog("failed to register AoR $tu\n");
exit;
}
一次 REGISTER 的完整旅程:
- 进主 route:先
mf_process_maxfwd_header(10)检查 Max-Forwards(防死循环)。 - 没有 To-tag,说明是新请求(不是对话内的续传)。
- 判断方法是 REGISTER。
save("location"):由registrar模块把这条注册写进usrloc模块管理的 location 表(内存里,可选落库),同时自动生成 200 OK 应答给话机。exit结束。
生产环境里,REGISTER 之前通常还要插一段鉴权:用 auth / auth_db 模块做 digest 认证(www_challenge 发起挑战、www_authorize 校验),确认这个人真是他自己,防止别人冒名注册。默认配置为了免依赖把鉴权关掉了——这在生产上绝对不能照抄。
流程二:一次呼叫(INVITE)
A 要打给 B。核心难点在于:SIP 是逐跳(hop-by-hop)的,OpenSIPS 得处理好"新请求"和"对话内后续请求"两种情况。我把 residential 配置的呼叫路径拆成几步:
第一步,区分新请求还是对话内请求。 有 To-tag 说明是已建立对话内的后续消息(ACK、BYE、re-INVITE):
if (has_totag()) {
# 对话内请求,按 record-route 定下的路径走
if (!loose_route()) { ... }
if (is_method("BYE")) {
do_accounting("log","failed");
}
route(relay);
exit;
}
第二步,新 INVITE 要做事务和录路(record-route)。 这两个概念是骨架:
# 吸收重传,不重复建事务
t_check_trans();
# 让后续的对话内请求还能回到本机
if (!is_method("REGISTER|MESSAGE"))
record_route();
# 只对 INVITE 记账
if (is_method("INVITE")) {
do_accounting("log");
}
第三步,查被叫在哪,然后转发。 从 usrloc 里查 B 注册时留下的地址:
# 从 location 表查被叫的当前联系地址
if (!lookup("location","method-filtering")) {
t_reply(404, "Not Found");
exit;
}
route(relay);
第四步,relay 路由真正把请求发出去:
route[relay] {
if (is_method("INVITE")) {
t_on_branch("per_branch_ops"); # 每个分支发出前的钩子
t_on_reply("handle_nat"); # 收到应答时的钩子
t_on_failure("missed_call"); # 失败时的钩子
}
if (!t_relay()) {
send_reply(500,"Internal Error");
}
exit;
}
t_relay() 是整份配置里最重要的一个函数——它由 tm(transaction module,事务模块)提供,负责有状态地把请求转发出去、管理重传、匹配应答。忘了调 t_relay(),呼叫就石沉大海,这是新手第一大坑。
配上前面注册的 record_route(),后续的 ACK、BYE 才能沿着同一条路径回到 OpenSIPS,整个对话才闭环。
用一张图把这条呼叫链路画清楚:
sequenceDiagram
participant Alice as Alice (UAC)
participant OS as OpenSIPS (proxy)
participant Bob as Bob (UAS)
Alice->>OS: INVITE
OS->>OS: lookup(location) 查 usrloc
OS->>OS: record_route() 记录路径
OS->>Bob: INVITE (t_relay())
Bob-->>OS: 200 OK
OS-->>Alice: 200 OK (onreply_route)
Alice->>OS: ACK
OS->>Bob: ACK (loose_route) 对话内路由
Note over Alice,Bob: RTP 媒体流(不经过 OpenSIPS,除非用 rtpproxy)
Alice->>OS: BYE
OS->>Bob: BYE
注意最后那条:语音流(RTP)默认直接在 Alice 和 Bob 之间跑,不经过 OpenSIPS。这正是它能扛高并发的原因——它只处理小小的信令包。只有当双方在 NAT 后面互相看不见时,才需要 rtpproxy/rtpengine 出来中继媒体。
五、代码结构
克隆下来的仓库,粗看文件很多,但脉络清晰。分三层理解:
Core(仓库根目录的那堆 .c/.h)
这是引擎本身,不涉及具体业务。挑几个关键的:
main.c:进程入口,负责启动、fork worker、装载配置。cfg.lex/cfg.y:那门路由脚本语言的词法和语法分析器(flex + bison)。route.c/action.c:执行路由脚本、跑每个 action 的核心。msg_translator.c/parser/:解析和重建 SIP 消息。parser/是个独立目录,专管把文本 SIP 拆成结构体。mem/:内存管理,区分 pkg(进程私有)和 shm(共享)两套分配器——这是理解 OpenSIPS 并发模型的关键。net/:网络传输层,TCP/UDP 收发、io_wait事件循环。pt.c:进程表,管理那一组 worker 进程。sr_module.c:模块加载和 API 注册机制(dlopen动态加载.so)。pvar.c/transformations.c:脚本里$ru、$fu这类伪变量(pseudo-variable)和{s.len}这类变换的实现。
Modules(modules/,193 个模块)
OpenSIPS 的功能几乎全在模块里,core 只是骨架。193 个模块按用途大致分几类:
| 类别 | 代表模块 | 干什么 |
|---|---|---|
| 骨架(几乎必用) | tm、rr、sl、maxfwd、sipmsgops |
事务、录路、无状态回复、跳数、消息操作 |
| 注册与定位 | usrloc、registrar、mid_registrar |
用户位置存储、注册处理 |
| 鉴权 | auth、auth_db、auth_jwt、auth_aka |
digest / JWT / AKA 认证 |
| 数据库 | db_mysql、db_postgres、db_sqlite、db_text |
各种 DB 后端(统一 DB API) |
| 缓存 | cachedb_redis、cachedb_mongodb、cachedb_local |
分布式/本地缓存 |
| 路由策略 | dispatcher、drouting、load_balancer、carrierroute |
负载均衡、动态路由、运营商路由 |
| 对话与状态 | dialog、topology_hiding |
对话跟踪、拓扑隐藏 |
| NAT / 媒体 | nathelper、rtpproxy、rtpengine |
NAT 穿透、RTP 中继 |
| 传输协议 | proto_udp、proto_tcp、proto_tls、proto_ws、proto_wss |
各种 SIP 传输层(含 WebRTC 用的 WSS) |
| 集群与高可用 | clusterer |
多节点数据同步 |
| 安全 | fraud_detection、permissions |
防欺诈、权限控制 |
| 管理接口 | mi_fifo、mi_http、mi_html |
运行时管理(MI) |
每个模块目录下都有 README(由 docbook XML 生成),写清了它导出的函数、参数、伪变量。这是查 API 的第一手资料。
Lib(lib/)与其他
lib/ 放公共库(JSON、CSV、URL、digest 鉴权、hash 等),db/ 是数据库抽象层,mem/ 内存,evi/ 事件接口,mi/ 管理接口框架。理解了 "core 提供机制、module 实现策略" 这条主线,整个仓库就不再是一团乱麻。
六、最佳实践
真正跑生产,下面几条比语法更重要:
- 一切转发都走事务(
t_relay)。 除非你非常清楚自己在干无状态转发,否则新请求一律用tm模块的t_relay(),让它替你管重传和应答匹配。别自己forward()硬转。 - 对所有需要闭环的对话做
record_route()。 不然 ACK/BYE 找不到回来的路,对话半路断裂。记账、计费、拓扑隐藏也都依赖它。 - 鉴权别省。 默认配置关了鉴权是为了免依赖,生产必须开
auth_db+ digest,否则任何人都能冒名注册、盗打。 - NAT 要专门处理。 大量话机在 NAT 后面。用
nathelper检测、fix_nated_contact/fix_nated_sdp修正,媒体不通时上rtpproxy/rtpengine中继。这是实战里最耗时的一块。 - 限流与防欺诈前置。 用
pike(流控)、fraud_detection(异常呼叫模式)在最前面挡住攻击和话费盗刷——SIP 服务器天天被扫。 - 善用 MI 管理接口。
mi_fifo/mi_http让你运行时查状态、看统计、reload 数据,不用重启。配opensips-cli工具更顺手(老的opensipsctl已被移除)。 - 配置模块化。 用
route(name)把逻辑拆成命名子路由,别把几百行塞进一个route{}。examples/templates/下有 M4 模板可以起步。 - 持久化和高可用。 注册、对话状态默认在内存,重启即失。生产用 DB 落库或
clusterer做多节点同步。
七、常见坑
老程序员的经验是:框架的坑,往往长得都一样。OpenSIPS 这几个几乎人人踩过:
- 483 Too Many Hops / 513 Message Too Large 的死循环。 十有八九是本域匹配没配对:OpenSIPS 没认出"这条请求是发给我自己的",于是又把它转给了自己,直到 Max-Forwards 归零。解法:配好
alias=你的域名,让is_myself()能正确判断。这是官方 INSTALL 文档里专门列出来的头号问题。 - prefix 前后不一致,启动就找不到配置。 前面编译章节说过:
make和make install的 prefix 必须一致,否则二进制里硬编码的配置路径和实际安装路径对不上。 - 忘了
t_relay(),呼叫石沉大海。 脚本逻辑跑完了却没真正转发,对方永远等不到 INVITE。 - 忘了
record_route(),对话中途断。 INVITE 通了,但 BYE 挂不掉、re-INVITE 失败,因为后续请求回不到 OpenSIPS。 - NAT 后媒体不通:信令通,听不到声音。 典型症状是"电话接通了但双方都是哑巴"。因为 SDP 里写的是内网 IP。要用
nathelper修 SDP,必要时用 rtpproxy 中继。 - shm 内存耗尽。 对话/事务状态都在共享内存,并发一高、又没设合理超时(
fr_timeout、对话生命周期),shm 会被撑爆。上线前用-m(shm 大小)、-M(pkg 大小)算好容量。 - 改了
DEFS不make proper直接重编。 会遇到莫名其妙的运行时错误,清理重编即可。 - 把默认配置直接搬上生产。 residential 配置是教学用的最小示例,关了鉴权、没落库、没 NAT 处理、没限流——照抄上线等于裸奔。
八、上手清单
想从零打通第一路电话,按这个顺序来:
- 装依赖:
gcc make bison flex libssl-dev(要 DB 再加对应 lib)。 - 拉源码编译:
git clone→make all→sudo make install(注意 prefix 一致)。 - 改配置:编辑
/usr/local/etc/opensips/opensips.cfg,把socket=udp:127.0.0.1:5060里的 IP 换成你的真实网卡地址,设好alias=你的域名。 - 启动:
opensips -f /usr/local/etc/opensips/opensips.cfg,先前台带-D看日志。 - 两个软电话注册:用 Linphone/Zoiper 注册两个账号到你的服务器(默认无鉴权,随便填用户名)。
- 互拨验证:A 拨 B,看能不能接通。
- 看状态:用
opensips-cli或mi_fifo查在线注册、事务统计。 - 加鉴权和 NAT:验证通过后,再逐步加
auth_db、nathelper,向生产靠拢。
总结:先分清信令和媒体,再谈路由
OpenSIPS 不难,难的是一开始就把它和 FreeSWITCH、和媒体服务器混为一谈。记住这条主线:它是一台只管 SIP 信令、不碰语音数据的可编程路由引擎;它的全部威力,来自那门"每来一条消息就执行一段路由脚本"的语言,和背后 193 个各司其职的模块。
理解了 core 提供机制、module 实现策略、脚本编排流程这三层,再回头看任何一份 opensips.cfg,你都能顺着 route{} 一步步读懂它在干什么。剩下的,就是在真实流量里,把鉴权、NAT、限流、高可用一块块补齐。
行动清单
- [ ] 编译时确认
make与make install的 prefix 一致 - [ ] 配好
alias,避免 483 死循环 - [ ] 所有需要闭环的对话都
record_route() - [ ] 所有转发都走
t_relay()(有状态) - [ ] 生产环境务必开启
auth_dbdigest 鉴权 - [ ] NAT 场景配
nathelper,媒体不通上 rtpproxy/rtpengine - [ ] 前置
pike限流 +fraud_detection防盗刷 - [ ] 用 DB 或
clusterer解决状态持久化和高可用 - [ ] 上线前用
-m/-M算好 shm/pkg 容量
参考
- OpenSIPS 源码与文档:https://github.com/OpenSIPS/opensips
- 官方模块列表:https://www.opensips.org/Documentation/Modules
- opensips-cli 管理工具:https://github.com/OpenSIPS/opensips-cli