WebRTC GCC 拥塞控制算法(REMB-GCC)

news2025/7/12 11:38:35

目录

一. 前言

二. REMB-GCC算法原理

1. 接收端基于延时梯度的带宽预估

(1)Arrival-time filter

(2)Overuse Detector

(3)Adaptive threshold

(4)Remote Rate Controller

(5)Remb Processing

2. 发送端基于丢包的带宽预估

三. 参考资料


一. 前言

        在网络传输中,所有应用程序都需要拥塞控制以避免网络出现拥塞的情况,对于音视频传输也是如此,假设发送方以某个码率值发送数据,但是网络出现拥塞,此时发送方应该降低发送码率,当网络拥塞情况好转,发送方又可以提升发送码率。

        GCC 算法是出自 Google 的一种结合延时梯度和丢包率的拥塞控制算法,WebRTC 默认使用该算法。

        该算法有两个版本,版本一是由接收端对延时梯度进行估计,计算预估带宽后通过 RTCP REMB 报文反馈给发送端,发送端基于丢包率也进行带宽评估,最终的发送码率选用二者的较小值,这个版本称为 REMB-GCC。

        版本二是将使用延时梯度以及丢包率进行估算的逻辑都放在发送端,接收端仅定时向发送端反馈收包情况,这个版本称为 TFB-GCC。

        本文主要讲解 REMB-GCC,关于 TFB-GCC 也会在后续文章进行讲解。

二. REMB-GCC算法原理

        如上是 REMB-GCC 的架构图,左边是发送端部分,右边是接收端部分。

        接收端负责收包并计算延时梯度,根据延时梯度评估网络当前处于 overuse/normal/underuse 状态,并评估接收端最大码率,这部分主要分为 Arrival filter,Overuse Detector,Adaptive threshold,Remote Rate Controller 以及 Remb Processing 模块。

        发送端使用丢包率进行带宽预估,最终结合接收端反馈的带宽以及基于丢包率预估的带宽选择较小值作为最终带宽,这个最终带宽会通知给 Encoder,Pacer 和 FEC 模块以供模块内部做一些调整。

1. 接收端基于延时梯度的带宽预估

        如上所示,发送端在 T(i-1) 和 T(i) 发送了两个数据包,接收端分别在 t(i-1) 和 t(i) 接收到了这两个数据包,那么延时梯度 d(t(i)) = [t(i) - t(i-1)] - [T(i) - T(i-1)]

        在理想状态下的网络传输,d(t(i)) 应该为 0,如果网络发生拥塞,T(i) 时刻发出来的包被接收端接收需要更长的时间,此时 d(t(i)) 大于 0,如果 d(t(i)) 大于 0 且越来越大,说明网络拥塞更严重,如果 d(t(i)) 大于 0 但是越来越小,说明网络拥塞状况处于好转状态。

(1)Arrival-time filter

        WebRTC 中计算延时梯度并不是以单个包为单位的,而是以一组包为单位。

        为了得到发送端两组包的时间差,发送端在发送每个 RTP 包都需要携带一个 RTP 扩展字段:abs-send-time,这个扩展记录了这个包发送时的时间信息,它的数据长度是 24bit,高 6 bit 表示秒数的整数部分,低 18 bit 表示剩余的秒数部分,这意味着它是每 64s 环绕一次并且单位精度为 3.8us,Absolute Send Time 的字段解释请看这里。

        下面是 WebRTC 计算延时梯度的代码。

bool InterArrival::ComputeDeltas(uint32_t timestamp,
                                 int64_t arrival_time_ms,
                                 int64_t system_time_ms,
                                 size_t packet_size,
                                 uint32_t* timestamp_delta,
                                 int64_t* arrival_time_delta_ms,
                                 int* packet_size_delta) {
  assert(timestamp_delta != NULL);
  assert(arrival_time_delta_ms != NULL);
  assert(packet_size_delta != NULL);
  bool calculated_deltas = false;
  if (current_timestamp_group_.IsFirstPacket()) {
    // We don't have enough data to update the filter, so we store it until we
    // have two frames of data to process.
    current_timestamp_group_.timestamp = timestamp;
    current_timestamp_group_.first_timestamp = timestamp;
    current_timestamp_group_.first_arrival_ms = arrival_time_ms;
  } else if (!PacketInOrder(timestamp)) {
    return false;
  } else if (NewTimestampGroup(arrival_time_ms, timestamp)) {
    // First packet of a later frame, the previous frame sample is ready.
    if (prev_timestamp_group_.complete_time_ms >= 0) {
      *timestamp_delta =
          current_timestamp_group_.timestamp - prev_timestamp_group_.timestamp;
      *arrival_time_delta_ms = current_timestamp_group_.complete_time_ms -
                               prev_timestamp_group_.complete_time_ms;
      // Check system time differences to see if we have an unproportional jump
      // in arrival time. In that case reset the inter-arrival computations.
      int64_t system_time_delta_ms =
          current_timestamp_group_.last_system_time_ms -
          prev_timestamp_group_.last_system_time_ms;
      if (*arrival_time_delta_ms - system_time_delta_ms >=
          kArrivalTimeOffsetThresholdMs) {
        RTC_LOG(LS_WARNING)
            << "The arrival time clock offset has changed (diff = "
            << *arrival_time_delta_ms - system_time_delta_ms
            << " ms), resetting.";
        Reset();
        return false;
      }
      if (*arrival_time_delta_ms < 0) {
        // The group of packets has been reordered since receiving its local
        // arrival timestamp.
        ++num_consecutive_reordered_packets_;
        if (num_consecutive_reordered_packets_ >= kReorderedResetThreshold) {
          RTC_LOG(LS_WARNING)
              << "Packets are being reordered on the path from the "
                 "socket to the bandwidth estimator. Ignoring this "
                 "packet for bandwidth estimation, resetting.";
          Reset();
        }
        return false;
      } else {
        num_consecutive_reordered_packets_ = 0;
      }
      assert(*arrival_time_delta_ms >= 0);
      *packet_size_delta = static_cast<int>(current_timestamp_group_.size) -
                           static_cast<int>(prev_timestamp_group_.size);
      calculated_deltas = true;
    }
    prev_timestamp_group_ = current_timestamp_group_;
    // The new timestamp is now the current frame.
    current_timestamp_group_.first_timestamp = timestamp;
    current_timestamp_group_.timestamp = timestamp;
    current_timestamp_group_.first_arrival_ms = arrival_time_ms;
    current_timestamp_group_.size = 0;
  } else {
    current_timestamp_group_.timestamp =
        LatestTimestamp(current_timestamp_group_.timestamp, timestamp);
  }
  // Accumulate the frame size.
  current_timestamp_group_.size += packet_size;
  current_timestamp_group_.complete_time_ms = arrival_time_ms;
  current_timestamp_group_.last_system_time_ms = system_time_ms;

  return calculated_deltas;
}

        current_timestamp_group_ 是当前收包的这组包的信息,prev_timestamp_group_ 为上一组收齐的包的信息,它们的类型为 TimestampGroup,字段含义解释如下。

        首先如果是这组包的第一个包,先记录 first_timestamp,first_arrival_ms 等信息,并更新这组包的 size,timetamp,complete_time_ms,last_system_time_ms,之后再收到包会先判断该包的发送时间戳是否大于这组包首包的发送时间戳,如果是则会统计到这组包中,更新 timestamp,size,complete_time_ms 和 last_system_time_ms 信息,否则忽略该包。

        如果判断到 NewTimestampGroup(arrival_time_ms, timestamp),说明该包已经是新的一组包了,此时计算 current_timestamp_group_ 与 prev_timestamp_group_ 的接收时间差(arrival_time_delta_ms),发送时间差(timestamp_delta)以及组包间的包大小差值(packet_size_delta),之后送入 OveruseEstimator 判断当前延时梯度

备注:包属于新的一组包的判断逻辑是当前包的发送时间戳与这组包首包发送时间戳绝对时间大于 5ms,之所以这样判断是因为 WebRTC 发送端有个平滑发送模块,它的发送时间间隔是 5ms 发送一批包。

        OveruseEstimator::Update 函数用于估计延时梯度,它并不是直接使用上一次计算得到的延时梯度值,而是将延时梯度传入该函数,通过卡尔曼滤波算法后得到延时梯度值,本文不讲解卡尔曼滤波算法细节,后续文章会单独说明该算法实现。

void OveruseEstimator::Update(int64_t t_delta,
                              double ts_delta,
                              int size_delta,
                              BandwidthUsage current_hypothesis,
                              int64_t now_ms) {
    // 代码省略,详见 src/modules/remote_bitrate_estimator/overuse_estimator.cc
}

(2)Overuse Detector

        OveruseDetector::Detect 函数会判断当前带宽的使用状态,它根据延时梯度 T 与动态阈值的大小关系判断当前处于链路带宽处于 overuse/normal/underuse 状态,动态阈值 threshold_ 将在下一小节 Adaptive threshold 中讲解。

a. 如果 T > threshold_,说明网络网络拥塞队列在增大,目前处于拥塞状态,如果拥塞持续时间大于 overusing_time_threshold_,并且延时梯度比上一次延时梯度大,判断处于 overuse 状态,注意不是一旦大于阈值就判断处于 overuse,需要持续一段时间并且延时梯度在变大才判断处于 overuse

b. 如果 T < -threshold_,说明网络拥塞队列在变小,拥塞情况在改善,判断处于 underuse 状态

c. 如果 -threshold_ <= T <= threshold_,判断处于 normal 状态

(3)Adaptive threshold

        如上所述,过载检测器通过比较延时梯度与阈值的大小关系判断当前的带宽使用状况,理想网络情况下延时梯度为 0,但是即便正常的带宽占用情况下,延时梯度也可能在 0 上下波动,因此阈值的设置很重要,如果阈值是固定值,设置太大可能检测不到网络拥塞,设置太小可能又太过敏感,因此 WebRTC 使用了一种自适应动态阈值的方式。

计算方式:threshold(t(i)) = threshold(t(i-1)) + k * [t(i) - t(i-1)] * ( | d(t(i)) | - threshold(t(i-1)) )

其中 k 表示变化率,当 | d(t(i)) | < threshold(t(i-1)) 时,k 值为 0.00018,否则 k 值为 0.01,

threshold(t(i)) 表示当我们计算第 i 组包后需要新确定的阈值,threshold(t(i-1)) 表示计算第 i-1 组包后确定的阈值,t(i) - t(i-1) 表示两组包计算延时梯度的时间差,d(t(i)) 表示当前算出的延时梯度。

        从计算方式可以看到,当处于 normal 状态时,阈值会以一个较小的变化率减小,当处于 overuse/underuse 时,阈值会以一个相对大一点的变化率增大。

(4)Remote Rate Controller

        通过 Overuse Detector 确定当前处于 overuse/normal/underuse 状态后,需要根据当前状态对最大码率值做出调整,如下图所示。

当处于 overuse 状态,对应处于 Decr 状态,此时应该降低最大码率值,并且降低为原来最大码率值的 0.85 倍

当处于 underuse 状态,对应 Hold 状态,此时应该维持当前最大码率不变

当处于 normal 状态,对应 Incr,此时可以适当增大码率,增大为原来最大码率值的 1.08 倍

        WebRTC 对应的 remote rate controller 调整最大码率的代码如下。

void AimdRateControl::ChangeBitrate(const RateControlInput& input,
                                    Timestamp at_time) {
  absl::optional<DataRate> new_bitrate;
  DataRate estimated_throughput =
      input.estimated_throughput.value_or(latest_estimated_throughput_);
  if (input.estimated_throughput)
    latest_estimated_throughput_ = *input.estimated_throughput;

  // An over-use should always trigger us to reduce the bitrate, even though
  // we have not yet established our first estimate. By acting on the over-use,
  // we will end up with a valid estimate.
  if (!bitrate_is_initialized_ &&
      input.bw_state != BandwidthUsage::kBwOverusing)
    return;

  ChangeState(input, at_time);

  // We limit the new bitrate based on the troughput to avoid unlimited bitrate
  // increases. We allow a bit more lag at very low rates to not too easily get
  // stuck if the encoder produces uneven outputs.
  const DataRate troughput_based_limit =
      1.5 * estimated_throughput + DataRate::KilobitsPerSec(10);

  switch (rate_control_state_) {
    case kRcHold:
      break;

    case kRcIncrease:
      if (estimated_throughput > link_capacity_.UpperBound())
        link_capacity_.Reset();

      // Do not increase the delay based estimate in alr since the estimator
      // will not be able to get transport feedback necessary to detect if
      // the new estimate is correct.
      // If we have previously increased above the limit (for instance due to
      // probing), we don't allow further changes.
      if (current_bitrate_ < troughput_based_limit &&
          !(send_side_ && in_alr_ && no_bitrate_increase_in_alr_)) {
        DataRate increased_bitrate = DataRate::MinusInfinity();
        if (link_capacity_.has_estimate()) {
          // The link_capacity estimate is reset if the measured throughput
          // is too far from the estimate. We can therefore assume that our
          // target rate is reasonably close to link capacity and use additive
          // increase.
          DataRate additive_increase =
              AdditiveRateIncrease(at_time, time_last_bitrate_change_);
          increased_bitrate = current_bitrate_ + additive_increase;
        } else {
          // If we don't have an estimate of the link capacity, use faster ramp
          // up to discover the capacity.
          DataRate multiplicative_increase = MultiplicativeRateIncrease(
              at_time, time_last_bitrate_change_, current_bitrate_);
          increased_bitrate = current_bitrate_ + multiplicative_increase;
        }
        new_bitrate = std::min(increased_bitrate, troughput_based_limit);
      }

      time_last_bitrate_change_ = at_time;
      break;

    case kRcDecrease: {
      DataRate decreased_bitrate = DataRate::PlusInfinity();

      // Set bit rate to something slightly lower than the measured throughput
      // to get rid of any self-induced delay.
      decreased_bitrate = estimated_throughput * beta_;
      if (decreased_bitrate > current_bitrate_ && !link_capacity_fix_) {
        // TODO(terelius): The link_capacity estimate may be based on old
        // throughput measurements. Relying on them may lead to unnecessary
        // BWE drops.
        if (link_capacity_.has_estimate()) {
          decreased_bitrate = beta_ * link_capacity_.estimate();
        }
      }
      if (estimate_bounded_backoff_ && network_estimate_) {
        decreased_bitrate = std::max(
            decreased_bitrate, network_estimate_->link_capacity_lower * beta_);
      }

      // Avoid increasing the rate when over-using.
      if (decreased_bitrate < current_bitrate_) {
        new_bitrate = decreased_bitrate;
      }

      if (bitrate_is_initialized_ && estimated_throughput < current_bitrate_) {
        if (!new_bitrate.has_value()) {
          last_decrease_ = DataRate::Zero();
        } else {
          last_decrease_ = current_bitrate_ - *new_bitrate;
        }
      }
      if (estimated_throughput < link_capacity_.LowerBound()) {
        // The current throughput is far from the estimated link capacity. Clear
        // the estimate to allow an immediate update in OnOveruseDetected.
        link_capacity_.Reset();
      }

      bitrate_is_initialized_ = true;
      link_capacity_.OnOveruseDetected(estimated_throughput);
      // Stay on hold until the pipes are cleared.
      rate_control_state_ = kRcHold;
      time_last_bitrate_change_ = at_time;
      time_last_bitrate_decrease_ = at_time;
      break;
    }
    default:
      assert(false);
  }

  current_bitrate_ = ClampBitrate(new_bitrate.value_or(current_bitrate_));
}

(5)Remb Processing

        接收端根据 Remote Rate Controller 确定好新的最大接收码率值后需要通过 RTCP REMB 报文反馈给发送端。

        REMB 报文使用的是 PT=206,FMT=15 的 RTCP 报文,具体格式如下。

SSRC of packet sender:接收端反馈该报文时,该值可以填为 0

SSRC of media source:一般都为 0

Unique identifier:REMB 报文的特殊标识,固定为 0x52454D42

Num SSRC:SSRC feedback 字段中 SSRC 的个数

BR Exp,BR Mantissa:二者共同表示反馈的最大码率值,Exp 表示指数部分,Mantissa 表示尾数部分

SSRC feedback:发送端发送的流的 SSRC 列表

        WebRTC 中 RTCP REMB 报文一般是每 200ms 反馈一次,但是当检测到可用带宽小于上次预估的 97% 时则会立刻反馈。

2. 发送端基于丢包的带宽预估

        发送端基于丢包的带宽预估思想主要是根据丢包率来判断是否拥塞。

        当丢包率大于 10% 时认为拥塞,此时应该主动降低发送码率减少拥塞;当丢包率小于 2% 时认为网络状况较好,可以适当提高发送码率,探测是否有更多的可用带宽;当丢包率介于 2% ~ 10% 时认为网络状况一般,此时保持与上一次相同的发送码率即可。

        对于丢包率的获取,发送端通过 RTCP RR 报文的 fraction lost 来判断,它描述了从上一次 RR 报文发送后到本次 RR 报文期间的丢包率。

         对于目标码率的调整方式,WebRTC 处理代码如下所示。 

        至此 REMB-GCC 接收端和发送端预估码率并确定最终码率的逻辑已经讲解完成,REMB-GCC 算法目前已经被 Google 放弃维护,因为其预估逻辑分布在发送端和接收端,Google 又推出了 TFB-GCC 以取代 REMB-GCC,TFB-GCC 接收端仅需要定期反馈收包的状况,不需要进行带宽预估。预估带宽的逻辑都放在发送端计算,带宽预估的原理基本类似,后续会出一篇文章讲解 TFB-GCC。

三. 参考资料

A Google Congestion Control Algorithm for Real-Time Communication

RTCP message for Receiver Estimated Maximum Bitrate

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/17880.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【SQL】之索引

【SQL】之索引简单的索引设计方式innodb中的索引设计方式迭代一次迭代两次迭代三次btree聚簇索引二级索引&#xff08;非聚簇&#xff09;联合索引&#xff08;非聚簇&#xff09;InnoDB的B树索引的注意事项myISAM中索引设计方案索引是帮助mysql高效获取数据的数据结构简单的索…

查询

一、顺序查询 普通查找方式&#xff1a; int SeqSearch(int a[],int n,int k) {int i 0;while (i < n && a[i] ! k)i;if (i > n)return 0;elsereturn i 1; } 优化版查找方式&#xff1a; int OPSeqSearch(int a[], int n, int k) {int i 0;a[n] k;while …

全志A33使用主线U-Boot方法

最近在研究A33主线相关的资源&#xff0c;目前主线uboot和内核都对A33有极好的支持了&#xff0c;所以现在把我在使用过程中遇到的问题和使用方法做个记录&#xff0c;首先是下载主线uboot源码&#xff0c;网址为https://ftp.denx.de/pub/u-boot/&#xff0c;我下载的版本为202…

点云 ICP学习-IterativeClosestPoint

目录 一、pcl中 点云配准算法 二、关于svd原理求解部分 三、pcl IterativeClosestPoint 完成demo 一、pcl中 点云配准算法 PCL 库中 ICP 的接口及其变种&#xff1a; 点到点&#xff1a;pcl::IterativeClosestPoint< PointSource, PointTarget, Scalar >点到面&…

RocketMQ——Mac电脑OS系统docker安装Dashboard

文章目录引言安装下载dashboard镜像docker pull镜像查看镜像运行容器启动容器查看容器日志问题解决方案解决方案说明登录dashboard界面关注微信公众号&#xff1a;CodingTechWork&#xff0c;一起学习进步。引言 前面的文章已经介绍过如何在OS系统上安装并启动使用RocketMQ&…

Canal 安装与入门

MySQL Binlog 简介 https://blog.csdn.net/weixin_44371237/article/details/127904514 MySQL 主从复制过程 1&#xff09;Master 主库将改变记录&#xff0c;写到二进制日志(Binary Log)中&#xff1b; 2&#xff09;Slave 从库向 MySQL Master 发送 dump 协议&#xff0c…

基于QT的考试管理系统设计与实现

目录 一、项目概要 4 1.1项目名称 4 1.2项目目标 4 1.3软件概要 4 1.4功能描述 5 1.5开发环境 5 1.6关键技术 6 1.7开发体制 6 1.8开发阶段 6 二、软件详细需求 7 2.1学生登陆主界面 7 2.2管理员登陆主界面 8 2.3 学生考试系统实现 9 2.4学生练习系统实现 10 2.5试题管理系统实…

mongoDB mapreduce使用总结

大家都知道&#xff0c;mongodb是一个非关系型数据库&#xff0c;也就是说&#xff0c;mongodb数据库中的每张表是独立存在的&#xff0c;表与表之间没有任何依赖关系。在mongodb中&#xff0c;除了各种CRUD语句之外&#xff0c;还给我们提供了聚合和mapreduce统计的功能&#…

JVM 彻底搞懂JVM内存区域及直接内存

面试题&#xff1a;直接内存会导致OOM么&#xff1f; 程序计数器 代表当前线程所执行的字节码所在的行号&#xff0c;配合字节码解释器获取下一条需要执行的字节码指令。 代码中的分支、循环、跳转、异常处理、线程恢复都要依靠它来实现。 程序计数器是线程私有的&#xff0…

进程控制的一些具体操作

目录进程控制进程终止进程退出的方式进程等待进程等待的方法wait使用方法waitpid使用方法进程程序替换替换函数execl函数execv函数execlp函数execvp函数execle函数execve函数---->只有这一个是系统调用&#xff0c;其他都是库函数execvpe函数补充几个知识: %s/被替换的文件…

代码随想录——冗余连接II(并查集)

题目 在本问题中&#xff0c;有根树指满足以下条件的 有向 图。该树只有一个根节点&#xff0c;所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都有且只有一个父节点&#xff0c;而根节点没有父节点。 输入一个有向图&#xff0c;该图由一个有着 n 个节点&am…

vb.net自定义白板

希沃白板在学校里基本上是一直使用的&#xff0c;但是在非希沃电脑里面是没有启动白板的 简答介绍思路和具体的功能 1、背景颜色和画笔颜色自由切换、画笔粗细1~20可以调节。 2、画笔样式&#xff1a;虚线、点线、短线 3、基本图形&#xff1a;矩形&#xff0c;正方形&…

程序员级别分析,看看你是哪个级别

关于程序员的工资众说纷纭&#xff0c;有说开七八千的&#xff0c;也有人说每月上万的&#xff0c;但不管怎么说&#xff0c;程序员的工资是真的比一些文职、行政人员岗位挣得多&#xff0c;大家都是靠自己的能力赚钱&#xff0c;这没什么可比的&#xff0c;况且大家都是在学习…

JAVASE零基础到高级教程(1)------ 集成开发环境安装使用

一 什么是环境变量 环境变量是在操作系统中⼀个具有特定名字的对象&#xff0c;它包含了⼀个或者多个应⽤程序所将使⽤到的 信息。例如Windows和DOS操作系统中的path环境变量&#xff0c;当要求系统运⾏⼀个程序⽽没有告诉它程 序所在的完整路径时&#xff0c;系统除了在当前⽬…

前端框架 Electron 使用总结

目录 一、基础搭建 通过脚手架搭建 1、Electron官方案例搭建环境 2、查看调试 3、菜单的使用 4、图标配置 5、项目打包 web应用相信每位程序员都不陌生&#xff0c;PC端应用可能会底层开发的就不是太多了&#xff0c;下面的这套技术栈就是为前端程序员快速一键搭建windo…

Linux学习——网络编程基础及TCP服务器

目录 一、网络采用分层的思想&#xff1a; 二、各层典型的协议&#xff1a; 三、网络的封包和拆包&#xff1a; 四、网络编程的预备知识 4.1.SOCKET 4.2 IP地址 4.3 端口号 4.4 字节序 五、TCP编程API TCP协议分成了两个不同的协议&#xff1a;可靠传输&#xff1a;用来检测网络…

读书笔记-学习GNU Emacs-3终篇

学习本书目的&#xff1a; emacs的学习一直是陆陆续续看博客和上手实践&#xff0c;这次想通过阅读"学习GNU Emacs"这本书好好系统的再复习下emacs。 yps:读技术书应该是带着一定的目的去读的&#xff0c;最简单的目的可能就是为了学好某一项技术或者复习下某一项技…

[附源码]java毕业设计社区健康服务平台管理系统lunwen

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

IDEA利用maven建立javaWeb项目(IDEA 2021.3.3)

1、在Idea中配置maven (1)、打开Idea&#xff0c;点击File&#xff0c;然后点击Settings&#xff0c;进入设置&#xff0c;或者直接按CtrlAltS进入设置 (2)、先在左上角的搜索框输入maven&#xff0c;找到maven后单击&#xff0c;然后在右边的maven home path的右边选择你的m…

置信度--学习笔记

置信区间是衡量测量精度的一个指标&#xff0c;也能显示出估算有多稳定&#xff0c;也就是说如果重复做某项实验&#xff0c;得到的结果与最初的估计有多接近。步骤&#xff1a; 确定要测试的情况&#xff1a;如“A大学男生的平均体重是80公斤”&#xff0c;则后续就是要测试在…