深入浅出WebRTC—ULPFEC

news2024/11/16 0:50:51

FEC 通过在发送端添加额外的冗余信息,使接收端即使在部分数据包丢失的情况下也能恢复原始数据,从而减轻网络丢包的影响。在 WebRTC 中,FEC 主要有两种实现方式:ULPFEC 和 FlexFEC,FlexFEC 是 ULPFEC 的扩展和升级,两者被纳入同一个实现框架中。本文主要分析 ULPFEC 实现,重点关注 FEC 的实现原理,尽量避开繁杂的实现细节。

1. 静态结构

1)RtpVideoSender 是发送端控制中心,其接收码率更新的通知,使用 FecController 生成 FEC 保护比率,并将 FEC 保护比率设置到 VideoFecGenerator,控制 FEC 保护码率大小。

2)FecController 根据 RtpVideoSender 设置的保护模式(NACK/FEC)来决定选择什么样的保护方法:VCMNackMethod、VCMFecMethod 或 VCMNackFecMethod。基于 RtpVideoSender 更新的目标码率、帧率、丢包率等一系列参数计算得到 FEC 保护比率,

3)VideoFecGenerator 根据 RtpVideoSender 设置的保护比率(还有其他参数,具体参考 FecProtectionParams)生成 FEC 报文。

ULPFEC 和 FlexFEC 是两种不同的 FEC 实现,复用相同的 FEC 处理框架,如下图所示,通过 FlexfecSender 可以看到, FlexFEC 是 ULPFEC 的一个扩展。

2. 流程框架

FEC 框架可以分为发送端和接收端两部分,发送端实现较为复杂,又可以分为参数计算、数据生成和数据发送三个部分;接收端实现相对简单,主要就是做数据恢复。

下图展示了 FEC 发送端相关实现逻辑。

1)参数计算:当估计带宽发生变化或丢包率发生变化时,需要重新计算生成多少比例的 FEC 码率来保护原始码率。码率分配器会通知 VideoSendStream 新的目标码率、丢包率、RTT等参数。此时,会触发 RtpVideoSender 重新计算编码码率和 FEC 保护比率。编码码率会设置到编码器,控制编码器的码率输出。FEC 保护比率会设置到 RTPSenderEgress,用来控制 FEC 报文的生成。

2)数据生成:PacingController 负责平滑发送报文,报文最终通过 RtpSenderEgress 发送到网络,在发送报文的同时,会调用 VideoFecGenerator(UlpfecGenerator::AddPacketAndGenerateFec) 生成 FEC 报文。生成的 FEC 报文会先保存在 VideoFecGenerator。

3)数据发送:PacingController 每次发送完报文后,都会调用 PacketRouter::FetchFec 从 VideoFecGenerator 中拉取所有生成的 FEC 报文,并将这些报文插入发送队列,和普通媒体报文一起发送出去。

以下代码是码率更新时的处理逻辑,总体逻辑非常清晰,重新计算编码码率和保护码率,并把编码码率设置到编码器,实现对编码器输出码率的控制,保护码率的控制在 OnBitrateUpdated 内部实现。

uint32_t VideoSendStreamImpl::OnBitrateUpdated(BitrateAllocationUpdate update) {
  if (update.stable_target_bitrate.IsZero()) {
    update.stable_target_bitrate = update.target_bitrate;
  }

  // 计算编码码率和保护码率(内部还会计算 FEC 比率)
  rtp_video_sender_->OnBitrateUpdated(update, stats_proxy_->GetSendFrameRate());

  // 获取编码码率
  encoder_target_rate_bps_ = rtp_video_sender_->GetPayloadBitrateBps();

  // 获取保护码率
  const uint32_t protection_bitrate_bps =
    rtp_video_sender_->GetProtectionBitrateBps();

  ...

  // 更新编码器目标码率(包括其他一系列参数)
  video_stream_encoder_->OnBitrateUpdated(
    encoder_target_rate, 
    encoder_stable_target_rate, 
    link_allocation,
    rtc::dchecked_cast<uint8_t>(update.packet_loss_ratio * 256),
    update.round_trip_time.ms(), 
    update.cwnd_reduce_ratio);

  return protection_bitrate_bps;
}

以下是设置到 VideoFecGenerator 的 FEC 参数结构体。

struct FecProtectionParams {
	// FEC 报文数量 / 原始报文数量
	int fec_rate = 0;
	// 一组 FEC 报文保护原始报文最大帧数量
	int max_fec_frames = 0;
	// 固定为随机丢包类型
	FecMaskType fec_mask_type = FecMaskType::kFecMaskRandom;
};

3. 初始化

WebRTC 支持配置是否启用 FEC 和 NACK。在 RtpVideoSender 的构造函数中,会根据相关配置来决定是否创建 FEC 生成器,是创建 UlpfecGenerator 还是 FlexfecGenerator。

是否开启 FEC 和 NACK 会设置到 FecControllerDefault,一般情况,FEC 和 NACK 都会启用。

fec_controller_->SetProtectionMethod(fec_enabled, NackEnabled());

FecControllerDefault 根据设置的参数,确定当前可以使用什么保护方法,不同保护方法会有不同的保护策略。

void FecControllerDefault::SetProtectionMethod(bool enable_fec, bool enable_nack) {
  media_optimization::VCMProtectionMethodEnum method(media_optimization::kNone);
  if (enable_fec && enable_nack) {
    method = media_optimization::kNackFec;
  } else if (enable_nack) {
    method = media_optimization::kNack;
  } else if (enable_fec) {
    method = media_optimization::kFec;
  }
  MutexLock lock(&mutex_);
  loss_prot_logic_->SetMethod(method);
}

4. 参数计算

4.1. 编码码率

编码码率计算比较有意思,正常思路是计算新的保护码率,用估计码率减去保护码率剩下的就是编码码率。但这里不是这样干的,直接等于新目标码率减去基于历史数据统计的当前保护码率,代码如下所示。虽然这样计算出来的编码码率具有滞后性,由于新的 FEC 保护比率已经更新,真实保护码率会基于新的 FEC 保护比率进行调整,定时器会驱动不断调用 UpdateFecRates,最终会将编码码率调整到位。

uint32_t FecControllerDefault::UpdateFecRates(uint32_t estimated_bitrate_bps,
  int actual_framerate_fps, uint8_t fraction_lost, std::vector<bool> loss_mask_vector,
  int64_t round_trip_time_ms) {

  ...

  // 设置 FEC 参数到 RtpSenderEgress,同时获取当前几个发送速率
  protection_callback_->ProtectionRequest(
    &delta_fec_params, 
    &key_fec_params, 
    &sent_video_rate_bps,
    &sent_nack_rate_bps, 
    &sent_fec_rate_bps);

  // 计算当前总发送速率
  uint32_t sent_total_rate_bps =
    sent_video_rate_bps + sent_nack_rate_bps + sent_fec_rate_bps;

  // 保护开销(比率)保持不变
  if (sent_total_rate_bps > 0) {
    protection_overhead_rate =
      static_cast<float>(sent_nack_rate_bps + sent_fec_rate_bps) / sent_total_rate_bps;
  }

  // 不超过 50%
  protection_overhead_rate =
    std::min(protection_overhead_rate, overhead_threshold_);

  // 编码码率等于估计码率减去当前统计的保护码率
  return estimated_bitrate_bps * (1.0 - protection_overhead_rate);
}

4.2. 保护比率

FEC 保护比率的计算非常复杂,计算过程分为两步,第一步是基于码率和丢包率查表,如下表所示,这是基于 kFecRateTable 数组绘制的可视化表格,由于数据量太大,省略了大部分数据。

表格中每一行代表一个码率,分 0 - 49 共 50 个级别,对应 30FPS 的码率范围从200kbps 到 8000kbps。每一列代表一个丢包率,分 0 - 128 共 129 个级别,对应丢包率从 0 到 50%。查表时,将码率需要转换为某个码率级别,将丢包率转换为某个丢包率级别,较差位置的数字即为 FEC 保护比率。

第二步就是对查表得到 FEC 保护比率进行调整,代码如下所示。如果 RTT 很小,则关闭非关键帧的 FEC,优先使用 NACK,但关键帧还是继续使用 FEC 保护。

bool VCMNackFecMethod::ProtectionFactor(const VCMProtectionParameters* parameters) {
  // 计算 FEC 比率,设置 _protectionFactorK 和 _protectionFactorD
  VCMFecMethod::ProtectionFactor(parameters);

  if (_lowRttNackMs == -1 || parameters->rtt < _lowRttNackMs) {
    // 低 RTT 场景(RTT < 20ms),非关键帧不使用 FEC(保护因子为0)
    _protectionFactorD = 0;
    VCMFecMethod::UpdateProtectionFactorD(_protectionFactorD);
  } else if (_highRttNackMs == -1 || parameters->rtt < _highRttNackMs) {
    // 中等 RTT 场景
    VCMFecMethod::UpdateProtectionFactorD(_protectionFactorD);
  }

  return true;
}

5. 生成 FEC 数据

5.1. 计算条件

在生成 FEC 报文之前,需要确定使用多少原始报文以及使用哪些原始报文,WebRTC 对此设计了几个约束:

1)最少报文数量

报文数量不能太少了,由 min_num_media_packets_ 决定,由 MinimumMediaPacketsReached() 做出限制。

2)帧边界限制

等到帧结束标志时才会计算 FEC。尽量保证对于一个完整的帧,其保护策略是一致的。

3)最大帧数量

这是一个保护条件,记录的帧数量超过设定值,不再管其他数量限制了,必须计算 FEC。

4)开销误差限制

FEC 保护比率决定 FEC 开销,但由于四舍五入等计算精度问题,使得 FEC 目标保护比率和 FEC 真实保护比率会有一定误差。当原始报文数量较多时,这个差异会比较小,原始报文较少时,这个差异可能会很大。假设 FEC 目标保护比率为 10%,原始报文数量是 10 个,生成目标 FEC 报文为 10 * 10% = 1 个,真实计算时会进行 Q8 格式转换,向上取整,强制至少 1 个 FEC 报文等,可能计算需要 2 个 FEC 报文,此时,目标保护比率和计算得到的保护比率达到了 100%,远超设置的最大误差,这是不行的。

void UlpfecGenerator::AddPacketAndGenerateFec(const RtpPacketToSend& packet) {
  {
    MutexLock lock(&mutex_);
    // 如果有等待更新的FEC参数,则更新当前参数并清除待更新标记。
    if (pending_params_) {
      current_params_ = *pending_params_;
      pending_params_.reset();
      // FEC 比率 > 31.4%(80/255),至少需要 4 个包才能计算 FEC
      if (CurrentParams().fec_rate > kHighProtectionThreshold) {
        min_num_media_packets_ = kMinMediaPackets;
      } else { // 否则,允许至少1个媒体包参与FEC计算。
        min_num_media_packets_ = 1;
      }
    }
  }

  // 记录当前分组中包含关键帧
  if (packet.is_key_frame()) {
    media_contains_keyframe_ = true;
  }

  // 读取 RTP 头的 marker 标志
  const bool complete_frame = packet.Marker();

  // 将用来计算 FEC 报文的原始报文缓存起来,ulpfec 的 mask 最多记录 48 个报文
  if (media_packets_.size() < kUlpfecMaxMediaPackets) {
    auto fec_packet = std::make_unique<ForwardErrorCorrection::Packet>();
    fec_packet->data = packet.Buffer();
    media_packets_.push_back(std::move(fec_packet));
    last_media_packet_ = packet; // 用于复制 RTP 头
  }

  // 记录帧数量
  if (complete_frame) {
    ++num_protected_frames_;
  }

  auto params = CurrentParams();

  if (complete_frame &&
    // 已经保护足够多的帧
    (num_protected_frames_ >= params.max_fec_frames ||
    // 实际开销与目标开销之差小于最大允许偏差,并且已经收集到足够数量的媒体包
    (ExcessOverheadBelowMax() && MinimumMediaPacketsReached()))) {

    constexpr int kNumImportantPackets = 0;

    // 为什么不使用 unequal protection?
    constexpr bool kUseUnequalProtection = false;

    // FEC 编码
    fec_->EncodeFec(media_packets_, 
      params.fec_rate, 
      kNumImportantPackets,
      kUseUnequalProtection, 
      params.fec_mask_type,
      &generated_fec_packets_);
  }
}

5.2. 掩码生成

由于 WebRTC 的 FEC 采用的是 Charity Code,虽然有了 FEC 比率,但安排哪个 FEC 报文去保护哪几个原始报文,即如何确定 FEC 报文的掩码表,也是一个头疼的事情。

掩码的设置非常灵活,针对随机丢包和突发丢包,WebRTC 提前准备了两张掩码表:kPacketMaskRandomTbl和kPacketMaskBurstyTbl,多少原始报文,生成多少 FEC 报文,直接查表就能得到每个 FEC 报文的掩码,大大简化了掩码的生成过程,提高了程序处理效率。

WebRTC 目前只使用 kPacketMaskRandomTbl 掩码表,如下所示。“kPacketMaskRandomX”中 X 表示原始包文数量,可以看到此表最多覆盖 12 个原始报文。

const uint8_t kPacketMaskRandomTbl[] = {
    12,
    kPacketMaskRandom1,  // 2 byte entries.
    kPacketMaskRandom2,
    kPacketMaskRandom3,
    kPacketMaskRandom4,
    kPacketMaskRandom5,
    kPacketMaskRandom6,
    kPacketMaskRandom7,
    kPacketMaskRandom8,
    kPacketMaskRandom9,
    kPacketMaskRandom10,
    kPacketMaskRandom11,
    kPacketMaskRandom12,
};

以 kPacketMaskRandom3 为例,kMaskRandom3_1 表示 3 个原始报文生成 1 个 FEC 报文,FEC 保护比率为 33%;kMaskRandom3_2 表示 3 个原始报文生成 2 个 FEC 报文,FEC 保护比率为 66%;kMaskRandom3_3 表示 3 个原始报文生成 3 个 FEC 报文,FEC 保护比率为 100%。表项已经到头了,WebRTC 允许最大的 FEC 保护比率为 100%。

#define kPacketMaskRandom3 3, \
  kMaskRandom3_1, \
  kMaskRandom3_2, \
  kMaskRandom3_3

以 kMaskRandom3_3 为例,每一行对应一个 FEC 报文的掩码,取前 3bits。

#define kMaskRandom3_3 \
  0xc0, 0x00, \
  0xa0, 0x00, \
  0x60, 0x00

5.3. 掩码应用

下面以 12个 原始媒体报文使用 4 个 FEC 报文的随机保护为例,讲解掩码的应用,查表结果如下:

#define kMaskRandom12_4 \
  0x8b, 0x20, \
  0x14, 0xb0, \
  0x22, 0xd0, \
  0x45, 0x50

转换为二进制如下所示,灰色填充部分为未启用 bit:

保护逻辑示意图如下所示,实线框为原始媒体报文,虚线框为 FEC 报文:

如果增加一个原始媒体报文,则超过12个限制,不能再查表了,掩码需要由程序代码动态生成。生成逻辑比较简单:每个 FEC 报文只保护“索引对 FEC 报文总数取模与 FEC 报文索引相等”的原始媒体报文,生成的掩码如下图所示:

保护逻辑示意图如下所示。0 号 FEC 报文保护 0、4、8、12 号原始报文;1 号 FEC 报文保护 1、5、9 号原始报文;2 号 FEC 报文保护 2、6、10 号原始报文;3 号 FEC 报文保护 3、7、11 号原始报文。显然,超过12个报文的保护更加均匀,而且每个原始媒体报文只会被一个 FEC 报文保护。

另外,WebRTC 支持分级保护,分级保护分如下几种模式:

enum ProtectionMode {
  // 重点保护和非重点保护不交叉,重点保护保护重要报文,非重点保护保护剩余报文
  kModeNoOverlap,
  // 重点保护和非重点保护交叉,重点保护只会保护重要报文,非重点保护会保护所有报文
  kModeOverlap,
  // 在kModeOverlap之上,加强对首个报文的保护力度
  kModeBiasFirstPacket,
};

kModeNoOverlap模式

假设前四个报文为重要报文,分配 2 个 FEC 报文进行保护,剩余的 9个 报文分配 2 个FEC 报文进行保护,查表结果如下:

#define kMaskRandom4_2 \
  0xc0, 0x00, \
  0xb0, 0x00

#define kMaskRandom9_2 \
  0xaa, 0x80, \
  0xd5, 0x00

4 个 FEC 的掩码转换为二进制如下所示:

保护逻辑示意图如下所示:

kModeOverlap模式

假设前四个报文为重要报文,分配 2 个 FEC 报文进行保护,另外 2 个 FEC 报文要保护所有原始报文。前 2 个 FEC 报文的掩码可以通过查表得到,后面 2 个 FEC 报文保护的原始报文数量超过 12 个,只能动态生成,最终掩码转换为二进制如下图所示:

保护逻辑示意图如下所示:

kModeBiasFirstPacket

kModeBiasFirstPacket 模式,是在kModeOverlap之上,加强对第一个报文的保护,掩码表如下所示:

保护逻辑示意图如下所示,3 号 FEC 报文增加了对 0 号报文的保护。

6. 发送 FEC 数据

6.1. 视频报文封装

编码出来的视频报文,如果协商了 RED,会使用 RED 封装。

bool RTPSenderVideo::SendVideo(int payload_type,
                               absl::optional<VideoCodecType> codec_type,
                               uint32_t rtp_timestamp,
                               Timestamp capture_time,
                               rtc::ArrayView<const uint8_t> payload,
                               size_t encoder_output_size,
                               RTPVideoHeader video_header,
                               TimeDelta expected_retransmission_time,
                               std::vector<uint32_t> csrcs)
{
  ...

  if (red_enabled()) {
    std::unique_ptr<RtpPacketToSend> red_packet(new RtpPacketToSend(*packet));
    BuildRedPayload(*packet, red_packet.get());
    red_packet->SetPayloadType(*red_payload_type_);
    red_packet->set_is_red(true);
    red_packet->set_packet_type(RtpPacketMediaType::kVideo);
    red_packet->set_allow_retransmission(packet->allow_retransmission());
    rtp_packets.emplace_back(std::move(red_packet));
  } else {
    packet->set_packet_type(RtpPacketMediaType::kVideo);
    rtp_packets.emplace_back(std::move(packet));
  }

  ...
}

6.2. FEC 报文封装

FEC 报文也是使用 RED 封装。

std::vector<std::unique_ptr<RtpPacketToSend>> UlpfecGenerator::GetFecPackets() {
  if (generated_fec_packets_.empty()) {
    return std::vector<std::unique_ptr<RtpPacketToSend>>();
  }
  last_media_packet_->SetPayloadSize(0);

  std::vector<std::unique_ptr<RtpPacketToSend>> fec_packets;
  fec_packets.reserve(generated_fec_packets_.size());

  size_t total_fec_size_bytes = 0;
  for (const auto* fec_packet : generated_fec_packets_) {
		// 创建一个新的 RTP 报文
    std::unique_ptr<RtpPacketToSend> red_packet =
        std::make_unique<RtpPacketToSend>(*last_media_packet_);

		// 使用 RED 封装
    red_packet->SetPayloadType(red_payload_type_);

		// FEC 包的 mark 标记无意义
    red_packet->SetMarker(false);

		// 增加一个字节的 RED 头
    uint8_t* payload_buffer = red_packet->SetPayloadSize(
        kRedForFecHeaderLength + fec_packet->data.size());

    // Primary RED header with F bit unset.
    // See https://tools.ietf.org/html/rfc2198#section-3
		// 
		// 0 1 2 3 4 5 6 7
    // +-+-+-+-+-+-+-+-+ 
		// | 0 | Block PT |
    // +-+-+-+-+-+-+-+-+ 
		//
		// 设置 RED 头的 Block PT
    payload_buffer[0] = ulpfec_payload_type_;  // RED header.
    
		// 拷贝 FEC 数据
		memcpy(&payload_buffer[1], fec_packet->data.data(), fec_packet->data.size());

		// 累加 FEC 数据
    total_fec_size_bytes += red_packet->size();

		// 设置 RTP 头负载类型
    red_packet->set_packet_type(RtpPacketMediaType::kForwardErrorCorrection);

		// FEC 报文不重传,不会存放到 history 列表
    red_packet->set_allow_retransmission(false);

		// 设置 RED 报文
    red_packet->set_is_red(true);

		// FEC 报文不需要再被保护
    red_packet->set_fec_protect_packet(false);

    fec_packets.push_back(std::move(red_packet));
  }

	// 进入下一轮编码
  ResetState();

  MutexLock lock(&mutex_);

	// 更新 FEC 码率
  fec_bitrate_.Update(total_fec_size_bytes, clock_->CurrentTime());

  return fec_packets;
}

6.3. 发送流程

如下图所示,PacingController 不停的发送报文,发送的报文都要经过 RtpSenderEgress,RtpSenderEgress 根据设置的 FEC 参数,调用 VideoFecGenerator 不停的生成 FEC 报文,这些生成 FEC 报文都临时缓存在 VideoFecGenerator 中。PacingController 在发送报文的过程中,会不停的拉取 FEC 报文,将 FEC 报文插入发送队列,最后跟随原始报文一起发送出去。

7. 恢复原始数据

发送端的逻辑比较简单,收到报文后如何恢复丢失报文,内部具体如何恢复的代码逻辑暂未分析。

1)接收端收到的视频报文都会送给 RtpVideoStreamReceiver2 进行处理。

2)RtpVideoStreamReceiver2 会将所有报文都扔给 UlpfecReceiver 处理。

3)UlpfecReceiver 内部会进行判断,如果发现有丢包,会尝试进行 FEC 解码,然后将原始报文和恢复后的报文都回调给 RtpVideoStreamReceiver2。

4)RtpVideoStreamReceiver2 将收到的原始报文插入到 PacketBuffer 等待解码。

接收端 RtpVideoStreamReceiver2 会根据报文的 payload type 判断是否是 RED 报文,如果是 RED 报文,则将报文一股脑都扔给 UlpFecReceiver 处理,代码实现如下。由此可见,ulpfec 必须搭配 RED 才能生效。

void RtpVideoStreamReceiver2::ReceivePacket(const RtpPacketReceived& packet) {
  if (packet.payload_size() == 0) {
    NotifyReceiverOfEmptyPacket(packet.SequenceNumber());
    return;
  }

  // payload type 为 RED 的数据包,都要先交给 UlpfecReceiver 进行处理
  // 发送端已将所有所有视频报文和 FEC 报文都使用 RED 封装
  if (packet.PayloadType() == red_payload_type_) {
    ParseAndHandleEncapsulatingHeader(packet);
    return;
  }

  // 从 FEC 逛一圈回来的报文(“原始报文”+“恢复报文”),此时 payload type 已被替换为
  // 真实媒体数据类型

  // 根据 payload type 获取解包器
  const auto type_it = payload_type_map_.find(packet.PayloadType());
  if (type_it == payload_type_map_.end()) {
    return;
  }

  // 如果是 H264 报文,则可能要做 STAP-A、FU-A、Single 解包
  absl::optional<VideoRtpDepacketizer::ParsedRtpPayload> parsed_payload =
  type_it->second->Parse(packet.PayloadBuffer());
  if (parsed_payload == absl::nullopt) {
    return;
  }

  OnReceivedPayloadData(std::move(parsed_payload->video_payload), packet,
    parsed_payload->video_header);
}

8. 总结

ULPFEC 实现的核心是 FEC 保护比率的计算和掩码表的生成,FEC 保护比率决定了能使用多少 FEC 报文来保护原始报文,掩码表决定了 FEC 报文如何保护原始报文。围绕这两个核心概念,涉及如何生成 FEC 报文,如何打包和解包、如何发送和接收以及如何恢复原始报文等相关处理逻辑。基于 Parity Code 的 FEC 算法具有兼容性好、灵活性高、计算开销小等优点,但也有一些不足之处,相比 Red-Solomon 算法,其保护码率的信息冗余度更高,这样会带来更高的空间开销,而且生成应对各种丢包模式的掩码表及恢复逻辑也会比较复杂。

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

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

相关文章

8.持久化

队列和消息都可以持久化。 持久化的目的就是让消息不丢失。 RabbitMQ本身退出&#xff0c;或者由于某种原因崩溃时造成的消息丢失。 RabbitMQ一旦宕机&#xff0c;就会造成队列和消息都丢失了。 RabbitMQ重启之后&#xff0c;非持久化的队列和消息都不存在了。 队列持久化…

项目部署--最原始的方法

服务器环境搭建 以腾讯云为例&#xff1a; 1.可以先用这个使用一个月的 2.访问服务器官网&#xff1a;腾讯云官网&#xff0c;进去先登录&#xff0c;再点击 控制台&#xff0c;找到 轻量应用服务器&#xff0c;进去之后会看见使用的服务器&#xff0c;有一个 公网IP&#xff…

[Spring Boot]Protobuf解析MQTT消息体

简述 本文主要针对在MQTT场景下&#xff0c;使用Protobuf协议解析MQTT的消息体 Protobuf下载 官方下载 https://github.com/protocolbuffers/protobuf/releases网盘下载 链接&#xff1a;https://pan.baidu.com/s/1Uz7CZuOSwa8VCDl-6r2xzw?pwdanan 提取码&#xff1a;an…

C语言:数组-学习笔记(万字笔记)——翻新版

目录 前言&#xff1a; 1、 数组的概念 1.1 什么是数组 1.2 为什么学习数组&#xff1f; 2. ⼀维数组的创建和初始化 2.1 数组创建 2.2 数组的初始化 2.3 数组的类型 2.3.1 什么是数组类型&#xff1f; 2.3.2 数组类型的作用 3、 一维数组的使用 3.1 数组下标 3.2 数…

ZYNQ 入门笔记(零):概述

文章目录 引言产品线Zynq™ 7000 SoCZynq UltraScale™ MPSoCZynq UltraScale RFSoCVersal™ Adaptive SoC 开发环境 引言 Xilinx FPGA 产品线从经济型的 Spartan、Artix 系列到高性能的 Kintex、Virtex、Versal 系列&#xff0c;可以说涵盖了 FPGA 的绝大部分应用场景&#x…

SpringBoot 最大连接数及最大并发数是多少

SpringBoot 最大连接数及最大并发数 Spring Boot 是一个基于 Spring 框架的快速开发框架&#xff0c;它本身并不直接管理数据库连接或网络连接的最大连接数和最大并发数。这些参数通常由底层的基础设施和组件来控制&#xff0c;例如&#xff1a; 数据库连接池&#xff1a;Spri…

Web 3.0革新:社交金融与边玩边赚开启用户数据主权时代

目录 Web 3.0与社交商业模式 传统社交平台的问题 去中心化社交创新 Mirror&#xff1a;去中心化内容发布平台 Lens Protocol&#xff1a;去中心化社交图谱 Maskbook&#xff1a;隐私保护的社交方式 Web 3.0与与边玩边赚模式 经济模型解析 新商业模式的探索 Axie Infi…

C++——模板初阶 | STL简介

P. S.&#xff1a;以下代码均在VS2019环境下测试&#xff0c;不代表所有编译器均可通过。 P. S.&#xff1a;测试代码均未展示头文件stdio.h的声明&#xff0c;使用时请自行添加。 博主主页&#xff1a;Yan. yan.                        …

maven私服上传jar包 400 Bad Request 错误

文章目录 前言一、直接看报错二、问题处理三 maven 私服配置说明总结 前言 maven仓库的私服,一般会存放公司或者个人封装的jar包,用来共享给二次开发和协作伙伴用,很方便 第一次发布没有问题,但是我第二次发布,开始报错了 一、直接看报错 [外链图片转存失败,源站可能有防盗链…

十五届蓝桥杯JAVA B组题目详解(持续更新中)

试题 B: 类斐波那契循环数 我发现蓝桥杯的题目现在就是要费时间去理解&#xff0c;所以还是审题很重要&#xff0c;这道题的思路就是&#xff0c;一个n位数的前n个数&#xff0c;都是对应的位数上的值&#xff0c;比如说12345&#xff0c;五位数是吧&#xff0c;那数列S的前五位…

自主巡航,目标射击

中国机器人及人工智能大赛 参赛经验&#xff1a; 自主巡航赛道 【机器人和人工智能——自主巡航赛项】动手实践篇-CSDN博客 主要逻辑代码 #!/usr/bin/env python #coding: utf-8import rospy from geometry_msgs.msg import Point import threading import actionlib impor…

数据结构(Java):七大排序算法【多方法、多优化、多细节】

目录 1、排序的概念 1.1 排序 1.2 排序的稳定性 1.3 内部排序&外部排序 1.4 各排序算法总结对比 2、 插入排序 2.1 &#x1f338;直接插入排序 2.2 &#x1f338;希尔排序 3、 选择排序 3.1 &#x1f338;直接选择排序 3.2 直接选择排序优化 3.3 &#x1f338;…

【PyTorch】图像多分类项目

【PyTorch】图像二分类项目 【PyTorch】图像二分类项目-部署 【PyTorch】图像多分类项目 【PyTorch】图像多分类项目部署 多类图像分类的目标是为一组固定类别中的图像分配标签。 目录 加载和处理数据 搭建模型 定义损失函数 定义优化器 训练和迁移学习 用随机权重进行训…

HC-SR04超声波测距模块使用方法和例程(STM32快速移植)

基于STM32和HC-SR04模块实现超声波测距功能 HC-SR04硬件概述HC-SR04超声波距离传感器的核心是两个超声波传感器。一个用作发射器&#xff0c;将电信号转换为40 KHz超声波脉冲。接收器监听发射的脉冲。如果接收到它们&#xff0c;它将产生一个输出脉冲&#xff0c;其宽度可用于…

磁盘作业1

新添加一块硬盘&#xff0c;大小为5g&#xff0c;给这块硬盘分一个mbr格式的主分区&#xff08;大小为3g&#xff09;&#xff0c;给此主分区创建ext2的文件系统&#xff0c;挂载到/guazai1目录&#xff0c;并写入文件内容为 "this is fist disk" 文件名为1.txt的文件…

五分钟学会 Docker Registry 搭建私有镜像仓库

在上一篇文章《前端不懂 Docker &#xff1f;先用它换掉常规的 Vue 项目部署方式》中&#xff0c;我们学习了如何使用 aliyun 私有镜像仓库&#xff0c;也了解到可以使用 Docker Registry 搭建私有镜像仓库。这篇文章就分享下实操过程。 registry 是官方提供的 registry 镜像&…

【数据结构--查找】

目录 一、查找&#xff08;Searching&#xff09;的概念1.1、基本概念1.2、算法的评价指标 二、顺序查找2.1、算法思想2.2、算法实现2.2.1、常规顺序查找2.2.2、带哨兵的顺序查找 2.3、效率分析2.4、优化2.4.1、针对有序表2.4.2、被查效率不相等 三、折半查找3.1、算法思想3.2、…

<数据集>学生课堂行为识别数据集<目标检测>

数据集格式&#xff1a;VOCYOLO格式 图片数量&#xff1a;13899张 标注数量(xml文件个数)&#xff1a;13899 标注数量(txt文件个数)&#xff1a;13899 标注类别数&#xff1a;8 标注类别名称&#xff1a;[js, tt, dk, zt, dx, zl, jz, xt] # 举手 js # 抬头听课 …

新版GPT-4omini上线!快!真TM快!

大半夜&#xff0c;OpenAI突然推出了GPT-4o mini版本。 当我看到这条消息时&#xff0c;正准备去睡觉。mini版本质上是GPT-4o模型的精简版本&#xff0c;没有什么革命性的创新&#xff0c;因此我并没有太在意。 结果今天早上一觉醒来发现伴随GPT-4o mini上线&#xff0c;官网和…

Vue3+ element plus 前后分离admin项目安装教程

前后分离admin项目安装 前后分离admin项目安装基于 vue3.x CompositionAPI typescript vite element plus vue-router-next pinia&#xff0c;适配手机、平板、pc 的后台开源免费模板&#xff0c;希望减少工作量&#xff0c;帮助大家实现快速开发。 下载源码 前往gite…