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

eXosip 是事件驱动的:发出去的东西没有返回值,结果全在事件里。 所以「我发出去了」不是事件,「对端回了我」才是——这个区别决定了代码怎么写。

📝 本节和前面几节的关系

09.02.06 把库初始化好了,这一节讲它的主循环。 08.02.03 的 WS-Discovery 也是「发出去等应答」, 但那边是同步等,这边是异步事件——两种模型各有适用场景。

一、四行主循环

while (1) {
    eXosip_event_t *ev = eXosip_event_wait(0, 200);
    if (ev == NULL) continue;
    switch (ev->type) { /* 分发 */ }
    eXosip_event_free(ev);
}

第二行那两个参数是「秒」与「毫秒」:0, 200 表示「最多等 200 毫秒」, 这是一个带超时的阻塞,不是「不等」。 如果两个都给 0,它就变成「立即返回」——而循环体里没有阻塞,CPU 就会被跑满。 这种「忙等」在监控软件里很常见,表现为「什么也不干但 CPU 占一个核」。 第四行那个 free 不能漏,每个事件都是一块堆内存。

二、事件有哪几类

事件 什么时候来 你要做什么
REGISTRATION_SUCCESS 注册成功 记录、开始续期
REGISTRATION_FAILURE 注册失败 重试或报警
CALL_RINGING 对端振铃 提示用户
CALL_ANSWERED 接通 开始收媒体
CALL_CLOSED 挂断 清理资源

第二行那个「失败事件」必须处理:很多人只写了成功分支, 于是注册失败时程序「安静地什么都不做」——现场表现就是「设备一直不在线,也不报错」。 第三、四行的区别是业务上的:振铃要提示用户「在响」,接通才开始准备收流。 混在一起处理会导致「还没接通就开始等媒体」,白等几秒。 第五行那个「清理」包括: 停掉 RTP 接收、关闭文件、释放会话结构——漏了就是「通话记录越积越多」。

三、没有「我发出去了」这种事件

这一点最容易误解:

动作 有事件吗 原因
发出 INVITE 没有 本地动作不算外部输入
收到 180 有 外部输入
发出去了但没人回 没有 只能靠超时

第三行是最麻烦的:「发出去了、对端没回」这种情况不会产生任何事件, 要等库内部的超时(或者自己计时)才知道失败。 所以「程序卡住」有两种完全不同的原因:① 事件循环在转,但对端没回(等超时就好); ② 事件循环本身写错了(比如 wait 参数传 0,0 在忙等)。 区分办法很简单:在循环里打一行日志,看它转不转、多久转一圈。

四、同步等与异步事件

模型 例子 适合
同步等 08.02 的 WS-Discovery 一问一答
异步事件 eXosip 多路并发

第一行那种「发一条等一条」写起来简单, 但没法同时处理多件事——等注册应答的时候收不到呼叫请求。 第二行那种适合「一台服务器管几百路」:一个循环里处理所有连接的所有事件, 不用开几百个线程。 代价是「状态要自己维护」:因为是异步的,「这通电话进行到哪一步」必须存在自己的结构里。 这也是为什么「用库」不等于「不用懂流程」。

五、真跑一次看事件顺序

Demo 里跑了注册、呼叫、挂断,事件按业务顺序来:注册成功 → 振铃 → 接通 → 关闭。 注意「注册成功」只有一个事件, 而实际上发了两条 REGISTER——「第一轮收到 401」不产生事件, 因为库把它当内部过程处理掉了。 这一点很能说明「库的抽象层次」:它把「401 加摘要重发」封成一个动作, 调用方只看到「成功」或「失败」。 所以如果你要观察细节(比如确认 nonce 有没有变), 只能开日志或者抓包——事件这一层是不够的。

六、动手 Demo:事件循环,所有结果都从这出来

Demo 跑完整流程,然后把事件按顺序取出来打印。 事件类型名用的是 eXosip 的枚举名,这样对照头文件能直接查。

uac = S.UAC(uas_port)
uac.register()
uac.invite()
uac.ack()
uac.bye()

while True:
    ev = uac.event_wait()
    if ev is None:
        break
    print(ev[0], ev[1])

实跑输出:

--------------------------------------------------------------------
事件循环,所有结果都从这出来
--------------------------------------------------------------------
**eXosip 的编程模型是「事件驱动」**:**发出去的东西没有返回值,结果全在事件里。**

**典型写法就这四行**:

  while (1) {
      eXosip_event_t *ev = eXosip_event_wait(0, 200);
      if (ev == NULL) continue;
      switch (ev->type) { ... }
      eXosip_event_free(ev);
  }

**第二行那两个参数要注意**:**第一个是「等多少秒」、第二个是「等多少毫秒」**,
**都给 0 就是「立即返回」**——**那样会变成忙等、CPU 跑满。**
**所以 `0, 200` 的意思是「最多等 200 毫秒」**,**这是一个「带超时的阻塞」不是「不等」。**

**真跑一次,看事件按什么顺序来**:

  #     事件类型                        附带信息
  ---------------------------------------------------
  1     EXOSIP_REGISTRATION_SUCCESS     Expires=3600
  2     EXOSIP_CALL_RINGING             180 Ringing
  3     EXOSIP_CALL_ANSWERED            200 OK
  4     EXOSIP_CALL_CLOSED              200 OK

**四个事件、顺序是「注册成功 → 振铃 → 接通 → 关闭」**——**正好是业务流程的顺序。**
**注意没有「呼叫发起」这个事件**:**「我发出了 INVITE」不是事件**,
**事件只表示「有外部输入发生了」。**
**所以「发了没反应」这种情况不会有事件**,**只能靠超时事件(或者自己在外面计时)发现。**

**这一点在排障时很关键**:**程序卡在「等接通」,说明事件循环在转、但对端没回 200**;
**程序完全不动,说明 `event_wait` 的调用方式有问题**(比如传了 0,0 在忙等)。
**两种「不动」的原因完全不同,别混着查。**

输出四个事件,顺序就是业务顺序——说明「事件是流程的自然产物」,不是随便排的。 注意 EXOSIP_CALL_CLOSED 只在挂断成功后出现:如果 BYE 没收到 200,这个事件不会来, 于是「对话还活着」——这就是 09.02.08 要讲的「对话没关掉」。 四个事件里没有「注册第一轮 401」,因为库把两轮注册封成了一个动作。 所以「事件数量」永远小于「报文数量」,这一点在估算日志量时有用。 最后提醒一句 event_wait 的返回:它返回的是 NULL 表示「这一轮没事件」, 不是「出错」。 所以循环里应该 continue 而不是 break——写错成 break 的话,程序会在第一个空闲轮次就退出。

30 秒实验

  1. 把 event_wait 的第二个参数从 200 改成 0,看 CPU 占用会不会上去。
  2. 在 BYE 之前把 UAS 停掉,看会不会收到 CALL_CLOSED。
  3. 连续发两次注册,看会不会收到两个 REGISTRATION_SUCCESS。

常见误区

  1. event_wait 返回 NULL 就 break。 NULL 只表示「这轮没事件」,应该 continue, 否则程序第一个空闲轮就退出。
  2. 两个超时参数都传 0。 变成忙等,CPU 跑满一个核,表现是「什么也不干但风扇狂转」。
  3. 只处理成功事件、不处理失败事件。 注册失败时程序静默, 现场表现是「设备一直不在线也不报错」。
自测三问(点开对答案) 1. 为什么「发出 INVITE」不是事件? —— **事件只表示「有外部输入发生了」**,本地动作不算。 所以「发出去了没回」不会有事件,只能靠超时发现。 2. `eXosip_event_wait(0, 200)` 是什么意思? —— **最多等 200 毫秒的阻塞**。 第一个参数是秒、第二个是毫秒,两个都给 0 就变成「立即返回」,会忙等。 3. 为什么事件数比报文数少? —— **库把内部过程封成了单个事件**:两轮注册只出一个「注册成功」,ACK 也不产生事件。

下一节讲事务与会话的管理——数量关系和内存泄漏都在这儿。

相关笔记