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 秒实验
- 在头文件里找
eXosip_options_build_request,验证「build+send 成对」的规律。 - 查一下你用的 eXosip 版本号,对照 3.x 与 5.x 的函数名差异。
- 把
ev->request里的头全遍历一遍,看厂家私有头长什么样。
常见误区
- 以为代码的分层和规范的分层一一对应。 代码把「事务用户」和「对话」合并进了 eXosip, 因为业务层不需要区分。
- 以为只用 eXosip 就够。 取厂家私有头必须碰 oSIP,
会拿到
ev->request自己遍历headers链表。 - 忽略「隐式声明」警告。 它默认返回
int,64 位下指针会被截断,表现为运行时随机崩溃。
自测三问(点开对答案)
1. 为什么代码里能把「事务用户」和「对话」合成一层? —— **因为对业务代码来说,「这个事务属于哪个对话」是库该管的事**。分成两层反而增加了上层的负担。 2. 「build + send」成对的设计有什么好处? —— **给了「发出前修改」的机会**:加私有头、改 Expires、改 Subject。只有一个函数就没地方插。 3. `ev->request` 用完要注意什么? —— **它属于事件,事件 `free` 之后失效**。 要先把需要的字段拷出来,再释放事件,否则会读到已释放内存。第九章到此结束:SIP 从报文到库全走完。下一章进 GB/T 28181,把这套东西用到国标报文上。
- ☐ (本章完,下一节 10.01 协议简介)