oSIP 与 eXosip 不是两个竞争库,是上下两层:底下管报文,上面管流程。 认清这一点,就明白「为什么不能只用 oSIP」——能用,但注册、对话、ACK 全要自己写。
第九章上半讲的是协议本身,从这一节开始讲「怎么用库实现它」。 和 08.08 的结构正好对称: 那边是 gSOAP 的两层(wsdl2h 生成期 + gSOAP 运行期),这边是 oSIP 与 eXosip。
一、两层各管什么
| 层 | 管什么 | 知道「注册」吗 |
|---|---|---|
| oSIP | 报文与事务 | 不知道 |
| eXosip | 流程与事件 | 知道 |
第三列是理解这一节的关键:oSIP 只认 RFC 3261 的语法与状态机, 在它眼里所有请求都一样,只是一条请求、一个事务。 它不知道「注册要两轮」、不知道「呼叫要发 ACK」、不知道「对话怎么匹配」—— 这些业务流程全是 eXosip 加的。 所以「只用 oSIP」的代价很具体:上面那些流程你都要自己实现一遍。
二、为什么分成两个库
这个分层不是「为了好看」,是历史与使用场景共同造成的:
| 原因 | 说明 |
|---|---|
| 有些人只要解析 | 服务器侧只用 oSIP 就够 |
| 协议与流程变化速度不同 | RFC 稳、业务活 |
| 体积 | 只用 oSIP 可以不链 eXosip |
第一行那个「只要解析」的场景比想象中多:做 SIP 网关、做报文分析、做压力测试, 这些都不需要「跑一通电话」,只需要看懂报文。 那时候链 eXosip 纯属浪费。 第二行是软件工程上的理由: RFC 3261 二十年没大改,而「注册怎么重试」「对话怎么保活」这类业务逻辑改得勤—— 分开就能各改各的。
三、函数名前缀能说明问题
把常用 API 按前缀数一遍,两层的工作量大致相当:
| 前缀 | 个数 | 管哪一层 |
|---|---|---|
osip_ |
10 | 协议与数据结构 |
eXosip_ |
13 | 流程与事件 |
但「数量相当」不等于「你都要用」:日常业务只调 eXosip_ 那一半,
osip_ 只在「要抠某个头的细节」时才碰。
比如「从 INVITE 里取厂家私有头」:eXosip 没有对应接口,
只能拿到 ev->request(一个 osip_message_t *)自己遍历。
所以真实项目的代码比例大概是「九成 eXosip、一成 oSIP」,但那一成往往是最难写的部分。
四、那个大写的 X 不是笔误
eXosip_ 里那个 X 必须大写:
| 写法 | 结果 |
|---|---|
eXosip_init |
正确 |
exosip_init |
链接不到 |
第二行那个错法在 Linux 上特别常见,因为很多示例代码是手抄的、抄的时候顺手改成了全小写。 报错表现是「未定义引用」,而符号名看起来没错——只是大小写不一致。 C 是大小写敏感的,这一点和 Python 一样, 但和 SQL、Windows 的注册表不一样——所以「大小写无所谓」这种直觉在这里是错的。
五、和 gSOAP 那套怎么对照
两章都在讲「用库实现一个协议」,结构可以对照着看:
| 项 | gSOAP(第八章) | oSIP/eXosip(本章) | |---|---|---|---| | 代码怎么来 | 工具生成 | 手写调用 | | 协议描述 | WSDL 文件 | 没有,靠规范 | | 两层分工 | 生成期/运行期 | 协议/流程 |
第一行是最大的差别: ONVIF 有 WSDL,所以能自动生成几千行代码;SIP 没有对应的描述文件,只能手写。 第二行那个「靠规范」是实话:SIP 的每个头、每个状态码都在 RFC 里写着, 没有机器可读的接口定义。 这带来一个后果:SIP 客户端的代码量比 ONVIF 客户端大得多, 但也更灵活——字段想怎么填就怎么填。
六、动手 Demo:两层分工与函数前缀
Demo 把 23 个常用 API 按前缀分组数一遍,再看两层各占多少。 这批函数名是从两层的头文件里挑的,不是随手写的。
apis = ["osip_init", "osip_parse", "osip_message_init",
...
"eXosip_init", "eXosip_listen_addr", "eXosip_event_wait"]
a = [x for x in apis if x.startswith("osip_")]
b = [x for x in apis if x.startswith("eXosip_")]
print(len(apis), len(a), len(b))
实跑输出:
--------------------------------------------------------------------
两层分工与函数前缀
--------------------------------------------------------------------
**oSIP 与 eXosip 不是两个竞争关系,是上下两层**:
层 管什么 典型函数
---------------------------------------------------
**oSIP** **报文与事务** `osip_message_*`
**eXosip** **流程与事件** `eXosip_*`
**这么分工的理由很实在**:**oSIP 只认 RFC 3261 的语法与状态机**,
**它不知道「注册」和「呼叫」的区别**——**在它眼里都只是一条请求、一个事务。**
**而「注册要两轮」「呼叫要发 ACK」这些流程,是 eXosip 加的。**
**下面把常用 API 按前缀数一遍**:
前缀 个数 管哪一层
------------------------------------------
`osip_` 10 **协议与数据结构**
`eXosip_` 13 **流程与事件**
**这 23 个里 `eXosip_` 占 13 个、`osip_` 占 10 个**——**两层的工作量在同一量级**;
**但对使用者来说,日常只需要调 `eXosip_` 那一半。**
**所以「我能不能只用 oSIP」这个问题的答案是「能,但很累」**:
**注册的两轮重试、对话的建立与匹配、ACK 的发送时机,全要自己写。**
**注意 `eXosip_` 里那个大写 `X`**:**它是库的命名习惯,不是笔误**;
**C 是大小写敏感的,写成 `exosip_init` 就链接不到。**
**这个坑在 Linux 上尤其常见**,因为很多示例代码是手抄的。
输出里 osip_ 10 个、eXosip_ 13 个——
两层基本相当,说明「协议解析」和「流程控制」的工作量在同一量级。
这个比例有参考价值:
如果你自己实现一个 SIP 客户端,会发现「解析报文」和「跑流程」写起来花的时间也差不多。
但那 13 个 eXosip_ 里,日常只用得上四五个:
init、listen_addr、register、call、event_wait。
其余的(message、options、publish)都是按需才用。
最后那句「大写 X」值得单独记一遍:eXosip_init 写成 exosip_init 是链接错而不是编译错,
因为编译器不检查外部符号是否存在,链接器才查。
30 秒实验
- 用
nm -D libexosip2.so | grep -i exosip_init看真实符号的大小写。 - 查一下
osip_message.h里有多少个osip_message_set_*函数。 - 想想你手上的项目:哪些地方其实只用 oSIP 就够。
常见误区
- 以为 oSIP 与 eXosip 是二选一的关系。 它们是上下两层,eXosip 依赖 oSIP, 装的时候必须先装 oSIP。
- 把
eXosip_写成exosip_。 链接期报未定义引用,而符号名看起来没错, 很容易误判成「库没装上」。 - 以为 SIP 也像 ONVIF 那样能靠 WSDL 生成代码。 SIP 没有接口描述文件,所有字段都要手写, 这也是 SIP 客户端代码量大的原因。
自测三问(点开对答案)
1. 为什么 oSIP「不知道注册要两轮」? —— **它只实现语法与事务状态机**,把每条请求当独立事务处理。 两轮注册属于业务流程,是 eXosip 加的。 2. 只链 oSIP 不链 eXosip 的合理场景是什么? —— **只解析或构造报文、不跑完整通话**:报文分析、网关转换、压测工具都属这类, 可以省掉 eXosip 的体积与依赖。 3. `eXosip_` 写成 `exosip_` 报的是编译错还是链接错? —— **链接错**。 编译器不检查外部符号存在与否,链接器才查,所以报「未定义引用」而不是「隐式声明」。下一节进 Windows 编译——三个库、一个顺序,错一步就链不上。
- ☐ (本节完,下一节 09.02.02 Windows系统下编译oSIP2和eXosip2)