最小可用的 UAC 就是四个动作:
初始化、注册、呼叫、事件循环——拼报文和算摘要全在库里。
但 listen_addr 那一步写错,程序不报错、只是一直没人来。
09.02.03 把库编好了,这一节用库写第一个程序。 09.01.05 用「手拼报文」的方式跑过同样的流程, 这一节是「库的视角」,可以对照着看差在哪。
一、四个动作
| 步 | 做什么 | 对应函数 |
|---|---|---|
| 1 | 初始化 | eXosip_init |
| 2 | 监听 | eXosip_listen_addr |
| 3 | 注册 | eXosip_register_* |
| 4 | 事件循环 | eXosip_event_wait |
第二行那个「监听」是最容易被跳过的一步:不监听也能发请求, 但对端回的应答收不到——表现是「注册一直超时」。 所以「初始化完就发注册」的代码是漏了一步的, 它可能在某些环境下「碰巧能跑」(比如 UAS 与 UAC 在同一台机器上),换到真实网络就必然失败。 第四行那个事件循环是 eXosip 的核心编程模型,所有结果(成功的、失败的)都从它出来, 函数本身没有返回值。
二、listen_addr 的三个参数
| 参数 | 本例 | 含义 |
|---|---|---|
| 协议 | IPPROTO_UDP |
UDP 或 TCP |
| 地址 | NULL |
所有网卡 |
| 端口 | 5060 | SIP 标准端口 |
第二行那个 NULL 是关键:它表示「绑定所有网卡」,换成具体 IP 就只听那一块。
多网卡的机器上这一条很重要:只听 127.0.0.1 的话,外部设备永远注册不上来,
而错误表现是「连接被拒」——看起来像端口没开。
所以排查「注册不上」时,先确认 listen_addr 绑的是 NULL 还是具体地址。
三、注册为什么要写两次函数
eXosip 把「构造」与「发送」拆成两个函数:
| 阶段 | 函数 | 能改什么 |
|---|---|---|
| 构造 | build_initial_register |
改头、改正文 |
| 发送 | send_register |
只能发 |
这个拆分不是啰嗦,是必需的:
很多场景要在发出前改点东西——比如加一个厂家的私有头、改 Expires、加 Subject。
如果只有一个函数,就没地方插这些改动。
同样的「build/send 成对」在 call、message 上也有,这是 eXosip 的统一风格。
注意 build 出来的 osip_message_t * 用完要 osip_message_free,
漏了就是内存泄漏——这类泄漏每次只漏几百字节,跑几天才看出来。
四、真跑一遍
Demo 里 UAS 与 UAC 都是本机起的,四步走完只需要几毫秒。
端口每次运行都不同,因为绑的是 0(系统分配)。
这个设计是为了「能多开」:同机起十个 UAC 不会撞端口,而真实设备当然是绑死 5060 的。
注册那一步返回 200,说明「401 之后带摘要再发」这一整套都通了;
如果返回 401,说明第二轮没发出去或者摘要算错。
呼叫那一步返回三个码,100+180+200 是全流程正常的样子;
少一个就说明某一环断了——比如只有 100 没有 180,多半是 UAS 那边没实现振铃。
五、和手拼报文那次差在哪
| 项 | 09.01.05(手拼) | 本节(用库) | |---|---|---|---| | 代码量 | 两百多行 | 几十行 | | 摘要计算 | 自己算 | 库算 | | 事务配对 | 自己管 | 库管 | | 看得见报文吗 | 看得见 | 看不见 |
第四行是代价:用库之后,报文的细节被藏起来了,
调试时要靠「抓包」或者打开库的日志(osip_trace_enable)。
所以「学协议」和「用库」是两条路:先手拼一遍,再用库,出了问题才知道该看哪儿。
反过来学,遇到怪现象就只能猜。
这也是本课把 09.01 放在 09.02 前面的原因:先看报文,再看封装。
六、动手 Demo:最小 UAC 与 UAS 真跑一遍
Demo 起一台本机 UAS,再用 UAC 走完注册、呼叫、挂断。 四步的返回码是真跑出来的,端口每次不同。
uas = S.UAS()
port = uas.start()
uac = S.UAC(port)
code1 = uac.register() # 两轮,最后 200
code2 = uac.invite() # ["100", "180", "200"]
uac.ack() # 无应答
code3 = uac.bye() # 200
print(code1, code2, code3)
实跑输出:
--------------------------------------------------------------------
最小 UAC 与 UAS 真跑一遍
--------------------------------------------------------------------
**这一节把 09.01 的流程用「库的写法」重跑一次**:**UAC 主动发起、UAS 被动应答。**
UAS 在 127.0.0.1:60482,UAC 在 127.0.0.1:60483
动作 结果 说明
------------------------------------------------
注册 **200** 两轮,401 后 200
呼叫 **100+180+200** 100/180/200
ACK (无应答) **确认,不等结果**
挂断 **200** 200 OK
**四步 4.89 毫秒跑完**,**和 09.01.05 那次是同一套流程**——**只是这次是「库的视角」:**
**调用方看不到报文,只看到返回码。**
**这正是封装的收益**(08.08.10 讲过):**拼报文、算摘要、配对事务全在库里**,
**上层只写四个动作。**
**最小 UAC 的骨架就是这四步**:
eXosip_init(); /* 1 初始化 */
eXosip_listen_addr(IPPROTO_UDP, NULL, 5060, AF_INET, 0);
eXosip_register_build_initial_register(...); /* 2 注册 */
eXosip_register_send_register(...);
eXosip_call_build_initial_invite(...); /* 3 呼叫 */
eXosip_call_send_initial_invite(...);
while (eXosip_event_wait(0, 200, &ev) == OSIP_SUCCESS) {
/* 4 事件循环:所有结果都从这里出来 */
eXosip_event_free(ev);
}
**注意第三行那个 `NULL`**:**它表示「所有网卡都监听」**,**换成具体 IP 就只听那一块。**
**多网卡的机器上这一条很重要**:**只听 `127.0.0.1` 的话,外部设备永远注册不上来**,
**而错误表现是「连接被拒」——看起来像端口没开。**
输出里四行返回码就是「库的视角」——调用方看不到报文,只看到结果。
注册那一行显示 200 而不是 401,因为库已经自动处理了「401 后带摘要重发」这一轮。
这就是「库帮你做了什么」最具体的例子。
呼叫那一行是 100+180+200 三个码连在一起,
说明临时应答也被暴露给调用方了——因为业务上要区分「在振铃」和「已接通」。
ACK 那行写「无应答」,这是 SIP 的规定(09.01.05 讲过):
它是确认不是请求,等它的应答会一直等到超时。
四步加起来几毫秒,
这个数字和 09.01.05 那次是同一个量级——
说明「用库」并不比「手拼」慢,快慢从来不是这一层的差别。
30 秒实验
- 把
listen_addr的地址从NULL改成127.0.0.1,再试一次。 - 在注册成功后等 3 秒再呼叫,看有没有区别。
- 把
eXosip_init的返回值忽略掉,改成不初始化直接注册,看会怎样。
常见误区
- 初始化完直接发注册,不调
listen_addr。 应答收不到,表现是「注册一直超时」, 而不是启动报错。 listen_addr绑127.0.0.1。 外部设备永远注册不上来,报「连接被拒」,看着像端口没开。build出来的消息用完不osip_message_free。 内存泄漏,每次几百字节, 跑几天才看得出来。
自测三问(点开对答案)
1. eXosip 为什么把「构造」和「发送」拆成两个函数? —— **因为很多场景要在发出前改东西**:加私有头、改 Expires、改 Subject。 只有一个函数就没地方插这些改动。 2. 不调 `listen_addr` 会怎样? —— **能发请求,但收不到应答**,表现是注册一直超时。程序不报错,所以很容易被判成网络问题。 3. 用库之后调试变难了,怎么补? —— **抓包,或者打开库自己的日志**(`osip_trace_enable`)。这也是本课先讲报文再讲库的原因。下一节看库里面的结构——一条报文在内存里长什么样。
- ☐ (本节完,下一节 09.02.05 oSIP的重要数据结构)