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 秒实验
- 把 INVITE 的 branch 改掉再发,看 UAS 是不是当成新事务。
- 连发两条 branch 相同的 INVITE,看 UAS 回几次。
- 把
Via里的rport去掉,看应答发到哪个端口。
常见误区
- 以为 100 Trying 表示「对方在准备接听」。 它只是「我收到请求了」,没有任何业务含义, 唯一作用是让主叫停重传。
- 以为 ACK 和 INVITE 的 branch 相同。 ACK 是独立事务,branch 不同; 它靠 CSeq 与那个 INVITE 关联。
- 以为分层是理论上的优雅。 它是互操作的前提:换传输、加 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 花哨得多。
- ☐ (本节完,下一节 09.01.03 SIP的特征及元素)