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

最小可用的 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 秒实验

  1. 把 listen_addr 的地址从 NULL 改成 127.0.0.1,再试一次。
  2. 在注册成功后等 3 秒再呼叫,看有没有区别。
  3. 把 eXosip_init 的返回值忽略掉,改成不初始化直接注册,看会怎样。

常见误区

  1. 初始化完直接发注册,不调 listen_addr。 应答收不到,表现是「注册一直超时」, 而不是启动报错。
  2. listen_addr 绑 127.0.0.1。 外部设备永远注册不上来,报「连接被拒」,看着像端口没开。
  3. build 出来的消息用完不 osip_message_free。 内存泄漏,每次几百字节, 跑几天才看得出来。
自测三问(点开对答案) 1. eXosip 为什么把「构造」和「发送」拆成两个函数? —— **因为很多场景要在发出前改东西**:加私有头、改 Expires、改 Subject。 只有一个函数就没地方插这些改动。 2. 不调 `listen_addr` 会怎样? —— **能发请求,但收不到应答**,表现是注册一直超时。程序不报错,所以很容易被判成网络问题。 3. 用库之后调试变难了,怎么补? —— **抓包,或者打开库自己的日志**(`osip_trace_enable`)。这也是本课先讲报文再讲库的原因。

下一节看库里面的结构——一条报文在内存里长什么样。

相关笔记