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

Call-ID 加两个 tag,三样东西拼起来才是一次会话——少一样就配不上。 而 CSeq 的规矩里有一条例外:ACK 不递增。 这一条例外造成的 481,是 SIP 排障里最常见的一个码。

📝 本节和前面几节的关系

09.01.02 说了「对话」这一层管整通通话,这一节讲它靠什么标识。 01.04.04 讲 RTSP 时也有 Session 号维系状态,SIP 没有那种会过期的会话号, 它用的是「三样拼起来」的办法。

一、三元组

成分 谁生成 在哪
Call-ID 主叫 Call-ID 头
local tag 自己 From 头
remote tag 对方 To 头

三样都要有,而且「对同一通电话来说,两边算出来的应该一样」: 主叫侧的 local 就是被叫侧的 remote,反过来也一样。 这就是为什么它叫「对话标识」:它是个「从哪边看都一致」的组合。 第二行那个 tag 只有发起方生成, 被叫方的 tag 是在第一个应答里带回来的(To 头会多一个 tag=)。 所以「对话建立」的准确时刻是:主叫收到带 To tag 的 1xx 或 2xx。 在那之前,这通电话还只是「一个事务」。

二、tag 从哪来,能不能自己定

规范只要求「全局唯一」,没规定怎么生成:

做法 常见度 风险
随机十六进制 最常见 低
固定值 测试时用 并发会串
按时间戳 少 同毫秒会撞

第二行那种是测试专用:固定 tag 让报文可复现, 但绝对不能上生产——两通并发电话撞了同一个 tag,服务端会把它们当成同一通。 第三行那个「同毫秒会撞」是真事:高并发下每秒几千通电话,毫秒级时间戳根本不够用。 工程上的做法是「随机 + 计数器」组合,保证同一进程内不重复、跨进程靠随机。

三、CSeq 的规矩与那条例外

规则很短:非 ACK 的请求,每发一次加一;ACK 沿用被确认那个的号。

顺序 方法 CSeq 规则
1 INVITE 1 起始值
2 ACK 1 不递增
3 re-INVITE 2 递增
4 ACK 2 不递增
5 BYE 3 递增

第二、四行就是那条例外:ACK 属于它确认的那个 INVITE 事务,所以号必须一样。 加了会怎样? 服务器回 481(Call/Transaction Does Not Exist)——因为它找不到「CSeq 是 2 的那个 INVITE」。 481 这个码在 SIP 排障里的地位,相当于 HTTP 的 404:它基本只说明「你给的标识我这儿没有」。 所以看到 481,第一反应应该是查 CSeq 与 branch,而不是查网络。

四、一个对话里有多个事务

这是最容易在日志里看晕的地方:

东西 这通电话里有几个 标识
对话 1 个 Call-ID + 两 tag
事务 3 个以上 branch
消息 8 条左右 不标识

第二行那个「3 个以上」:INVITE、ACK、BYE 各一个,re-INVITE 再来两个。 所以日志里同一通电话会看到好几个 branch,它们是「同一个对话里的不同事务」。 排障时的正确顺序是:先用 Call-ID 把这一通电话的所有报文捞出来,再按 CSeq 排序看顺序, 最后才逐个事务看 branch 有没有对上。 反过来从 branch 入手会漏报文——因为一个 branch 只对应一对请求应答。

五、挂断之后对话就没了

这一点常被忽略,但它影响程序设计:

时刻 对话还在吗 能不能再发请求
200 OK 之后 在 能(re-INVITE)
BYE 之后 没了 不能
超时之后 没了 不能

第二行要记牢:BYE 一发出,这个 Call-ID + tag 的组合就作废了,再用它发请求会得到 481。 所以「断线重连」必须重新 INVITE、重新生成 Call-ID,不能拿旧的接着用。 第三行那个「超时」也值得一提:SIP 没有像 RTSP 那样的 Session 超时头, 对话什么时候算消失,靠的是「双方都不再发请求」这个事实。 所以有些实现会定期发 re-INVITE 保活对话。

六、动手 Demo:三元组怎么把两条报文认成一次会话

Demo 用固定 Call-ID 与 tag 拼两条报文,证明「参数一样报文就一样」,再算一遍 CSeq 序列。

a = S.build_req("INVITE", cseq=1, callid="[email protected]",
                from_tag="aaa111")
b = S.build_req("INVITE", cseq=1, callid="[email protected]",
                from_tag="aaa111")
print(a == b, len(a.encode()))

n = 0
for m in ("INVITE", "ACK", "INVITE", "ACK", "BYE"):
    if m != "ACK":
        n += 1
    print(m, n)

实跑输出:

--------------------------------------------------------------------
三元组怎么把两条报文认成一次会话
--------------------------------------------------------------------
**SIP 的会话标识是三样东西拼起来的,缺一样就配不上**:

  成分           本例                     谁生成
  -----------------------------------------------------
  Call-ID        [email protected]     **全局唯一**
  local tag      aaa111                   From 里那个
  remote tag     —(响应里才有)          To 里那个

**两次拼出来的报文一模一样**(361 字节),
**因为 Call-ID 与 tag 都是显式给的**——**这就是「可复现」的关键:标识不能靠随机数。**
**真实客户端里 Call-ID 是随机生成的**,**但测试时必须能固定**,**否则同一个 bug 复现不了。**

**看一个真实的对话里 CSeq 怎么走**:

  顺序     方法       CSeq     规则
  ----------------------------------------
  1        INVITE     1        递增
  2        ACK        1        **不递增**
  3        INVITE     2        递增
  4        ACK        2        **不递增**
  5        BYE        3        递增

**第三列那个「ACK 不递增」是硬规矩**:**ACK 的 CSeq 必须和它确认的那个 INVITE 一样**,
**因为 ACK 属于同一个事务。**
**所以「ACK 的 CSeq 加一」是初学者最常犯的错**——**加了之后设备回 481(事务不存在)。**
**481 这个码要记住**:**它基本只出现在「CSeq 或 branch 对不上」的时候。**

第一行那个 True 是这一节的基础: 标识固定,报文就完全可复现——这是所有 SIP 测试脚本的前提。 如果 Call-ID 用随机数,同一条命令跑两次报文就不一样, 而「不一样」会让「对比两次抓包」这种最常用的排障手段失效。 第二段那个循环是 CSeq 规则的直译:if m != "ACK" 就是那条例外, 去掉这个判断,ACK 也会递增,结果全错。 输出会是 1、1、2、2、3——两个 1 和两个 2 就是「INVITE 与它的 ACK 同号」。 这一段代码只有五行,但它是「为什么 ACK 会回 481」的完整解释: 多加了 1,服务器就在自己的事务表里找不到那个号。

30 秒实验

  1. 把 Call-ID 改成随机数,看跑两次的字节数还一不一样。
  2. 给 ACK 用加一的 CSeq 发一次,看 UAS 回什么码。
  3. 把 tag 换成同一个固定值,并发发两条 INVITE,看服务端怎么处理。

常见误区

  1. 以为 ACK 的 CSeq 要加一。 ACK 沿用被确认那个 INVITE 的号,加了就是 481, 而 481 看起来像「会话不存在」,容易误判成掉线。
  2. 以为 tag 可以随便固定。 测试可以、生产不行, 两通并发电话撞同一个 tag 会被服务端当成同一通。
  3. BYE 之后还想用原来的 Call-ID 继续发。 对话已经作废,必须重新 INVITE 并生成新 Call-ID。
自测三问(点开对答案) 1. 对话标识的三样分别谁生成? —— **Call-ID 由主叫生成、local tag 由自己生成、remote tag 由对方生成。** 主叫侧的 local 等于被叫侧的 remote。 2. 看到 481 应该先查什么? —— **先查 CSeq 与 branch**,而不是查网络。 481 的含义是「你给的标识我这儿没有」,属于标识错而不是链路断。 3. 为什么排障要先按 Call-ID 捞报文? —— **一个 branch 只对应一对请求应答**,从 branch 入手会漏; Call-ID 能捞出整通电话的全部报文,再按 CSeq 排序就对上了。

下一节把八条报文完整跑一遍——注册、呼叫、挂断全走通。

相关笔记