一通电话一个对话、三个以上的事务、八条左右的报文—— 这个数量关系能帮你判断「哪里没清掉」。 「事务数只涨不跌」通常不是 bug,而是「对话没关掉」。
09.02.07 讲了事件,这一节讲事件背后那些要被回收的东西。 09.01.02 讲过事务与对话的分层,这一节用「数量」把它落到实处。
一、三个层次的数量
| 单位 | 一通电话 | 三通 |
|---|---|---|
| 对话 | 1 | 3 |
| 事务 | 3 以上 | 9 以上 |
| 报文 | 8 左右 | 24 左右 |
第二行那个「3 以上」是下限:INVITE、ACK、BYE 各一个, re-INVITE(换编码、保持会话)会再增加。 所以「事务数暴涨」通常不是 bug,而是「对话没关掉,事务也跟着不清」——两者是连着的。 第三行那个「8 左右」和 09.01.05 的实测吻合: 两轮注册 4 条(2 请求 2 应答)、呼叫 5 条、挂断 2 条。
二、谁负责回收
| 东西 | 谁回收 | 什么时候 |
|---|---|---|
eXosip_event_t |
你 | 用完立刻 |
osip_message_t |
你 | build 之后用完 |
| 事务 | 库 | 事务结束后 |
| 对话 | 库 | BYE 之后 |
前两行是「你自己建的、你自己放」,后两行是「库建的、库放」——这条界线不能混。
所以「手动 free 事务」是错的(09.02.05 讲过),而「不 free 事件」也是错的。
判据很简单:谁调用了 _init 或 _build,谁就负责 _free。
库内部建的东西,库自己会放。
三、对话没关掉会怎样
这是长跑程序最典型的内存问题:
| 情况 | 后果 |
|---|---|
| BYE 丢了、没收到 200 | 对话一直不结束 |
| 对端异常断电 | 要等超时 |
| 自己的程序异常退出没清 | 重新注册时残留 |
第一行那个最常见:BYE 发出去了,但对端没回 200(网络抖了一下)。 库会按 Timer F 重传,重传到 32 秒还没回应,才算超时、才回收对话。 所以「挂断后对话立刻消失」是错的期待——它会存在最多 32 秒。 如果这时又发起同一路的新呼叫,服务端可能还有旧记录。 第二行那种「对端断电」更麻烦: 没有任何报文通知你,只能靠「RTP 收不到包」或者「注册续期失败」间接发现。
四、怎么观察有没有泄漏
两条实用办法:
| 办法 | 看什么 | 判据 |
|---|---|---|
| 打点日志 | 对话数、事务数 | 是否回到基线 |
| 压力测试 | 内存曲线 | 是否锯齿 |
第一行那个「打点」要选对位置:在「呼叫成功」和「挂断完成」两处各打一行, 看两个计数是不是同步增减。 如果只有增没有减,立刻就能定位。 第二行那个「锯齿」是关键判据:正常程序的内存应该「涨上去又落回来」; 如果每次涨一点、从不回落,就是泄漏。 注意要跑够久:每次泄漏几百字节的话,跑十分钟看不出来,跑一天才明显。
五、数量关系的实用价值
这三行数字不只是「知识」,它能当判据用:
| 现象 | 可能的原因 |
|---|---|
| 一通电话有 30 条报文 | 重传了 |
| 只有 4 条 | 某一步短路了 |
| 对话数远大于在线通道数 | 旧的没清 |
第一行那个「30 条」说明丢了包:每丢失一次就多一条重传, 而重传的报文内容完全一样(同一个 branch)——抓包看到重复的 branch 就是重传。 第二行那个「只有 4 条」说明流程没走完:可能注册失败就退出了,或者呼叫被拒(4xx)。 所以「数报文条数」是一个很快的初筛手段,比逐条读报文快得多。
六、动手 Demo:三通呼叫,事务与会话各几个
Demo 起三台 UAC,各自走一遍完整流程,把三通电话的标识摆在一起看。 如果三行的 Call-ID 或远端 tag 有重复,就说明标识生成有问题。
for i in (1, 2, 3):
uac = S.UAC(uas_port)
uac.register()
codes = uac.invite()
uac.ack()
uac.bye()
print(uac.port, uac.dialog_key())
uac.close()
实跑输出:
--------------------------------------------------------------------
三通呼叫,事务与会话各几个
--------------------------------------------------------------------
**事务和对话的区别在 09.01.02 讲过**,**这一节用「数量」把它量化。**
第几通 UAC 端口 Call-ID 前 10 位 远端 tag 应答
----------------------------------------------------------------------
1 60489 52fd4fd201… 7a8c01cb 100+180+200
2 60490 2825f55db2… 8e0ad327 100+180+200
3 60491 69ef5fdfc1… 645e7562 100+180+200
**三行的 Call-ID 与远端 tag 全不同**——**这是「三通独立对话」的证据。**
**而它们共用一个 UAS 端口**,**因为服务端本来就要处理多路。**
**数量关系可以这样记**:
单位 一通电话里几个 三通总共
----------------------------------------
对话 **1** **3**
事务 **3 以上** **9 以上**
报文 **8 左右** **24 左右**
**第二行那个「3 以上」是下限**:**INVITE、ACK、BYE 各一个**,
**re-INVITE 或者被叫再挂一次就更多。**
**所以「事务数暴涨」通常不是 bug,而是「对话没关掉」。**
**清理这件事必须自己做**:**`eXosip_event_free` 只释放事件本身**,
**事务和对话的记录要等「对话结束」才回收**。
**如果 BYE 丢了、对话一直不结束**,
**服务端就会一直占着这几条记录**——**这就是「长跑几天内存缓慢上涨」的常见原因。**
三行的 Call-ID 与远端 tag 全不同,这是「三通独立对话」的证据。 而这正是「标识必须唯一」的实际意义: 如果两通电话的 Call-ID 撞了,服务端会把它们当成同一通——表现是「挂断一路,另一路也断了」。 这个 bug 在测试时很难发现(因为只有一路),上了现场多路并发才暴露。 所以 Demo 里三通并排看,是刻意的:「唯一性」这种事情只有摆在一起才能验证。 注意三个 UAC 用的是同一个 UAS 端口,因为服务端本来就该处理多路; 而它们各自的本地端口不同,这是「同一台机器跑多个客户端」的正常状态。
30 秒实验
- 把三通呼叫的 Call-ID 手动改成一样,看 UAS 会不会混。
- 在
bye()之前把 UAS 停掉,观察「对话没关掉」的表现。 - 跑 100 通呼叫,用
tasklist或ps看内存涨不涨。
常见误区
- 以为挂断后对话立刻消失。 BYE 丢了要等超时才回收,最长 32 秒; 这期间服务端可能还有旧记录。
- 手动 free 事务。 事务是库建的、库放的,手动释放会打乱状态机。 判据是「谁 init 谁 free」。
- 只跑十分钟就判断没有内存泄漏。 每次几百字节的话要跑一天,短时间看不出来。
自测三问(点开对答案)
1. 一通电话里有几个对话、几个事务? —— **对话 1 个、事务 3 个以上**(INVITE、ACK、BYE),re-INVITE 会再增加。 2. 「事务数只涨不跌」最可能是什么原因? —— **对话没关掉**。事务跟着对话走,对话不结束事务也不回收,常见于 BYE 丢包后一直等超时。 3. 一通电话出现 30 条报文说明什么? —— **重传了**。每丢一次包就多一条重传,而重传报文内容相同、branch 也相同,抓包能直接看出来。下一节收口:把 eXosip 的整张栈图讲清,第九章就完了。
- ☐ (本节完,下一节 09.02.09 eXosip协议栈简介)