当前位置: 首页 > news >正文

深入浅出WebRTC—ULPFEC

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 和 _protectionFactorDVCMFecMethod::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 个包才能计算 FECif (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 PTpayload_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 算法,其保护码率的信息冗余度更高,这样会带来更高的空间开销,而且生成应对各种丢包模式的掩码表及恢复逻辑也会比较复杂。

相关文章:

深入浅出WebRTC—ULPFEC

FEC 通过在发送端添加额外的冗余信息&#xff0c;使接收端即使在部分数据包丢失的情况下也能恢复原始数据&#xff0c;从而减轻网络丢包的影响。在 WebRTC 中&#xff0c;FEC 主要有两种实现方式&#xff1a;ULPFEC 和 FlexFEC&#xff0c;FlexFEC 是 ULPFEC 的扩展和升级&…...

Python从0到100(四十三):数据库与Django ORM 精讲

前言&#xff1a; 零基础学Python&#xff1a;Python从0到100最新最全教程。 想做这件事情很久了&#xff0c;这次我更新了自己所写过的所有博客&#xff0c;汇集成了Python从0到100&#xff0c;共一百节课&#xff0c;帮助大家一个月时间里从零基础到学习Python基础语法、Pyth…...

Redis-主从模式

目录 前言 一.主从节点介绍 二.配置redis主从结构 二.主从复制 四.拓扑结构 五.数据同步 全量复制&#xff08;Full Sync Replication&#xff09; 局部复制&#xff08;Partial Replication&#xff09; Redis的学习专栏&#xff1a;http://t.csdnimg.cn/a8cvV 前言 …...

加速决策过程:企业级爬虫平台的实时数据分析

摘要 在当今数据驱动的商业环境中&#xff0c;企业如何才能在海量信息中迅速做出精准决策&#xff1f;本文将探讨企业级爬虫平台如何通过实时数据分析加速决策过程&#xff0c;实现数据到决策的无缝衔接。我们聚焦于技术如何赋能企业&#xff0c;提升数据处理效率&#xff0c;…...

字典树(前缀树)数组实现(只能查26个单词)

这段代码实现了一个基于 Trie 树的字典树&#xff08;Trie&#xff09;数据结构&#xff0c;用于存储和检索字符串。其中包含以下几个方法. insert(String word): 向 Trie 树中插入一个单词。首先将单词转换为字符数组&#xff0c;然后遍历字符数组&#xff0c;逐个字符在 Trie…...

CTF-pwn-虚拟化-vmmware 前置

文章目录 参考vmware逃逸简介虚拟机和主机通信机制(guest to host)共享内存&#xff08;弃用&#xff09;backdoor机制Message_Send和Message_RecvGuestRPC实例RpcOutSendOneRawWork实例 vmware-rpctool info-get guestinfo.ip各个步骤对应的backdoor操作Open RPC channelSend …...

thinkphp8结合layui2.9 图片上传验证

<?php declare (strict_types 1);namespace app\index\validate;use think\Validate;class Upload extends Validate {/*** 定义验证规则* 格式&#xff1a;字段名 > [规则1,规则2...]** var array*/protected $rule [image > fileExt:jpg,png|fileSize:204800|fi…...

农村污水处理难题:探索低成本高效解决方案

农村污水处理难题&#xff1a;探索低成本高效解决方案 农村污水处理作为国家生态文明建设的重要一环&#xff0c;面临着诸多挑战&#xff0c;尤其是技术落后、管理分散、资源匮乏等问题。物联网技术的引入&#xff0c;为解决这些痛点提供了创新途径&#xff0c;实现了对污水处…...

lightningcss介绍及使用

lightningcss介绍及使用 一款使用 rust 编写的 css 解析器&#xff0c;转换器、及压缩器。 特性 特别快&#xff1a;可以在毫秒级别解析、压缩大量的 css 文件&#xff0c;而且比其他工具的打包结果更小给值添加类型&#xff1a;许多其他css解析器会将值解析成一个无类型的 …...

HTTP服务的应用

1、编辑json请求参数&#xff1b; 2、把json发送到服务url&#xff0c;接收服务的返回参数&#xff1b; 3、解析返回参数。 procedure TfrmCustomQuery.btnFullUpdateClick(Sender: TObject); varfrm: TfrmInputQueryConditionEX;b_OK: Boolean;sBeginDate, sEndDate, sJSON…...

uni-app:踩坑路---scroll-view内使用fixed定位,无效的问题

前言&#xff1a; emmm&#xff0c;说起来这个问题整得还挺好笑的&#xff0c;本人在公司内&#xff0c;奋笔疾书写代码&#xff0c;愉快的提交测试的时候&#xff0c;测试跟我说&#xff0c;在苹果手机上你这个样式有bug&#xff0c;我倒是要看看&#xff0c;是什么bug。 安卓…...

MySQL4.索引及视图

1.建库 create database mydb15_indexstu; use mydb15_indexstu;2.建表 2.1 student表学&#xff08;sno&#xff09;号为主键&#xff0c;姓名&#xff08;sname&#xff09;不能重名&#xff0c;性别&#xff08;ssex&#xff09;仅能输入男或女&#xff0c;默认所在系别&a…...

MongoDB - 聚合阶段 $match、$sort、$limit

文章目录 1. $match 聚合阶段1. 构造测试数据2. $match 示例3. $match 示例 2. $sort 聚合阶段1. 排序一致性问题2. $sort 示例 3. $limit 聚合阶段 1. $match 聚合阶段 $match 接受一个指定查询条件的文档。 $match 阶段语法&#xff1a; { $match: { <query> } }$ma…...

ModuleNotFoundError: No module named ‘scrapy.utils.reqser‘

在scrapy中使用scrapy-rabbitmq-scheduler会出现报错 ModuleNotFoundError: No module named scrapy.utils.reqser原因是新的版本的scrapy已经摒弃了该方法,但是scrapy-rabbitmq-scheduler 没有及时的更新,所以此时有两种解决方法 方法一.将scrapy回退至旧版本,找到对应的旧版…...

vue3+ts+vite+electron+electron-packager打包成exe文件

目录 1、创建vite项目 2、添加需求文件 3、根据package.json文件安装依赖 4、打包 5、electron命令运行 6、electron-packager打包成exe文件 Build cross-platform desktop apps with JavaScript, HTML, and CSS | Electron 1、创建vite项目 npm create vitelatest 2、添…...

使用脚本搭建MySQL数据库基础环境

数据库的基本概念 数据&#xff08;Data&#xff09; 描述事物的符号记录 包括数字&#xff0c;文字&#xff0c;图形。图像&#xff0c;声音&#xff0c;档案记录等。 以记录形式按统一格式进行存储 表 将不同的记录组织在一起 用来储存具体数据 数据库 表的集合&#xff0c;是…...

Parameter index out of range (2 > number of parameters, which is 1【已解决】

文章目录 1、SysLogMapper.xml添加注释导致的2、解决方法3、总结 1、SysLogMapper.xml添加注释导致的 <!--定义一个查询方法&#xff0c;用于获取日志列表--><!--方法ID为getLogList&#xff0c;返回类型com.main.server.api.model.SysLogModel,参数类型为com.main.se…...

rk3588s 定制版 USB adb , USB2.0与USB3.0 区别,adb 由typeC 转换到USB3.0(第二部分)

硬件资源&#xff1a; rk3588s 核心板定制的地板 软件资源&#xff1a; 网盘上的 android12 源码 1 硬件上 客户只想使用 type c 接口中的 usb2.0 OTG 。在硬件上&#xff0c;甚至连 CC芯片都没有连接。 关于一些前置的知识。 1 USB2.0 与 USB3.0 的区别。 usb3.0 兼容2.0 …...

Cookie与Session 实现登录操作

Cookie Cookie 是网络编程中使用最广泛的一项技术&#xff0c;主要用于辨识用户身份。 客户端&#xff08;浏览器&#xff09;与网站服务端通讯的过程如下图所示&#xff1a; 从图中看&#xff0c;服务端既要返回 Cookie 给客户端&#xff0c;也要读取客户端提交的 Cookie。所…...

通过IEC104转MQTT网关轻松接入阿里云平台

随着智能电网和物联网技术的飞速发展&#xff0c;电力系统中的传统IEC 104协议设备正面临向现代化、智能化转型的迫切需求。阿里云作为全球领先的云计算服务提供商&#xff0c;其强大的物联网平台为IEC 104设备的接入与数据处理提供了强大的支持。本文将深入探讨钡铼网关在MQTT…...

lua 游戏架构 之 游戏 AI (五)ai_autofight_find_way

这段Lua脚本定义了一个名为 ai_autofight_find_way 的类&#xff0c;继承自 ai_base 类。 lua 游戏架构 之 游戏 AI &#xff08;一&#xff09;ai_base-CSDN博客文章浏览阅读238次。定义了一套接口和属性&#xff0c;可以基于这个基础类派生出具有特定行为的AI组件。例如&…...

vue3+openLayers点击标记事件

<template><!--地图--><div class"distributeMap" id"distributeMap"></div> </template> <script lang"ts" setup> import { onMounted, reactive } from "vue"; import { Feature, Map, View }…...

深入分析 Android ContentProvider (三)

文章目录 深入分析 Android ContentProvider (三)ContentProvider 的高级使用和性能优化1. 高级使用场景1.1. 数据分页加载示例&#xff1a;分页加载 1.2. 使用 Loader 实现异步加载示例&#xff1a;使用 CursorLoader 加载数据 1.3. ContentProvider 与权限管理示例&#xff1…...

养宠浮毛异味双困扰?性价比高的宠物空气净化器推荐

家里养了两只银渐层&#xff0c;谁懂啊&#xff01;一下班打开家门就看到家里飘满了猫浮毛雪&#xff0c;空气中还传来隐隐约约的异味。每天不是在吸毛的路上&#xff0c;就是在洗猫砂盆的路上&#xff0c;而且空气中的浮毛还很难清理干净&#xff0c;这是最让人头疼的问题。 …...

maven项目容器化运行之3-优雅的利用Jenkins和maven使用docker插件调用远程docker构建服务并在1Panel中运行

一.背景 在《maven项目容器化运行之1》中&#xff0c;我们开启了1Panel环境中docker构建服务给到了局域网。在《maven项目容器化运行之2》中&#xff0c;我们基本实现了maven工程创建、远程调用docker构建镜像、在1Panel选择镜像运行容器三大步骤。 但是&#xff0c;存在一个问…...

docker 打包orbbec

docker pull humble容器 sudo docker run -it osrf/ros:humble-desktop docker 启动容器 sudo docker run -u root --device/dev/bus/usb:/dev/bus/usb -it -v /home/wl:/share --name wl4 osrf/ros:humble-desktop /bin/bash新开一个终端 查看本地存在的容器&#xff1a;…...

无涯·问知财报解读,辅助更加明智的决策

财报解读就像是给公司做一次全面的体检&#xff0c;是理解公司内部运作机制和市场表现的一把钥匙&#xff0c;能够有效帮助投资者、分析师、管理层以及所有市场参与者判断一家公司的健康程度和发展潜力。 星环科技无涯问知的财经库内置了企业年报及财经类信息&#xff0c;并对…...

【Apache Doris】数据副本问题排查指南

【Apache Doris】数据副本问题排查指南 一、问题现象二、问题定位三、问题处理 本文主要分享Doris中数据副本异常的问题现象、问题定位以及如何处理此类问题。 一、问题现象 问题日志 查询报错 Failed to initialize storage reader, tablet{tablet_id}.xxx.xxx问题说明 查…...

【HarmonyOS】关于鸿蒙消息推送的心得体会(二)

【HarmonyOS】关于鸿蒙消息推送的心得体会&#xff08;二&#xff09; 前言 推送功能的开发与传统功能开发还是有很大区别。首先最大的区别点就在于需要多部门之间的协同&#xff0c;作为鸿蒙客户端开发&#xff0c;你需要和产品&#xff0c;运营&#xff0c;以及后台开发一起…...

零基础入门:创建一个简单的Python爬虫管理系统

摘要&#xff1a; 本文将手把手教你&#xff0c;从零开始构建一个简易的Python爬虫管理系统&#xff0c;无需编程基础&#xff0c;轻松掌握数据抓取技巧。通过实战演练&#xff0c;你将学会设置项目、编写基本爬虫代码、管理爬取任务与数据&#xff0c;为个人研究或企业需求奠…...

【Node.js基础04】node.js模块化

一&#xff1a;什么是模块化 在Node.js中&#xff0c;每个文件都可视为一个独立的模块。模块化提高了代码的复用性&#xff0c;按需加载&#xff0c;具有独立的作用域 二&#xff1a;如何实现多个文件间导入和导出 1 CommonJS标准&#xff08;默认&#xff09;-导入和导出 …...

数据库——单表查询

一、建立数据库mydb8_worker mysql> use mydb8_worker; 二、建立表 1.创建表 mysql> create table t_worker(department_id int(11) not null comment 部门号,-> worder_id int(11) primary key not null comment 职工号,-> worker_date date not null comment…...

dsa加训

refs: OI Wiki - OI Wiki (oi-wiki.org) 1. 枚举 POJ 2811 熄灯问题 refs : OpenJudge - 2811:熄灯问题 如果要枚举每个灯开或者不开的情况&#xff0c;总计2^30种情况&#xff0c;显然T。 不过我们可以发现&#xff1a;若第i行的某个灯亮了&#xff0c;那么有且仅有第i行和第…...

SpringBoot源码(1)ApplicationContext和BeanFactory

1、调用getBean方法 SpringBootApplication public class SpringBootDemoApplication {public static void main(String[] args) {ConfigurableApplicationContext applicationContext SpringApplication.run(SpringBootDemoApplication.class, args);applicationContext.get…...

CANoe编程实例--TCP/IP通信

1、简介 本实例将使用目前常用的开发工具C#来开发服务器端&#xff0c;以CANoe端作为客户端。服务器端和客户端&#xff0c;通过TCP/IP连接&#xff0c;实现数据交换。 首先在服务器端建立一个监听Socket&#xff0c;自动创建一个监听线程&#xff0c;随时监听是否有客户端的连…...

Neuron协议网关的北向应用插件开发

目录 概述 指令处理层开发​ 应用层开发​ .open​ .close​ .init​ .uninit​ .start​ .stop​ .setting​ .request​ 插件设置文件​ 适配华为的思路 概述 最近研究了一段时间的Neuron协议网关&#xff0c;前面的博文也提到它虽然能够把数据发到华为的IoT平台上…...

【BUG】已解决:You are using pip version 10.0.1, however version 21.3.1 is available.

You are using pip version 10.0.1, however version 21.3.1 is available. 目录 You are using pip version 10.0.1, however version 21.3.1 is available. 【常见模块错误】 【解决方案】 欢迎来到英杰社区https://bbs.csdn.net/topics/617804998 欢迎来到我的主页&#…...

electron-builder打包vue2项目不显示element-ui图标

1、使用版本 vue ^2.6.14element-ui ^2.15.14vue-cli-plugin-electron-builder 2.1.1 2、解决办法 1&#xff09; 如果是简单的图标可以使用图片代替&#xff08;这种对于elementui组件的图标还是不会显示&#xff09; 2&#xff09;在vue.config.js配置 const { defineCon…...

controller层-请求格式为json-请求方法为get

前置条件 get请求映射&#xff0c;内容和PostMapping一致&#xff0c;需要请求参数更换为get数据 请求过程&#xff1a;用户请求--初始化DispatcherServlet及对接和分发用户请求--controller--service 用户请求&#xff1a;http://ip:port/user/getinfo 请求方法&#xff1a;ge…...

【Linux】网络通信基础:应用层协议、HTTP、序列化与会话管理

文章目录 前言1. 应用层自定义协议与序列化1.1 什么是应用层&#xff1f;1.2 再谈 "协议"1.3 序列化 和 反序列化 2. HTTP 协议3. 认识 URL(统一资源定位符)4. urlencode和urldecode5. HTTP 协议请求与响应格式5.1 HTTP 请求5.2 HTTP 响应 6. HTTP 的方法6.1 GET 方法…...

@NotNull、@NotEmpty 和 @NotBlank 区别

NotNull、NotEmpty 和 NotBlank 是 Java Bean Validation (JSR 380) 规范中定义的注解&#xff0c;通常用于验证对象的属性是否满足特定的条件。这些注解常用于后端验证&#xff0c;确保接收到的数据符合预期。 NotNull 用途&#xff1a;验证一个对象是否不为null。 注意&#…...

大模型应用—大模型赋能网络爬虫

大模型赋能网络爬虫 简单来说,网页抓取就是从网站抓取数据和内容,然后将这些数据保存为XML、Excel或SQL格式。除了用于生成潜在客户、监控竞争对手和市场研究外,网页抓取工具还可以用于自动化你的数据收集过程。 借助AI网页抓取工具,可以解决手动或纯基于代码的抓取工具的…...

在 Qt 中获取 MouseMove 事件

在编写 Qt 程序时&#xff0c;我希望在鼠标移动时&#xff08;即使鼠标在另一个窗口上&#xff09;能够调用 mouseMoveEvent(QMouseEvent* event) 方法。目前&#xff0c;在我的 mainwindow.cpp 文件中&#xff0c;我有如下代码&#xff1a; void MainWindow::mouseMoveEvent(…...

自动驾驶系列—智能巡航辅助功能中的路口通行功能介绍

自动驾驶系列—智能巡航辅助功能中的车道中央保持功能介绍 自动驾驶系列—智能巡航辅助功能中的车道变换功能介绍 自动驾驶系列—智能巡航辅助功能中的横向避让功能介绍 自动驾驶系列—智能巡航辅助功能中的路口通行功能介绍 文章目录 2. 功能定义3. 功能原理4. 传感器架构5. 实…...

如何为WordPress网站设置多语言站点

随着全球化的发展&#xff0c;拥有一个支持多语言的站点已成为提升用户体验、扩大受众范围的重要手段。本文将详细介绍如何为WordPress网站设置多语言站点&#xff0c;提供两种最佳方案详解&#xff0c;帮助您轻松实现多语言站点的搭建与管理。无论您是选择在同一站点内发布多语…...

【RHCE】综合真机实验(shell完成)

目录 题目&#xff1a; 需求描述 实操 一、服务端&#xff08;servera&#xff09; 1.ip配置 2.更改主机名 3.创建本地仓库 4.DNS服务 1.下载软件包和防火墙允许 2.配置主配置文件 3.配置区域文件 1.named.exam 2.named.fangxiang 4.重启服务 5.验证结果&#x…...

【Python】成功解决conda创建虚拟环境时出现的CondaHTTPError: HTTP 000 CONNECTION FAILED错误

【Python】成功解决conda创建虚拟环境时出现的CondaHTTPError: HTTP 000 CONNECTION FAILED错误 &#x1f308; 欢迎莅临我的个人主页&#x1f448;这里是我深耕Python编程、机器学习和自然语言处理&#xff08;NLP&#xff09;领域&#xff0c;并乐于分享知识与经验的小天地&a…...

苹果笔记本电脑如何优化系统 苹果电脑系统优化软件哪个好 cleanmymac x怎么用

随着时间的推移&#xff0c;你可能会发现你的MacBook运行速度变慢&#xff0c;甚至在执行一些基本任务时也会感觉到卡顿。这不仅影响了工作效率&#xff0c;也大大降低了使用体验。但别担心&#xff0c;优化你的Mac系统比做早餐还简单。本文将用一种轻松的风格向你介绍7种简单易…...

Vue数组操作之sort详解

在 Vue.js 中&#xff0c;sort() 方法用于对数组进行排序。它会改变原数组&#xff0c;并返回排序后的数组。默认情况下&#xff0c;sort() 方法按照字母顺序&#xff08;Unicode 编码顺序&#xff09;对数组中的元素进行排序。如果需要按照其他规则排序&#xff0c;可以传递一…...

解决 Android 应用安装错误:INSTALL_FAILED_BAD_PERMISSION_GROUP

解决 Android 应用安装错误&#xff1a;INSTALL_FAILED_BAD_PERMISSION_GROUP 在开发 Android 应用时&#xff0c;我们有时会遇到安装错误。这篇文章将讨论一种常见的错误&#xff1a;INSTALL_FAILED_BAD_PERMISSION_GROUP&#xff0c;并介绍解决方法。 问题描述 在尝试安装…...