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

SIP 分了四层,每一层只跟相邻那层说话——这是它能同时跑在 UDP、TCP、TLS 上的原因。 而「事务」这一层的匹配规则只有四样东西,其中一样是七个固定字符。

📝 本节和前面几节的关系

09.01.01 看了报文长什么样,这一节看报文怎么被处理。 08.01.02 讲 SOAP 时也提过分层(HTTP 上面套 SOAP),SIP 的分层更严格: 它的每一层都是规范里写死的状态机。

一、四层,各管一件事

层 管什么 错了会怎样
语法与传输 怎么变成字节 报文根本发不出
事务 请求和应答配对 重传失控
事务用户 谁来用事务 UAC/UAS 逻辑错
对话 一次呼叫的生命期 挂不断、接不上

第二行是 SIP 最有特色的一层:它专门负责「这一个请求对应哪一个应答」, 并且管着重传与超时。 为什么要把这件事单独抽一层? 因为 UDP 不保证送达——如果没有事务层,每个应用都要自己写重传、自己配对, 而重传的节奏(T1 起翻倍)是规范定死的,各写各的就互通不了。 第四行那个「对话」是比事务更大的单位:一个对话里可以有多个事务, 比如一次通话里先 INVITE、再 re-INVITE(换编码)、最后 BYE。

二、一条 INVITE 引出三条应答

这正好解释了 Demo 里那个「发一条、收三条」:

应答 属于哪一层 作用
100 Trying 事务层 停重传
180 Ringing 对话层之前 告诉主叫在振铃
200 OK 对话层 建立对话

第一行那个 100 是「事务层的」,不是「对话层的」—— 它是临时应答(1xx),只表示「我收到这个请求了」。 所以 100 里没有 To tag(有的实现会给),因为它还没进入对话。 第二、三行才是真正的「对方的态度」:180 说「我在响铃」,200 说「接通了」。 一个 INVITE 事务从发出到收到最终应答(2xx~6xx)算结束, 而 INVITE 事务特殊在:它还要等 ACK 才算真正结束。 这就是为什么 INVITE 事务比其他事务多两个 Timer(G 和 H)。

三、事务匹配的四样东西

要素 从哪来 说明
branch Via 头 一对请求应答共用
sent-by Via 头的地址 发方 host:port
method 起始行 ACK 是特例
magic cookie branch 前七位 必须 z9hG4bK

第一行是主键:同一事务的所有报文 branch 相同,不同事务 branch 一定不同。 第四行那个 cookie 是 RFC 3261 加的: 它让对端一眼看出「这是新规矩的 branch」—— 没有这七个字符,就会被按 RFC 2543 的老逻辑解析,而老逻辑的 branch 只是个随机串、不保证唯一。 第三行那个 ACK 特例要单独说:ACK 的 branch 和它确认的 INVITE 不同(因为它是新事务), 但它的 CSeq 号和那个 INVITE 相同——所以匹配 ACK 靠 CSeq,不靠 branch。 这一条是 SIP 里最绕的规则,也是 481 报错的主要来源。

四、对话与事务的区别

| 比 | 事务 | 对话 | |---|---|---|---| | 标识 | branch | Call-ID + 两个 tag | | 范围 | 一次请求应答 | 整通通话 | | 数量 | 一个对话里有多个 | 一通只有一个 | | 结束 | 收到最终应答 | BYE 之后 |

第一行右边那三样就是 09.01.04 的主题。 第三行值得琢磨:一通电话里至少有 INVITE 事务、ACK 事务、BYE 事务三个, re-INVITE 还会有更多。 所以「事务号」和「通话号」是两个层次的东西, 日志里混着看会晕——排障时先问「现在看的是哪一层」。

五、为什么分层能带来互操作

这是分层最实际的收益,不是理论上的优雅:

变化 要改哪层 上层受影响吗
UDP 换 TCP 只改传输层 不受影响
加 TLS 只改传输层 不受影响
换编码 只改 SDP 信令不变

前两行是 SIP 能用于公网的原因:sips: 走 TLS 时,事务层和对话层完全不用改。 第三行那个「换编码只改 SDP」也是真的:INVITE 报文本身不变, 变的只是正文那几行 m= 与 a=rtpmap。 反过来如果协议不分层(比如 H.323 把编码协商写进信令里), 加一种编码就要动信令格式——这就是 09.01.06 要对比的东西。

六、动手 Demo:一条 INVITE 引出四条报文

Demo 真起一台 UAS,发一条 INVITE,看它回几条、各是什么。 顺便把 branch 拿出来量一下长度。

uas = S.UAS()
port = uas.start()
inv = S.build_req("INVITE", cseq=1, body=S.sdp())
sock = uas._sock()
sock.sendto(inv.encode(), (uas.host, uas.port))
for _ in range(3):
    data, _a = sock.recvfrom(65535)
    print(S.parse(data.decode())["start"], len(data), "字节")

实跑输出:

--------------------------------------------------------------------
一条 INVITE 引出四条报文
--------------------------------------------------------------------
**SIP 是分层协议,四层各管一件事**:

  层             管什么                   这层的实体
  ----------------------------------------------------------
  语法与传输     怎么变成字节             UDP/TCP
  事务           **请求和应答配对**       事务状态机
  事务用户       谁来用事务               **UAC/UAS 核心**
  对话           **一次呼叫的生命期**     dialog

**下面真起一台 UAS,发一条 INVITE,看它回几条**:

  UAS 回的报文            字节     作用
  ------------------------------------------
  SIP/2.0 100 Trying      280      停重传
  SIP/2.0 180 Ringing     281      进下一层
  SIP/2.0 200 OK          584      进下一层

**三条报文 2.11 毫秒内全到**,**方向全是 UAS → UAC**。
**注意 100 Trying 的作用**:
**它不是「我在处理」,而是「别再重传了」**——**UAC 收到它就把 Timer A 停掉。**
**180 和 200 的区别是「振铃」与「接通」**。
**重传在收到第一条 1xx 时就停**,
**100 只是最常见的那一条**——**真机上先收到 180 也照样停 Timer A。**

**事务匹配靠四样东西,少一样就算新事务**:

  要素             本例的值               说明
  -----------------------------------------------------------
  branch           z9hG4bKe495a8c992      **长度 17**
  sent-by          192.168.1.100:5060     发方地址
  method           INVITE                 ACK 特例
  magic cookie     z9hG4bK                **必须这七个字符**

**第四行那个 `z9hG4bK` 是 RFC 3261 硬规定的**:**branch 必须以它开头**,
**否则对端会按 RFC 2543 的老规矩解析。**
**长度 17 是「7 个 cookie + 10 位摘要」**,**摘要用 MD5 前 10 位,够用又不长。**

输出里那三条应答正好对应三个层次:100 是事务层的、180 与 200 是对话层的。 注意 100 只有 280 字节,200 有 584 字节——差的那 300 多字节主要是 SDP 正文与 Contact 头, 因为只有最终应答才需要告诉对方「往哪发媒体」。 branch 长度 17 = 7 个 cookie + 10 位 MD5 前缀。 17 这个数字不是规范定的,规范只要求「够唯一」; 真实设备常用 20~30 位,因为要防止高并发下碰撞。 表里那个 sent-by 显示 192.168.1.100:5060, 它和 branch 一起才构成「事务主键」—— 只看 branch 不看 sent-by,两台设备撞了同一个 branch 就会串。

30 秒实验

  1. 把 INVITE 的 branch 改掉再发,看 UAS 是不是当成新事务。
  2. 连发两条 branch 相同的 INVITE,看 UAS 回几次。
  3. 把 Via 里的 rport 去掉,看应答发到哪个端口。

常见误区

  1. 以为 100 Trying 表示「对方在准备接听」。 它只是「我收到请求了」,没有任何业务含义, 唯一作用是让主叫停重传。
  2. 以为 ACK 和 INVITE 的 branch 相同。 ACK 是独立事务,branch 不同; 它靠 CSeq 与那个 INVITE 关联。
  3. 以为分层是理论上的优雅。 它是互操作的前提:换传输、加 TLS 都只动最底一层, 上面的状态机一行不用改。
自测三问(点开对答案) 1. 为什么 SIP 要把「事务」单独抽成一层? —— **因为 UDP 不保证送达**,重传与配对必须统一。 若各应用自写,T1 起翻倍的节奏不一致,设备之间就互通不了。 2. 事务匹配靠哪四样?哪一样是 ACK 的特例? —— **branch、sent-by、method、magic cookie。** ACK 靠 CSeq 而不是 branch 匹配, 这是它「特殊」的地方。 3. 一个对话里能有多少个事务? —— **至少三个**(INVITE、ACK、BYE),re-INVITE 更多。 事务是「一次请求应答」,对话是「整通通话」。

下一节看 SIP 的地址怎么写——它比 HTTP 的 URL 花哨得多。

相关笔记