抓 RTP 要剥三层壳:以太网 14、IP 20、UDP 8,才见到 12 字节的 RTP 头。 54 字节的固定开销意味着「小包非常浪费」。
3.1.3 讲会话怎么走,这一节讲怎么在抓包里看到它。 2.8 的三条经验在这里要用一遍,只是过滤条件换成 UDP。
一、三层壳与 54 字节
一个 RTP 包在线上要被包三层:以太网 14、IP 20、UDP 8。 加上 RTP 头 12,一共 54 字节是「必须花掉的」。
算一下开销比例: ① 载荷 100 字节 → 154 字节在线,开销 54%。 ② 载荷 1000 字节 → 1054 字节,开销 5.4%。 ③ 载荷 1200 字节 → 1254 字节,开销 4.5%。
所以「一帧切很多小包」的代价被低估了。 1080p 关键帧 20 KB,按 1200 切 17 片,头部开销 918 字节,约 4.5%。 而如果按 300 字节切,就是 68 片,开销 3672 字节,占到 18%。
1440 字节是个常用上限,不是 1500。 因为要留出 54 字节的头,1500 − 54 = 1446。取 1440 是个整数好算,也留了点余量。
二、Wireshark 怎么认出 RTP
Wireshark 不会「按协议字段」去认 RTP,它按端口猜。 规则是:偶数 UDP 端口当 RTP,下一个奇数当 RTCP。
所以你会遇到两种情况: ① 端口是 5000/5001 这种成对的 → 直接正确解析。 ② 端口是奇数对奇数、或者 6002/6005 这种 → 显示成 Data。这时右键选 Decode As → RTP 手动指定。
为什么 Wireshark 不做得更聪明? 因为 RTP 头没有魔术字。 前两字节是 V/P/X/CC/M/PT,取值空间很小,很多协议的载荷碰巧长得像。硬猜会大面积误判。
一个实用技巧:先看 SDP。
SDP 里的 m=video 0 RTP/AVP 96 没给端口,但 SETUP 的响应里 server_port=6000-6001 给了。
知道端口再填 Decode As,比瞎猜快。
三、四个必看的统计
Wireshark 的 Telephony → RTP → Show All Streams 里有四组数:
① 丢包率(Lost):按序号算出来的,最直接。 ② 抖动(Jitter):到达间隔的波动,单位毫秒。超过 30 毫秒就该警惕。 ③ 乱序(Out of order):序号到了但比前面的小。少量正常,持续出现说明有多路径或缓冲异常。 ④ 最大间隔(Max Delta):两个连续包之间最长隔了多久。这个数最能反映「卡不卡」。
四组数里最该先看的是丢包率。 因为丢包、抖动、延迟这三者里,丢包是最容易定位也最容易解决的:换链路、降码率、开 FEC,都直接有效。
四、三个常见误判
① 「抓到 100 个包就是收到 100 个」 不对。抓包点如果在网卡收包之后、应用读之前,那是「到了但可能被丢在缓冲区」。 真正「应用收到了几个」只能看应用自己的计数。 ② 「丢包率 0 就是没问题」 不一定。抖动大照样卡。 包都到了,但前后间隔忽大忽小,播放器照样要加深缓冲。 ③ 「RTP 乱序是网络问题」 也可能是发送端的问题。 有些服务器多线程发包,序号分配和发送不是原子的,自己就把顺序搞乱了。
判断方法很简单:看乱序是否集中在某些时段。 如果只在流量高峰出现,是网络;如果一直有,是发送端实现问题。
五、动手 Demo:造一个 RTP 包再从 pcap 里剥回来
手工拼一个「以太网 + IP + UDP + RTP」的帧,写进 pcap 结构里。 再按层剥开,核对每层的字段是否对得上。
import socket, struct
rtp = struct.pack("!BBHII", 0x80, 96, 1000, 3600 * 7, 0x1A2B3C4D)
rtp += bytes([0x41]) + b"y" * 60
udp = struct.pack("!HHHH", 5000, 6000, 8 + len(rtp), 0) + rtp
ip = struct.pack("!BBHHHBBH4s4s", 0x45, 0, 20 + len(udp), 1, 0,
64, 17, 0, socket.inet_aton("127.0.0.1"),
socket.inet_aton("127.0.0.1")) + udp
eth = b"\x00" * 12 + struct.pack("!H", 0x0800) + ip
print("一帧 %d 字节" % len(eth))
# 剥三层壳
print("IP 协议号 = %d" % eth[14 + 9])
sp, dp, ulen = struct.unpack("!HHH", eth[14 + 20:14 + 26])
print("UDP %d -> %d,长度 %d" % (sp, dp, ulen))
b0, b1, seq, ts, ssrc = struct.unpack("!BBHII", eth[14 + 28:14 + 40])
print("RTP V=%d PT=%d 序号=%d TS=%d SSRC=0x%08X"
% (b0 >> 6, b1 & 127, seq, ts, ssrc))
# 开销比例
for n in (100, 500, 1000, 1200):
print("载荷 %4d -> 线上 %4d,头占 %.1f%%"
% (n, n + 54, 54 * 100.0 / (n + 54)))
实跑输出:
--------------------------------------------------------------------
RTP 的抓包分析——从 pcap 里剥三层壳
--------------------------------------------------------------------
**① 造一个以太网 + IP + UDP + RTP 的包,写进 pcap**
以太网 14 + IP 20 + UDP 8 + RTP 73(12 头 + 61 载荷)= 一帧 115 字节
**② 逐层剥开,核对每层的说法**
层 字节 关键字段 值
---------------------------------------------------
以太网 14 类型 0x0800(IPv4)
IP 20 协议号 17(UDP)
UDP 8 长度 81(含头)
RTP 12 SSRC 0x1A2B3C4D
实测:IP 协议号 = 17,UDP 端口 5000 -> 6000,UDP 长度 81
RTP:V=2 PT=96 序号=1000 时间戳=25200 SSRC=0x1A2B3C4D
**③ 三条抓包时的经验**
**① RTP 是 UDP,所以 `tcp.port == 554` 一条都筛不出来。**
**要用 `udp.port == 6000` 或者 `rtp` 这个过滤器。**
**② Wireshark 认 RTP 靠「端口范围猜」**:**它默认把偶数的 UDP 端口当 RTP、下一个奇数当 RTCP。**
**猜错了整段会显示成 Data,这时右键选 Decode As -> RTP。**
**③ 看 RTP 的统计用 Telephony -> RTP -> Show All Streams。**
**丢包率、抖包、乱序全在里面,比一个个数包快得多。**
**记住这个层次的字节数:14 + 20 + 8 + 12 = 54。**
**一个 100 字节载荷的 RTP 包,在线上其实要占 154 字节——开销超过载荷的一半。**
**所以小包会很浪费,这也是「一帧尽量少切片」的原因。**
一帧 115 字节 = 14 + 20 + 8 + 73,其中 RTP 那 73 字节是「12 头 + 61 载荷」。 IP 协议号 17 是 UDP,6 是 TCP。这个数字是判断「这层装的是什么」最快的办法。 载荷 100 字节时头占了 35%,而载荷 1200 时只占 4.5%。这就是「别切太碎」的量化理由。
30 秒实验
- 把 RTP 载荷从 60 字节改成 1200,看整帧多大、头占比降到多少。
- 把目的端口从 6000 改成 6005,想一下 Wireshark 会怎么猜——它会把 6005 当 RTCP。
- 把 UDP 长度字段故意写错,看解析时会不会报「长度不符」,这就是抓包里「Malformed」的来源。
常见误区
- 「
rtsp过滤器能筛出 RTP」——不能。RTP 是 UDP,要用rtp或udp.port。 - 「丢包率 0 就不卡」——抖动大照样卡。
- 「乱序一定是网络问题」——发送端多线程发包也会乱序,看是否集中在高峰时段。
自测三问(点开对答案)
1. 一个 RTP 包在线上有几层壳?共多少字节固定开销? —— **三层:以太网 14、IP 20、UDP 8,加 RTP 头 12,共 54 字节。** 2. Wireshark 靠什么认 RTP? —— **靠端口猜:偶数当 RTP、下一个奇数当 RTCP。**猜错了要手动 Decode As。 3. Telephony 里最该先看哪个数? —— **丢包率。**它最容易定位,也最容易通过换链路、降码率解决。- ☐ (本节完,下一节 3.2.1 RTCP的5种分组类型)