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

SIP 只干一件事:把「我要和你通话」这句话可靠地送到对方——声音图像它一概不管。 一条报文 628 字节,读完就知道它为什么三十年后还在用。

📝 本节和前面几节的关系

前八章讲的是 ONVIF:它用 SOAP 走 HTTP 运 XML。 从这一章换一套信令:SIP 走 UDP 运文本,它是 GB/T 28181 的底座。 两者要解决的问题一样(怎么让两台设备谈好一次会话),只是选择的技术路线不同。

一、它管信令,不管媒体

这是理解 SIP 的第一道分界线,也是最多人搞混的地方:

它管 它不管
呼叫的建立与拆除 音视频数据本身
对方在不在、忙不忙 编码格式怎么压
媒体往哪发(地址端口) 丢包了怎么办

真正运音视频的是 RTP(第一章讲过), SIP 只负责在通话开始前把「对端地址、端口、编码」这三件事谈好。 所以「SIP 连不上」和「有画面但花屏」是两个完全不同的问题——前者查信令,后者查 RTP。 分不清这一点,排障会一直走错方向。

二、一条报文看全貌

SIP 报文分五段,跟 HTTP 长得几乎一样:

段 内容 关键
起始行 方法 + URI + 版本 说明要干什么
头部 若干 名字: 值 谁发的、发给谁
空行 一个 CRLF 分隔头与体
正文 通常是 SDP 媒体怎么谈

第二行才是重点:头部里有八个是必需的(Demo 里数过),缺一个对端就不认。 八个必需头分别是:Via、Max-Forwards、From、To、Call-ID、CSeq、Contact、 Content-Length。 它和 HTTP 的关系值得说清:两者都是「文本 + 头 + 空行 + 体」的结构, HTTP 的方法是 GET/POST,SIP 的方法是 INVITE/BYE。 这不是巧合:SIP 的设计者明确参考了 HTTP 与 SMTP,为的就是「抓包能看懂」。

三、十三个方法,日常只用五个

方法 干什么 用得多不多
INVITE 发起会话 最常用
ACK 确认收到最终应答 最常用
BYE 结束会话 最常用
CANCEL 取消还没答的请求 常用
REGISTER 向服务器登记地址 最常用
OPTIONS 探活、问能力 常用
MESSAGE 传短消息 国标有用
SUBSCRIBE 订阅事件 少

前五行加第六行,就是全部日常。 剩下七个(NOTIFY、INFO、PRACK、REFER、PUBLISH、UPDATE、 MESSAGE 之外还有)在监控项目里几乎不出现。 ACK 那个位置要特别注意:它不是「请求」而是「确认」, 所以它没有应答——这一点在 09.01.04 会展开,也是初学者最容易写错的地方。

四、「三次握手」在 SIP 里长什么样

SIP 的会话建立不是三次握手,而是「请求—应答—确认」的扩展版:

步 谁发 内容
1 UAC INVITE(我请你通话)
2 UAS 100/180/200
3 UAC ACK(我收到了)

第三行那个 ACK 是 SIP 独有的设计: TCP 有三次握手保证可靠,而 SIP 主要跑 UDP——UDP 没有确认机制,所以要自己在应用层补一个。 补的方法就是 ACK:200 OK 要被反复重传,直到收到 ACK 为止(Timer G/H 管这事)。 这条设计带来一个后果:如果 ACK 丢了,对方会一直重传 200 OK, 而主叫方以为通话已经建立了——两边状态不一致,这就是「幽灵通话」。

五、SIP 到底解决了什么问题

回到「为什么要有个协议」这个问题:

没有 SIP 时 有 SIP 之后
每台设备一套私有信令 一套通用语言
换厂家就要改代码 换厂家只换个 URI
地址写死在配置里 注册 + 查找

第三行那个「注册 + 查找」是架构上的关键:设备启动时向服务器登记「我在这个 IP」, 主叫只要知道对方的「号码」,不用知道 IP。 这就是为什么国标平台能用一串 20 位编码找到任意一台摄像机—— 编码是名字,IP 是地址,SIP 负责把名字翻译成地址。 这个思路和 DNS 一样,只不过 DNS 查的是域名到 IP,SIP 查的是号码到 IP。

六、动手 Demo:一条 SIP 请求到底多少字节

Demo 把一条完整的 INVITE 报文打出来,再量几种方法的字节。 报文是按 RFC 3261 的格式拼的,里面的 20 位编码用的是 GB/T 28181 的形状。

msg = S.build_req("INVITE", cseq=1, body=S.sdp())
print(msg)
print("全长", len(msg.encode("utf-8")), "字节")

head, _sep, body = msg.partition("\r\n\r\n")
hl = head.split("\r\n")
print("起始行", len((hl[0] + "\r\n").encode()), "字节")
print("头部", len(("\r\n".join(hl[1:]) + "\r\n").encode()), "字节")
print("SDP  ", len(body.encode()), "字节")

实跑输出:

--------------------------------------------------------------------
一条 SIP 请求到底多少字节
--------------------------------------------------------------------
**先把一条真报文完整打出来**,**因为 SIP 的所有秘密都在文本里:**

  INVITE sip:34020000001320000001@3402000000 SIP/2.0
  Via: SIP/2.0/UDP 192.168.1.100:5060;rport;branch=z9hG4bKbedc329c8d
  Max-Forwards: 70
  From: <sip:34020000001320000002@3402000000>;tag=3d1f9c2a
  To: <sip:34020000001320000001@3402000000>
  Call-ID: [email protected]
  CSeq: 1 INVITE
  Contact: <sip:[email protected]:5060>
  Content-Type: application/sdp
  Content-Length: 222

  v=0
  o=34020000001320000002 0 0 IN IP4 192.168.1.100
  s=Play
  c=IN IP4 192.168.1.100
  t=0 0
  m=video 10000 RTP/AVP 96 98 97
  a=recvonly
  a=rtpmap:96 PS/90000
  a=rtpmap:98 H264/90000
  a=rtpmap:97 MPEG4/90000
  y=0100000001


**这一条 628 字节**(含 SDP 正文 222 字节)。

**再把几种常用方法各拼一条,量字节**:

  报文             字节     说明
  -------------------------------------
  OPTIONS          375      无正文
  REGISTER         377      无正文
  MESSAGE          375      无正文
  INVITE           373      无正文
  INVITE + SDP     628      带媒体描述

**四个「无正文」的字节数几乎一样**,**因为 SIP 的头部结构固定、长度只差方法名那几个字母。**
**这就是文本协议的特点**:**开销可预测、可肉眼读、但比二进制大一圈。**

**那条 INVITE 按空行切成四段,加起来应该正好是全长**:

  段           行数     字节
  ---------------------------
  起始行       1        52
  头部         9        352
  空 行        1        2
  SDP 正文     11       222

**四段相加 628 字节,实测全长 628 字节**——**对得上,说明没漏段也没重复算。**

**必需的头只有八个**:`Via`、`Max-Forwards`、`From`、`To`、`Call-ID`、`CSeq`、`Contact`、
`Content-Length`。
**少任何一个,设备都不认**;**剩下的都是可选**(`Subject`、`Allow`、`Supported` 这些)。
**GB/T 28181 里 `Subject` 是常用的**,
**因为要把媒体参数塞进 INVITE**——**这一条在第九章后面会再遇到。**

输出里前 27 行就是那条报文,它能直接粘进 Wireshark 的「Decode As SIP」里看。 四段相加 628,实测也是 628——这个自洽说明「分段没漏、也没重复算」, 是核对报文结构最笨也最可靠的办法。 注意起始行 52 字节、头部 352 字节: 一条 51 字符的起始行里,光那个 SIP URI 就占 35 字符—— 20 位编码是国标的代价,它让每个地址都变长了。 四个「无正文」的方法字节数只差 0~4 字节:375/377/375/373, 差的就是方法名长度(REGISTER 比 INVITE 长两个字母,而它在起始行和 CSeq 里各出现一次, 正好多 4 字节)。 这个「多 4 字节」能对上,说明报文拼接没有多余空格—— 手写报文时最容易犯的错就是多一个空格,而设备对起始行的空格数量是严格的。

30 秒实验

  1. 把 Max-Forwards 删掉再发,看 UAS 回什么码。
  2. 给 Contact 加一个不存在的端口,看设备还能不能回。
  3. 把起始行里的 SIP/2.0 改成 SIP/3.0,看对端怎么处理。

常见误区

  1. 以为 SIP 负责传音视频。 它只管信令,媒体走 RTP。 所以「SIP 通了但没画面」是正常现象,要查 RTP 那一层。
  2. 以为 ACK 也是一个请求、也该有应答。 ACK 是确认不是请求,服务器收到后什么都不回, 等它的应答会一直等到超时。
  3. 以为 SIP 头可以随便少几个。 八个必需头缺一个就不认, 而且报错往往只是「400 Bad Request」,不说是哪个头缺了。
自测三问(点开对答案) 1. SIP 和 RTP 的分工怎么一句话说清? —— **SIP 谈「在哪发」,RTP 运「发什么」。** 媒体地址、端口、编码由 SDP 谈好, 之后的数据全走 RTP,SIP 不再参与。 2. 为什么 SIP 要有 ACK 这种「没有应答的请求」? —— **因为 SIP 主要跑在 UDP 上,没有传输层确认**。 ACK 就是应用层补的确认,让 200 OK 的重传知道什么时候可以停。 3. 「注册 + 查找」解决了什么工程问题? —— **把「号码」和「IP」解耦**。主叫只要知道 20 位编码,不用知道设备当前 IP; 设备换 IP 只需重新注册。

下一节拆开看一条 INVITE 为什么会引出三条应答——那是分层的功劳。

相关笔记