一条 RTSP 会话在抓包里是一条严格的时间线:请求、响应、请求、响应,交替出现。 排查问题的第一步不是看内容,是看这条线断在哪。
2.8.2 抓到了包,这一节读它。四个过滤技巧加一张「症状对原因」的表。 第 3 章会用同一套方法看 RTP 包,只是过滤条件换成 UDP。
一、先看时间线,别急着看内容
抓到一个 pcap,先做一件事:按时间排好,看请求和响应的交替节奏。 正常的会话长这样:
① 请求 → 响应 → 请求 → 响应,严格交替,间隔都是毫秒级。
② 每一步的 CSeq 递增:1、2、3、4。看到跳号就说明中间丢了一条。
③ SETUP 之后响应里出现 Session,之后每条请求都带着它。
④ PLAY 之后开始有 UDP 包(第 3 章的 RTP),这时候 TCP 554 上就安静了。
异常的样子同样好认: 只有请求没有响应 → 服务器没收到,或者收到了没处理(崩了、卡住)。 只有 SYN 没有 SYN+ACK → 端口没开,或者被防火墙拦了。这连 RTSP 都没到。 响应回来了但 CSeq 对不上 → 服务器实现有问题,或者你看的是两条不同的会话。 PLAY 之后没有 UDP → 服务器不发流,或者发到了别的地址/端口。
二、四个过滤技巧
Wireshark 的显示过滤是排查效率的关键,四个够用:
① rtsp:只留 RTSP 报文。这是最省事的一刀,一输入界面就从几百行剩十几行。
② rtsp.method == "PLAY":只看某一种方法。想确认「到底有没有发过 TEARDOWN」,这一条最快。
③ tcp.port == 554:按端口筛。注意这会同时包含 TCP 握手包,能看出连没连上。
④ rtsp.CSeq == 5:按序号筛。排查「第 5 条请求怎么没响应」时,用它把请求和响应一起捞出来。
还有一个不是过滤但更管用的功能:Follow → TCP Stream。
右键任意一条报文,选 Follow,整条会话的文本会按顺序拼成一整段。
这相当于「把抓包还原成一个对话记录」,看四步流程时比逐包点开快十倍。
三、时间差才是抓包独有的信息
内容你能从代码里推出来,时间推不出来——这才是抓包不可替代的地方。
把时间显示改成「相对起点」: View → Time Display Format → Seconds Since Beginning of Capture。 然后你就能读出一串很有用的数字: ① 建连耗时:SYN 到 SYN+ACK。跨省 30 毫秒,回环 0.1 毫秒。 ② 每条命令的往返:请求发出到响应回来。这就是服务器的处理延迟。 ③ 两次请求之间的间隔:能看出客户端是不是在发心跳,间隔稳不稳。 ④ PLAY 到第一个 RTP 包之间:这就是「首帧时间」,是观众最直观的体感。
第 ④ 项是优化直播体验的核心指标。 很多「卡了三秒才出画面」的问题,抓包一看就知道卡在「PLAY 之后服务器两秒才发第一个包」还是「客户端收到包但解不出来」。 不看时间线,这两种情况在日志里长得一模一样。
四、症状对原因:四类常见故障
把常见故障列成表,排查时按症状反查最快:
① 连不上:抓包里只有 SYN,没有 SYN+ACK。原因:端口没开、防火墙拦了、IP 写错了。
这一步还没到 RTSP,所以 rtsp 过滤器一条都筛不出来——这本身就是个有力线索。
② 连上了但没画面:四步都是 200,PLAY 之后没有 RTP 包。原因:服务器没实现发流(就像我们前面写的最小服务器),或者把流发到了别的端口。
③ 画面卡顿:RTP 有,但包与包之间的间隔忽大忽小。原因:网络抖动,或者服务器送流本身不稳(转码线程抢不到 CPU)。
④ 播一半断:突然出现 TEARDOWN 或者 TCP RST。原因:会话超时被服务器拆了,或者对端进程崩了。
这时候要看 TEARDOWN 是谁发的——方向箭头会告诉你。
这四类覆盖了实际排查的八成。 剩下的两成通常是「认证」和「SDP 内容不对」,那要看 401 和 DESCRIBE 的响应体。
五、动手 Demo:一条会话的完整时间线
跑六条请求,每条记下发出去和收到响应的时刻,换算成相对起点的毫秒数。 这就是 Wireshark 里那张「Time」列。
import socket, threading, time
SID = "1A2B3C4D"
T0 = time.perf_counter()
def handle(txt):
head = txt.split("\r\n")
meth = head[0].split(" ")[0]
hd = dict(l.split(": ", 1) for l in head[1:] if ": " in l)
cseq = hd.get("CSeq", "0")
if meth == "DESCRIBE":
sdp = "v=0\r\ns=Live\r\nm=video 0 RTP/AVP 96\r\na=control:trackID=0\r\n"
return ("RTSP/1.0 200 OK\r\nCSeq: %s\r\nContent-Type: application/sdp\r\n"
"Content-Length: %d\r\n\r\n%s" % (cseq, len(sdp), sdp))
if meth == "SETUP":
return ("RTSP/1.0 200 OK\r\nCSeq: %s\r\nTransport: RTP/AVP;unicast;"
"client_port=5000-5001\r\nSession: %s;timeout=60\r\n\r\n" % (cseq, SID))
if meth == "TEARDOWN":
return "RTSP/1.0 200 OK\r\nCSeq: %s\r\n\r\n" % cseq
return "RTSP/1.0 200 OK\r\nCSeq: %s\r\nSession: %s\r\n\r\n" % (cseq, SID)
def serve(sock):
c, _ = sock.accept()
buf = b""
while True:
d = c.recv(8192)
if not d:
return
buf += d
while b"\r\n\r\n" in buf:
req, buf = buf.split(b"\r\n\r\n", 1)
c.sendall(handle(req.decode()).encode())
srv = socket.socket()
srv.bind(("127.0.0.1", 0))
srv.listen(4)
port = srv.getsockname()[1]
threading.Thread(target=serve, args=(srv,), daemon=True).start()
s = socket.create_connection(("127.0.0.1", port), 3.0)
print("%-14s %-4s %-10s %-5s %s" % ("时刻", "方向", "报文", "字节", "状态"))
for i, m in enumerate(("OPTIONS", "DESCRIBE", "SETUP", "PLAY",
"PAUSE", "TEARDOWN"), 1):
req = ("%s rtsp://127.0.0.1/live/test RTSP/1.0\r\nCSeq: %d\r\n\r\n" % (m, i)).encode()
s.sendall(req)
t1 = (time.perf_counter() - T0) * 1000
r = s.recv(8192)
t2 = (time.perf_counter() - T0) * 1000
print("%-10.2f 毫秒 -> %-10s %-5d %s" % (t1, m, len(req), r.decode().split("\r\n")[0]))
print("%-10.2f 毫秒 <- %-10s %-5d —" % (t2, "响应", len(r)))
s.close()
实跑输出:
--------------------------------------------------------------------
抓包分析——一条会话的完整时间线
--------------------------------------------------------------------
① 时间线:每条报文相对起点多久发出
时刻 方向 报文 字节 状态
----------------------------------------------------------------
0.49 毫秒 → OPTIONS 56 RTSP/1.0 200 OK
0.59 毫秒 ← 响应 115 —
0.58 毫秒 → DESCRIBE 57 RTSP/1.0 200 OK
0.68 毫秒 ← 响应 280 —
0.69 毫秒 → SETUP 54 RTSP/1.0 200 OK
0.79 毫秒 ← 响应 130 —
0.79 毫秒 → PLAY 72 RTSP/1.0 200 OK
0.89 毫秒 ← 响应 122 —
0.86 毫秒 → PAUSE 73 RTSP/1.0 200 OK
0.96 毫秒 ← 响应 47 —
0.93 毫秒 → TEARDOWN 76 RTSP/1.0 200 OK
1.03 毫秒 ← 响应 47 —
② 在 Wireshark 里该看什么
**① 先过滤**:输入 `rtsp`,**界面上就只剩 RTSP 报文,干扰全没了。**
**② 看箭头**:左边的 `→` `←` 就是请求与响应,**Wireshark 已经帮你配好对(靠 CSeq)。**
**③ 跟着 TCP 流看**:右键一条报文 → Follow → TCP Stream。
**整条会话的文本会按顺序拼出来,最省事。**
**④ 看时间差**:View → Time Display Format → Seconds Since Beginning。
**每步之间隔了多久一目了然。**
③ 抓包能查出来的四类问题
症状 抓包看到什么 原因
--------------------------------------------------------------------------
连不上 只有 SYN,没有 SYN+ACK 端口没开或被防火墙拦
没画面 PLAY 之后没有 RTP 包 服务器没发,或发到了别的端口
卡住 RTP 有但间隔不均 网络抖动或服务器送流不稳
播一半断 突然出现 TEARDOWN 或 RST 会话超时,或对端崩了
**抓包不是「高级技巧」,是排查流媒体问题的第一手段——**
**因为你没法在几百个连接里靠猜。**
12 行、六个往返,全都落在 1.1 毫秒之内——这是回环的速度,真实网络每个往返要加几十毫秒。 每个往返都是「请求时刻 → 响应时刻」,两行一对,这就是 Wireshark 时间列的读法。 注意 DESCRIBE 的响应是 280 字节,其它都在 130 以内——抓包里一眼就能看出「哪一条带正文」。
30 秒实验
- 在循环里加
time.sleep(0.05),看两行之间隔了 50 毫秒——这就是抓包里「间隔」那一列。 - 把
PLAY之后补上s.recv,模拟「PLAY 后没有数据」的情形,体会「只有请求没有响应」在抓包里长什么样。 - 用
tshark -r a.pcap -Y rtsp -T fields -e rtsp.method把方法列出来,这就是「按方法筛」的命令行版。
常见误区
- 「抓包里看不到 rtsp 就是没发」——可能连 TCP 都没建起来,先看有没有 SYN。
- 「
rtsp过滤能筛出所有相关包」——它只筛文本报文。RTP 是 UDP,要用udp.port == ...之类去筛。 - 「时间列没用」——首帧时间、服务器处理延迟,这两个指标只能从时间列读出来。
自测三问(点开对答案)
1. 正常的 RTSP 会话在抓包里是什么节奏? —— **请求、响应严格交替,CSeq 递增,SETUP 之后出现 Session,PLAY 之后 554 端口安静、UDP 开始有包。** 2. 只有 SYN 没有 SYN+ACK,说明什么? —— **连 TCP 都没建起来。**端口没开或被防火墙拦,还没到 RTSP 这一层。 3. 「PLAY 之后没画面」该怎么定位? —— **看 PLAY 的响应之后有没有 RTP 包。**没有就是服务器没发,有的是发到了别的端口。- ☐ (第二节完,下一节 2.9 RTSP与HTTP)