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

webrtc代码走读(四)-QOS-NACK实现-发送端

1 核心流程与交互关系表

发送端 NACK 实现的核心流程与交互关系如下表所示:

发送端核心操作媒体接收端操作
1. 发送 RTP 报文,并将报文存入 packet_history_ 缓存队列-
2. 接收来自接收端的 RTCP NACK 报文1. 检测 RTP 报文丢失,生成并发送 RTCP NACK 报文
3. 触发 RTPSender::OnReceivedNack 处理 NACK 反馈-
4. 调用 RTPSender::ReSendPacket 重发丢失的 RTP 报文2. 接收重发的 RTP 报文,完成丢包恢复

img

2、核心函数走读

发送端 NACK 实现分为三大核心流程:RTP 报文缓存RTCP NACK 处理RTP 报文重发,以下将逐一拆解每个流程的关键函数与源码细节。

2.1 流程1:发送 RTP 报文并缓存到 packet_history_

发送端通过 Pacer( pacing 控制器)发送 RTP 报文时,会将媒体报文(需支持重传)存入 packet_history_ 队列,为后续重发提供数据来源。同时,需通过 SetStorePacketsStatus 配置队列长度,确保缓存能覆盖合理的重传窗口。

2.1.1 关键函数调用链

ProcessThreadImpl::Process  // 线程调度入口,触发 Pacer 处理
-> PacedSender::Process     // Pacing 发送器主逻辑
-> PacingController::ProcessPackets  // 控制报文发送节奏
-> PacketRouter::SendPacket  // 路由报文到对应模块
-> ModuleRtpRtcpImpl2::TrySendPacket  // RTP/RTCP 模块发送预处理
-> RtpSenderEgress::SendPacket  // 最终发送 RTP 报文,并触发缓存

2.1.2 核心函数:RtpSenderEgress::SendPacket(报文发送与缓存触发)

// 函数功能:发送 RTP 报文到网络,并将可重传的媒体报文存入 packet_history_ 缓存
// 参数说明:
//   *packet: 待发送的 RTP 报文对象
//   options: 发送选项(如 QoS 优先级、是否允许重传等)
//   pacing_info: Pacing 相关信息(如发送时间、比特率等)
// 返回值:bool - 报文是否成功发送到网络
const bool send_success = SendPacketToNetwork(*packet, options, pacing_info);// 关键逻辑:无论发送是否成功,均处理报文缓存(确保重传时能找到报文)
// 条件1:is_media - 是否为媒体报文(非 RTCP、非 Padding 等)
// 条件2:packet->allow_retransmission() - 报文是否允许重传(由发送端配置决定)
if (is_media && packet->allow_retransmission()) {// 将报文存入缓存,记录当前时间(用于后续 RTT 校验)packet_history_->PutRtpPacket(std::make_unique<RtpPacketToSend>(*packet),  // 复制 RTP 报文now_ms  // 当前时间戳(毫秒),标记报文首次发送时间);
} 
// 处理重传报文的状态更新:若当前报文是重传报文,标记原报文为“已发送”
else if (packet->retransmitted_sequence_number()) {packet_history_->MarkPacketAsSent(*packet->retransmitted_sequence_number());
}

2.1.3 核心函数:RtpPacketHistory::PutRtpPacket(报文缓存实现)

// 函数功能:将 RTP 报文存入缓存队列,以序列号(SequenceNumber)为索引,支持重传时快速查询
// 参数说明:
//   packet: 待缓存的 RTP 报文(智能指针,确保内存安全)
//   send_time_ms: 报文发送时间戳(可选,首次发送时传入)
void RtpPacketHistory::PutRtpPacket(std::unique_ptr<RtpPacketToSend> packet,absl::optional<int64_t> send_time_ms
) {RTC_DCHECK(packet);  // WebRTC 断言:确保 packet 非空(调试用)MutexLock lock(&lock_);  // 加锁,保证多线程下缓存操作线程安全int64_t now_ms = clock_->TimeInMilliseconds();  // 获取当前系统时间// 若缓存模式为“禁用”,直接返回(不缓存任何报文)if (mode_ == StorageMode::kDisabled) {return;}// 断言:确保当前报文允许重传(与 RtpSenderEgress::SendPacket 中的条件一致)RTC_DCHECK(packet->allow_retransmission());// 清理过期报文:删除缓存中超过最大缓存时间/数量的报文,避免内存泄漏CullOldPackets(now_ms);// 1. 获取当前报文的序列号,计算其在缓存队列中的索引const uint16_t rtp_seq_no = packet->SequenceNumber();  // RTP 报文序列号(16位)int packet_index = GetPacketIndex(rtp_seq_no);  // 根据序列号计算索引(队列位置)// 2. 处理重复报文:若该序列号的报文已存在,删除旧报文(避免状态不一致)if (packet_index >= 0 &&static_cast<size_t>(packet_index) < packet_history_.size() &&packet_history_[packet_index].packet_ != nullptr) {RTC_LOG(LS_WARNING) << "Duplicate packet inserted: " << rtp_seq_no;  // 打印警告日志RemovePacket(packet_index);  // 删除旧报文packet_index = GetPacketIndex(rtp_seq_no);  // 重新计算索引(旧报文删除后索引可能变化)}// 3. 扩展缓存队列:若索引小于0(报文序列号小于队列中所有报文),在队列头部插入空元素for (; packet_index < 0; ++packet_index) {packet_history_.emplace_front(nullptr, absl::nullopt, 0);}// 4. 扩展缓存队列:若索引超过队列长度(报文序列号大于队列中所有报文),在队列尾部插入空元素while (static_cast<int>(packet_history_.size()) <= packet_index) {packet_history_.emplace_back(nullptr, absl::nullopt, 0);}// 5. 断言:确保索引合法(防止越界访问)RTC_DCHECK_GE(packet_index, 0);  // 索引 >= 0RTC_DCHECK_LT(packet_index, packet_history_.size());  // 索引 < 队列长度RTC_DCHECK(packet_history_[packet_index].packet_ == nullptr);  // 目标位置为空(无重复)// 6. 存入缓存:创建 StoredPacket 对象,保存报文、发送时间和插入顺序packet_history_[packet_index] = StoredPacket(std::move(packet),  // 转移报文所有权到缓存send_time_ms,       // 发送时间戳packets_inserted_++ // 插入计数器(用于排序或清理优先级));// 7. (可选)Padding 优先级处理:若启用 Padding 优先级,将报文加入优先级集合if (enable_padding_prio_) {// 若优先级集合超过最大长度,删除最后一个元素(LRU 策略)if (padding_priority_.size() >= kMaxPaddingHistory - 1) {padding_priority_.erase(std::prev(padding_priority_.end()));}// 将当前缓存的报文加入优先级集合auto prio_it = padding_priority_.insert(&packet_history_[packet_index]);RTC_DCHECK(prio_it.second) << "Failed to insert packet into prio set.";  // 断言插入成功}
}

2.1.4 缓存队列配置:SetStorePacketsStatus

packet_history_ 的缓存长度需根据媒体类型(视频/音频)分别配置,确保能覆盖典型的 RTT 窗口(避免重传时报文已被清理)。

  • 视频缓存配置:在 CreateRtpStreamSenders 函数中初始化

    // 创建 ModuleRtpRtcpImpl2 实例(RTP/RTCP 核心模块)
    std::unique_ptr<ModuleRtpRtcpImpl2> rtp_rtcp(ModuleRtpRtcpImpl2::Create(configuration)
    );
    rtp_rtcp->SetSendingStatus(false);  // 初始关闭发送状态
    rtp_rtcp->SetSendingMediaStatus(false);  // 初始关闭媒体发送状态
    rtp_rtcp->SetRTCPStatus(RtcpMode::kCompound);  // 启用复合 RTCP 模式(支持 NACK)
    // 启用缓存,设置最小缓存长度(kMinSendSidePacketHistorySize 为预定义常量,通常为 1000+)
    rtp_rtcp->SetStorePacketsStatus(true, kMinSendSidePacketHistorySize);
    
  • 音频缓存配置:在 ChannelSend::RegisterSenderCongestionControlObjects 函数中初始化

    void ChannelSend::RegisterSenderCongestionControlObjects(RtpTransportControllerSendInterface* transport,RtcpBandwidthObserver* bandwidth_observer
    ) {RTC_DCHECK_RUN_ON(&worker_thread_checker_);  // 断言在工作线程执行RtpPacketSender* rtp_packet_pacer = transport->packet_sender();  // 获取 PacerPacketRouter* packet_router = transport->packet_router();  // 获取报文路由RTC_DCHECK(rtp_packet_pacer);RTC_DCHECK(packet_router);RTC_DCHECK(!packet_router_);rtcp_observer_->SetBandwidthObserver(bandwidth_observer);  // 设置带宽观察者rtp_packet_pacer_proxy_->SetPacketPacer(rtp_packet_pacer);  // 绑定 Pacer 代理// 启用音频缓存,固定设置缓存长度为 600(音频报文小,缓存更多以应对高丢包)rtp_rtcp_->SetStorePacketsStatus(true, 600);constexpr bool remb_candidate = false;  // 不作为 REMB(带宽估计)候选packet_router->AddSendRtpModule(rtp_rtcp_.get(), remb_candidate);  // 注册 RTP 模块到路由packet_router_ = packet_router;
    }
    
2.2 流程2:处理接收端的 RTCP NACK 报文

接收端检测到 RTP 丢包后,会发送 RTCP NACK 报文(携带丢包序列号列表)。发送端通过 RTCP 接收模块解析该报文,提取丢包序列,并传递给 RTPSender 准备重传。

img

2.2.1 关键函数调用链

RTCPReceiver::IncomingPacket  // 接收 RTCP 报文(从网络层获取)
-> RTCPReceiver::ParseCompoundPacket  // 解析复合 RTCP 报文(NACK 属于复合报文的一部分)
-> RTCPReceiver::TriggerCallbacksFromRtcpPacket  // 触发 RTCP 报文回调(分发到对应处理器)
-> RTCPReceiver::HandleNack  // 专门处理 NACK 报文,提取丢包序列号
-> ModuleRtpRtcpImpl::OnReceivedNack  // 转发 NACK 信息到 RTPSender
-> RTPSender::OnReceivedNack  // 最终处理 NACK,准备重传

2.2.2 核心函数:RTCPReceiver::HandleNack(解析 NACK 报文)

// 函数功能:解析 RTCP NACK 报文,提取丢包序列号,存入 packet_information 供后续处理
// 参数说明:
//   rtcp_block: RTCP 报文头部(包含 NACK 报文的类型、长度等信息)
//   packet_information: 输出参数,存储 NACK 相关信息(丢包序列、报文类型标记等)
void RTCPReceiver::HandleNack(const CommonHeader& rtcp_block,PacketInformation* packet_information
) {rtcp::Nack nack;  // NACK 报文解析对象// 第一步:解析 RTCP 报文块,若解析失败(格式错误),跳过该报文并计数if (!nack.Parse(rtcp_block)) {++num_skipped_packets_;  // 统计跳过的无效报文数return;}// 第二步:校验 NACK 报文的目标 SSRC 是否匹配当前发送端的媒体 SSRC// receiver_only_: 若当前是纯接收端(不发送媒体),忽略 NACK// main_ssrc_: 当前发送端的媒体 SSRC(NACK 报文中的 media_ssrc 需与之匹配)if (receiver_only_ || main_ssrc_ != nack.media_ssrc()) {return;  // 不是发给当前发送端的 NACK,忽略}// 第三步:提取 NACK 报文中的丢包序列号,存入 packet_information// nack.packet_ids(): 返回丢包序列号列表(std::vector<uint16_t>)packet_information->nack_sequence_numbers.insert(packet_information->nack_sequence_numbers.end(),nack.packet_ids().begin(),nack.packet_ids().end());// 第四步:更新 NACK 统计信息(用于监控和调试)for (uint16_t packet_id : nack.packet_ids()) {nack_stats_.ReportRequest(packet_id);  // 记录每个丢包序列号的请求次数}// 第五步:标记 packet_information 的报文类型为“NACK”,供后续回调识别if (!nack.packet_ids().empty()) {packet_information->packet_type_flags |= kRtcpNack;  // 置位 NACK 标记++packet_type_counter_.nack_packets;  // 统计接收的 NACK 报文总数packet_type_counter_.nack_requests = nack_stats_.requests();  // 统计总 NACK 请求数packet_type_counter_.unique_nack_requests = nack_stats_.unique_requests();  // 统计唯一丢包数}
}

2.2.3 核心函数:ModuleRtpRtcpImpl::OnReceivedNack(NACK 信息转发)

// 函数功能:将解析后的 NACK 丢包序列和 RTT 信息转发给 RTPSender,触发重传准备
// 参数说明:
//   nack_sequence_numbers: 丢包序列号列表(从 NACK 报文中提取)
void ModuleRtpRtcpImpl::OnReceivedNack(const std::vector<uint16_t>& nack_sequence_numbers
) {// 检查 RTPSender 是否存在(若未初始化,忽略 NACK)if (!rtp_sender_) {return;}// 检查缓存是否启用且丢包列表非空(无缓存则无法重传,空列表无需处理)if (!StorePackets() || nack_sequence_numbers.empty()) {return;}// 计算 RTT(往返时间):用于后续重传频率控制(避免短时间内重复重传)int64_t rtt = rtt_ms();  // 优先从 RtcpRttStats 获取 RTT(若已统计)if (rtt == 0) {  // 若 RTT 未统计,从 RTCPReceiver 中获取远程 SSRC 的 RTTrtcp_receiver_.RTT(rtcp_receiver_.RemoteSSRC(),  // 接收端的 SSRCNULL,  // 忽略发送端到接收端的延迟&rtt,  // 输出 RTT(毫秒)NULL,  // 忽略抖动NULL   // 忽略延迟偏差);}// 将 NACK 丢包序列和 RTT 传递给 RTPSender,触发重传逻辑rtp_sender_->packet_generator.OnReceivedNack(nack_sequence_numbers, rtt);
}
2.3 流程3:重发 NACK 反馈的 RTP 报文

RTPSender 接收 NACK 丢包序列后,从 packet_history_ 中提取对应报文,校验重传条件(如 RTT 间隔、是否已在重传队列等),并通过 Pacer 以高优先级重发报文。

2.3.1 核心函数:RTPSender::OnReceivedNack(重传触发入口)

// 函数功能:处理 NACK 丢包序列,逐个触发报文重传
// 参数说明:
//   nack_sequence_numbers: 丢包序列号列表
//   avg_rtt: 平均 RTT(用于重传频率控制)
void RTPSender::OnReceivedNack(const std::vector<uint16_t>& nack_sequence_numbers,const int32_t avg_rtt
) {// 设置 RTT 到缓存:缓存中用于校验重传间隔(避免短时间内重复重传)// 5 + avg_rtt:增加 5ms 偏移,应对网络抖动packet_history_->SetRtt(5 + avg_rtt);// 遍历丢包序列号列表,逐个尝试重传for (uint16_t seq_no : nack_sequence_numbers) {// 调用 ReSendPacket 重传当前序列号的报文,返回重发的字节数const int32_t bytes_sent = ReSendPacket(seq_no);// 若重传失败(bytes_sent < 0),放弃后续所有丢包的重传(避免连锁失败)if (bytes_sent < 0) {RTC_LOG(LS_WARNING) << "Failed resending RTP packet " << seq_no << ", Discard rest of packets.";break;}}
}

2.3.2 核心函数:RTPSender::ReSendPacket(报文重发实现)

该函数是重传逻辑的核心,包含 缓存校验重传通道选择优先级配置 三大关键逻辑。

// 函数功能:重发指定序列号的 RTP 报文,处理重传通道、优先级和速率限制
// 参数说明:
//   packet_id: 待重传报文的序列号
// 返回值:int32_t - 重发的字节数(<0 表示重传失败)
int32_t RTPSender::ReSendPacket(uint16_t packet_id) {// 第一步:查询报文在缓存中的状态(是否存在、是否已在重传队列)absl::optional<RtpPacketHistory::PacketState> stored_packet =packet_history_->GetPacketState(packet_id);// 若报文不存在或已在重传队列,返回 0(无需处理)if (!stored_packet || stored_packet->pending_transmission) {return 0;}// 第二步:获取报文大小(用于速率限制和统计)const int32_t packet_size = static_cast<int32_t>(stored_packet->packet_size);// 第三步:判断是否使用 RTX 通道重传(RTX 是专门的重传通道)// RtxStatus() & kRtxRetransmitted:检查 RTX 重传模式是否启用const bool rtx = (RtxStatus() & kRtxRetransmitted) > 0;// 第四步:从缓存中提取报文并标记为“待重传”(防止重复重传)std::unique_ptr<RtpPacketToSend> packet =packet_history_->GetPacketAndMarkAsPending(packet_id,// 匿名函数:处理报文封装(RTX 或普通通道)[&](const RtpPacketToSend& stored_packet) -> std::unique_ptr<RtpPacketToSend> {// 子步骤1:速率限制校验(避免重传占用过多带宽)if (retransmission_rate_limiter_ &&!retransmission_rate_limiter_->TryUseRate(packet_size)) {// 若速率超限,返回空指针(重传失败)return nullptr;}// 子步骤2:选择重传通道并封装报文std::unique_ptr<RtpPacketToSend> retransmit_packet;if (rtx) {// 方案1:使用 RTX 通道重传(推荐)// BuildRtxPacket:将原报文封装为 RTX 格式(携带原序列号和 SSRC)retransmit_packet = BuildRtxPacket(stored_packet);} else {// 方案2:与普通媒体报文混传(不推荐,会影响丢包率统计)retransmit_packet = std::make_unique<RtpPacketToSend>(stored_packet);}// 子步骤3:标记重传报文的原序列号(供接收端识别)if (retransmit_packet) {retransmit_packet->set_retransmitted_sequence_number(stored_packet.SequenceNumber());}return retransmit_packet;});// 若提取报文失败(如速率超限、报文已过期),返回 -1(重传失败)if (!packet) {return -1;}// 第五步:配置重传报文的属性packet->set_packet_type(RtpPacketMediaType::kRetransmission);  // 标记为“重传报文”(用于优先级)packet->set_fec_protect_packet(false);  // 重传报文无需再做 FEC 保护(原报文已做)// 第六步:将重传报文加入 Pacer 队列(按高优先级发送)std::vector<std::unique_ptr<RtpPacketToSend>> packets;packets.emplace_back(std::move(packet));  // 转移报文所有权到队列paced_sender_->EnqueuePackets(std::move(packets));  // 加入 Pacer 发送队列// 返回重发的字节数(成功)return packet_size;
}

2.3.3 重传关键校验:RtpPacketHistory::GetPacketAndMarkAsPending(RTT 与状态校验)

// 函数功能:从缓存中提取报文,校验重传条件(RTT 间隔、是否已在重传),并标记状态
// 参数说明:
//   sequence_number: 待提取报文的序列号
//   encapsulate: 封装函数(用于 RTX 或普通通道处理)
// 返回值:std::unique_ptr<RtpPacketToSend> - 提取的报文(空指针表示失败)
std::unique_ptr<RtpPacketToSend> RtpPacketHistory::GetPacketAndMarkAsPending(uint16_t sequence_number,rtc::FunctionView<std::unique_ptr<RtpPacketToSend>(const RtpPacketToSend&)> encapsulate
) {MutexLock lock(&lock_);  // 线程安全锁// 条件1:缓存禁用,返回空if (mode_ == StorageMode::kDisabled) {return nullptr;}// 条件2:获取缓存中的报文,不存在则返回空StoredPacket* packet = GetStoredPacket(sequence_number);if (packet == nullptr) {return nullptr;}// 条件3:报文已在重传队列(pending_transmission_ 为 true),返回空(避免重复重传)if (packet->pending_transmission_) {return nullptr;}// 条件4:RTT 校验(避免短时间内重复重传,减轻网络负担)int64_t now_ms = clock_->TimeInMilliseconds();if (!VerifyRtt(*packet, now_ms)) {// 校验失败:距离上次重传时间小于 RTT,返回空return nullptr;}// 封装报文(RTX 或普通通道)std::unique_ptr<RtpPacketToSend> encapsulated_packet = encapsulate(*packet->packet_);// 若封装成功,标记报文为“待重传”if (encapsulated_packet) {packet->pending_transmission_ = true;}return encapsulated_packet;
}// 辅助函数:RTT 校验逻辑
bool RtpPacketHistory::VerifyRtt(const RtpPacketHistory::StoredPacket& packet,int64_t now_ms
) const {// 仅对已发送过的重传报文进行校验(首次重传无需校验)if (packet.send_time_ms_ &&  // 报文有发送时间戳packet.times_retransmitted() > 0 &&  // 已重传过至少一次now_ms < *packet.send_time_ms_ + rtt_ms_) {  // 当前时间 - 上次发送时间 < RTT// 校验失败:短时间内重复重传,可能报文仍在网络中return false;}return true;
}

2.3.4 重传优先级配置:GetPriorityForType(确保重传报文优先发送)

重传报文需按高优先级发送,以减少延迟。WebRTC 通过 RtpPacketMediaType 定义报文类型,再通过 GetPriorityForType 映射优先级。

// 函数功能:根据报文类型获取发送优先级(数值越小,优先级越高)
// 参数说明:
//   type: 报文类型(音频、视频、重传、FEC、Padding 等)
// 返回值:int - 优先级数值
int GetPriorityForType(RtpPacketMediaType type) {switch (type) {case RtpPacketMediaType::kAudio:// 音频优先级最高(实时性要求最高)return kFirstPriority + 1;case RtpPacketMediaType::kRetransmission:// 重传报文优先级次之(需尽快恢复丢包,避免卡顿)return kFirstPriority + 2;case RtpPacketMediaType::kVideo:// 普通视频报文优先级中等return kFirstPriority + 3;case RtpPacketMediaType::kForwardErrorCorrection:// FEC(前向纠错)报文优先级低于普通视频return kFirstPriority + 3;case RtpPacketMediaType::kPadding:// Padding(填充)报文优先级最低(无实际数据,仅用于带宽探测)return kFirstPriority + 4;default:RTC_CHECK_NOTREACHED();  // 断言:无其他类型}
}// Pacer 队列中使用优先级:EnqueuePacket 时传入优先级,确保高优先级报文先发送
void PacingController::EnqueuePacket(std::unique_ptr<RtpPacketToSend> packet) {RTC_DCHECK(pacing_bitrate_ > DataRate::Zero()) << "SetPacingRate must be called before InsertPacket.";  // 断言 Pacing 速率已配置// 先获取报文优先级,再存入队列(避免报文移动后无法访问类型)const int priority = GetPriorityForType(*packet->packet_type());// 按优先级加入内部队列(Pacer 会优先发送低数值优先级的报文)EnqueuePacketInternal(std::move(packet), priority);
}

2.3.5 RTX 通道配置(推荐)

RTX(Retransmission)是专门的重传通道,与普通媒体通道分离,可避免重传报文影响正常媒体的丢包率统计。启用 RTX 需配置以下三个核心参数:

// 1. 配置 NACK 缓存历史时间(确保重传时报文未被清理)
rtp_config_.nack.rtp_history_ms = kNackHistoryMs;  // kNackHistoryMs 通常为 1000ms(1秒)// 2. 配置 RTX 通道的 payload type(与普通媒体的 payload type 区分)
rtp_config_.rtx.payload_type = payload_type;  // 如 96(需与接收端协商一致)// 3. 配置 RTX 通道的 SSRC(与普通媒体的 SSRC 区分,避免混淆)
rtp_config_.rtx.ssrcs.push_back(rtx_ssrc);  // 如 0x12345678(需唯一)

3、核心总结

发送端 NACK 是 WebRTC 保障实时媒体传输质量的关键机制,其核心逻辑可概括为以下三点:

  1. 报文缓存:通过 packet_history_ 队列·34缓存可重传的 RTP 报文,按序列号索引,支持快速查询;同时根据媒体类型(视频/音频)配置合理的缓存长度,平衡内存占用与重传覆盖率。

  2. NACK 处理:接收端通过 RTCP NACK 报文反馈丢包序列,发送端解析后提取丢包序列号,结合 RTT 信息控制重传节奏,避免短时间内重复重传。

  3. 优先级重传:重传报文通过 RtpPacketMediaType::kRetransmission 标记为高优先级,确保 Pacer 优先发送;推荐使用 RTX 专用通道重传,避免影响正常媒体的丢包率统计和带宽估计(GCC)。

http://www.dtcms.com/a/524747.html

相关文章:

  • 主成分分析(PCA)在计算机图形学中的深入解析与应用
  • Kubernetes:实战Pod共享存储
  • 合肥市建设工程造价管理站网站ps网站背景图片怎么做
  • 5118网站是免费的吗网站如何防止重登录
  • 网络编程实战02·从零搭建Epoll服务器
  • IP数据报分片 题
  • 杭州设计 公司 网站建设适合小企业的erp软件
  • 全面掌握PostgreSQL关系型数据库,创建用户创建数据库操作,笔记09
  • 西安市网站制作公司购物商城排名
  • 思维大反转——往内走如实觉察
  • 计算机视觉——从环境配置到跨线计数的完整实现基于 YOLOv12 与质心追踪器的实时人员监控系统
  • 《商户查询缓存案例》使用案例学习Redis的缓存使用;缓存击穿、穿透、雪崩的原理的解决方式
  • 物联网固件安全更新中的动态密钥绑定与验证机制
  • YOLO学习——图像分割入门 “数据集制作和模型训练”
  • 网站域名的用处如何建设黔货出山电子商务网站
  • 三步搭建 AI 客服系统:用 PandaWiki 打造永不掉线的智能客服
  • 现在做一个网站多少钱网站制作商城
  • 详解EMQX2-EMQX功能使用
  • Vue3 中使用 CesiumJS
  • 【Trae+AI】Express框架01:教程示例搭建及基础原理
  • C# 中的 `as` 关键字:安全类型转换
  • Java 14 新特性Record、instanceof、switch语法速览
  • 网站的空间域名做公司网站的南宁公司
  • 中英文网站建站太原网站建设招聘
  • 建一个网站大概需要多长时间2016网站开发语言
  • 在Windows上使用Selenium + Chrome Profile实现自动登录爬虫
  • 八股-2025.10.24
  • 力扣2576. 求出最多标记下标
  • 做网站需要什么配置服务器工业产品设计软件
  • 大型语言模型基础之 Prompt Engineering:打造稳定输出 JSON 格式的天气预报 Prompt