🎬 第二章 RTSP流媒体协议
📄 一句话概括

一条 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 秒实验

  1. 在循环里加 time.sleep(0.05),看两行之间隔了 50 毫秒——这就是抓包里「间隔」那一列。
  2. 把 PLAY 之后补上 s.recv,模拟「PLAY 后没有数据」的情形,体会「只有请求没有响应」在抓包里长什么样。
  3. 用 tshark -r a.pcap -Y rtsp -T fields -e rtsp.method 把方法列出来,这就是「按方法筛」的命令行版。

常见误区

  1. 「抓包里看不到 rtsp 就是没发」——可能连 TCP 都没建起来,先看有没有 SYN。
  2. 「rtsp 过滤能筛出所有相关包」——它只筛文本报文。RTP 是 UDP,要用 udp.port == ... 之类去筛。
  3. 「时间列没用」——首帧时间、服务器处理延迟,这两个指标只能从时间列读出来。
自测三问(点开对答案) 1. 正常的 RTSP 会话在抓包里是什么节奏? —— **请求、响应严格交替,CSeq 递增,SETUP 之后出现 Session,PLAY 之后 554 端口安静、UDP 开始有包。** 2. 只有 SYN 没有 SYN+ACK,说明什么? —— **连 TCP 都没建起来。**端口没开或被防火墙拦,还没到 RTSP 这一层。 3. 「PLAY 之后没画面」该怎么定位? —— **看 PLAY 的响应之后有没有 RTP 包。**没有就是服务器没发,有的是发到了别的端口。

相关笔记