第 1 章:SIP 协议——电话世界的 HTTP
本章解决的痛点:抓包能看到 INVITE 消息,但为什么没声音?通话 30 秒后自动断开是为什么?理解 SIP 信令与 RTP 媒体的分离,是排查一切通话故障的起点。
1.1 为什么需要 SIP?从 PSTN 到 VoIP 的演进
1.1.1 电路交换的困局
在传统 PSTN(公共交换电话网络)时代,每一次通话都需要建立一条专属的物理电路。你拿起电话拨号,运营商的交换机就在两地之间拉起一根"虚拟专线",通话期间这条线路被独占,即使你们双方都在沉默,线路也无法被他人使用。
这种模式的问题很明显:
- 资源浪费:语音数据有天然的静默期(一人说话时另一人在听),但电路一直被占用
- 扩容成本高昂:每增加一路通话就需要物理线路,跨洋电话费用居高不下
- 灵活性差:只能传语音,无法承载视频、即时消息等多媒体业务
1.1.2 分组交换的崛起与 H.323 的失败
20 世纪 90 年代,互联网兴起,人们开始思考:能不能像传文件一样传语音?将语音数据切分成小包,通过网络"尽力而为"地传输,这就是 VoIP(Voice over IP)的核心思想。
最早的 VoIP 协议是 H.323,由 ITU(国际电信联盟)制定。它过于复杂——完整的协议栈包含数十个配套协议,实现一个 H.323 终端需要处理复杂的 ASN.1 编解码和状态机。这导致 H.323 设备价格昂贵、互操作性差,最终在市场竞争中被淘汰。
1.1.3 SIP 的胜出:简单即正义
1996 年,IETF(互联网工程任务组)推出了 SIP(Session Initiation Protocol) ,RFC 3264 定义了它的核心规范。SIP 的设计哲学与 HTTP 一脉相承:
| 特性 | HTTP | SIP |
|---|---|---|
| 文本格式 | 是,人类可读 | 是,人类可读 |
| 请求-响应模型 | GET/POST → 200 OK | INVITE → 200 OK |
| 无状态/有状态 | 无状态 | 有状态(事务层) |
| 可扩展头域 | 是 | 是 |
SIP 只负责"建立、修改、终止会话",不关心媒体如何传输。这种极简的分层设计让它迅速成为 VoIP 的事实标准。
实战对比:
# HTTP 请求
GET /index.html HTTP/1.1
Host: example.com
# SIP 请求
INVITE sip:bob@example.com SIP/2.0
Via: SIP/2.0/UDP pc33.example.com;branch=z9hG4bK776asdhds
Max-Forwards: 70
To: Bob <sip:bob@example.com>
From: Alice <sip:alice@example.com>;tag=1928301774
Call-ID: a84b4c76e66710@pc33.example.com
CSeq: 314159 INVITE
Contact: <sip:alice@pc33.example.com>
Content-Type: application/sdp
Content-Length: 142
# SDP 内容(描述媒体参数)
v=0
o=alice 2890844526 2890844526 IN IP4 pc33.example.com
s=-
c=IN IP4 pc33.example.com
t=0 0
m=audio 49172 RTP/AVP 0
a=rtpmap:0 PCMU/8000
1.1.4 信令与媒体的分离——理解 VoIP 的第一性原理
这是本章最重要的概念,也是 90% 新手困惑的根源:
SIP 是信令协议(Signaling),负责"打电话这个动作";RTP 是媒体协议(Media),负责"传输语音内容"。两者独立运行,可能走不同的网络路径。
类比现实:
- 信令 = 你拨号、对方振铃、对方接听、通话结束挂机的"动作序列"
- 媒体 = 你们实际说话的声音
在 PSTN 时代,信令和媒体走在同一条电路上。但在 VoIP 中:
- SIP 消息通常走 UDP 5060 端口(或 TCP/TLS)
- RTP 媒体流走 UDP 10000-20000 等高端口范围
这就是为什么你能抓到 INVITE 但听不到声音:
- SIP 通道通了(防火墙放行了 5060)
- 但 RTP 通道被防火墙/NAT 阻断(高端口未放行)
- 或者 SDP 中的 IP 地址是内网地址,对端无法直接发送 RTP 到正确的公网地址
这是贯穿全书的核心认知:排查故障时,先查信令(SIP),再查媒体(RTP)。
1.2 SIP 协议栈全景:UAC、UAS、Proxy、B2BUA
1.2.1 角色定义
SIP 网络中有四种核心角色:
| 角色 | 全称 | 职责 | 类比 |
|---|---|---|---|
| UAC | User Agent Client | 发起 SIP 请求 | 浏览器发送 HTTP 请求 |
| UAS | User Agent Server | 接收 SIP 请求并响应 | Web 服务器返回页面 |
| Proxy | Proxy Server | 转发 SIP 消息,路由决策 | HTTP 代理/负载均衡 |
| B2BUA | Back-to-Back User Agent | 作为中间人分别与两边建立对话 | NAT 网关、呼叫中心 |
1.2.2 FreeSWITCH 为什么是 B2BUA?
这是理解 FreeSWITCH 架构的关键。
传统 SIP Proxy 只是"传话筒":
Alice ──INVITE──→ Proxy ──INVITE──→ Bob
↑ ↓
└────────────200 OK──────────────────┘
Proxy 不修改消息体,只根据路由规则转发。通话建立后,RTP 直接在 Alice 和 Bob 之间传输(如果网络允许)。
FreeSWITCH 作为 B2BUA:
Alice ──INVITE──→ FreeSWITCH ──INVITE──→ Bob
│ │ │
│ │ │
└────RTP 流─────────┘ │
└────────RTP 流─────┘
FreeSWITCH 与 Alice 建立一段对话(Leg A),与 Bob 建立另一段对话(Leg B)。两边看到的 SIP 消息都是 FS 发出的,FS 可以完全控制两边的参数。
B2BUA 的优势:
- 媒体控制:可以录音、转码、混音(会议)
- 拓扑隐藏:内网拓扑不暴露给外网
- 协议转换:SIP ↔ WebRTC、UDP ↔ TCP/TLS
- 增值业务:IVR、队列、呼叫转移
代价:所有媒体都经过 FS,对服务器性能要求高。
1.3 请求消息深度解析
SIP 定义了 14 种请求方法,最常用的是以下 6 种:
1.3.1 INVITE——发起会话的"敲门"
INVITE 是最复杂的 SIP 消息,它触发一次完整的呼叫建立流程。
消息结构解剖:
INVITE sip:bob@example.com SIP/2.0 ← Start-Line(方法 + URI + 版本)
Via: SIP/2.0/UDP pc33.example.com:5060;branch=z9hG4bK776asdhds ← 路径记录
Max-Forwards: 70 ← 防环路,每经过一跳减 1
To: Bob <sip:bob@example.com> ← 逻辑接收者(显示名 + URI)
From: Alice <sip:alice@example.com>;tag=1928301774 ← 逻辑发送者 + 标签
Call-ID: a84b4c76e66710@pc33.example.com ← 唯一标识一次通话
CSeq: 314159 INVITE ← 序列号,用于消息排序和重传检测
Contact: <sip:alice@pc33.example.com> ← 直接联系地址(绕过 Proxy)
User-Agent: FreeSWITCH-mod_sofia/1.10.10 ← 软件标识
Allow: INVITE, ACK, BYE, CANCEL, OPTIONS, MESSAGE ← 支持的方法
Supported: timer, path, replaces ← 支持的扩展
Content-Type: application/sdp ← 消息体类型
Content-Length: 142 ← 消息体字节数
[空行]
v=0 ← SDP 协议版本
o=alice 2890844526 2890844526 IN IP4 pc33.example.com ← 会话所有者
s=- ← 会话名(可选)
c=IN IP4 pc33.example.com ← 连接信息(IP 地址)
t=0 0 ← 时间(0 0 表示永久)
m=audio 49172 RTP/AVP 0 ← 媒体描述:音频,端口 49172,RTP,编码 0
a=rtpmap:0 PCMU/8000 ← 属性:编码 0 是 PCMU,采样率 8kHz
关键头域详解:
Via:记录消息经过的路径,用于响应路由。每经过一个 Proxy,会在顶部添加一个新的 Via。branch 参数是事务唯一标识,必须以 z9hG4bK 开头(表示符合 RFC 3261)。
From/To:逻辑上的发送者和接收者。注意 To 中没有 tag 参数——这是被叫方在响应中添加的。From tag + To tag + Call-ID 三者共同唯一标识一个 SIP 对话(Dialog)。
Call-ID:全局唯一标识一次通话。通常格式为 随机数@主机名。
CSeq:命令序列号,同一个 Call-ID 内递增。用于检测丢包、乱序和重传。INVITE 的 CSeq 必须是递增的偶数(或任意数,但重传 INVITE 不改变 CSeq)。
Contact:告诉对方"直接联系我这个地址",后续请求(如 BYE)可以直接发到 Contact URI,而不必经过 Proxy。
1.3.2 ACK——确认收到最终响应
INVITE 是唯一需要三次握手的 SIP 方法:
Alice ──INVITE────────→ Bob
Alice ←──100 Trying───── Bob (临时响应,表示收到请求)
Alice ←──180 Ringing──── Bob (临时响应,表示正在振铃)
Alice ←──200 OK───────── Bob (最终成功响应)
Alice ──ACK────────────→ Bob (确认收到 200 OK)
为什么需要 ACK?因为 SIP 通常跑在不可靠的 UDP 上,200 OK 可能丢失。如果没有 ACK,Bob 不知道 Alice 是否收到了成功响应,会不断重发 200 OK。
重要区别:
- 针对 1xx 临时响应(100/180/183),不需要 ACK
- 针对 2xx 最终响应(200),必须发送 ACK
- 针对 3xx/4xx/5xx/6xx 错误响应,也需要 ACK
1.3.3 BYE——优雅地挂断
通话任何一方想结束,发送 BYE:
BYE sip:bob@192.168.1.2:5060 SIP/2.0
Via: SIP/2.0/UDP pc33.example.com:5060;branch=z9hG4bK77ef4c2312983.1
To: Bob <sip:bob@example.com>;tag=a6c85cf
From: Alice <sip:alice@example.com>;tag=1928301774
Call-ID: a84b4c76e66710@pc33.example.com
CSeq: 231 BYE
Content-Length: 0
对方回复 200 OK 确认挂断。
与 CANCEL 的区别:
- CANCEL:用于取消尚未建立的通话(在收到 200 OK 之前)
- BYE:用于终止已经建立的通话(在收到 200 OK 之后)
1.3.4 REGISTER——告诉服务器"我在哪"
SIP 使用 URI(如 sip:alice@example.com)标识用户,但 URI 不直接对应 IP 地址。REGISTER 用于将 AOR(Address of Record,记录地址)绑定到当前 Contact 地址:
REGISTER sip:example.com SIP/2.0
Via: SIP/2.0/UDP 10.0.0.1:5060;branch=z9hG4bKna9a8s
To: Alice <sip:alice@example.com>
From: Alice <sip:alice@example.com>;tag=4fa2972e
Call-ID: nGFT9Mbxdf@10.0.0.1
CSeq: 1 REGISTER
Contact: <sip:alice@10.0.0.1:5060>;expires=3600
Content-Length: 0
关键参数:
expires=3600:注册有效期 3600 秒(1 小时),之后需要重新注册- 服务器会返回
200 OK,包含当前绑定的 Contact 列表
在 FreeSWITCH 中,注册信息存储在 sofia_reg_database 或外部数据库中,可通过 sofia status profile internal reg 查看。
1.4 响应消息与事务状态机
1.4.1 状态码分类
SIP 响应状态码与 HTTP 类似,分为 6 类:
| 类别 | 范围 | 含义 | 示例 |
|---|---|---|---|
| 1xx | 100-199 | 临时响应(Informational) | 100 Trying, 180 Ringing |
| 2xx | 200-299 | 成功(Success) | 200 OK |
| 3xx | 300-399 | 重定向(Redirection) | 302 Moved Temporarily |
| 4xx | 400-499 | 客户端错误(Client Error) | 404 Not Found, 486 Busy Here |
| 5xx | 500-599 | 服务器错误(Server Error) | 500 Server Internal Error |
| 6xx | 600-699 | 全局错误(Global Failure) | 603 Decline, 604 Does Not Exist |
1.4.2 常见状态码实战解读
100 Trying:Proxy/UAS 已收到请求,正在处理。这是可选的,用于防止客户端重传。
180 Ringing:被叫正在振铃。通常触发主叫端的回铃音。
183 Session Progress:带媒体的早期会话。可以在接通前播放彩铃或语音提示。
200 OK:请求成功。对于 INVITE,表示对方已接听,消息体包含最终的 SDP 答案。
302 Moved Temporarily:重定向。常用于呼叫转移:
SIP/2.0 302 Moved Temporarily
Via: SIP/2.0/UDP proxy.example.com;branch=z9hG4bK742b49
To: Bob <sip:bob@example.com>;tag=2c236de9
From: Alice <sip:alice@example.com>;tag=9fxced76sl
Call-ID: 3848276298220188511@alice.example.com
CSeq: 1 INVITE
Contact: <sip:bob@mobile.bob.com:5060>
Content-Length: 0
Alice 收到 302 后,应向新的 Contact 地址(bob@mobile.bob.com)重新发起 INVITE。
401 Unauthorized / 407 Proxy Authentication Required:需要认证。常见于注册或呼叫时未提供正确密码。
404 Not Found:URI 对应用户不存在。检查用户名拼写或 Domain 配置。
486 Busy Here:被叫忙。可能是对方正在通话,或策略拒绝。
488 Not Acceptable Here:SDP 协商失败。双方没有共同的编解码器,或媒体参数不兼容。
500 Server Internal Error:服务器内部错误。检查 FreeSWITCH 日志 freeswitch.log。
503 Service Unavailable:服务不可用。可能是后端网关故障,或达到并发限制。
1.4.3 事务(Transaction)与会话(Session)的区别
这是 SIP 协议的核心概念,也是理解超时机制的钥匙:
事务(Transaction) :一个请求及其所有响应的集合,由 Via branch + CSeq + Call-ID 唯一标识。
事务有明确的状态机:
Calling ──→ Proceeding ──→ Completed ──→ Terminated
(INVITE已发) (收到1xx) (收到2xx-6xx) (计时器超时)
会话(Session) :两个 UA 之间的长期关联,由 From tag + To tag + Call-ID 唯一标识。会话从 INVITE 的 200 OK 开始,到 BYE 结束。
关键区别:
- 事务超时(如 32 秒无响应)会触发重传或失败
- 会话超时(如 RTP 超时)会触发自动挂断
这就是为什么会有"30 秒掉线"问题:
- SIP 会话实际还在(信令通道正常)
- 但 RTP 媒体流中断(NAT 超时、防火墙阻断)
- FreeSWITCH 的
rtp-timeout参数(默认 60 秒)检测到无媒体后,发送 BYE 挂断
1.5 对话(Dialog)与会话的生命周期
1.5.1 Dialog 的建立
SIP Dialog 在收到 200 OK(针对初始 INVITE)时正式建立。此时:
- 双方都知道对方的 Contact 地址
- 双方都分配了 RTP 端口
- 可以双向发送 BYE 终止通话
Dialog 建立后,后续请求(如 BYE、Re-INVITE)可以直接发送到 Contact 地址,路径更短:
# 初始 INVITE 经过多个 Proxy
Alice → Proxy1 → Proxy2 → Bob
# Dialog 建立后的 BYE 直接发送
Alice → Bob
1.5.2 Re-INVITE 与会话修改
通话建立后,任何一方可以发送新的 INVITE(称为 Re-INVITE)来修改会话参数:
- 保持(Hold):
a=sendonly或a=inactive - 恢复:
a=sendrecv - 转码协商:添加/删除编解码器
- 更新 RTP IP/端口(NAT 场景)
1.5.3 僵尸通话(Orphan Call)的产生与清理
什么是僵尸通话:
- 网络异常导致 BYE 消息丢失
- 一方认为通话已结束,另一方仍在等待
- 持续占用系统资源(端口、内存、计费)
检测机制:
- SIP 层面:FreeSWITCH 的
session-timeout(默认 1800 秒),超时无消息发送 OPTIONS 检测 - RTP 层面:
rtp-timeout(默认 60 秒),超时无媒体流则挂断 - 应用层面:自定义心跳(如 WebSocket keepalive)
实战配置(conf/sip_profiles/internal.xml):
<!-- RTP 超时:60秒无媒体则挂断 -->
<param name="rtp-timeout" value="60"/>
<param name="rtp-hold-timeout" value="1800"/>
<!-- 会话超时:30分钟发送 re-INVITE 刷新 -->
<param name="session-timeout" value="1800"/>
<param name="minimum-session-expires" value="90"/>
实战:用 sngrep 抓包分析一次完整通话
环境准备
# 安装 sngrep(Debian/Ubuntu)
apt-get install sngrep
# 启动实时捕获(需要 root)
sudo sngrep -d eth0
# 或分析 pcap 文件
sngrep -r capture.pcap
抓包场景:Alice (101) 呼叫 Bob (102)
步骤 1:注册(REGISTER)
101 ──REGISTER──→ FreeSWITCH
101 ←──200 OK───── FreeSWITCH (expires: 3600)
观察点:Contact 头中的 IP 是内网还是公网?如果是内网,后续需要考虑 NAT 穿越。
步骤 2:发起呼叫(INVITE → 200 OK → ACK)
Alice ──INVITE──→ FS ──INVITE──→ Bob
Alice ←─100 Try── FS ←─100 Try── Bob
Alice ←─180 Ring─ FS ←─180 Ring─ Bob
Alice ←─200 OK─── FS ←─200 OK─── Bob
Alice ──ACK────── FS ──ACK──────→ Bob
观察点:
- SDP 中的
c=行(连接地址)和m=行(媒体端口) - 双方协商的编解码器(RTPmap)
步骤 3:媒体传输(RTP) 此时打开 Wireshark,过滤 rtp,可以看到双向的 RTP 流:
- 源端口/目标端口
- 序列号(Sequence Number)连续性
- 时间戳(Timestamp)递增
步骤 4:挂断(BYE → 200 OK)
Alice ──BYE────── FS ──BYE──────→ Bob
Alice ←─200 OK─── FS ←─200 OK─── Bob
故障排查速查表
| 现象 | 排查点 | 工具 |
|---|---|---|
| INVITE 无响应 | 网络连通性、防火墙 5060 端口 | ping, telnet |
| 401/407 错误 | 认证信息(username/password) | fs_cli, sngrep |
| 488 Not Acceptable | 编解码器不匹配 | sngrep 查看 SDP |
| 单通(一方无声音) | RTP 方向、NAT、防火墙 | sngrep, tcpdump |
| 30-60 秒掉线 | RTP 超时、NAT 保活 | fs_cli show calls |
| 180 Ringing 但无回铃音 | 183 Early Media、SDP 问题 | sngrep |
本章小结
- SIP 是信令,RTP 是媒体,两者分离是 VoIP 的核心特征。看到 INVITE 只是第一步,确保 RTP 连通才有声音。
- FreeSWITCH 是 B2BUA,它与每一方分别建立对话。这带来了灵活性(录音、转码),也要求更高的服务器性能。
- Via/From/To/Call-ID/CSeq 是消息路由的五要素,理解它们的含义是阅读 SIP 消息的基础。
- 事务(Transaction)与会话(Session)有独立的状态机,理解超时机制能解释"为什么 30 秒掉线"。
- sngrep 是 SIP 排查的首选工具,学会用它抓包分析,胜过背诵所有配置参数。
练习题
- 为什么 INVITE 需要 ACK 确认,而 REGISTER 不需要?
- 抓包时发现 Call-ID 相同但 CSeq 不同,这是什么场景?
- 被叫收到 INVITE 后立即回复 486 Busy,后续的 ACK 由谁发送?
- 在 NAT 环境下,SDP 中的
c=IN IP4 192.168.1.100会导致什么问题?如何解决?
延伸阅读
- RFC 3261: SIP: Session Initiation Protocol(核心规范)
- RFC 3264: An Offer/Answer Model with the Session Description Protocol (SDP)
- FreeSWITCH 官方文档: developer.signalwire.com/freeswitch/
下一章预告:SDP——媒体协商的艺术。我们将深入解析 offer/answer 模型,理解编解码器协商、hold/resume 的 SDP 实现,以及带宽管理的策略。