1. 为什么抓SRT不是从推流端开始,而是先看握手指令
我第一次被SRT握手控制包“教育”,是在一条远程直播链路半夜卡死的时候。当时推流端显示码率正常,播放端却隔几秒就缓冲一次。我习惯性地去查带宽、查丢包、查CPU,折腾了半小时一无所获。后来把UDP 9000端口的流量拖进Wireshark,按时间线逐包看,才发现连接根本没有真正建立起来——每一次播放器都在原地重发握手控制包,压根没进入媒体数据阶段。从那一刻起我养成了一个习惯:排查SRT流第一件事不是看视音频负载,而是先看握手。
SRT(Secure Reliable Transport)本质上是基于UDP的应用层可靠传输协议,用来承载高质量、低延迟的直播视频。OS层面的UDP没有“连接”概念,所以SRT把连接建立完全放在了应用层。谁来主动连接、连接双方是什么身份、用哪个库的版本、要加密还是明文、推流时携带的Stream ID是什么、缓冲延迟设多少——这些信息几乎全部由握手控制包承载。也就是说,握手包不是一句“你好”就完事的开场白,它是整个SRT会话的“身份证+合同”。
这也是为什么标题是“握手控制包”而不是“SRT数据包”。数据包只负责搬运TS流或MPEG-TS格式的码流,出了问题往往只能看到宏观现象;而握手控制包里面带着十几条协商信息,任何一条不一致都可能让后面的数据流变成一团乱麻。对做直播运维、瘦服务端开发或者底层协议分析的人来说,先把握手读透了,等于拿到了一把打开SRT排障大门的钥匙。
我在之后的很多次实测里都验证过这个逻辑:如果一个连接能稳定跑完全程,握手包的协商参数一定是干净一致的;如果频繁重连、画面黑屏、CPU不高但音频断断续续,八成能在握手阶段找到一条不匹配的字段。下面我就从包结构、模式差异、扩展字段到抓包排查,把SRT握手控制包完整拆一遍。
2. 握手控制包的包体结构:一场高信息量的网络“相亲”
先说整体形态。SRT一个UDP包可以分两类:数据包和控制包。握手属于控制包,而且通常是连接发生后的第一个控制包。拿Wireshark解析结果看,一个典型的SRT握手控制包由“公共控制头 + 握手载荷 + 握手扩展字段”三块拼起来,公共头负责标识这是谁发出的、发给谁的、什么时刻发的;载荷负责双方基本身份和能力;扩展字段负责附加的个性化需求。
2.1 公共控制头:时间戳与Socket ID
公共头里最容易被忽略但也值得重视的是时间戳和目的Socket ID。
时间戳是微秒级的,由发送方生成。它不只是给Wireshark排序用的,SRT在握手之后计算RTT、做丢包重传和缓冲时间同步,都会参考握手阶段的时间基准。第一次握手包里的时间戳往往会成为后面TSBPD协商的锚点,如果两边时间戳基准对不上,后面就可能出现“包到了但被判定过期”这种诡异问题。
目的Socket ID是一个32位标识,用来把UDP包对应到某个SRT会话实例。第一次从Caller发出去的握手包里,这个值常见为初始减一或由库自动生成;Listener收到后再用自己的Socket ID回包。它的作用类似门牌号,这么多UDP连接共用同一个端口时,没有这个字段系统根本不知道该把包交给谁。排障时如果发现同一个UDP端口上同时出现多个不同的Socket ID,说明上层可能起了多个SRT连接实例,别被这种“串包”干扰判断。
再往前一点,公共控制头里还有“控制包类型”字段,值0对应握手(Wireshark里会显示为Handshake)。抓包时不要只看注释层写着SRT就认为是握手,得落到控制包类型这一层确认。CMO和我都吃过这个亏,把ACER包当成握手包去分析,浪费了十分钟。
2.2 握手载荷:版本、状态、连接类型与加密参数
公共头后面紧跟着握手载荷,这一块字段较多,我把常见字段整理成一张表,抓包时对照着看就不会晕。
| 字段 | 含义 | 排障价值 |
|---|---|---|
| Peer Version | 对端SRT库版本号,比如1.4.3 | 确认双方库版本跨度 |
| Peer State | 握手进行到哪一步的状态标识 | 判断卡在请求还是应答 |
| Connection Type | Caller、Listener或Rendezvous | 快速识别模式配错 |
| Socket ID | 本端会话标识 | 区分同一端口上的多路连接 |
| Encryption Field | 加密算法标识,比如AES-128/192/256或无加密 | 排查密码不匹配 |
| Extension Block | 后续扩展字段的起点标志 | 定位Stream ID等参数 |
Peer Version就是SRT库的版本。为什么强调它?因为SRT规范一直在演进,老版本库和新版本库握手时对相同字段的解释可能有差异。比如某直播中台从1.2升级到1.4后,原本能连的客户端突然反复重连,抓包发现Client报1.2版本的握手包,Server用1.4版本字段应答,两边虽然都认识“握手”,但对扩展字段的解析逻辑不同,最终谁也没等来对方的数据。
Peer State一般不是一个简单的“成功/失败”,它会在握手阶段多次变化。以Caller/Listener模式来看,大致是“初始请求→收到Cookie→带Cookie确认→完成”。Wireshark里能看到不同握手包的状态值在跳。如果状态反复回到“请求”而不进入“确认”,基本可以判定在Cookie交换环节出了问题。
Connection Type就是握手时双方约定的连接模式。这个字段往大说影响了整个数据流的方向和重传策略:Caller主动发起连接,Listener被动监听,Rendezvous则两边都在发起、没主没次。连接类型不一致是握手失败的高频原因之一,比如一边配的Caller一边配的Caller,两边都在“主动打电话”,电话占线没人接;或者一边Listener一边Listener,大家都不拨号,永远无法建立。
Encryption Field则记录加密需求。SRT的加密基于AES,握手阶段要先告诉对端“我用哪种长度”。常见的有0表示不加密、AES-128对应16字节密钥、AES-192对应24字节、AES-256对应32字节。如果到后面KMREQ/KMREP密钥交换阶段对不上,客户端往往表现为“握手过去了但立刻断流”。
不要把加密参数当成和握手无关的存在。SRT的加密不是数据包层单独搞的,握手载荷里已经把加密算法预声明了,后续的密钥材料交换是握手扩展的延续。所以分析握手包时看不到加密字段的,可以直接期待对端跑明文;看到加密字段的,密码不匹配大概率会在这里留下痕迹。
3. 三种连接模式下的握手差异:Caller、Listener与Rendezvous
握手过程并不是所有模式都一个样。这一点对排查尤其关键,因为很多人拿到一个握手包就习惯性套“请求—应答”去看,遇到Rendezvous就懵了。我按抓包时的实际时间线,把三种模式拆开讲。
3.1 Caller/Listener的两次往返与Cookie机制
Caller/Listener类似打电话:Caller先拨号,Listener接听。完整的握手大致会看到这么几步:
- Caller向Listener的端口发送第一个握手控制包,包内带着自己的版本、连接类型、Socket ID。
- Listener收到后,回一个握手控制包,同时附带一个一次性Cookie。这个Cookie不是用来登录的,是为了防止有人伪造IP刷端口攻击。Listener通过Cookie验证“你确实能收到我回发的包”。
- Caller拿到Cookie后,再次发送包含该Cookie的握手控制包。
- Listener校验Cookie通过后,回最后一个握手确认包,之后才开始真正的媒体数据传输。
这里有个常见的误区:很多人以为SRT和TCP一样,三次握手结束就建立了,看到前三个包出现就觉得“应该通了”。实际上Caller/Listener模式中,最后一个确认包和之后的KMREQ等扩展交换同样重要。我遇到过一台编码器,它发了三步握手后迟迟没有等来确认包,又因为超时反复从头开始重发,看起来网络里全是握手,实际上一帧画面都没有。
3.2 Rendezvous为什么只需要一次对称握手
Rendezvous模式要放到另一个语境里理解。它不区分主动和被动,适合点对点场景,尤其是两边都在NAT后面、希望互相“找”到对方的情况。抓包时你看到的不是一条线上“请求—响应”,而是两条线互相发握手包。
“对称”这两个字怎么理解?两边都认为自己是连接发起方,所以会同时向外发送握手控制包,也都可能在同一个端口上监听。收到对端包后,双方各自回应,再完成参数交换。理论上双方不需要像Caller/Listener那样由某一方存心挖坑、另一方跳过坑再回来,所以看起来更简洁。
但简洁不意味着好配。Rendezvous模式下两边的时间戳、Socket ID和版本必须能互相“自洽”,任何一边解析不了对方的扩展字段,握手就中断了。再加上如果两边启动时间差距很大,先启动的那一方会一直重发,直到另一台上线。很多人在P2P拉流时手一抖把其中一边配成Caller,另一边配成Rendezvous,结果连接怎么都建立不了。抓包看到只有单方向重复发握手,第一反应就应该是检查模式字段两边是不是都一致。
3.3 模式对照表:抓包时第一眼该看什么
| 模式 | 握手形态 | 常见场景 | 抓包特征 |
|---|---|---|---|
| Caller | 主动发起,经过Listener的Cookie挑战 | 推流端、播放器 | 从单端口向Socket ID发起,持续出现握手确认 |
| Listener | 被动响应,下发Cookie后等待确认 | 接收服务器、网关 | 等待连接,收到握手后返回Cookie字串 |
| Rendezvous | 双向同时发起,对称协商 | P2P、NAT穿透 | 两个IP端口互发握手包,不区分主从 |
在实际直播系统中,Caller/Listener最常见。OBS推流到SRT服务器,播放器走SRT拉流,基本都是Caller往Listener撞。Rendezvous多数用在两台编码器直接对传、或者两个不受管制的设备间建立临时链路。抓包时先把连接类型字段看了,再开始分析后面的字段,能省一半时间。我自己踩过好几次“拿着一份Listener的握手指南去排查Rendezvous故障”的坑,所以特别想说一句:协议分析的第一步,永远是确认当前场景,不是套模板。
4. 握手扩展字段的实战价值:Stream ID、TSBPD与密钥协商
如果说握手载荷是双方身份和基本能力,握手扩展字段就是个性化的“需求清单”。SRT允许在握手包里附带一系列扩展,每个扩展用“命令号+长度+值”的方式排布。所谓TLV(Type-Length-Value)就是这么个概念。Wireshark解析到位后,你在包里看到的往往不是裸的数字,而是已经翻译过的友好名称。
4.1 扩展字段的TLV组织方式
握手扩展区不是一个独立的包,它是嵌在握手包后面的数据结构。每个扩展项有三个部分:一个数字命令号标明这是什么扩展,一个长度字段告诉解析器后面要读多少字节,最后是具体值。解析时按顺序读,读到长度越界就说明包截断了或者扩展区被误判。排障时如果Wireshark显示“malformed packet”,首先要怀疑抓包大小设置不全(pcap的snaplen太小),然后是握手包在传输中被UDP切碎,最后才考虑协议栈本身的问题。
常见的握手扩展命令,我用表格整理一下:
| 扩展名 | 含义 | 排障价值 |
|---|---|---|
| SRT_CMD_TSBPD | 时间戳基准与包延时协商 | 延迟参数不对时会在这里出问题 |
| SRT_CMD_STREAM_ID | 流标识字符串 | 鉴权失败时优先查这里 |
| SRT_CMD_KMREQ | 密钥材料请求 | 加密连接建立时出现 |
| SRT_CMD_KMREP | 密钥材料响应 | 密钥协商完成后出现 |
| SRT_CMD_MSS | 最大分段大小 | 某些MTU协商异常时查找 |
这些扩展不一定每次抓包都齐全。比如不加密场景下就没有KMREQ/KMREP,只有明文握手后直接进入媒体传输。如果看到本应出现的密钥协商扩展缺失,那就先查两边的加密参数是否一致,别急着看网络路径。
4.2 Stream ID如何实现接入鉴权与路由
Stream ID是我在实战中用得最多的握手扩展之一,它几乎相当于应用层的URL路径。推流端在握手包里带上一串自定义字符,接收端(Listener)可以在握手上层直接做路由和鉴权,不需要等媒体数据到位。
比如我们可以约定一种格式:
streamid=#!::r=live/camera1,m=publish,u=alice,p=passwd这里的r通常代表资源路径,m代表用途(publish还是play),u和p可以放业务侧的用户名密码。Listener端收到握手包后会在建立媒体会话之前做校验。校验不过就直接不回复最终握手确认,客户端只会看到“连接超时”。抓包时如果发现对方发了包含Stream ID的握手包,但后续无确认、无扩展、无数据,基本就是鉴权被拒。
很多人会把Stream ID的功能和TCP层的四元组、IP白名单搞混。IP白名单只能判断“从哪里来”,Stream ID可以判断“来干什么、要连哪一路视频”。中大型SRT网关会按照Stream ID把不同直播间、不同摄像头的信号调度到不同的处理单元,这已经远超普通握手检查的范畴了。如果你在排查接入右侧的问题,优先看Stream ID字段是否和你配置的完全相同。有时候只是少了个#!::前缀,握手就变得像陌生人互不交谈。
4.3 加密参数协商:KMREQ/KMREP与握手的衔接
SRT的加密不是“握手确认后立即加密”,而是有一套自己的密钥交换流程。握手载荷里的Encryption Field先声明加密算法和密钥长度,随后在握手扩展区发起密钥材料请求KMREQ,对端返回KMREP。这一步和握手主流程穿插在一起,虽然中间还会涉及放在有没有共同持,但抓包时你完全可以把KMREQ/KMREP当作握手的第二阶段。
如果密码不匹配,常见的表现是:握手主流程已经完成,双方都回确认了,但KMREQ发出后KMREP迟迟不来或者直接回错误,然后连接被迅速丢弃。从Wireshark时间轴上看就是“一堆握手成功后紧接着回复一个断开或超时”。这种场景下别去翻媒体数据包,直接在扩展区检查密钥协商的状态字段,再对照两边配置的passphrase与pbkeylen是否一致,通常十分钟内能定位。
另外提醒一句,密钥长度要和加密算法匹配。AES-128需要16字节,AES-192需要24字节,AES-256需要32字节。在URL参数里写错pbkeylen或者漏写passphrase,抓包时大概率会在KMREQ/KMREP这一层看到异常。与其依赖服务器日志,不如先看握手扩展区有没有异常记录。
5. 用Wireshark复现一次完整握手:从抓包到定位失败
纸上谈兵差不多够了,下面进入实操。不管你是运维还是做协议分析,用Wireshark把SRT握手过程完整看一遍,比任何文档都直观。
5.1 打造能看懂SRT的抓包环境
首先你需要一个能触发SRT握手的流量源。本地起一个最小闭环就行,不一定要真推直播。我刚入门时用的是ffmpeg加ffplay,一条命令行就搞定。
推流端(并以Caller模式发出握手):
ffmpeg -re -f lavfi -i testsrc=size=640x480:rate=25 -c:v libx264 -preset ultrafast -tune zerolatency -f mpegts "srt://127.0.0.1:9000?mode=caller&latency=200"拉流端(以Listener模式等待连接并回握手):
ffplay -probesize 32 -analyzeduration 0 "srt://127.0.0.1:9000?mode=listener&latency=200"跑起来后,在Wireshark里选择Loopback网卡(本地流量)或你真正的物理网卡,过滤条件先写一个保守的:
udp.port == 9000如果Wireshark能自动识别SRT协议,你会在Protocol列看到SRT。如果只是显示UDP,说明当前版本没有带SRT解析器或需要手动启用协议解析。可以在“Protocols - SRT”里确认一下,或者把包导出到较新版本的Wireshark里再看。
5.2 实测中握手成功和失败的报文差异
抓下来后不要急着滚动,先在过滤栏里把范围缩窄。不同Wireshark版本的字段名可能不同,常见有两种写法,我都说一下:
srt.packet_type == 0 srt.proto.type == 0如果一条不生效就换另一条。握手包通常是整个抓包里最先出现的那几包,落在TCP/UDP连接之外。你会看到类似这样的显示:
- SRT Control Packet
- Control Packet Type: Handshake (0)
- Timestamp: xxx
- Destination SRT Sock ID: xx
- Handshake:
- Version: 1.4.3
- Connection Type: Caller
- Encryption: AES-128
- Extension Field...
一个成功握手的时序通常大概长这样:
- 第一个Caller握手包,目的Socket ID为0或初值。
- Listener回握手,携带Cookie、自己的Socket ID。
- Caller再次发握手,携带Cookie和Stream ID。
- Listener回最后握手确认。
- 加密场景下紧接着可能是KMREQ/KMREP。
- 经过很短间隔,开始出现SRT数据包。
失败的时序则有很强的“重复”特征。最典型的就是同一个方向反复发握手请求包,间隔几百毫秒或一两秒,但一直没有对应的回应;或者回应了几次后始终没有进入数据包阶段。这种周期性重发说明双方在“当前连接能不能继续”上没有达成一致,卡在了中间某一步。
5.3 一次“播放器反复重连”的现场排查链路
去年帮朋友排查过一个后台不友好的播放器。画面黑屏,播放器每隔两三秒就重连一次,服务端日志只显示“new connection”然后“disconnect”,没有任何错误码。我用Wireshark在播放器一侧抓包,过滤udp.port == 9000后看到了大量握手控制包。
排查链路是这样走下来的:
- 发现存在周期性的“请求—响应—断开”模式,前两个握手包能正常交换,但第三个握手确认迟迟不来。
- 在第二个握手包里看到了Listener返回的Cookie和Server端SRT版本,在第一个握手包里看到了Caller声明的Stream ID。
- 对比服务端配置,发现服务端限制了允许的Stream ID白名单,而播放器发送的Stream ID末尾多了一个空格字符。
- 修正播放器URL参数后,重新抓包,第三、第四个握手包正常返回,随后开始出媒体数据包。
这个例子给我最大的教训是:很多SRT“连不上”的问题,协议层面并没有崩溃,只是握手扩展区里一个看不到的字符串差异让服务端拒绝了会话。如果不抓握手控制包,只看服务端日志或播放器表面现象,很难定位到一个空格身上。所以现在遇到SRT连接问题,我基本默认流程就是:先抓包、过滤握手、比较成功和失败两组包的字段差异。
6. 握手控制包相关的坑,能避一个是一个
最后聊几个我这些年反复踩过的坑,它们不全是握手包结构本身的问题,但都会在握手控制包上现出原形。
6.1 版本号不一致:看似通了实际卡死
SRT库版本跨度大时,老客户端会发一个旧版握手格式,新版服务端理解不了,就会选择不回确认,或者回到一种“兼容但什么都不干”的状态。抓包时最迷惑的是你能看到前几个握手包有来有往,但之后没有任何数据包。这时候去看Peer Version字段,如果两端库版本隔了好几个大版本,不要犹豫,先统一版本范围再继续排。
我在一次现场踩得很深:客户端用的FFmpeg自带的libsrt是1.3版,服务端编译了1.5版SDK,前两个包正常回,第三个握手包携带的扩展字段被服务端解析成未知指令,导致后续直接断流。整个过程在信令层看起来完全正常,但媒体数据就是起不来。
6.2 密码与加密类型不匹配的表现
加密连接中,握手载荷里带着加密算法标志,扩展区才是真正的密钥协商。密码不匹配时,常见表现和版本不兼容很相似:主握手好像完成了,但立即收到对端的关闭或超时。不要看到手握手成功就松懈,记得等KMREQ/KMREP阶段走完再下结论。我现在抓包时习惯把过滤条件写长一点:
udp.port == 9000 and (srt.packet_type == 0 or srt)这样主握手和后续密钥协商都会留在视野里,方便一次看完整个建连生命周期。
6.3 Rendezvous模式下的NAT与初始化顺序
Rendezvous模式本身不是握手包格式复杂,而是网络环境配合难。两边都在NAT后面时,UDP打洞往往依赖“同时开始”和“同端口”。如果其中一边先启动个几秒钟,它发出的握手包可能被NAT丢弃,后面的重发又路径不对,结果两边都感觉对方“没上线”。抓包时如果看到只有单方向在重复发握手,别急着改代码,先在防火墙里确认UDP监听端口放行,并确保两边约定的端口号完全一致。
Rendezvous模式下Socket ID的辨别也会和Caller/Listener不同。没有明显的“主动方”和“被动方”,你可能看到两个包都标记为握手请求。这是正常的,不用觉得奇怪,关键看后续两个握手包是否互相确认并进入数据阶段。
6.4 从握手包中快速提取目标参数的小技巧
每次抓包都点一条条看太累,我习惯用tshark把握手关键字段一次性导出来。比如:
tshark -r srt.pcapng -Y "udp.port == 9000" -T fields -e frame.number -e srt.type -e srt.version -e srt.connection_type 2>/dev/null | head -50如果你的Wireshark版本字段名不同,先跑tshark -G fields | grep srt找字段,把名字替换上去。用这种命令批量扫一遍握手包,模式是否两边一致、版本是否有跨度、有没有重复连接,一目了然。
我个人操作时的另一个习惯,是用Wireshark的“Time Reference”把第一个握手包设为时间起点,看后续包相对它的延迟。正常局域网内握手总耗时通常在个位数毫秒级,公网场景几十毫秒甚至上百毫秒也可能接受。如果相对延迟忽大忽小,说明UDP路径上可能存在严重抖动,即使握手成功,后面直播稳定性也很难保障。
说到底,SRT握手控制包就是一个“信息密度极高”的报头聚集体。它把双方能不能协作、按什么规则协作,在正式推流之前就全部写清楚了。学会从握手包里抽丝剥茧,比盲目调重传参数、加FEC有效太多。
最后分享一个我自己的小习惯:每次新接入一台SRT编码器或者一个新播放器,不是直接就推流,而是先空跑一次连接,抓一份基准握手包存起来。后面出问题时拿异常包和基准包一对比,一翻字段差异就知道是谁变了。这套方法帮我解决过好几次匪夷所思的兼容性问题,也算是我在SRT协议分析上最值回票价的经验。