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

回环流量不经过网卡,所以普通网卡抓不到——这是 localhost 抓包失败的唯一原因。 换一块专门的环回适配器(Windows 上的 Npcap Loopback,Linux 上的 lo)就解决了。

📝 本节和前面几节的关系

2.8.1 装好了工具,这一节解决「抓不到本机流量」这个最常见的卡点。 2.8.3 开始读这些包。

一、为什么抓不到 localhost

127.0.0.1 的流量不走网卡。 操作系统在协议栈里直接把数据从发送缓冲区搬到接收缓冲区,整条路径完全在内核内存里完成。

所以挂在网卡上的抓包驱动,一帧都看不到。 不是配置错了,是物理上就没经过那里。

要抓它,只能用「假网卡」: ① Windows:Npcap 会装一块叫 Npcap Loopback Adapter 的虚拟网卡。它不连任何硬件,只拦回环那一层。 ② Linux:lo 接口本来就是干这个的,一直都在,直接选就行。 ③ macOS:也是 lo0。

这条知识在调试流媒体时特别值钱: 大部分人先在单机上跑通(服务器和客户端都在 127.0.0.1),这时候抓不到包,就会误以为「代码没发数据」——其实是抓错了地方。

二、Windows 上要确认的两件事

① 安装 Npcap 时勾「Support loopback traffic (Npcap Loopback Adapter)」。 这个选项默认是勾上的,但如果你之前装过老版本可能没有。 补救办法是重跑 Npcap 安装包(在 Wireshark 目录下能找到 npcap-*.exe),勾上再装一次,Wireshark 不用重装。

② 抓包界面上要能看到 Adapter for loopback traffic capture。 如果只有以太网和 WLAN 两块,就说明没装成。

还有一个坑:Windows 上有时候会有多块「回环」条目,包括 Hyper-V、WSL、VMware 造的虚拟网卡。 认准名字里带 Loopback 的那一块,别选成 vEthernet。

三、Linux 上一条命令就够

sudo tshark -i lo -f "tcp port 554" -w /tmp/rtsp.pcap

三个参数: ① -i lo:指定回环接口。不加 -i 它会抓所有接口,噪声大得多。 ② -f "tcp port 554":BPF 过滤。注意这是「抓的时候过滤」,比抓下来再筛效率高得多——大流量下这是必须的。 ③ -w /tmp/rtsp.pcap:写文件。不写 -w 就直接刷屏,几百行一秒,看不清。

BPF 写法值得记几个: host 192.168.1.64:只抓这台机器。 port 554:只抓 554 端口(RTSP)。 tcp and port 554:组合条件,and/or/not 都能用。 udp and portrange 5000-6000:抓一段端口里的 UDP,这就是抓 RTP 的写法(第 3 章会用到)。

四、环回的两个特点会影响你的判断

抓到回环包之后,有两点必须知道,否则会得出错误结论。

① 延迟极低。 回环上的往返是百微秒级(实测 94~172 微秒),而真实网络里一个 RTT 就是几十毫秒。 所以别拿回环的数字去估算线上性能——差一百倍不奇怪。

② 不经过网卡,所以没有以太网的某些字段。 回环适配器给的帧通常还是以太网格式,但前导码、CRC 这些根本没有。 所以你在回环上算出来的「每包多少字节」会比真实网络少几字节。

还有一个正面好处:回环上没有其他流量干扰。 真实网卡上跑着浏览器、微信、系统更新,一秒钟几百个包;回环上只有你自己那一对进程,干净得多。 所以学习协议时,先在回环上抓,再上真实网络。

五、动手 Demo:环回上真跑一遍,逐条记账

本机起一个 RTSP 服务器,客户端拉四步,把每一对报文的上下行字节都记下来。 顺便算一下:这些字节在真实网络上还要加多少层壳。

import socket, threading, time

SID = "1A2B3C4D"
LOG = []

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;server_port=6000-6001\r\n"
                "Session: %s;timeout=60\r\n\r\n" % (cseq, SID))
    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)
for i, m in enumerate(("OPTIONS", "DESCRIBE", "SETUP", "PLAY"), 1):
    req = ("%s rtsp://127.0.0.1/live/test RTSP/1.0\r\nCSeq: %d\r\n\r\n" % (m, i))
    t0 = time.perf_counter()
    s.sendall(req.encode())
    r = s.recv(8192)
    us = (time.perf_counter() - t0) * 1e6
    LOG.append((m, len(req.encode()), len(r), us))
s.close()

for m, up, down, us in LOG:
    print("%-9s 上行 %3d  下行 %3d  往返 %.0f 微秒" % (m, up, down, us))
print("合计 上行 %d 字节  下行 %d 字节"
      % (sum(x[1] for x in LOG), sum(x[2] for x in LOG)))
print("加三层壳之后,一条 60 字节的请求在线上是 60+14+20+20 = 114 字节")

实跑输出:

--------------------------------------------------------------------
抓本地包——环回上的流量长什么样
--------------------------------------------------------------------
① 环回上真跑一遍,逐条记账

  #     请求         上行字节     下行字节     往返
  ---------------------------------------------------------
  1     OPTIONS      56           115          1610 微秒
  2     DESCRIBE     57           280          172 微秒
  3     SETUP        54           130          145 微秒
  4     PLAY         72           122          94 微秒

  **四条请求一共上行 239 字节、下行 647 字节。**
  **注意 DESCRIBE 的下行最大,因为它带回了一份 SDP。**

② 在 Wireshark 里你看到的是同一批东西,只是多了三层壳

  层             多出来多少字节     内容
  --------------------------------------------------------------------
  以太网帧头     14                 源 MAC、目的 MAC、类型
  IP 头          20                 源 IP、目的 IP、协议号、TTL
  TCP 头         20                 源端口、目的端口、序号、标志位
  RTSP 报文      变化的             我们上面记的那些

  **一条 60 字节的 RTSP 请求,在线上实际是 60 + 54 = 114 字节。**
**以太网还有 7 字节前导码和 4 字节校验,不过抓包通常看不到。**

③ 环回的两个特点

  **① 不经过网卡**:数据从发送缓冲区直接进接收缓冲区。
  **所以抓包工具要用专门的环回适配器,普通网卡抓不到。**
  **② 延迟极低**:上面几个往返都是百微秒级,**而真实网络里单个 RTT 就是几十毫秒。**
  **所以别拿环回的数字去估算线上性能。**

四条请求上行合计 239 字节、下行 647 字节,体量和 2.7 算的完全对得上。 DESCRIBE 的下行最大(280 字节),因为它是唯一带 SDP 正文的一步。 往返都是百微秒级——这是回环的特权。真网络上单个 RTT 就是几十毫秒,别拿这个数字估算线上性能。

30 秒实验

  1. 把 client_port 换成 5000-5001 之外的值,看上下行字节变不变——只有报文里的数字变了,长度不该变。
  2. 用 tshark -i lo -f "tcp port <端口>" -w a.pcap 抓一次,再用 Wireshark 打开,数一数是不是四对报文。
  3. 在客户端和服务端之间加 time.sleep(0.05),看往返时间怎么变——这就是「时间戳是抓包能给的唯一增量信息」。

常见误区

  1. 「抓不到是本机没流量」——回环不进网卡,普通网卡抓不到很正常。换环回适配器。
  2. 「-f 过滤和显示过滤是一回事」——-f 是 BPF,抓的时候就丢;Wireshark 输入框里的是显示过滤,抓完再筛。大流量下一定要用 -f。
  3. 「回环上的延迟能代表线上」——差一百倍。回环建连几乎零成本,真实网络要几十毫秒。
自测三问(点开对答案) 1. 为什么普通网卡抓不到 127.0.0.1 的包? —— **回环流量在内核里直接搬,不经过网卡。**要用环回适配器。 2. Windows 上要选哪块网卡? —— **名字里带 Loopback 的那块**(Npcap Loopback Adapter)。**别选成 vEthernet。** 3. `-f "tcp port 554"` 和 Wireshark 输入框里的过滤有什么不同? —— **前者在抓的时候就丢包(BPF),后者是抓完再筛。****大流量下必须用前者。**

相关笔记