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

eXosip 把 SIP 规范的四层重新切成了「应用 / eXosip / oSIP / 传输」,更贴合工程。 日常只调最上面那层,osip_ 那层只在「抠某个头」时才碰。

📝 本节和前面几节的关系

09.02.01 到 09.02.08 把库拆了一遍,这一节拼回去。 09.01.02 讲的是 SIP 规范的分层(语法/事务/事务用户/对话), 这里是「代码里的分层」,两者不是一一对应。

一、两张分层图不一样

规范的四层 代码里的四层
语法与传输 传输(socket)
事务 oSIP
事务用户 eXosip
对话 eXosip

最大的差别在第三、四行:规范把「事务用户」和「对话」分成两层,而代码里它们都在 eXosip 里。 为什么能合? 因为对「业务代码」来说,「这个事务属于哪个对话」是库该管的事,不该暴露给上层。 所以「代码的分层」是按「谁用」切的,「规范的分层」是按「职责」切的—— 两种切法都对,只是目的不同。

二、每层你调不调

层 你调不调 什么时候例外
应用 写 —
eXosip 天天调 —
oSIP 很少调 取私有头、改细节
传输 不调 要换 TCP/TLS 时

第三行那个例外很具体:eXosip 没有「取任意头」的接口, 只能拿到 ev->request(一个 osip_message_t *)自己去遍历。 所以「只用 eXosip」是理想状态,「必须碰 oSIP」是常态—— 真实项目里大概九成代码在 eXosip 层、一成在 oSIP 层。 第四行那个「要换 TCP/TLS」也是真的:listen_addr 的第一个参数换一下就行, 但证书配置要动传输层。

三、API 分族

族 典型函数 用在哪
eXosip_init 系 init/quit 启动与关闭
eXosip_register_* build/send 注册与续期
eXosip_call_* build/send/terminate 呼叫
eXosip_message_* build/send 短消息
eXosip_event_* wait/free 事件循环
osip_message_* get_*/set_* 抠细节

这个分族方式有个规律:每个业务动作都是「build + send」一对, 外加一个「terminate」或者「answer」这种终结动作。 记住这个模式,查文档就能猜函数名:要发 OPTIONS 就找 eXosip_options_build_request, 要发 INFO 就找 eXosip_message_build_request。 第三行那个 terminate 是挂断, 它内部会自己拼 BYE、自己算 CSeq——这就是「库帮你做了什么」的具体样子。

四、版本差异要注意

eXosip 的 API 在 3.x 与 5.x 之间改过几处:

| 项 | 3.x | 5.x | |---|---| | 注册构造 | eXosip_register_build_register | 名字带 initial | | 呼叫构造 | 参数个数不同 | 多一个替换头参数 |

所以「抄示例代码」之前先看版本号:编译报「隐式声明」基本就是版本不对, 而不是少引了头文件。 「隐式声明」这个警告在 C 里很危险:它默认函数返回 int, 而实际返回指针的话,在 64 位机器上指针会被截断成 32 位——表现为「运行时随机崩溃」。 所以看到「隐式声明」不能忽略,它不是「风格问题」而是「潜在崩溃」。

五、第九章收口

九章走下来,从报文到库到栈,可以这样收:

层 你该会什么
报文 拼得出、读得懂、量得出字节
流程 注册两轮、呼叫三步、挂断两条
标识 三元组、CSeq 的 ACK 例外
库 四步初始化、事件循环、build+send

这四行里前三行是「协议」,第四行是「工程」。 只学第四行会「会用不会修」,只学前三行会「懂原理写不出代码」。 下一步是第十章, 那里要把 SIP 与 GB/T 28181 的报文格式、编码规则对上——本章讲的每个字段都会再出现一次。

六、动手 Demo:四层协议栈,各层谁来调

Demo 把四层与 API 分族列清楚,再说清「哪一层例外要碰」。 表里的 API 名都是从两层头文件里挑的真实函数。

# 典型调用链:应用 → eXosip → oSIP → socket
eXosip_call_build_initial_invite(ctx, &inv, to, from, route, subject)
eXosip_call_send_initial_invite(ctx, inv)
# 内部:osip_message_* 拼报文 → 事务 → socket sendto

# 事件回来时:
ev = eXosip_event_wait(0, 200)
msg = ev.request          # osip_message_t *,要抠细节就用它

实跑输出:

--------------------------------------------------------------------
四层协议栈,各层谁来调
--------------------------------------------------------------------
**eXosip 的结构比 SIP 规范的分层更「工程化」**:

  层             谁提供         你调不调
  -----------------------------------------------
  **应用**       你的代码       —
  **eXosip**     libexosip2     **调(流程)**
  **oSIP**       libosip2       **少调(细节)**
  传输           socket         **不调**

**第二、三行那个「调不调」是这一节的核心**:**正常业务只用 eXosip 就够了**,
**而 oSIP 的 API 只在「要抠某个头的细节」时才碰。**
**比如「从 INVITE 里取厂家私有头」这种事**,**eXosip 没有对应接口**,
**只能拿到 `ev->request`(一个 `osip_message_t *`)自己去遍历。**

**下面把常用 API 按「干什么」分组**:

  族                      例子                             用在哪
  ------------------------------------------------------------------------
  `eXosip_init` 系        `init`/`quit`                   启动与关闭
  `eXosip_register_*`     `build`/`send`                  **注册与续期**
  `eXosip_call_*`         `build`/`send`/`terminate`     **呼叫**
  `eXosip_message_*`      `build`/`send`                  短消息
  `eXosip_event_*`        `wait`/`free`                   **事件循环**
  `osip_message_*`        `get_*`/`set_*`                 抠细节

**第三行那个 `terminate` 是「挂断」**,
**它内部会自己拼 BYE、自己算 CSeq**——**这就是「库帮你做了什么」的具体样子。**
**而如果只用 oSIP**,**这一步要自己 `osip_message_init`、自己 set 五个头、自己发、自己等 200。**

**最后一条经验关于「版本」**:
**eXosip 的 API 在 3.x 与 5.x 之间改过几处**(比如注册相关的函数名),
**所以抄示例代码前先看版本号**——**编译报「隐式声明」基本就是版本不对,而不是少引头文件。**

第一段那两行就是「build + send」成对,中间那些 osip_message_* 你看不到, 它们发生在库内部。 这就是分层的价值:拼报文的几十行代码被藏起来了, 上层只需要给「给谁、从谁、走哪条路由、主题是什么」四个参数。 第二段那个 ev.request 是「唯一的后门」:它把内部的 osip_message_t * 暴露出来, 让你能在需要时遍历所有的头。 注意这个指针的所有权: 它属于事件,事件 free 之后就不能再用了—— 所以「先拷贝要用的字段,再 free 事件」是正确顺序。 反过来写会读到已释放的内存,表现为「偶尔取到乱码」。

30 秒实验

  1. 在头文件里找 eXosip_options_build_request,验证「build+send 成对」的规律。
  2. 查一下你用的 eXosip 版本号,对照 3.x 与 5.x 的函数名差异。
  3. 把 ev->request 里的头全遍历一遍,看厂家私有头长什么样。

常见误区

  1. 以为代码的分层和规范的分层一一对应。 代码把「事务用户」和「对话」合并进了 eXosip, 因为业务层不需要区分。
  2. 以为只用 eXosip 就够。 取厂家私有头必须碰 oSIP, 会拿到 ev->request 自己遍历 headers 链表。
  3. 忽略「隐式声明」警告。 它默认返回 int,64 位下指针会被截断,表现为运行时随机崩溃。
自测三问(点开对答案) 1. 为什么代码里能把「事务用户」和「对话」合成一层? —— **因为对业务代码来说,「这个事务属于哪个对话」是库该管的事**。分成两层反而增加了上层的负担。 2. 「build + send」成对的设计有什么好处? —— **给了「发出前修改」的机会**:加私有头、改 Expires、改 Subject。只有一个函数就没地方插。 3. `ev->request` 用完要注意什么? —— **它属于事件,事件 `free` 之后失效**。 要先把需要的字段拷出来,再释放事件,否则会读到已释放内存。

第九章到此结束:SIP 从报文到库全走完。下一章进 GB/T 28181,把这套东西用到国标报文上。

相关笔记