WebRTC 拥塞控制

WebRTC 拥塞控制

WebRTC 是通过增加带宽、减少数据量、提高音视频质量、适当增加时延、更准确的带宽评估等方法来提升音视频服务质量的。

在这里插入图片描述

其中,减少数据量、适当增加时延、更准确的带宽评估被统称为拥塞控制。

WebRTC 的拥塞控制算法

  1. GCC(Google Congestion Control):Google 拥塞控制。基于丢包的带宽估计,其基本思想是根据丢包的多少来判断网络的拥塞程度。丢包越多则认为网络越拥塞,发送速率就需要降低;如果没有丢包,则说明网络状况较好,可以提高发送码率以探测是否有更多的带宽可用。
  2. BBR(Bottleneck Bandwidth and Round-trip propagation time):一种基于模型的拥塞控制算法,BBR 的模型包括两个显式估计参数:Bottleneck Bandwidth(瓶颈带宽)和 Round-trip Propagation Delay(双向往返传播延迟)。BBR 使用其模型寻找具有高吞吐量的操作点和低延迟,以在最佳工作点附近工作,系统需要保持两个条件:Rate Balance(最大发送速率)和 Full Pipe(BDP = BBR.BtlBw * BBR.RTprop)。BBR 用在 QUIC 协议中,WebRTC 引入 QUIC 是为了代替现有的 SCTP。
  3. PCC(Performance-oriented Congestion Control):基于性能导向拥塞控制(无模型方法),官方介绍:pccproject

GCC 根据其实现又可以分为:

  • Transport-CC:基于发送端的延时拥塞控制算法。也是利用区间延迟值,通过 TrendLine 滤波器(最小二乘法滤波器),通过斜率的增加或者减小来判断当前网络的拥塞状况。
  • Goog-REMB:基于接收端的延时拥塞控制算法。利用延迟值,通过卡尔曼滤波器估计出下一时刻的网络带宽趋势。但效果的准确性和公平性不如 Transport-CC,目前已经被 WebRTC 的新版本所淘汰。

Transport-CC 和 REMB 主要有两个区别:

  1. 计算的端不同:REMB 是在接收端计算的,接收端计算后再将结果返回给发送端进行控制,而在回传结果时,可能网络又发生了新的变化,这就造成了 REMB 的及时性不够;Transport-CC 将拥塞评估算法从接收端移动到了发送端,是通过接收端记录数据包到达时间,构造相关 RTCP 包,然后反馈给发送端,在发送端做带宽估计,从而进行拥塞控制实现的。这样做除了方便维护,也增加了相关算法的灵活性,因为大多数处理逻辑都放到了发送端。
  2. 滤波器不同:REMB 使用卡尔曼滤波器(Kalman),TCC 使用最小二乘法滤波器(Trendline)。最小二乘法滤波器在网络延时评估这方面比卡尔曼滤波器效果更好一些。

Goog-REMB

什么是 REMB?

REMB(Receiver Estimated Max Bitrate)是一种 RTCP 反馈消息,作为接收方,告诉发送方它可以接收的带宽是多少。

这是一种简单的拥塞控制方法。发送者不知道接收方的带宽情况,它需要有一个机制由接收方告诉它有多少带宽可供传输,这样发送方可以根据这个估计的带宽来调整分辨率和帧率。

工作原理

接收端的延时拥塞控制算法 Goog-REMB 的工作原理:

在这里插入图片描述

左侧为发送端,用于控制码流的发送;右侧为接收端,用于拥塞评估和码流计算。

组成成分

GoogREMB 是基于延时的接收端拥塞控制算法,主要包括以下 5 个部分:

  1. RemoteBitrate Estimator
  2. Inter Arrival
  3. OverUse Estimator
  4. OverUse Detector
  5. AIMD Rate Controller

RemoteBitrate Estimator

RemoteBitrate Estimator 是接收端延时拥塞控制算法的管理模块。一方面与外面模块打交道,从网络收/发模块获取RTP包的传输信息用于拥塞评估,或将内部评估的下一时刻的发送码率(大小)输出给网络收/发模块,让其通知发送端进行流控;另一方面,它还要组织内部的 Inter Arrival、OverUse Estimator 等模块,根据当前观测到的延时差和之前的评估值推测出下一时刻的网络拥塞情况。

Inter Arrival

作用:将数据包按帧进⾏分组,然后对相邻的两组数据包进⾏单向梯度计算。

在 WebRTC 中,延迟梯度不是一个个包来计算的,而是通过将包分组,然后计算这些包组之间的延迟,这样做可以减少计算次数,同时减少误差。

包组的划分原则为在一个 burst_time 间隔内的一系列的包构成一个组。一般建议 burst_time 为 5ms。

计算的内容有 3 项:

  1. 每组数据包的发送时长:相邻数据包的发送时间戳之差。
  2. 每组数据包的接收时长:相邻数据包的接收时间戳之差。
  3. 两组数据包大小之差。

在这里插入图片描述

OverUse Estimator

作用:利⽤ Inter Arrival 模块的计算结果,通过卡尔曼滤波器估算出下⼀时刻发送队列的增⻓趋势。

由于网络带宽是不断变化的,我们无法直接对其进行测量(我们不能通过向网络发送探测包的方式进行测量,这样做不但得不到结果,反而会增加网络负担),所以只能通过数据包传输时延这个间接值对带宽进行估算。

从上图中我们发现:两组数据包的发送间隔比较小,而接收间隔比较大。由此可以计算出一个延时差 di = (ti - ti-1) - (Ti - Ti-1),这个值与网络状况息息相关,它是由以下几个因素产生的:

  1. 两组数据包大小之差导致的延时
  2. 发送队列变化导致的延时
  3. 传输通道的微小噪声导致的延时

卡尔曼滤波器的计算过程在此不做解释(因为我也看不懂),总之,OverUse Estimator 模块以 Inter Arrival 的计算结果作为输入,利用卡尔曼滤波器根据前面的状态值和当前的观测值推算出网络带宽 Ci 和发送队列的延时梯度 mi(即队列大小的变化量)。

OverUse Detector

作用:⽤于检测当前网络的拥塞状态。

OverUse Detector 模块利用 OverUse Estimator 模块计算出的发送队列延时梯度 mi 与自适应阈值进行比较 γi

  1. 如果 mi > γi,表示网络即将拥塞,状态为 kBwOverusing,应该减少发送端的发包量。
  2. 如果 mi < -γi,表示网络资源充足,状态为 kBwUnderusing,应该加大发送端的发包量。
  3. 否则,表明发包量与带宽匹配,状态为 kBwNormal,此时可以尝试增大发包量以抢占更多的带宽。

对于 OverUse Detector 模块而言,除了更新当前状态以外,还需要不断更新 γi 值,计算公式如下:γi = γi-1 + Δti * ki * (|mi| - γi-1)。其中,Δti = ti - ti-1,是相邻数据包的接收时间戳之差,mi 是 OverUse Estimator 模块计算出的发送队列延时梯度,ki 是一个增长系数,在不同的情况下取值不同,公式如下:ki = kd = 0.039 if |mi| < γi-1 else ku = 0.0087。可以看出当发送队列延时梯度小于自适应阈值时,ki 系数较大,γi 值减小得较快;否则,ki 系数较小,γi 将以一个较慢的速度增加。

AIMD Rate Controller

作用:计算发送码流大小。它通过 OverUse Detector 模块检测出的当前网络状态来变更⾃⼰的状态,并计算出发送码流的大小。

AIMD Rate Controller 模块的状态变化如下所示:

在这里插入图片描述

状态转换表(空白处表示保持状态):

+----+--------+-----------+------------+--------+
|     \ State |   Hold    |  Increase  |Decrease|
|      \      |           |            |        |
| Signal\     |           |            |        |
+--------+----+-----------+------------+--------+
|  Over-use   | Decrease  |  Decrease  |        |
+-------------+-----------+------------+--------+
|  Normal     | Increase  |            |  Hold  |
+-------------+-----------+------------+--------+
|  Under-use  |           |   Hold     |  Hold  |
+-------------+-----------+------------+--------+

当确定好码流控制器的状态后,就可以计算下一时刻发送端应该发送码流的大小:

在这里插入图片描述

当码流大小计算好后,AIMD Rate Controller 模块会生成 RTCP 消息包,将码流大小反馈给发送端。发送端收到消息后再进行码流控制。

Transport-CC

工作原理

发送端的延时拥塞控制算法 Transport-CC 的工作原理:

在这里插入图片描述

左侧为发送端,用于网络拥塞的评估和控制;右侧为接收端,仅需要将收到的数据包的基础信息(如丢包数、延时时间等)通过 RTCP 反馈给发送端即可。

组成成分

  • GoogCcNetworkContorller:类似于 Goog-REMB 的 RemoteBitrate Estimator 模块,是发送端延时拥塞控制算法的管理模块。当收到接收端返回的 RTCP 包后,它会根据包的内容调用调用子模块评估出下一时刻网络的拥塞状态和码流大小,然后将评估出的码流交由 Pacer 和 Encoder 进行码流控制。
  • SendSideBandwidthEstimation:比较基于发送端延时评估出的码流值、基于接收端延时评估出的码流值、基于丢包评估出的码流值的大小,取三者的最小值作为最终的码流值。
  • DelayBaseBwe:发送端的延时拥塞评估模块。由 AIMD Rate Controller、Inter Arrival、TrendLine 三部分组成。
  • AIMD Rate Controller:和 Goog-REMB 一致。
  • Inter Arrival:和 Goog-REMB 一致。
  • TrendLine:最小二乘法滤波器。

TrendLine

TrendLine 的基本思想是通过已有的观测数据,总能找到⼀条线,使得所有观测数据到这条线的误差(距离)的平⽅和最小,而这条线就是 Trendline 要求得的值。

在这里插入图片描述

从原点出发,⼀定有⼀条直线 y=k*x,使得从测量值到该直线的误差的平⽅和最小。这条直线的斜率就是数据⾛向的趋势,有时候它是上扬的,有时候它是下降的。WebRTC 发送端拥塞评估算法正是利⽤这个趋势来评估下⼀个时刻的⽹络拥塞状态的,如果斜率向上,说明线路拥塞,如果斜率向下,说明拥塞缓解。

WebRTC 可以获得每个包组接收与发送的时延差,即 di 。每隔⼀段时间(⼀个窗⼝期)就计算⼀下这段时间内所有的 di值,然后通过最小二乘法求出直线的斜率,再根据这条直线的斜率评估出下⼀时刻的⽹络状态和码流大小。

a c c i = ∑ ( d 0 + d 1 + . . . + d i ) acc_i=\sum (d_0+d_1+...+d_i) acci=(d0+d1+...+di)

y i = α ∗ y i − 1 + ( 1 − α ) ∗ a c c i y_i = \alpha * y_{i-1} + (1 - \alpha)* acc_i yi=αyi1+(1α)acci

在上述公式中,di 表示当前包组到达时长与包组发送时长之差。正常情况下该值有正有负,即当网络状况不好时该值不断增长,而网络状况变好时该值不断下降,所以长时间看di的累加值应该趋于0。WebRTC 将累计的 di 作为 yi 值,同时考虑到某个时刻包组可能出现较大的抖动,为了使yi更平滑,真正的 yi 由上面第二个式子得出。其中,α=0.9。

xi 的计算非常简单,式子为:xi = ti - first_arrival。它是当前包组最后一个包的接收时间与传输开始时第一个包的接收时间之差。你可能会觉得这个值非常大,不过没关系,因为后面的计算用的都是相对值。

有了 xi 和 yi 之后,就可以求它们的平均值了。这⾥需要注意的是,WebRTC 中是按窗口求平均值的,默认窗口大小 n = 20(该值可以动态变化)。求 x、y 的平均值的公式如下:

x ˉ = x i + x i + 1 + ⋯ + x i + n − 1 n \bar{x} = \frac{x_i + x_{i+1} + \cdots + x_{i+n-1}}{n} xˉ=nxi+xi+1++xi+n1

y ˉ = y i + y i + 1 + ⋯ + y i + n − 1 n \bar{y} = \frac{y_i + y_{i+1} + \cdots + y_{i+n-1}}{n} yˉ=nyi+yi+1++yi+n1

有了 x,y 以及它们在窗口内的平均值后,就可以通过它们找到⼀条误差平方和最小的直线,并求出它的斜率,公式如下:

在这里插入图片描述

实际上,这里求出的 ki 值与接收端延时拥塞控制算法中的mi值表达的是同⼀个含义,即在这个窗⼝期内发送队列的增长梯度。因此,像过载状态检测、目标码流的控制等都可以延⽤之前的算法,而 WebRTC 也正是这样做的。

基于丢包的拥塞评估算法的原理

上面讲的两种基于延时的拥塞评估算法,是通过一段时间内网络延时趋势来判断下一时刻网络是否会发生拥塞的算法,这种预判的方法可以有效防止拥塞的真正发生。而基于丢包的拥塞评估算法是当网络出现大量丢包后采用的一种应急手段,所以它在码流控制方面比前面两种算法要严格很多。

基于丢包的拥塞评估算法在 WebRTC 的 SendSideBandwidthEstimation 类的 UpdateEstimate() 方法中实现。规则如下:

  1. 当丢包率小于 2% 时认为网络状况较好,可以适当提高发送码率,探测是否有更多的可用带宽;
  2. 当丢包率介于 2% ~ 10% 时认为网络状况一般,此时保持与上一次相同的发送码率即可。这样可以避免一些网络固有的丢包被错判为网络拥塞而导致降低码率,而这部分的丢包则需要通过其他的如NACK或FEC等手段来恢复。
  3. 当丢包率大于 10% 时认为拥塞,此时应该主动降低发送码率减少拥塞。

具体公式如下:

A s ( t k ) = { A s ( t k − 1 ) ( 1 − 0.5 f l ( t k ) ) , f l ( t k ) > 0.1 1.08 A s ( t k − 1 ) , f l ( t k ) < 0.02 A s ( t k − 1 ) , o t h e r w i s e A_s(t_{k}) = \begin{cases} A_s(t_{k-1})(1-0.5f_l(t_k)), & f_l(t_k)>0.1 \\ 1.08A_s(t_{k-1}), & f_l(t_k)<0.02 \\ A_s(t_{k-1}), & otherwise \end{cases} As(tk)= As(tk1)(10.5fl(tk)),1.08As(tk1),As(tk1),fl(tk)>0.1fl(tk)<0.02otherwise

源码实现:

在这里插入图片描述

WebRTC 通过 RTCP协议的 RR(Receive Report)反馈报文来获取接收端的丢包率。

RR 报文格式和字段含义如下所示:

在这里插入图片描述

WebRTC 拥塞控制的一般流程

  1. 发送端启动时先设置一个初始带宽,然后发送音视频数据给接收端。
  2. 接收端定期向发送端反馈数据的延时及丢包情况(通过 RTCP)。
  3. 发送端接收延时及丢包情况,输入到拥塞评估模块评估出新的带宽。
  4. 拥塞评估模块通知编码器,让编码器重新调整码率。
  5. 拥塞评估模块同时也通知 Pacer 模块,控制码流发送速度。
  6. 不断重复上述步骤,从而实时的适应网络的情况。

在这里插入图片描述

Pacer

在视频通信中,单帧视频可能有上百 KB,如果是当视频帧被编码器编码出来后,就立即进行 RTP 打包发送,瞬时会发送大量的数据到网络上,可能会引起网络衰减和通信恶化。

WebRTC 引入 Pacer,它会根据拥塞评估模块评估出来的码率,按照最小单位时间(5ms)做时间分片进行递进发送数据,避免瞬时对网络的冲击。Pacer 的目的就是让视频数据按照评估码率均匀的分布在各个时间片里发送, 所以在弱网的 WiFi 环境,pacer 是个非常重要的关键步骤。

以下 WebRTC 中 Pacer 的模型关系:

在这里插入图片描述

WebRTC 中 Pacer 的流程分为三步:

  1. 如果一帧图像被编码和 RTP 切分打包后,先会将 RTP 报文存在待发送的队列中,并将报文元数据(packet id、size、timestamp、重传标示)送到 Pacer queue 进行排队等待发送,插入队列的元数据会进行优先级排序。
  2. Pacer timer 会触发一个定时任务事件来计算 budget,budget 会算出当前时间片网络可以发送多少数据,然后从 Pacer queue 当中取出报文元数据进行网络发送。
  3. 如果 Pacer queue 没有更多待发送的报文,但 budget 却还可以发送更多的数据,这个时候 pacer 会进行 padding 报文补充。

Pacer queue 和 优先级

Pacer queue 并不是一个先进先出的队列,而是一个基于优先级排序的多维链表。

报文优先级规则:

  1. 最先判断报文的QoS(Quality of Service,服务质量)等级,等级越小的优先级越高。
  2. 其次是判断重发标示,重发的报文比普通报文的优先级更高。
  3. 再次是判断视频帧timestamp,越早的视频帧优先级更高。

Pacer 每次触发发送事件时是先从 Pacer queue 的最前面取出优先级最高的报文进行发送,这样做的目的是让视频在传输的过程中延迟尽量小,重传的报文尽快能到达防止等待卡顿。Pacer queue 还可以设置最大延迟,如果超过最大延迟,会计算 Pacer queue 中数据发送所需要的码率,并且会把这个码率替代 target bitrate 作为 budget 参考码率来加速发送。

budget

budget 是个评估单位时间内可以发送多少数据量的一个机制,因为Pacer 是会根据 pace timer 定时来触发发送检查。budget 会根据评估出来的参考码率计算这次定时事件能发送多少字节,可以表示为:

remain_bytes = deltatime * targetbitrate / 8

其中,deltatime是上次检查时间点和这次检查时间点的时间差,targetbitrate 是 Pacer 的参考码率,是由 estimator 根据网络状态评估出来的,remain_bytes 每次触发发包时会减去发送报文的长度 size,如果 remain_bytes > 0,继续从 Pacer queue 中取下一个报文进行发送,直到 remain_bytes<=0 或者 Pacer queue 没有更多的报文。

Pacer 延迟

Pacer 产生的延迟可以表示为:delay = frame_size * 8 / bitrate。

假如评估出来的码率是10mbps,一个视频关键帧的大小是300KB,那么这个关键帧造成的Pacer delay是240ms。从实际应用观察到的关键帧引起的Pacer delay在200 ~ 400毫秒之间,这个值相对于视频传输来说是比较大的,但是不严重。

WebRTC为了减少这个延迟,会评估出尽量大的bitrate。那么怎么评估出尽量大的码率呢?从前面的estimator描述中我们知道要发送出尽量多的数据才能评估尽量大的码率,但是视频编码器不会发送多余的数据,所以WebRTC引入了padding机制来保障发送尽量大的数据来探测网络带宽上限。

padding

padding的工作原理很简单,就是在单位时间片内把budget还剩余的空闲用padding数据填满。

padding除了保障能Pacer delay尽量小外,它可以让有限的带宽获得尽可能好的视频质量。

我个人认为padding只是适合点对点通信,一旦涉及到多点分发,会因为padding占用很多服务转发带宽,这并不是一件好事情。

拥塞控制算法比较

GCC 的优点

GCC = Goog-REMB + Transport-CC。

灵敏度高,能够及时响应,提前避免拥塞。基于发送端、接收端延时和基于丢包的预估值可以很好的预测网络拥塞。

GCC 所面临的问题和难点

  1. 带宽对发送数据量的强相关:剧烈波动的发送量导致在某些场景下带宽预估的剧烈波动。波动这么剧烈的场景下,怎样在既保持带宽预估稳定的同时又不降低发送量呢?此问题在现有WebRTC的策略下很难去解决。
  2. 带宽恢复速度:GCC 算法的核心是慢升快降。这个问题现无法在算法层面去解决,如果增加恢复速度后会更容易导致一些场景下带宽高估和带宽波动。恢复速度和带宽稳定两者当前算法很难做到兼容。
  3. 带宽准确性:GCC 算法如果要适应高抖动高延时等网络变化场景,其带宽预测值会很容易下降到不可接受的范围。在一些固定丢包场景下,GCC 的带宽准确性会变得非常差。
  4. 带宽稳定性:GCC 的带宽稳定性和发送码率准确性强相关,和丢包率和延时又是强相关,所以波动范围会变化非常大。

BBR 算法的优缺点

BBR核心思路就是通过调整发送量去探测到网络的最大带宽和最小延时。

  • 最大带宽通过发送超过网络容量的数据去获取,当增加发送量后延时也开始增加时,此时就是最大带宽。
  • 最小延时通过发送低于网络容量的数据去计算,当降低发送量后延时不降低时,此时的RTT就是最小延时。

找到下图的理想点(BDP),就是我们现在可以发送的最大带宽。

在这里插入图片描述

BBR设置了一个状态机用于不断获取当前变化网络的最小RTT和最大带宽,但是探测BDP的过程只占整个周期的2%,98%的场景是用于以正常速率去传输的。

毫无疑问,BBR天生就可以解决限制带宽场景下的带宽预估稳定性和准确性问题,针对4G网络这种肥长网络模型也有对应的策略,而且其带宽预估的准确性会很好。但是发送数据的强相关性还是个问题,BBR在类似桌面共享这种场景下的发送码率变化剧烈的场景下估计适应性不会很好。

优点:

  1. Probe_RTT 阶段的隐藏弱化
  2. 上行网络丢包带宽补偿
  3. 上行网络RTT突变以及高Jitter场景优化
  4. 下行链路抖动以及丢包的优化
  5. Padding流量的优化
  6. 快速上探机制的实现

缺点:

  1. 收敛速度慢
  2. 抗丢包能力不足
  3. ProbeRTT状态只发送四个包,不适合低延时流媒体应用
  4. 发送码率周期性波动
  5. 上下行RTT和Loss相互影响(估计是有潜在bug)

PCC 算法的优缺点

PCC 的目标是开发一种性能明显优于TCP传统算法的传输协议,且保持一定的实际可部署性。这种性能的改进是根据各种网络设置的吞吐量和各种公平的度量来衡量的。其和以往传统的基于网络事件(丢包、延时变化或者抖动)的hardwired-mapping硬连接映射不同,是基于更广泛的网络条件来实时作出发送速率决策。其具有网络学习的能力,这一点是传统网络所不具备的。

PCC通过在执行期间不断以不同的速率发送数据进行A/B测试去测量网络,最终的目的是通过目标函数去进行凸优化,从而找到全局最优解,并以此速率去运行。

对比PCC和BBR,两者都是类似A/B测试去测量网络,BBR使用网络白盒建模的方式去转换表现测量;PCC使用黑盒机制,在特定速率发送时去观察表现矩阵以及效用函数,来调整发送策略。

优点:

  1. 在2018年发明的 PCC 算法的变种——vivace算法的测试性能要好于BBR,尤其是在抗丢包性能上。

缺点:

  1. 算法太新了,现在还处于理论实现的地步,业界并没有什么实际的应用。
  2. WebRTC中 的 PCC 算法存在各种问题,且使用起来效果非常不好。
  3. 目标函数采用的观测变量是 RTT,所以还是会存在上下行相互影响的场景,需要花更多的时间去专门优化。

实测 WebRTC 中的 GCC、BBR、PCC 算法

测试环境:

  • Windows平台编译WebRTC的p2p demo,对等连接
  • 网损控制使用ATC去设置
  • 有线连接

网损场景设置:

  • 带宽限制:500kbps、1mbps、2mbps
  • 丢包限制:10%、20%
  • 延时限制:100ms 200ms
  • 抖动限制:100ms 200ms
  • 混合网损模拟A:1mbps+10%+100ms延时
  • 混合网损模拟B:10%+100ms延时+100ms抖动

测试结果:

在这里插入图片描述

参考

  1. https://blog.youkuaiyun.com/qq_31231915/article/details/136174583
  2. https://zhuanlan.zhihu.com/p/635921020
  3. https://segmentfault.com/a/1190000040502265
  4. https://blog.youkuaiyun.com/qq_40179458/article/details/127873140
  5. https://blog.youkuaiyun.com/liuhongxiangm/article/details/122560409
  6. https://blog.youkuaiyun.com/yinshipin007/article/details/128500737
  7. https://www.jianshu.com/p/9061b6d0a901
  8. https://www.cnblogs.com/WillingCPP/p/16807167.html
  9. https://www.jianshu.com/p/69f6111eb3e5
  10. https://blog.youkuaiyun.com/Magic_o/article/details/130004164
  11. https://blog.youkuaiyun.com/Magic_o/article/details/130155999
  12. https://blog.youkuaiyun.com/yinshipin007/article/details/129363664
  13. https://blog.youkuaiyun.com/qw225967/article/details/120706627
  14. https://blog.youkuaiyun.com/linux_vae/article/details/100853896
  15. https://zhuanlan.zhihu.com/p/550612203
  16. https://blog.youkuaiyun.com/weixin_38102771/article/details/128218672
  17. https://www.cnblogs.com/wangyiyunxin/p/11122003.html
  18. https://blog.youkuaiyun.com/zego_0616/article/details/134878167
  19. https://zhuanlan.zhihu.com/p/448850999
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

UestcXiye

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值