SRT Sockets , Send List & Channel:
考虑套接字1和套接字2,每个都有自己的发送缓冲区。SndQ包含一个待发送的数据包列表。有一个线程持续检查发送缓冲区。当一个数据包准备好发送时,会创建一个CSnode来标识该数据包所属的套接字,并在SndQ中创建一个对应的对象,该对象将指向发送缓冲区。
每个数据包都有一个时间戳,指示何时发送它。SndQ列表按照待处理的时间戳排序。如果发送线程确定套接字1的发送缓冲区有一个数据包已准备好,那么它会将该数据包添加到SndQ队列中。如果SndQ队列为空,则将数据包条目放在队列的开头,并附上其时间戳,这决定了何时需要处理该数据包。
来自套接字2的另一个发送缓冲区也可以添加到这个 SndQ 中。发送线程会请求缓冲区提供一个数据包,并将根据速率计算数据包之间的间隔。
时间戳与数据包间隔一起将决定数据包在 SndQ 中重新插入的位置(可能是在套接字 1 缓冲区的数据包之前或之后)。
SndQ(发送队列)决定从哪个 SRT 套接字的发送缓冲区获取下一个数据包。发送缓冲区是绑定到套接字的,而 SndQ 则更多地与通道相关。多个套接字可以发送到同一目的地,因此它们可以被多路复用。
这里的多路复用: "多路复用"指的是多个套接字(sockets)可以同时发送数据到同一个目的地,这些数据通过共享的传输通道传输。具体来说,尽管发送缓冲区是与各自的套接字绑定的,但 SndQ(发送队列)更像是一个通道,可以接受来自多个套接字的发送请求。通过将多个套接字的数据包整合到同一个发送队列中,SndQ 可以协调这些数据包的发送,使它们在同一网络连接上有序地传输。这种方式提高了网络资源的利用率,避免了为每个套接字建立单独的连接所带来的开销。
当一个数据包被添加到套接字时,SndQ(发送队列)会被更新。当一个数据包准备好发送时,它会根据其时间戳被重新插入到 SndQ 的适当位置。
处理过程发生在 SndQ 中。对于每个数据包,有一个线程会检查是否到了发送的时间。如果没有,它什么也不做。否则,它会请求 SRT 套接字提供要发送到通道的数据包。SndQ 中的条目只是对 SRT 套接字的引用。
当一个数据包被写入发送缓冲区时,它会被添加到 SndQ 中,SndQ 会通过 CSnode 进行验证,以确保不会产生重复的条目。它会反复移除条目并在适当的位置插入新的条目。 不同 SRT 套接字中的数据包时间戳是本地的,定义为相对于当前时间的发送时间。当一个数据包被添加到 SndQ 时,它的时间戳会与当前时间进行比较,以确定其位置。
发送缓冲区和 SndQ(发送队列)的操作是解耦的。数据包被添加到缓冲区,然后通知 SndQ 有东西要发送。它们各自有自己的行为。
发送缓冲区的内容是由应用程序线程(发送者)添加的。然后有另一个线程与 SndQ 交互,它负责根据缓冲区的输入速率来控制数据包的间隔。通过在缓冲区中调整数据包的间隔来调整输出。
Packet Acknowledgement(ACKs):
在特定的时间间隔内(参考 ACK、ACKACK 和往返时间),接收方会发送一个 ACK(确认报文),这会导致发送方将被确认的数据包从其发送缓冲区中移除,从而释放缓冲区空间以供重复利用。
具体来说,ACK 包含了一个序列号,这个序列号是接收方已经成功接收的最新数据包的下一个序列号。也就是说,如果接收方最新收到的包的序列号是 n,那么 ACK 中包含的序列号就是 n+1。
在没有发生数据包丢失的情况下,这个序列号将是 ACK(n+1),表示发送方可以安全地移除序列号小于 n+1 的所有数据包,因为这些数据包已经成功到达接收方。
总结:这段话解释了在数据传输过程中,接收方通过发送 ACK 来通知发送方哪些数据包已经成功接收,以便发送方可以清理发送缓冲区,优化资源利用。当没有数据丢失时,ACK 中的序列号总是已接收数据包的最大序列号加一。
例如,如果接收方发送了针对数据包6的 ACK(见下文),这意味着所有序列号小于该序列号的数据包都已被接收,可以从发送方的缓冲区中移除。
在发生丢包的情况下,ACK(seq) 是丢失列表中第一个数据包的序列号,也就是最后连续接收的数据包的序列号加 1。
当发生数据包丢失时,接收方发送的 ACK(seq) 中的序列号 seq 是丢失列表中第一个丢失的数据包的序列号。这个序列号等于最后一个连续接收到的数据包的序列号加 1。
也就是说,如果接收方在接收数据时发现某些数据包没有按顺序到达,或者中间有数据包缺失,它会通过 ACK 通知发送方哪些数据包已经成功接收,哪些需要重传。
举个例子:
-
假设发送方发送了序列号为 1、2、3、4、5、6 的数据包。
-
接收方成功接收了序列号为 1、2、3 的数据包,但序列号为 4 的数据包丢失,随后又接收到了序列号为 5、6 的数据包。
-
由于序列号为 4 的数据包丢失,接收方无法形成连续的序列。
-
此时,接收方会发送 ACK(4),表示序列号小于 4 的数据包已成功接收,但从序列号 4 开始的数据包需要重传。
-
发送方接收到 ACK(4) 后,会知道需要重传序列号为 4 的数据包。
总结:
-
ACK(seq) 中的 seq 表示接收方期望下一个收到的数据包的序列号。
-
在没有丢包的情况下,ACK(seq) 等于已成功接收的最大序列号加 1。
-
在发生丢包的情况下,ACK(seq) 指向第一个缺失的数据包,提示发送方需要从该序列号开始重传未收到的数据包。
Packet Retransmission(NAKS) 数据包重传 :
如果数据包 #4 到达了接收方的缓冲区,但数据包 #3 没有到达,则会向发送方发送一个 NAK(否定确认)。该 NAK 被添加到一个压缩列表中(即周期性 NAK 报告),该报告会定期发送,以减少单个 NAK 本身在传输过程中可能被延迟或丢失的风险。
如果数据包 #2 到达了,但数据包 #3 没有到达,那么当数据包 #4 到达时,会立即发送一个 NAK(否定确认)来解决重排序问题( 曾尝试检测并稍微延迟发送 NAK(否定确认),以观察数据包是否会随后到达。如果数据包 3 在数据包 4 之后到达,NAK 就会被取消。但这会导致接收方延迟某些数据包的处理,而继续发送其他数据包,从而使数据包顺序错乱。选择特定的数据包并更改预期到达的顺序会引入一种抖动。 )
Packet Acknowledgment in SRT:
UDT 草案定义了一个周期性的 NAK 控制包,用于携带所有丢失数据包的列表。然而,UDT4 的实现中禁用了这一功能,并注释说明更倾向于在超时后进行重传。对于检测到的丢失数据包(即接收到后续数据包时),只会发送一次 NAK。如果这个 NAK 丢失了,ACK 将会在该数据包处阻塞,阻止更多数据包被传递给接收应用程序,直到丢失列表被清除。在发送端,由于从未收到 NAK,丢失的数据包不会被添加到丢失列表中,而针对之后丢失的数据包发送的 NAK 会阻止未被 ACK 的数据包重传。
UDT 在处理拥塞时选择了通过阻止重传直到丢失列表被清空的实现方式,这在根本上是错误的,因为首先重传丢失列表中的数据包很可能无法解除接收端的阻塞。SRT 中的后续修改(例如周期性 NAK 报告、基于时间戳的数据包传递以及过晚数据包丢弃等)减少了因 ACK 超时而进行重传的情况。
timestamp-based Packet Delivery(tsbPD):
该功能使用了 UDT 数据包头部中的时间戳。SRT TsbPD 设计的最初目的是在解码引擎的输入端再现编码引擎的输出。该设计是在没有传输上限的情况下构想的,因为数据包传输越快,丢失的数据包就会越快重传,从而实现更低的延迟。然而,SRT 原型在网络状况不佳时非常占用带宽,并且可能无限制地占用网络资源。
原始 SRT TsbPD 设计的另一个问题是受 CPU 性能限制。TS 数据包中的时间戳是基于系统生成和打包数据包的速度。如果接收方没有相同的 CPU 处理能力,则无法再现发送方的模式。
在当前版本的 SRT 中,TsbPD 允许接收方以与编码器将数据包传递给 SRT 发送方时相同的速度将数据包传递给解码器。基本上,接收到的数据包中的发送方时间戳在释放给应用程序之前会调整为接收方的本地时间(补偿时间漂移或不同时区)。SRT 可以根据配置的接收方延迟(以毫秒为单位)来延迟数据包的释放。较高的延迟可以容纳更高的均匀数据包丢失率或更大的突发数据包丢失。超过“播放时间”接收的数据包将被丢弃。
数据包时间戳(以微秒为单位)相对于 SRT 连接创建时间。原始 UDT 代码使用数据包发送时间来为数据包打时间戳。这对于 TsbPD 功能来说不合适,因为重传数据包会使用新的时间(当前发送时间),将它们插入到流中的正确位置时会导致乱序。数据包是根据头字段中的序列号进行插入的。 当应用程序首次将数据包提交给 SRT 发送方时,数据包的起始时间(以微秒为单位)已经被记录。TsbPD 功能使用此时间戳来标记数据包的首次传输以及后续的重传。这个时间戳( 时间戳还可以帮助以时间长度而不是字节或数据包来衡量恢复缓冲区。基于时间的统计信息对操作员来说更容易理解,并且不依赖于内容的比特率。)和配置的延迟一起决定了恢复缓冲区的大小和数据包在目的地交付的时间点。
UDT 协议不使用数据包时间戳,因此该更改不会影响该协议或其现有的拥塞控制方法。
Fast Retransmit:
UDT4 在超时时未能成功确认的数据包的原生重传机制并不适用于实时数据。只要丢失列表中有数据包,未被确认的数据就不会被重传。在数据包丢失呈均匀分布的情况下,当重传定时器到期时,丢失列表中通常有内容,这会引发一系列连锁反应,破坏实时数据的传输流(如拥塞窗口、发送缓冲区满、数据包丢失等问题) 。
快速重传功能通过在拥塞窗口未满之前重传未确认的数据包来解决这个问题。发送方将那些在合理时间内(基于往返时间 RTT 和丢包的原始时间戳)未收到确认的数据包加入到丢失列表中 。
快速重传减少了接收缓冲区的大小,从而降低了延迟。与在拥塞窗口已满时进行的重新重传相比,它还能使丢包计数器的变化更加平稳。然而,该功能非常耗费带宽。此 SRT 发送方的功能保留是为了与 SRT 1.0 接收方的互操作性。当周期性 NAK 报告功能激活时,快速重传很少被触发,仅作为一种监控机制保留。
Periodic NAK Reports:
SRT 1.0 在恢复超过 2% 的数据包丢失时效率不高。发送方会重传许多数据包,往往不止一次,但并不确定这些数据包是否真的丢失。在某些情况下,最大带宽开销和延迟无法支撑这样的丢包率 。
UDT4 的原生代码中禁用了周期性 NAK 报告功能。该功能在 SRT 1.1.0 接收端中被恢复,从而提高了 SRT 在高丢包环境下的功能性,并提升了其在所有丢包条件下的性能。该实现大约需要两倍于丢包率的重传带宽开销。在 SRT 配置参数的限制范围内,可以恢复高达 10% 的数据包丢失 。
SRT 的周期性 NAK 报告以 RTT/2 的周期发送,最低为 20 毫秒(而 UDT4 的设置为 300 毫秒)。NAK 控制包包含丢失数据包的压缩列表,因此只有丢失的数据包会被重传。通过将 NAK 报告周期设置为 RTT/2,可能会发生丢失的数据包被重传多次的情况,但在 NAK 包丢失时,这有助于保持低延迟 。
Too-late-Packet-Drop(过迟数据包丢弃):
此功能在 SRT 1.0.5 中引入,允许发送方丢弃那些无法及时传送的数据包。在 SRT 发送方中,当启用过迟数据包丢弃功能时,如果某个数据包的时间戳早于 SRT 延迟的 125%,则该数据包被视为无法及时传送,可能会被编码器丢弃。这样,I 帧尾部的数据包在传送之前就可以被丢弃。
最近的接收端(SRT >= 1.1.0)会阻止存在问题的发送端(SRT <= 1.0.7)在发送端启用数据包丢弃功能。最近的发送端(SRT >= 1.1.0)如果 SRT 延迟低于 1000 毫秒,则至少保留数据包 1000 毫秒(对较大的 RTT 来说不足够)。
在接收端,大型 I 帧的尾部数据包可能会非常迟到,并且不会被 SRT 接收缓冲区保留。它们会直接传递给应用程序。接收缓冲区耗尽后,如果发现有数据包丢失,已经没有时间进行重传。此时,接收端会跳过丢失的数据包。
Bidirectional Transmission Queues(双向传输队列):
SRT 还预见到了接收端有自己的传输队列的情况,并且在双向通信中,发送端有一个对应的接收队列。
可以将其理解为发送端和接收端之间的一个标准点对点 SRT 会话,其中每个节点都有一个发送(Tx)缓冲区以及一个接收(Rx)缓冲区。发送端的 Tx 与接收端的 Rx 进行通信,而接收端的 Tx 与发送端的 Rx 进行通信。与常规的单向会话一样,Tx/Rx 的延迟值必须匹配。
在握手数据包中,发送方将提供其首选的 Tx 延迟以及建议的‘对端延迟’(接收方的 Tx 延迟值)。接收方会用相应的值进行回应。提议的延迟值在一个 RTT 周期内由双方评估(并选择较大的值)。
ACKs、ACKACKs & Round Trip time:
往返时间(RTT)是指数据包来回传输所需的时间。SRT 无法直接测量单向传输时间,因此使用 RTT/2,该值是基于 ACK 计算的。接收方发送的 ACK 会触发发送方几乎无延迟地发送 ACKACK。ACK 发送并收到 ACKACK 的时间就是 RTT :
ACKACK 告诉接收方停止发送 ACK 位置,因为发送方已经知道了该信息。否则,带有过时信息的 ACK 会继续定期发送。同样地,如果发送方没有收到 ACK,它不会停止传输。
发送确认有两种条件。一种是基于 10 毫秒的定时器(ACK 周期)发送完整 ACK( ACK 间隔(数据包数量)和 ACK 周期(毫秒)曾经是可配置的。)。对于高比特率传输,可以发送‘轻量 ACK’( 轻量 ACK 是较短的 ACK(仅包含报头 + 1 个 32 位字段)。它不会触发 ACKACK ),即针对一系列数据包的 ACK。在 10 毫秒的时间间隔内,通常会发送和接收大量数据包,导致发送方的 ACK 位置无法快速前进。为了解决这个问题,在接收方收到 64 个数据包后(即使 ACK 周期尚未完全结束),会发送一个轻量 ACK。
ACK 类似于 ping,而对应的 ACKACK 类似于 pong,用于测量 RTT。每个 ACK 都有一个编号,ACKACK 对应相同的编号。接收方会在队列中保留所有 ACK 的列表以进行匹配。与包含当前 RTT 以及控制信息字段(CIF)中其他值的完整 ACK 不同,轻量 ACK 只包含序列号(详见下方图表)。所有控制消息都会直接发送,并在接收到时立即处理,但 ACKACK 的处理时间可以忽略不计(其所需时间已包含在往返时间内)。
RTT 由接收方计算,并在下一个完整 ACK 中发送。需要注意的是,SRT 会话中的第一个 ACK 可能包含初始的 100 毫秒 RTT 值,因为早期的计算可能不够精确 :
发送方始终从接收方获取 RTT。它没有类似于 ACK/ACKACK 机制的功能(即不能发送一条确保立即返回且不需要处理的消息).
Drift Management:
当发送方进入“已连接”状态时,它会通知应用程序有一个已准备好传输的套接字接口。此时,应用程序可以开始发送数据包。应用程序以一定的输入速率将数据包添加到SRT发送方的缓冲区中,然后数据包会按照预定的时间从缓冲区传输到接收方。
为了保持发送方和接收方的缓冲区水平正常,需要同步时间,同时考虑时区和往返时间(卫星链接可达2秒)。考虑到加减法的舍入误差,以及系统时间可能不同步的情况,约定的时间基准每分钟会漂移几微秒。这种漂移在多天内可能累积到发送方或接收方的缓冲区溢出或耗尽,从而严重影响视频质量。SRT有一个时间管理机制来补偿这种漂移。
当接收到一个数据包时,SRT会确定预期时间与其时间戳之间的差异。时间戳是在接收方计算的。往返时间(RTT)告诉接收方本应花费多少时间。SRT维护发送缓冲区延迟窗口前沿时间与接收方对应时间(当前时间)之间的参考关系。这允许将时间转换为实际时间,以便基于本地时间参考调度事件。接收方会采样时间漂移数据,并定期计算数据包时间戳修正因子,该修正因子通过调整数据包之间的间隔应用于每个接收到的数据包。
接收方对时间漂移数据进行采样,并定期计算数据包时间戳的修正因子,通过调整数据包之间的间隔来应用于每个接收到的数据包(这个周期是基于数据包数量而不是时间长度,以确保足够的采样数量,且与媒体流的数据包速率无关。通过使用大量采样,可以减轻网络抖动对时间漂移估计的影响。由于时间漂移非常缓慢(只有在经过数小时后才会影响流媒体),因此不需要快速反应。)。当接收到数据包时,并不会立即交给应用程序。随着时间的推移,接收方知道任何丢失或丢弃的数据包的预期时间,并可以利用这些信息用另一个数据包填补接收队列中的“空洞”。
接收方使用本地时间来调度事件,例如,确定是否该立即交付某个数据包。数据包中的时间戳只是对会话开始的引用。当接收到带有发送方时间戳的数据包时,接收方会参考会话的开始时间重新计算其时间戳。开始时间来源于会话连接时的本地时间。数据包的时间戳等于“当前时间”减去“开始时间”,其中“开始时间”是创建套接字的时间点。
Loss List:
发送方维护一个从NAK报告生成的丢包列表(loss list)。在调度传输时,它会检查丢包列表中的数据包是否有优先级,如果有优先级则优先发送。否则,它将发送SndQ列表中的下一个数据包。需要注意的是,当一个数据包被传输时,它会留在缓冲区中,以防没有被接收到。
NAK数据包被处理以填充丢包列表。随着延迟窗口的推进,发送队列中的数据包被丢弃时,会检查这些被丢弃或重传的数据包是否在丢包列表中,以确定是否可以从列表中移除它们,防止不必要的重传。发送队列和丢包列表的操作通过改变ACK位置(ACKPOS)来管理。
当ACKPOS推进到某个位置时,所有比该位置更早的数据包都会从发送队列中移除。
当接收方遇到无法成功接收下一播放数据包的情况时,它将“跳过”该数据包并发送一个伪ACK。对于发送方而言,这个伪ACK被视为真实的ACK,因此它会表现得好像数据包已被接收。这有助于发送方和接收方之间的同步。发送方并不会知道某个数据包被跳过的事实。跳过的数据包会被记录在接收方的统计信息中。
发送方所能看到的是它接收到的NAK(负确认)数据包,并且有一个计数器记录重传的数据包。如果某个数据包没有收到ACK,它将保留在丢包列表中,并可能被多次重传。丢包列表中的数据包具有优先级。
如果丢包列表中的数据包持续阻塞发送队列,最终会导致发送队列被填满。当发送队列满了时,发送方会开始丢弃数据包,甚至不会第一次发送它们。编码器(或其他应用程序)可能会继续提供数据包,但由于没有空间容纳它们,它们最终会被丢弃。SRT无法察觉这些“未发送的数据包”,这些数据包也不会在SRT统计信息中报告。
这种未发送数据包的情况并不常见。发送缓冲区中保存的数据包数量有一个上限,基于配置的延迟时间来确定。那些无法及时重传并播放的旧数据包会被丢弃,为发送应用程序产生的较新实时数据包腾出空间。当配置了低延迟时,SRT会至少等待一秒才丢弃数据包。这一秒的限制源自SRT作为传输方式时处理MPEG I帧的行为。I帧非常大(通常比其他数据包大8倍),因此需要更多时间来传输。它们可能太大,无法在延迟窗口中保留,从而导致数据包被丢弃。为防止这种情况,SRT设置了至少一秒(或延迟值)才会丢弃数据包。这允许在使用小延迟值时处理较大的I帧。