做通信协议调试这行当,SIP是你绕不过去的一道坎。不管是搞VOIP网关、接运营商线路,还是自己搭PBX给公司换掉模拟电话,信令这东西不弄明白,出了问题就只能干瞪眼。SIP协议全称Session Initiation Protocol,说人话就是通信世界的“电话接线员”,它负责把一通呼叫从A端完好无损地带到B端,至于两边接通之后说什么、怎么传声音,那是RTP和SDP的活。很多人一听到“信令”两个字就头大,觉得那是高大上的二进制玩意儿。其实恰恰相反,SIP是明文文本协议,格式上跟HTTP非常像,用抓包工具看得一清二楚,门槛远没有想象中高。
这篇文章我会从SIP消息结构讲起,把注册、呼叫建立、挂断、会话修改这些核心信令流程逐个拆开,配上可以直接对着看的报文示例,中间穿插我自己调试设备时踩过的坑和总结的排查思路。适合刚开始接触VoIP的运维、做网关集成的开发,以及被客户电话打得没脾气的技术支持同学参考。题外话说一句,网上搜SIP关键词,跳出来一堆“Mac关闭SIP”的教程,那个SIP是macOS的系统完整性保护机制,跟咱们通信圈这个会话发起协议八竿子打不着,别搜混了。下面咱们进入正题。
1. 先搞清楚SIP是什么,以及它凭什么能成为主流
1.1 SIP的定位:只负责“牵线”,不负责“说话”
很多人第一次接触SIP最大的困惑是:SIP通了,是不是就能打电话了?答案是否定的。SIP协议只管会话的建立、修改和拆除,它本身不承载任何语音数据。真正把声音从一端送到另一端的是RTP协议,而双方用什么样的编码格式、采样率、端口,是靠SIP消息里携带的SDP会话描述协议来协商的。
打个比方,SIP是打电话时的“总机接线员”,帮你把两个分机连起来;SDP就是双方在通话前对好的“通话规格”——说中文还是英文、语速多少;而RTP才是那条真正传声音的“线路”。所以调试的时候你会看到SIP信令握手成功,但通话就是没声音,这时候问题大概率出在RTP媒体流或者SDP协商结果上,跟信令本身没关系了。
一套完整的VOIP通话流程,通常涉及三个协议角色:SIP负责信令控制,SDP负责媒体参数协商,RTP负责媒体数据传送。三者的关系捋顺了,后面排查问题就有了基本框架。
1.2 为什么H.323被SIP打得找不着北
在SIP出现之前,视频会议和VOIP领域的主流协议是H.323,那是ITU-T制定的一个庞大协议族。H.323的问题在于太复杂,它用ASN.1二进制编码,消息结构对人不友好,调试时必须上专业协议分析仪才能看懂,开发技术门槛也高。而SIP是IETF这帮互联网工程师设计的,思路就是“像HTTP一样简单”。你直接用tcpdump、Wireshark就能抓到明文信令,看到哪个字段不对立刻就能定位问题。
我最早调试H.323设备的时候,包里全是二进制编码的PER结构,看一条消息的报文字段得靠解码工具,效率极低。后来切到SIP,那种畅快感就像从手工记账切换到了Excel。加上SIP的扩展性特别好,新需求来了通过扩展方法就能实现,比如即时消息用MESSAGE、文件传输用MSRP、点击回呼用REFER,整个生态越滚越大。现在市面上主流的通信平台,从FreeSWITCH、Asterisk到Kamailio、OpenSIPS,再到商业IPPBX,清一色是SIP的天下,链路对接也基本都基于SIP。
1.3 SIP网络里那些角色的关系
理解SIP网络架构,先记三个角色:UAC(用户代理客户端,发起呼叫的一方)、UAS(用户代理服务器,接收呼叫的一方)、SIP服务器(负责注册、路由、重定向)。A手机拨给B,A就是UAC,B是UAS,中间经过的服务器可以充当注册服务器、代理服务器或者重定向服务器。
实际部署中,一台IPPBX通常同时承担三种角色:它作为注册服务器给分机发账号,作为代理服务器帮分机转发呼叫,配置里设了呼叫转移的话还可能充当重定向服务器。分机们不需要互相知道IP地址,只需要知道服务器的地址就行。这个模型跟办公室找前台转接电话是一个道理:你不需要知道对方工位在哪,拨个内线让前台帮你转就行了。
2. SIP消息结构拆解:像读HTTP报文一样读SIP
2.1 请求与响应:起始行规定了每一次交互的基调
SIP消息由三部分组成:起始行、头字段、消息体。请求消息的起始行叫请求行,格式是“方法 SIP-URI SIP版本”;响应消息的起始行叫状态行,格式是“SIP版本 状态码 原因短语”。消息头和消息体之间用一个空行分隔。
我随便贴一个最简单的INVITE请求,你大概率在抓包里经常看到:
INVITE sip:1002@192.168.1.10 SIP/2.0 Via: SIP/2.0/UDP 192.168.1.20:5060;branch=z9hG4bK7a83d;rport Max-Forwards: 70 From: "1001" <sip:1001@192.168.1.10>;tag=ae8d2f To: <sip:1002@192.168.1.10> Call-ID: c3d2f0a8e7b1@192.168.1.20 CSeq: 1 INVITE Contact: <sip:1001@192.168.1.20:5060> Content-Type: application/sdp Content-Length: 159 v=0 o=1001 2890844526 2890844526 IN IP4 192.168.1.20 s=SIP Call c=IN IP4 192.168.1.20 t=0 0 m=audio 4000 RTP/AVP 8 0 101 a=rtpmap:0 PCMU/8000 a=rtpmap:8 PCMA/8000 a=rtpmap:101 telephone-event/8000响应的状态行长这样:
SIP/2.0 200 OK Via: SIP/2.0/UDP 192.168.1.20:5060;branch=z9hG4bK7a83d;received=192.168.1.20;rport=5060 From: "1001" <sip:1001@192.168.1.10>;tag=ae8d2f To: <sip:1002@192.168.1.10>;tag=9f8e7d6 Call-ID: c3d2f0a8e7b1@192.168.1.20 CSeq: 1 INVITE Contact: <sip:1002@192.168.1.30:5080> Content-Type: application/sdp Content-Length: 143 v=0 o=1002 2890844530 2890844530 IN IP4 192.168.1.30 s=SIP Call c=IN IP4 192.168.1.30 t=0 0 m=audio 6000 RTP/AVP 0 101 a=rtpmap:0 PCMU/8000 a=rtpmap:101 telephone-event/8000你看,无非就是把“我是谁、我从哪来、我要干什么、我的条件是什么”四件事说清楚,比HTTP多了一部分会话描述。调试的时候习惯这一类格式,看几分钟就能上手。
2.2 必须背下来的状态码
SIP响应状态码分六类,跟HTTP的状态码分法几乎一样:
| 状态码范围 | 含义 | 常见例子 |
|---|---|---|
| 1xx | 临时应答,请求正在处理中 | 100 Trying、180 Ringing、183 Session Progress |
| 2xx | 成功,请求已被接收处理 | 200 OK |
| 3xx | 重定向,需要到新的地址重试 | 302 Moved Temporarily |
| 4xx | 客户端错误,请求本身有问题 | 401 Unauthorized、403 Forbidden、404 Not Found、486 Busy Here、488 Not Acceptable Here |
| 5xx | 服务器错误 | 500 Server Internal Error、503 Service Unavailable |
| 6xx | 全局失败,所有服务器都处理不了 | 603 Decline |
状态码是排查问题最快的入口。用户说“拨号没反应”,你先看抓包里返回的是486(对方忙)、404(号码不存在)还是408(超时没响应),方向基本就定了。我最常碰见的是488,这个后面讲SDP协商时会展开,因为它十有八九是双方编解码谈不拢导致的。
2.3 SIP URI:地址长什么样
SIP地址的格式是sip:user@domain:port,其中domain可以是域名也可以是IP地址,端口默认可省略。比如sip:1001@192.168.1.10:5060,代表用户1001在192.168.1.10这个SIP服务器上,通过5060端口通信。URI里还可以带参数,如sip:1001@192.168.1.10;transport=tcp,表示用TCP传输。
还有一种tel: URI,用来描述普通电话号码,比如tel:+8613800138000,在网关和运营商对接的场景很常见。两者可以互相转换,网关经常收到SIP INVITE后把Request-URI改成tel格式再转发到E1中继。记住一个原则:SIP URI描述的是一个“网络会话地址”,tel URI描述的是一个“普通电话号码”,别搞混。
3. 核心信令方法:每种方法解决什么事
3.1 六大基本方法:REGISTER、INVITE、ACK、BYE、CANCEL、OPTIONS
SIP协议标准定义了六种基本方法,绝大多数基础信令流程只要用这几个就够了:
| 方法名 | 作用 | 典型场景 |
|---|---|---|
| REGISTER | 分机向服务器登记自己当前的地址 | 软电话开机后自动注册 |
| INVITE | 发起一个会话请求,携带SDP媒体参数 | 拨打电话、呼叫视频 |
| ACK | 确认最终响应,特别用于确认INVITE的2xx响应 | 对方应答后,主叫发ACK |
| BYE | 主动结束一个已建立的会话 | 挂断电话 |
| CANCEL | 取消一个尚未被应答的请求(还没振铃或振铃中) | 拨出后对方还没接就挂断 |
| OPTIONS | 查询服务器或对端的能力 | 服务商用它做节点保活和状态探测 |
特别要强调ACK的重要性。INVITE和普通请求不一样,它属于“需要特别确认”的请求。如果被叫返回200 OK,主叫必须再发一个ACK确认收到,双方会话才算正式建立。如果主叫不回ACK,服务器会一直重传200 OK,直到放弃。这个机制挨着“可靠消息传递”的设计原则,现实中很多回铃音异常、呼叫刚接通就掉线的问题,追根溯源都是ACK这块处理不对。
OPTIONS这个方法经常被忽略,但它特别实用。运营商会定期向你的网关发OPTIONS探测,判断网关是否在线,很多SIP服务商的“心跳保活”就是靠它实现的。你如果看到抓包里有大量周期性的OPTIONS,别惊讶,那是正常的保活流量。
3.2 常用扩展方法:别被海量RFC吓到
实际业务中,六大基本方法往往不够用,于是出现了很多扩展方法。它们不是标准非要你支持,而是解决特定场景的:
| 扩展方法 | 作用 | 常见场景 |
|---|---|---|
| INFO | 会话中的带外信息传输 | 传递DTMF按键、计费信息 |
| UPDATE | 在不影响现有会话的前提下更新媒体参数 | 早期媒体转正式媒体的参数更新 |
| PRACK | 对1xx临时响应的可靠确认 | 配合100rel扩展使用 |
| SUBSCRIBE/NOTIFY | 订阅与通知事件 | 呈现状态、语音信箱状态 |
| MESSAGE | 发送即时消息 | SIP聊天、短信透传 |
| REFER | 请对方转接或转移会话 | 呼叫转移、代接 |
| PUBLISH | 发布事件状态 | 在线状态发布 |
我最常用的是INFO和REFER。跟运营商对接计费或接收DTMF按键时,如果对方不支持RFC 2833/4733的带内DTMF,就得靠INFO通道传;呼叫转移功能则几乎离不开REFER。平时不用背全部RF C,遇到一个查一个,用多了自然熟。
3.3 SIP事务与对话:理解状态的关键
SIP里有“事务”和“对话”两个概念,搞懂了它们,很多信令行为就变得顺理成章。
一个事务,指的是从客户端发送的一个请求开始,到收到最终响应为止的整个过程。比如“INVITE事务”包括INVITE请求、各个临时应答、最终响应,以及(对INVITE来说)还要算上ACK。CANCEL、ACK其实不属于被CANCEL的那个事务,它们在逻辑上限定了各自独立的生命周期。
一个对话,则是一对UAC和UAS之间持续一段时间的关系,靠三个字段标识:From标签、To标签、Call-ID。一通电话从INVITE开始建立,到BYE结束,这个期间所有请求都属于同一个对话。服务器和分机都靠这三个标识识别“这包消息属于哪一通电话”。
为什么老工程师总说“先看Call-ID,再看tag”?因为抓包里消息多而杂,同一次呼叫的所有消息Call-ID是相同的,先从Call-ID筛出属于这一通电话的报文,再通过tag变化看对方身份。如果你看到从A到B的INVITE的From tag在后续消息里被换了,那基本可以判定是某台设备在耍流氓,没按规矩保留tag。
4. 从注册到挂断:完整信令流程拆给你看
4.1 注册流程:REGISTER与401摘要认证
分机首次注册通常要经历一次“认证挑战”。客户端发REGISTER不带认证信息,服务器回401,并给出realm(认证域)、nonce(一次性的随机字符串)等参数,客户端用密码算出摘要响应,然后带Authorization再发一次REGISTER,服务器验证通过后回200 OK。
我用最常见的摘要认证举例,完整过程如下。第一步,软电话上线发REGISTER:
REGISTER sip:192.168.1.10 SIP/2.0 Via: SIP/2.0/UDP 192.168.1.20:5060;branch=z9hG4bK1245;rport Max-Forwards: 70 From: <sip:1001@192.168.1.10>;tag=1a2b3c To: <sip:1001@192.168.1.10> Call-ID: register001@192.168.1.20 CSeq: 1 REGISTER Contact: <sip:1001@192.168.1.20:5060> Expires: 3600 Content-Length: 0服务器回401:
SIP/2.0 401 Unauthorized Via: SIP/2.0/UDP 192.168.1.20:5060;branch=z9hG4bK1245;received=192.168.1.20;rport=5060 From: <sip:1001@192.168.1.10>;tag=1a2b3c To: <sip:1001@192.168.1.10>;tag=server_tag_01 Call-ID: register001@192.168.1.20 CSeq: 1 REGISTER WWW-Authenticate: Digest realm="asterisk", nonce="5e4f8a9b", algorithm=MD5 Content-Length: 0客户端计算摘要后,带着Authorization重发REGISTER:
REGISTER sip:192.168.1.10 SIP/2.0 Via: SIP/2.0/UDP 192.168.1.20:5060;branch=z9hG4bK1246;rport Max-Forwards: 70 From: <sip:1001@192.168.1.10>;tag=1a2b3c To: <sip:1001@192.168.1.10>;tag=server_tag_01 Call-ID: register001@192.168.1.20 CSeq: 2 REGISTER Contact: <sip:1001@192.168.1.20:5060> Authorization: Digest username="1001", realm="asterisk", nonce="5e4f8a9b", uri="sip:192.168.1.10", response="6f8d3c1b2a9e4f0c" Expires: 3600 Content-Length: 0服务器验证通过,回200 OK。摘要认证的计算逻辑不复杂,简单说就是用MD5把用户名、realm、密码组合一次,再把方法、URI组合一次,最后用随机数再加权一次。实际调试时不用自己算,Wireshark里有解密工具,关键是理解这个“先挑战、再应答”的流程。注册失败十有八九是密码不对、时间不同步(nonce过期)、或者realm不匹配。
4.2 主叫到被叫:一次标准呼叫的完整信令流程
现在假设公司有台FreeSWITCH服务器192.168.1.10,分机1001和1002都注册在它上面。1001拿软电话拨1002,整个信令流程可以分成三个阶段:呼叫建立、通话中、呼叫释放。我把关键报文按真实流程贴出来。
先看呼叫建立阶段:
// 消息1: 1001 -> FreeSWITCH INVITE sip:1002@192.168.1.10 SIP/2.0 Via: SIP/2.0/UDP 192.168.1.20:5060;branch=z9hG4bK7d3c1 Max-Forwards: 70 From: <sip:1001@192.168.1.10>;tag=caller_tag_a1 To: <sip:1002@192.168.1.10> Call-ID: call001@192.168.1.20 CSeq: 1 INVITE Contact: <sip:1001@192.168.1.20:5060> Content-Type: application/sdp Content-Length: 159 v=0 o=1001 2890844526 2890844526 IN IP4 192.168.1.20 s=SIP Call c=IN IP4 192.168.1.20 t=0 0 m=audio 4000 RTP/AVP 8 0 101 a=rtpmap:0 PCMU/8000 a=rtpmap:8 PCMA/8000 a=rtpmap:101 telephone-event/8000服务器的处理逻辑:收到INVITE后先回100 Trying,告诉1001“我知道了,正在帮你转”。然后按照被叫号码1002查注册表,找到1002的Contact地址是192.168.1.30:5080,于是作为代理服务器转发INVITE给1002,同时把自己加进Record-Route,确保后续请求能经过它。
// 消息2: FreeSWITCH -> 1001 SIP/2.0 100 Trying Via: SIP/2.0/UDP 192.168.1.20:5060;branch=z9hG4bK7d3c1;received=192.168.1.20;rport=5060 From: <sip:1001@192.168.1.10>;tag=caller_tag_a1 To: <sip:1002@192.168.1.10> Call-ID: call001@192.168.1.20 CSeq: 1 INVITE Content-Length: 0 // 消息3: FreeSWITCH -> 1002 INVITE sip:1002@192.168.1.30:5080 SIP/2.0 Via: SIP/2.0/UDP 192.168.1.10:5060;branch=z9hG4bKproxy888 Via: SIP/2.0/UDP 192.168.1.20:5060;branch=z9hG4bK7d3c1;received=192.168.1.20;rport=5060 Record-Route: <sip:192.168.1.10;lr> Max-Forwards: 69 From: <sip:1001@192.168.1.10>;tag=caller_tag_a1 To: <sip:1002@192.168.1.10> Call-ID: call001@192.168.1.20 CSeq: 1 INVITE Contact: <sip:1001@192.168.1.20:5060> Content-Type: application/sdp Content-Length: 1591002的软电话收到INVITE后开始振铃,回180 Ringing:
// 消息4: 1002 -> FreeSWITCH SIP/2.0 180 Ringing Via: SIP/2.0/UDP 192.168.1.10:5060;branch=z9hG4bKproxy888 Via: SIP/2.0/UDP 192.168.1.20:5060;branch=z9hG4bK7d3c1;received=192.168.1.20;rport=5060 From: <sip:1001@192.168.1.10>;tag=caller_tag_a1 To: <sip:1002@192.168.1.10>;tag=callee_tag_b2 Call-ID: call001@192.168.1.20 CSeq: 1 INVITE Contact: <sip:1002@192.168.1.30:5080> Content-Length: 0注意看,To里出现了callee_tag_b2这个tag。此时早期对话就建立了,1002的软电话已经知道这一通电话的归属。1001收到的是服务器转发过来的180,也会看到To里的tag。两边从这开始认“亲戚”。
1002接听,软电话回200 OK,并带上自己的SDP应答,表示“我选PCMU编码,我这边媒体在192.168.1.30的6000端口”:
// 消息5: 1002 -> FreeSWITCH SIP/2.0 200 OK Via: SIP/2.0/UDP 192.168.1.10:5060;branch=z9hG4bKproxy888 Via: SIP/2.0/UDP 192.168.1.20:5060;branch=z9hG4bK7d3c1;received=192.168.1.20;rport=5060 From: <sip:1001@192.168.1.10>;tag=caller_tag_a1 To: <sip:1002@192.168.1.10>;tag=callee_tag_b2 Call-ID: call001@192.168.1.20 CSeq: 1 INVITE Contact: <sip:1002@192.168.1.30:5080> Content-Type: application/sdp Content-Length: 143 v=0 o=1002 2890844530 2890844530 IN IP4 192.168.1.30 s=SIP Call c=IN IP4 192.168.1.30 t=0 0 m=audio 6000 RTP/AVP 0 101 a=rtpmap:0 PCMU/8000 a=rtpmap:101 telephone-event/8000服务器把200 OK转发给1001,然后1001回ACK。这里有个细节:ACK是点到点直接发的。如果通话双方都支持RTP直连,媒体流可能根本不经过服务器,只有信令走服务器。如果服务器强制B2BUA模式或者做媒体代理,RTP也会经过服务器中转。
// 消息6: FreeSWITCH -> 1001 SIP/2.0 200 OK (内容与消息5基本一致,Via里去掉proxy888那一层) // 消息7: 1001 -> FreeSWITCH ACK sip:192.168.1.10 SIP/2.0 Via: SIP/2.0/UDP 192.168.1.20:5060;branch=z9hG4bK7d3c4 Max-Forwards: 70 From: <sip:1001@192.168.1.10>;tag=caller_tag_a1 To: <sip:1002@192.168.1.10>;tag=callee_tag_b2 Call-ID: call001@192.168.1.20 CSeq: 1 ACK Content-Length: 0 // 消息8: FreeSWITCH -> 1002 ACK sip:1002@192.168.1.30:5080 SIP/2.0 (同上,经由服务器转发)以上过程走完,双方SDP协商完成:编码选了PCMU(G.711μ律),1001的RTP发送端口是4000,1002的RTP接收端口是6000,媒体流开始传输。通话中状态就没有SIP信令了,全是RTP包。
挂断时,假设1002先挂机:
// 消息9: 1002 -> FreeSWITCH BYE sip:192.168.1.10 SIP/2.0 Via: SIP/2.0/UDP 192.168.1.30:5080;branch=z9hG4bKbye99 Max-Forwards: 70 From: <sip:1002@192.168.1.10>;tag=callee_tag_b2 To: <sip:1001@192.168.1.10>;tag=caller_tag_a1 Call-ID: call001@192.168.1.20 CSeq: 101 BYE Content-Length: 0 // 消息10: FreeSWITCH -> 1001 BYE sip:1001@192.168.1.20:5060 SIP/2.0 (内容类似,由服务器改写Contact) // 消息11: 1001 -> FreeSWITCH SIP/2.0 200 OK // 消息12: FreeSWITCH -> 1002 SIP/2.0 200 OK这个完整流程跑一遍,你对SIP信令的基本节奏就有感觉了。我建议你本地搭一台服务器,用两个软电话实际拨一通,边拨边抓包,对照着看一遍,收获比读十遍文章都大。
4.3 呼叫失败与取消:486、CANCEL与487
真实环境里,不是每个呼叫都能接通。占线、无人接听、被叫拒接,每种失败都有对应的信令表现。
如果1002正在通话,1001拨它,1002的软电话或者服务器可以直接回486 Busy Here:
SIP/2.0 486 Busy Here Via: SIP/2.0/UDP 192.168.1.20:5060;branch=z9hG4bK7d3c1;received=192.168.1.20;rport=5060 From: <sip:1001@192.168.1.10>;tag=caller_tag_a1 To: <sip:1002@192.168.1.10>;tag=callee_tag_b2 Call-ID: call001@192.168.1.20 CSeq: 1 INVITE Content-Length: 0主叫收到486后要回一个ACK确认收到最终响应,这个ACK不需要经过服务器逐跳确认,但按协议规范还是要发。注意,非2xx的ACK和2xx的ACK机制上是有区别的,前者属于原来那个INVITE事务的组成部分,后者属于新的独立事务。
如果是振铃过程中,1001等不及了,直接挂机,软电话会发CANCEL:
CANCEL sip:1002@192.168.1.10 SIP/2.0 Via: SIP/2.0/UDP 192.168.1.20:5060;branch=z9hG4bK7d3c1 Max-Forwards: 70 From: <sip:1001@192.168.1.10>;tag=caller_tag_a1 To: <sip:1002@192.168.1.10> Call-ID: call001@192.168.1.20 CSeq: 1 CANCEL Content-Length: 0服务器收到CANCEL后,会回200 OK表示CANCEL成功,同时向被叫转发CANCEL。被叫侧收到CANCEL后,终止振铃,对原来的INVITE事务回487 Request Terminated,然后主叫侧再对这个487回ACK。整个流程看起来像“CANCEL 200 + 487 + ACK”三部曲。这里容易混淆的是:CANCEL收到200 OK只是代表“CANCEL这个请求被接受了”,并不是对话结束;真正的结束信号是487配合ACK。
4.4 通话中修改会话:re-INVITE与UPDATE
通话中改编码、加视频、保持通话,这些操作靠的是re-INVITE和UPDATE。re-INVITE是在已建立的对话里再次发起INVITE,用于修改会话参数;UPDATE则用于早期对话阶段修改参数,且不会影响整个对话的状态。
最典型的场景是呼叫保持。用户按下保持键,已经建立的会话里,保持方发出re-INVITE,SDP里把自己的媒体置为只发不收或只收不发:
INVITE sip:1002@192.168.1.30:5080 SIP/2.0 Via: SIP/2.0/UDP 192.168.1.20:5060;branch=z9hG4bKhold01 Max-Forwards: 70 From: <sip:1001@192.168.1.10>;tag=caller_tag_a1 To: <sip:1002@192.168.1.10>;tag=callee_tag_b2 Call-ID: call001@192.168.1.20 CSeq: 2 INVITE Contact: <sip:1001@192.168.1.20:5060> Content-Type: application/sdp Content-Length: 143 v=0 o=1001 2890844526 2890844527 IN IP4 192.168.1.20 s=SIP Call c=IN IP4 192.168.1.20 t=0 0 m=audio 4000 RTP/AVP 0 101 a=rtpmap:0 PCMU/8000 a=rtpmap:101 telephone-event/8000 a=sendonly对端回200 OK,SDP里带a=recvonly,双方就心照不宣地进入保持状态。取消保持时,再发一个re-INVITE,SDP恢复a=sendrecv。这个过程不需要重新振铃,对用户完全无感,但抓包里能清清楚楚看到一次“快速INVITE-200-ACK”循环。理解re-INVITE对排查通话中途单通、静音问题特别有帮助。
5. 关键头字段解读:调试时逐行看什么
5.1 必看头字段速查表
SIP头字段有几十个,绝大多数调试场景用得上的就那几个。我整理了一张速查表:
| 头字段 | 作用 | 调试要点 |
|---|---|---|
| Via | 记录请求经过的每一跳,防止环路 | 检查branch参数是否正确,received/rport有没有被改写 |
| From | 请求发起方标识,带tag参数 | 同一对话中From tag不能变 |
| To | 请求接收方标识,被叫响应后带tag | 比较响应中的To与实际被叫是否一致 |
| Call-ID | 全局唯一的呼叫标识 | 过滤同一通呼叫的所有消息 |
| CSeq | 命令序号+方法名,保证消息顺序 | 检查CSeq是否连续,方法名是否匹配 |
| Contact | 后续请求可以直接发往的地址 | NAT场景下重点看IP/端口是否正确 |
| Max-Forwards | 每经过一跳减1,默认70 | 出现408/环路时看它有没有被减到0 |
| Record-Route/Route | 强制后续请求经过代理 | 看代理是否插入了路由,路由是否带lr |
| Allow | 本端支持的方法列表 | 功能不起作用时先核对方法是否被支持 |
| Supported | 本端支持的特性扩展 | 涉及100rel/precondition时核对 |
| Session-Expires | 会话超时时间 | 掉话问题检查此字段 |
看到一段信令,养成习惯按这个顺序扫一遍。先确定消息类型,再看状态码定方向,然后通过Call-ID把同一通呼叫的消息串起来,接着比对CSeq、tag是否一致,最后看Contact、Route和SDP。这套读包顺序我用了很多年,基本百试百灵。
5.2 从抓包里快速定位问题的读包顺序
用Wireshark抓SIP包时,第一步先过滤sip,然后在“电话”菜单里可以按呼叫统计查看。但真正的实战中,我更喜欢直接用过滤表达式:
sip.Call-ID=="call001@192.168.1.20" sip.Method=="INVITE" sip.Status-Code >= 400 sip["Status-Code"] == "486" rtp先把同一通呼叫的所有SIP消息筛出来,按时间排序,看一个完整流程。然后重点关注:第一个非1xx的响应是什么?最终状态码多少?发起方有没有对最终响应回ACK或者BYE?SDP协商结果是什么?媒体流是否在预期端口上流动?
这里说一个经验:如果看到INVITE发出后,服务器回了100 Trying,然后卡住没有后续消息,十有八九是服务器在等下游超时。可能是被叫地址写错了、被叫不在线,或者防火墙把服务器到被叫的包丢了。如果看到的是408 Request Timeout,那基本可以确定是路由或防火墙问题,而不是被叫本身拒绝。注意区分“服务器没响应”和“被叫没响应”,这两类问题的排查方向完全不同。
5.3 NAT和端口相关的头字段陷阱
NAT环境是SIP调试的重灾区。私有网络里的分机通过家庭路由器上网,路由器把SIP信令的源地址和端口做了地址转换,但SIP报文里Via、Contact、SDP的IP和端口都是内网地址,服务器拿到这些信息去回包或转发媒体时,就会把包发到内网地址,结果可想而知。
解决方案通常在两个层面。信令层面,服务器要支持RFC 3581的rport机制,从请求的源地址和源端口推导出公网映射地址,然后改写Via的received和rport参数;还要按照Contact和SDP的实际来源改写这两处的IP和端口。设备自身层面,可以采用STUN探测公网映射,或者在路由器上做端口映射,把公网端口映射到内网设备。
踩坑最多的是路由器上的SIP ALG。很多家用路由器号称“为SIP优化”,实际上粗暴改写SIP报文里的端口和地址,有时候改对了,有时候改出个驴唇不对马嘴。我遇到过客户通话时单通、断断续续,折腾了一下午,最后进路由器把SIP ALG关掉,问题立刻消失。现在遇到SIP相关的异常,我的第一反应就是先请客户关掉SIP ALG,再重新抓包。这一条经验至少帮我省了几十个小时的无用功。
6. 常见故障排查实录
6.1 注册类问题速查表
分机注册不上,是运维每天都会碰到的问题。我把高频注册问题整理成一张速查表:
| 现象 | 报错 | 原因方向 | 检查项 |
|---|---|---|---|
| 注册超时 | 10060/408 | 网络不通、端口被封 | 能否ping通服务器、5060端口是否监听 |
| 认证失败 | 401/407 | 密码错误、realm不匹配 | 核对分机密码、realm配置 |
| 注册被拒 | 403 Forbidden | 服务器ACL限制、账号未启用 | 检查ACL白名单、分机状态 |
| 注册间隔太短 | 423 Interval Too Brief | 服务器要求最小注册周期 | 调整Expires大于Min-Expires |
| 找不到分机 | 404/604 | 分机号未创建、域名错误 | 核对分机号和服务器域名 |
注册问题的排查顺序:先看网络通不通,再看端口通不通,然后看认证流程,最后看服务器策略。我见过不少人一上来就翻服务器日志,其实先抓个包看信令交互,一分钟就能定位到前面三个环节。抓注册包时,重点看服务器回的是401(认证问题)还是403(策略问题),两个方向完全不同。
6.2 呼叫类问题实录
注册正常,呼叫失败,这就是更高一层的挑战了。下面几种情况是我这些年遇到最多的。
第一种,能注册但不能呼叫。这种情况往往不是信令协商的问题,而是拨号计划、ACL、权限配置的问题。检查服务器里这条呼叫路由是否匹配,分机的呼叫权限是否允许,以及ACL是否放行了信令来源IP。服务器日志和抓包双管齐下,通常很快能发现。
第二种,呼叫通了但没声音。这是“单通”或“全无声音”的典型场景。排查思路:先看SDP协商结果,确认双方选择的编码是否一致;然后看RTP包的IP和端口是否是公网可达的。如果SDP里写的是192.168.x.x而对方在远端的公网,那必然没声音。解决方向就是把媒体改走服务器中转,或者想办法让SDP携带公网可达的地址。还有一个常见原因是防火墙只放行了5060端口,没放行RTP端口范围(通常10000-20000),媒体包成了漏网之鱼。
第三种,回铃音问题。被叫已经振铃了,但主叫那边听不到“嘟——嘟——”的回铃音。这往往是因为服务器只发了180 Ringing,而运营商侧要求必须收到183 Session Progress或者早期媒体流才播放回铃音。解决办法是在服务器里把早期媒体触发方式配置好,让180带上SDP,或者干脆发183并播放本地回铃音。这个坑在对接运营商中继时特别常见,本地分机之间一般不受影响。
第四种,通话中突然掉线。常见原因有两个:一是NAT映射老化,长时间没媒体流量导致路由器的映射表被清掉,后续的SIP消息发不进来。解决方法是定期发OPTIONS/UPDATE保活,或者把NAT映射超时时间调大。二是Session-Expires协商不一致,会话定时器到期后没有成功刷新,服务器主动断开。查看抓包里是否有BYE之前出现Session-Expires相关的UPDATE失败,基本就能确认。
第五种,DTMF按键没反应。SIP里DTMF最常见的是通过RFC 4733的telephone-event带内音传输,协商时SDP里要有a=rtpmap:101 telephone-event/8000这一行,实际发送时RTP包里带有marker位和事件编码。如果协商里没有这一行,或者双方payload type的数字对不上,按键就会丢失。另外,有些IVR系统要求DTMF用SIP INFO传,这时就需要把网关或软电话的DTMF模式改成INFO。调试时先抓包看DTMF是作为RTP事件还是INFO消息发出,再对症下药。
6.3 抓包与压测工具:我的家用排查三板斧
工具不在多,顺手就行。我现在排查SIP问题最常用的三样:Wireshark、sngrep、SIPp。
Wireshark是通用抓包神器,分析SIP报文时可以右键“Follow Stream”把一次会话的完整信令流拉出来看,还能用“Telephony -> VoIP Calls”直接列出所有呼叫和状态。缺点是抓包文件大了以后,纯文本SIP和RTP混在一起,筛选起来稍微费劲。
sngrep是纯命令行工具,专门为SIP设计,界面类似top命令。它能按Call-ID把一通呼叫的所有SIP消息分组展示,还能看到呼叫流程概览,甚至直接在命令行里回放RTP,听一下音频有没有问题。排查线上服务器时,SSH连上去直接sngrep -q -d eth0,比在远端装Wireshark再导出文件方便太多。我强烈建议每个做SIP运维的人装一个。
SIPp是压测和模拟工具,可以构造大量并发呼叫测试服务器的承载能力。它的脚本是XML格式,需要写场景文件,刚开始学习成本略高,但跑通了之后非常顺手。压测前先确认服务器ACL里放行了测试机的IP,不然每个请求都被拒,测了个寂寞。另外压测时别用太小的间隔冲击线上服务器,我见过有人模拟10路呼叫把生产PBX压崩,最后被客户追着骂的场面。
7. 实操建议与学习路径
如果你是个新手,想用最短时间把SIP信令搞明白,我的建议是先搭一套本地实验环境。找一台普通的PC装FreeSWITCH或者Asterisk,再装两个软电话MicroSIP和Zoiper,注册两个分机号,然后拨一通电话,全程用Wireshark抓包,把本文第4章的流程对着报文逐条标一遍。这个过程一两个小时就能完成,但对SIP的理解会有一个质的飞跃。
学习过程中有个心态要摆正:SIP的文本语法很简单,真正难的是把每个字段放在正确的场景里理解。REGISTER的Contact和INVITE的Contact作用就不一样;同样叫200 OK,对REGISTER和INVITE的含义和使用方式也不同。这些细微差别靠背文档是很难记住的,必须结合抓包和实际故障去体会。
工具链上手顺序建议:先从软电话和Wireshark开始,然后学sngrep提升线上排查效率,最后学SIPp做压力和场景模拟。WebRTC网关、SIP over WebSocket这些高级话题,等基础信令流程烂熟于心之后再碰,会顺利得多。
最后再分享一个小技巧:调试时把抓包文件的过滤器保存为一套模板,比如sip || rtp,再配合“Telephony -> VoIP Calls”视图,一通电话从头到尾的信令和媒体状况一目了然。我现在处理任何SIP相关的问题,都是先抓包,再分析,最后才动配置。抓包不是万能的,但不抓包去瞎猜配置,十有八九会把问题越改越复杂。