📹 第九章 SIP及eXosip开源库应用
📄 一句话概括

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 秒实验

  1. 用 nm -D libexosip2.so | grep -i exosip_init 看真实符号的大小写。
  2. 查一下 osip_message.h 里有多少个 osip_message_set_* 函数。
  3. 想想你手上的项目:哪些地方其实只用 oSIP 就够。

常见误区

  1. 以为 oSIP 与 eXosip 是二选一的关系。 它们是上下两层,eXosip 依赖 oSIP, 装的时候必须先装 oSIP。
  2. 把 eXosip_ 写成 exosip_。 链接期报未定义引用,而符号名看起来没错, 很容易误判成「库没装上」。
  3. 以为 SIP 也像 ONVIF 那样能靠 WSDL 生成代码。 SIP 没有接口描述文件,所有字段都要手写, 这也是 SIP 客户端代码量大的原因。
自测三问(点开对答案) 1. 为什么 oSIP「不知道注册要两轮」? —— **它只实现语法与事务状态机**,把每条请求当独立事务处理。 两轮注册属于业务流程,是 eXosip 加的。 2. 只链 oSIP 不链 eXosip 的合理场景是什么? —— **只解析或构造报文、不跑完整通话**:报文分析、网关转换、压测工具都属这类, 可以省掉 eXosip 的体积与依赖。 3. `eXosip_` 写成 `exosip_` 报的是编译错还是链接错? —— **链接错**。 编译器不检查外部符号存在与否,链接器才查,所以报「未定义引用」而不是「隐式声明」。

下一节进 Windows 编译——三个库、一个顺序,错一步就链不上。

相关笔记