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 秒实验
- 把
event_wait的第二个参数从 200 改成 0,看 CPU 占用会不会上去。 - 在
BYE之前把 UAS 停掉,看会不会收到CALL_CLOSED。 - 连续发两次注册,看会不会收到两个
REGISTRATION_SUCCESS。
常见误区
event_wait返回NULL就break。NULL只表示「这轮没事件」,应该continue, 否则程序第一个空闲轮就退出。- 两个超时参数都传 0。 变成忙等,CPU 跑满一个核,表现是「什么也不干但风扇狂转」。
- 只处理成功事件、不处理失败事件。 注册失败时程序静默, 现场表现是「设备一直不在线也不报错」。
自测三问(点开对答案)
1. 为什么「发出 INVITE」不是事件? —— **事件只表示「有外部输入发生了」**,本地动作不算。 所以「发出去了没回」不会有事件,只能靠超时发现。 2. `eXosip_event_wait(0, 200)` 是什么意思? —— **最多等 200 毫秒的阻塞**。 第一个参数是秒、第二个是毫秒,两个都给 0 就变成「立即返回」,会忙等。 3. 为什么事件数比报文数少? —— **库把内部过程封成了单个事件**:两轮注册只出一个「注册成功」,ACK 也不产生事件。下一节讲事务与会话的管理——数量关系和内存泄漏都在这儿。
- ☐ (本节完,下一节 09.02.08 oSIP管理事务及会话)