📹 第二章 VLC及FFplay流媒体播放器
📄 一句话概括

抓包不是「看热闹」,是「验证你说的和它做的是不是一回事」。 pcap 文件就三个数字:24 字节全局头、16 字节每包头、54 字节协议壳——懂了这三个, 抓包文件你自己也能造。

📝 本节和前面几节的关系

上一节验证了「发」,这一节验证「谈」——把整段 RTSP 交互录下来。 自己写一遍 pcap 生成器,比看十遍 Wireshark 界面有用: 你会知道「界面里那一行」是怎么从字节里长出来的。 报文逐字格式在第一章 1.3/1.5,这一节管「怎么录、怎么看」。

一、pcap 文件长什么样

一个 pcap 就是「全局头 + 一堆包头 + 一堆原始字节」,没有索引、没有压缩。

部分 大小 写了什么
全局头 24 字节 魔数、版本、快照长度、链路类型
每包头 16 字节 秒、微秒、实长、原长
包数据 变长 从以太网头开始的整帧

魔数 0xA1B2C3D4 是判断字节序的开关:读出来是这个值就是小端,反过来就是大端。 pcap 文件坏了,第一件事就是看这四个字节。

链路类型那个字段决定了「包数据开头是什么」:1 是以太网,101 是原始 IP,105 是 802.11。 类型对不上,Wireshark 就会把整包标红。

二、54 这个数从哪来

以太网头 14 + IP 头 20 + TCP 头 20 = 54。

层 字节 里面有什么
以太网 14 目的 MAC 6 + 源 MAC 6 + 类型 2
IP 20 版本、长度、协议、源目的 IP
TCP 20 源目的端口、序列号、标志位

这个 54 是所有「抓包长度对不上」问题的答案:你在代码里数出 105 个字符, Wireshark 列表里显示 159,差的就是它。 而如果你只看 data[54:],拿到的正好是 RTSP 报文的第一个字节。

注意 TCP 头可以更长:带选项时(比如时间戳选项)会是 32 或 40 字节,那时偏移就不是 54 了。 所以正规做法是读 IP 头的长度字段算出来,不是写死 54。

三、Wireshark 里该按什么过滤

过滤表达式不多,记住六条够用。

| 想看什么 | 过滤式 | 说明 | |---|---|---|---| | RTSP 信令 | rtsp | 只有那五步,看得清 | | RTP 媒体 | rtp | 一秒几十个包,很多 | | 某一路流 | rtp.ssrc == 0x0A0B0C0D | 多路时必用 | | 某个包序 | rtp.seq == 100 | 追单个包 | | 只看请求 | rtsp.method | 过滤掉响应 | | 重建视频 | rtp → 菜单「解码为 RTP」 | 能直接放出来 |

第一行和最后一行是排查的两端:先用 rtsp 看握手对不对,再用「解码为 RTP」看画面能不能重建。 中间那四条是「多路场景」的必备:不按 SSRC 过滤,几十路混在一起根本看不出哪是哪。

四、抓包能证明什么,不能证明什么

这条界线要划清楚,否则会得出错误结论。

能证明 不能证明
握手顺序对不对 画面对不对
有没有丢包、丢在第几包 解码器解不解得开
延迟大概多少 客户端渲染快不快
编码参数是多少 画质好不好

右边这一列要靠「解码」才能回答,而抓包只到 RTP 载荷为止。 D23 里那句「Wireshark 只能标成 RTP 包,序列号 XXX」说的就是这个:H.264 载荷是二进制, 抓包软件没有解码器,认不出里面是画面。 想看图,得把载荷抽出来拆 NAL 再喂解码器——这就是第七章的活。

五、动手 Demo:写一个 pcap,再读回来

手工造 8 个包(OPTIONS 到 PLAY 一来一回),按 pcap 格式写盘,再按字节读回来, 把 TCP 载荷首行打出来。 这一步走通,Wireshark 的「神秘感」就没了。

def _pcap_write(path, pkts):
    with open(path, "wb") as f:
        # 全局头:魔数 版本 时区 精度 快照长 链路类型
        f.write(struct.pack("<IHHiIII", 0xA1B2C3D4, 2, 4,
                              0, 0, 65535, 1))
        for ts, data in pkts:
            # 每包头:秒 微秒 实长 原长
            f.write(struct.pack("<IIII", int(ts),
                                int((ts % 1) * 1e6),
                                len(data), len(data)))
            f.write(data)

def _eth_ip_tcp(payload, sport, dport, seq, ack):
    tcp = struct.pack("!HHIIBBHHH", sport, dport, seq, ack,
                      (5 << 4), 0x18, 8192, 0, 0)
    ip  = struct.pack("!BBHHHBBH4s4s", 0x45, 0, 20 + len(tcp)
                      + len(payload), 0, 0, 64, 6, 0,
                      bytes([192,168,1,100]), bytes([192,168,1,64]))
    eth = (b"\x00\x0c\x29\x11\x22\x33"
           b"\x00\x0c\x29\x44\x55\x66" + b"\x08\x00")
    return eth + ip + tcp + payload

实跑输出:

--------------------------------------------------------------------
把 RTSP 会话写成 pcap,再自己读回来
--------------------------------------------------------------------
**Wireshark 看到的「一条条」,本质就是个 pcap 文件。** 这一节先手写一个 pcap 生成器,
把完整 RTSP 会话灌进去,再解析回来。

  写入 **8 个包**(OPTIONS 到 PLAY,八条报文一来一回)

  读回来:魔数 0xA1B2C3D4,版本 2.4,快照长 65535,链路类型 1(1 = 以太网)
  包数 8,文件 1390 字节。

  #     时刻         TCP 载荷首行                                     长度
  ----------------------------------------------------------------------------
  1     0.000 秒     OPTIONS rtsp://192.168.1.64:554/Streaming/C…     105
  2     0.002 秒     RTSP/1.0 200 OK                                  78
  3     0.004 秒     DESCRIBE rtsp://192.168.1.64:554/Streaming/…     102
  4     0.006 秒     RTSP/1.0 200 OK                                  80
  5     0.008 秒     SETUP rtsp://192.168.1.64:554/Streaming/Cha…     134
  6     0.010 秒     RTSP/1.0 200 OK                                  130
  7     0.012 秒     PLAY rtsp://192.168.1.64:554/Streaming/Chan…     111
  8     0.014 秒     RTSP/1.0 200 OK                                  66

**pcap 三个数字要记住:24 是全局头,16 是每包头,54 是以太网+IP+TCP「壳」的长度。**
  所以 Wireshark 顶部那个 Frame 长度,永远等于「TCP 载荷 + 54」。
  你在代码里数出 105 个字符,它在列表里显示 159,差的就是壳。


**抓包能看到协议,但看不到内容**:RTP 里的 H.264 载荷是二进制,Wireshark 只能标成「RTP 包,
序列号 XXX」。
  想知道画面里是什么,得把载荷按 H.264 拆 NAL 再解码——那是第七章的事。

读回来报的是「魔数 0xA1B2C3D4、版本 2.4、快照长 65535、链路类型 1」,四个字段全部对得上, 说明写对了。 8 个包、1390 字节——算一下:24 + 8×16 = 152,剩下的 1238 全是被抓的数据。 pcap 的开销就只有这 152 字节。

时刻那一列每次跑都一样(0.000/0.002/…),因为时间戳是我自己写死的。 真抓包里这一列是系统时间,能看出往返时间——这也是为什么抓包能用来算延迟。

TCP 序列号这一列我没打印,但它必须递增:真实抓包里如果序列号倒退或重复,就是重传, 重传一多,播放就卡。这是抓包查卡顿的第一招。

30 秒实验

  1. 把全局头的魔数改成 0xD4C3B2A1(大小端反过来),用 Wireshark 打开,看它能不能读、报什么错。
  2. 把链路类型从 1 改成 101,再用 Wireshark 打开,看它把包解析成什么——这就是「类型对不上」的表现。
  3. 多加 20 个包,把时间戳改成真实递增(每次 +5 毫秒),算一下平均往返延迟。

常见误区

  1. 以为抓包能看出画面对不对。 抓包只到 RTP 载荷,H.264 是二进制,不解码看不出画面。 要看画面得走「解码为 RTP」。
  2. 以为 54 是固定偏移。 那是没选项的 TCP 头。 带选项时是 62 或 70,该读 IP 头长度字段算,不该写死。
  3. 以为抓到包就说明链路正常。 抓到包只说明「到网卡了」,丢包、乱序、 重传都得看 RTP 序列号和 TCP 序列号才知道。
自测三问(点开对答案) 1. pcap 的魔数有什么用? —— 判断字节序。读出来是 0xA1B2C3D4 说明是小端存的,反过来就是大端。文件打不开先看这四个字节。 2. Wireshark 显示包长 159,你的代码数出 105,为什么? —— 差的 54 是以太网 14 + IP 20 + TCP 20 的协议壳。 Wireshark 显示的是整帧长度,你数的是 TCP 载荷。 3. 为什么多路场景必须按 SSRC 过滤? —— 每路流有独立的序列号和时间戳,混在一起看会以为大量丢包和乱序。 按 SSRC 分开,每一路才是自洽的。

下一节换个角度:不再看工具,而是问「播放器到底由哪些零件组成」,把这一章的东西串成一张图。

相关笔记