深入浅出WebRTC—NACK
WebRTC 中的 NACK(Negative Acknowledgment)机制是实时通信中处理网络丢包的关键组件。网络丢包是常见的现象,尤其是在无线网络或不稳定连接中。NACK 机制旨在通过请求重传丢失的数据包来减少这种影响,从而保持通信的连续性和质量。
1. 总体架构
WebRTC NACK 总体架构如下图所示。
1)发送端发送 RTP 报文时会缓存一份到 RtpPacketHistory,收到 NACK 请求的时候从 RtpPacketHistory 获取对应缓存报文发送出去。RtpPacketHistory 收到 TransportFeedback 会将接收端确认的报文从缓存中移除。
2)接收端收到的所有报文都从 NackRequester 过一遍(只需要序列号),丢失了哪个报文门清。由于丢包和乱序无法分辨,NackRequest 在定时器驱动下发送 NACK 请求(极端丢包情况会发送关键帧请求)。
2. 发送端
2.1. 调用流程
发送端的调用流程由三条子流程组成:
1)发送出去的报文会被缓存到 RtpPacketHistory,用来响应 NACK 请求。
2)收到 TransportFeedback 将对应报文从 RtpPacketHistory 中移除。
3)收到 NACK 请求从 RtpPacketHistory 获取缓存报文发送出去。
2.2. RtpPacketHistory
RtpSenderEgress 负责报文发送,发送完后将报文缓存到 RtpPacketHistory。ModuleRtpRtcpImpl2 处理所有 RTCP 报文,NACK 请求交给 RTPSender 处理,RTPSender 从 RtpPacketHistory 获取请求重传的报文然后发送出去。
2.2.1. 重传条件
RtpSenderEgress 只会将满足条件的报文缓存到 RtpPacketHistory。正常的视频帧需要重传,但 FEC 报文不重传。另外,对于 simulcast 或 SVC,需要根据重传策略来决定,判断逻辑比较复杂,这里暂不分析。
void RtpSenderEgress::CompleteSendPacket(const Packet& compound_packet,bool last_in_batch) {...if (is_media && packet->allow_retransmission()) {packet_history_->PutRtpPacket(std::make_unique<RtpPacketToSend>(*packet), now);} else if (packet->retransmitted_sequence_number()) {packet_history_->MarkPacketAsSent(*packet->retransmitted_sequence_number());}...
}
2.2.2. 队列长度
缓存队列长度非常重要,太长的话,会引入较大延迟,太短的话,会导致重传 miss。因此,队列长度的设置需要在延迟和 miss 之间取得一个较好的平衡。
WebRTC 从时间和数量两个维度来对队列长度进行限制,其中,kMaxCapacity 是一个硬性数量限制,不管缓存的报文是否新鲜,都不能超过这个限制。
// packet_duration = max(1 second, 3x RTT).
static constexpr TimeDelta kMinPacketDuration = TimeDelta::Seconds(1);
static constexpr int kMinPacketDurationRtt = 3;// With kStoreAndCull, always remove packets after 3x max(1000ms, 3x rtt).
static constexpr int kPacketCullingDelayFactor = 3;// number_to_store_ = min(kMaxCapacity, kMinSendSidePacketHistorySize)
static constexpr size_t kMaxCapacity = 9600;
static const int kMinSendSidePacketHistorySize = 600;
void RtpPacketHistory::CullOldPackets()
{// 当前时间Timestamp now = clock_->CurrentTime();// 取 3 倍 RTT 和 1秒两者较大值,即不小于 1 秒TimeDelta packet_duration =rtt_.IsFinite()? std::max(kMinPacketDurationRtt * rtt_, kMinPacketDuration): kMinPacketDuration;while (!packet_history_.empty()) {// 队列中报文数量超过最大容量限制if (packet_history_.size() >= kMaxCapacity) {RemovePacket(0); // 移除最旧的报文continue;}// 取队列首报文进行判断const StoredPacket& stored_packet = packet_history_.front();// 正在重传中,退出if (stored_packet.pending_transmission_) {return;}// 还很新鲜(未超时),退出if (stored_packet.send_time() + packet_duration > now) {return;}// 首报文已经不新鲜,如果报文数量多或者首报文太老,才需要移除if (packet_history_.size() >= number_to_store_ ||stored_packet.send_time() + (packet_duration * kPacketCullingDelayFactor) <= now) {RemovePacket(0);} else {// No more packets can be removed right now.return;}}
}
2.2.3. PaddingMode
RtpPacketHistory 还可以用来生成带宽探测所需的 padding 报文,用真实报文当 padding 报文,既填充了码率又实现了冗余,一石二鸟。
RtpPacketHistory 中缓存了很多报文,挑选哪些报文做 padding 报文,支持三种 padding 模式:
enum class PaddingMode {// 选择最近缓存的报文作为 Padding 报文kDefault,// 基于发送时间、重传次数等因素选择更好的历史报文作为 Padding 报文kPriority,// 使用最近缓存的大包作为Padding报文kRecentLargePacket
};
对于 kPriority 模式,优先级定义如下:
bool RtpPacketHistory::MoreUseful::operator()(StoredPacket* lhs,StoredPacket* rhs) const {// 没有重传过的报文优先级更高if (lhs->times_retransmitted() != rhs->times_retransmitted()) {return lhs->times_retransmitted() < rhs->times_retransmitted();}// 时间越近的报文优先级越高return lhs->insert_order() > rhs->insert_order();
}
最新代码已经不再使用 kDefault 模式。
RtpPacketHistory::PaddingMode GetPaddingMode(const FieldTrialsView* field_trials) {if (!field_trials ||!field_trials->IsDisabled("WebRTC-PaddingMode-RecentLargePacket")) {return RtpPacketHistory::PaddingMode::kRecentLargePacket;}return RtpPacketHistory::PaddingMode::kPriority;
}
3. 接收端
3.1. 调用流程
NackRequester 是接收端的 NACK 控制核心,调用流程如下图所示。
1)RtpVideoStreamReceiver2 收到报文,在进行处理的同时也要通知 NackRequester。
2)NackRequester 内部有一个 NACK 请求队列,如果发现有丢包就会添加一个 NACK 请求项。
3)NackPeriodicProcessor 会定时调用 NackRequester 发送 NACK 请求。
4)通过层层调用将 NACK 请求发送出去。
3.2. NackRequester
每一个 RtpVideoStreamReceiver2 都持有一个 NackRequester,用来发起 NACK 请求。NackRequester 被 NackPeriodicProcessor 定时驱动,NACK 请求通过 NackSender 发送出去。如果丢包特别严重,NackRequester 会使用 KeyFrameRequestSender 发起关键帧请求。
3.2.1. NackList
NackList 是 NackRequester 内部的 NACK 请求队列。每次收到新的报文,与最近收到的报文 SN 进行比较,如果两个 SN 之间有空洞(SN 跳跃),认为有丢包,以空洞 SN 创建 NACK 请求项插入 NackList。
// 队列中首尾报文Sequence Number的最大跨度,适用于NackList、KeyFrameList和RecoveredList
constexpr int kMaxPacketAge = 10'000;
// 队列中最大报文数
constexpr int kMaxNackPackets = 1000;
// 最大重传次数
constexpr int kMaxNackRetries = 10;
因为空洞也可能是乱序导致,后续可能立即就会收到丢失报文,所以不能立即发送 NACK 请求。WebRTC 会启动一个定时器,确定 NackRequester 定时检查 NackList 中的 NACK 项,判断是否需要发送 NACK 请求。
决定选取哪些 NACK 项发起 NACK 请求,有不同筛选条件:
enum NackFilterOptions { kSeqNumOnly, kTimeOnly, kSeqNumAndTime };
1)kSeqNumOnly
基于报文乱序情况,每个 NACK 项插入队列时都会计算一个触发重传的 SN,表示后续收到此 SN 报文时,如果NACK 项还在队列中,且还没有发起过 NACK 请求,则立即触发一次。
每收到一个报文会检查此条件,当瞬时丢包比较严重的时候,能够比定时器更快触发 NACK 请求的发送,类似于 TCP 的快速重传机制。
2)kTimeOnly
每次发送 NACK 请求都会更新 NACK 的最近请求时间,如果最近请求时间距当前时间超过一个 RTT,则会重新触发 NACK 请求。此条件由定时器驱动进行检查。
3)kSeqNumAndTime
相当于“kSeqNumOnly || kTimeOnly”,只要一个条件满足就会触发 NACK 请求。(好像未使用)
3.2.2. KeyFrameList
KeyFrameList 存储每个关键帧的第一个报文,用来协助 NackList 进行收缩。对于视频来说,GOP 中的帧是有依赖关系的,如果前面的帧没有恢复,恢复后面的帧没有意义。因此,当 NackList 请求项溢出需要移除一些腾出空间时,WebRTC 是按照 GOP 粒度去丢弃历史久远的 NACK 请求项。
下面举例说明。假设有一个视频流,每个 GOP 由 5 个非 I 帧 报文和 2 个 I 帧报文组成,报文序列如下所示:
1,2,3,4,5,6,7,8,9,10,11,12,13,14,...
如果没有及时收到 3、4、11、13 四个报文,NackList 和 KeyFrameList 状态如下:
此时,如果需要创建新的 NACK 项,但 NackList 空间不够,需要丢弃 GOP1(3和4两个Nack项),状态如下:
NackList 空出两个表项,如果空间还不够,则从 KeyFrameList 中弹出表项,直到 SN 比 NackList 中的大,然后重复删除过程。
3.2.3. RecoveredList
NackRequester 内部有一个 RecoveredList,如果收到的是通过 FEC 或 RTX 恢复的报文,不会用来生成 NACK 请求项,而是被保存到 RecoveredList 中。在创建 NACK 请求项时,如果此报文已经被恢复了,则需要跳过。
为什么不把恢复报文当成普通的报文来处理,目前看是如果那样做会影响乱序的统计,而乱序的统计,又会影响前面讲到的 kSeqNumOnly 快速重传序号的计算。
3.3. 源码分析
3.3.1. OnReceivedPacket
这是 NackRequester 主函数,收到每个报文都需要调用此函数来生成或移除 NACK 请求项。
int NackRequester::OnReceivedPacket(uint16_t seq_num, bool is_keyframe,bool is_recovered) {bool is_retransmitted = true;// 初始化if (!initialized_) {newest_seq_num_ = seq_num;if (is_keyframe)keyframe_list_.insert(seq_num);initialized_ = true;return 0;}// 重复接收if (seq_num == newest_seq_num_)return 0;// 乱序包if (AheadOf(newest_seq_num_, seq_num)) {auto nack_list_it = nack_list_.find(seq_num);int nacks_sent_for_packet = 0;// 报文已经收到,移除 nack 项if (nack_list_it != nack_list_.end()) {nacks_sent_for_packet = nack_list_it->second.retries;nack_list_.erase(nack_list_it);}// 直方图统计乱序情况,重传报文的乱序不统计if (!is_retransmitted)UpdateReorderingStatistics(seq_num);return nacks_sent_for_packet;}// 保存关键帧报文序列号if (is_keyframe)keyframe_list_.insert(seq_num);// 关键帧报文太多了,清理下auto it = keyframe_list_.lower_bound(seq_num - kMaxPacketAge);if (it != keyframe_list_.begin())keyframe_list_.erase(keyframe_list_.begin(), it);// 经 FEC 或 RTX 恢复的报文if (is_recovered) {recovered_list_.insert(seq_num);// 恢复报文太多,清理下auto it = recovered_list_.lower_bound(seq_num - kMaxPacketAge);if (it != recovered_list_.begin())recovered_list_.erase(recovered_list_.begin(), it);// Do not send nack for packets recovered by FEC or RTX.return 0;}// 走到这里 seq_num 肯定比 newest_seq_num 大,newest_seq_num_ + 1, seq_num 之间// 可能存在 0 个或多个空洞,这些空洞就是需要发送nack的报文AddPacketsToNack(newest_seq_num_ + 1, seq_num);// 更新收到的最新序列号newest_seq_num_ = seq_num;// 这里仅发送基于序列号触发的 NACK 请求std::vector<uint16_t> nack_batch = GetNackBatch(kSeqNumOnly);if (!nack_batch.empty()) {nack_sender_->SendNack(nack_batch, /*buffering_allowed=*/true);}return 0;
}
3.3.2. AddPacketsToNack
当新收到报文与最近收的报文之间有空洞时,会调用此函数插入 NACK 请求项。这里要关注下,极端情况会清空 NACK 请求列表,直接发送关键帧请求。
void NackRequester::AddPacketsToNack(uint16_t seq_num_start, uint16_t seq_num_end) {// NACK 项太多了,清理下auto it = nack_list_.lower_bound(seq_num_end - kMaxPacketAge);nack_list_.erase(nack_list_.begin(), it);// 计算空洞数量uint16_t num_new_nacks = ForwardDiff(seq_num_start, seq_num_end);// 确保添加空洞 NACK 项后总 NACK 项不会超过最大限制if (nack_list_.size() + num_new_nacks > kMaxNackPackets) {// 先移除关键帧之前的 NACK 项while (RemovePacketsUntilKeyFrame() &&nack_list_.size() + num_new_nacks > kMaxNackPackets) {}// 还是腾不出足够的空间,则清空 NACK 队列,直接请求 I 帧if (nack_list_.size() + num_new_nacks > kMaxNackPackets) {nack_list_.clear();keyframe_request_sender_->RequestKeyFrame();return;}}// 遍历所有空洞创建 NACK 项for (uint16_t seq_num = seq_num_start; seq_num != seq_num_end; ++seq_num) {// 空洞报文可能已经被 FEC 或 RTX 恢复if (recovered_list_.find(seq_num) != recovered_list_.end())continue;// 使用乱序长度的中位数来计算触发重传的序列号NackInfo nack_info(seq_num, seq_num + WaitNumberOfPackets(0.5), clock_->CurrentTime());nack_list_[seq_num] = nack_info;}
}
3.3.3. GetNackBatch
定时器驱动调用此函数,定时检查发送 NACK 请求项。
void NackRequester::ProcessNacks() {// 定时器驱动,只获取基于时间条件判断需要处理的 NACK 项std::vector<uint16_t> nack_batch = GetNackBatch(kTimeOnly);if (!nack_batch.empty()) {nack_sender_->SendNack(nack_batch, /*buffering_allowed=*/false);}
}std::vector<uint16_t> NackRequester::GetNackBatch(NackFilterOptions options) {// 仅考虑序列号bool consider_seq_num = options != kTimeOnly;// 仅考虑时间bool consider_timestamp = options != kSeqNumOnly;// 当前时间Timestamp now = clock_->CurrentTime();// 筛选结果std::vector<uint16_t> nack_batch;auto it = nack_list_.begin();while (it != nack_list_.end()) {// 等待发送 NACK 的时间已经到了bool delay_timed_out = now - it->second.created_at_time >= send_nack_delay_;// 距离上一次发送 NACK 的时间也已经过去很久了bool nack_on_rtt_passed = now - it->second.sent_at_time >= rtt_;// 基于序列号的发送,只有在第一次发送Nack时生效bool nack_on_seq_num_passed =it->second.sent_at_time.IsInfinite() &&AheadOrAt(newest_seq_num_, it->second.send_at_seq_num);// 已经过了等待时间,基于时间和基于序列号两者满足其一if (delay_timed_out && ((consider_seq_num && nack_on_seq_num_passed) ||(consider_timestamp && nack_on_rtt_passed))) {nack_batch.emplace_back(it->second.seq_num);++it->second.retries; // 更新发送 NACK 请求次数it->second.sent_at_time = now; // 更新最近发送 NACK 请求时间// 已经达到最大请求次数限制,从队列中移除,不再请求了if (it->second.retries >= kMaxNackRetries) {it = nack_list_.erase(it);} else {++it;}continue;}++it;}return nack_batch;
}
4. 总结
WebRTC NACK 的实现简单明了,发送端缓存报文,接收端请求重传。但发送端和接收端实现关注重点不太一样。发送端是被动接收 NACK 请求,实现相对简单一些,重点关注缓存队列的长度。接收端需要主动发送发送 NACK 请求,实现会相对复杂一些,由于存在报文乱序,什么时候发起 NACK 请求是一个值得斟酌的事情。
除此之外,WebRTC 还考虑到了瞬间突发丢包的快速重传机制和基于关键帧的队列收缩等,这些都凸显了 WebRTC 对细节的掌控和重视。
相关文章:
深入浅出WebRTC—NACK
WebRTC 中的 NACK(Negative Acknowledgment)机制是实时通信中处理网络丢包的关键组件。网络丢包是常见的现象,尤其是在无线网络或不稳定连接中。NACK 机制旨在通过请求重传丢失的数据包来减少这种影响,从而保持通信的连续性和质量…...
简单工厂模式、工厂模式和抽象工厂模式的区别
简单工厂模式、工厂模式和抽象工厂模式都是创建型设计模式,它们之间在目的、实现方式和适用场景上存在显著的区别。以下是对这三种模式的详细比较: 一、定义与目的 简单工厂模式(Simple Factory Pattern) 定义: 简单工…...
JVM-垃圾回收与内存分配
目录 垃圾收集器与内存分配策略 引用 对象的访问方式有哪些?(句柄和直接指针) Java的引用有哪些类型? 如何判断对象是否是垃圾? 请列举一些可作为GC Roots的对象? 对象头了解吗? mark word(hashcode、分代、锁标志位)、…...
Jolt路线图
1. 引言 a16z crypto团队2024年7月更新了其Jolt路线图: 主要分为3大维度: 1)链上验证维度: 1.1)Zeromorph:见Aztec Labs团队2023年论文 Zeromorph: Zero-Knowledge Multilinear-Evaluation Proofs from…...
NEEP-EN2-2019-Text4
英二-2019-Text4摘自赫芬顿邮报《The Huffington Post》2018年6月的一篇名为“Let’s Stop Pretending Quitting Straws Will Solve Plastic Pollution”的文章。 以下为个人解析,非官方公开标准资料,可能有误,仅供参考。(单词解释…...
docker 部署wechatbot-webhook 并获取接口实现微信群图片自动保存到chevereto图库等
功能如图: docker部署 version: "3" services:excalidraw:image: dannicool/docker-wechatbot-webhook:latestcontainer_name: wechatbot-webhookdeploy:resources:limits:cpus: 0.15memory: 500Mreservations:cpus: 0.05memory: 80Mrestart: alwayspor…...
OpenWrt安装快速入门指南
在刷新 OpenWrt 固件之前,建议进行以下准备: 1、不要急于安装,慢慢来。如果在安装过程中出现奇怪之处,请先找到答案,然后再继续。 2、准备好设备的精确型号,以便能够选择正确的OpenWrt固件。 3、手上有关…...
AIGC Kolors可图IP-Adapter-Plus风格参考模型使用案例
参考: https://huggingface.co/Kwai-Kolors/Kolors-IP-Adapter-Plus 代码环境安装: git clone https://github.com/Kwai-Kolors/Kolors cd Kolors conda create --name kolors python=3.8 conda activate kolors pip install -r requirements.txt python3 setup.py install…...
从零开始学量化~Ptrade使用教程(七)——期权相关操作
期权交易 可点击证券代码右侧的选,进入期权选择菜单。通过选择标的商品,认购期权和认沽期权中间的选项(包括代码、成交价、幅度%、隐波%、内在价值、时间价值等),以及认购期权或认沽期权,选择所需的期权标的…...
TeamViewer关闭访问密码或固定一组密码不变
TeamViewer的新UI界面变化较大,网上的一些信息已经不再有效,更新后的访问密码在如下图所示: 演示的版本为7.21.4—— 设置每次你的设备访问的密码...
iMazing 3 换手机后苹果游戏数据还有吗 换iPhone怎么转移游戏数据
当你想要更换手机,无论是选择升级到最新款iPhone,或者换到“经典”旧款iPhone,单机游戏数据的转移总是让人发愁。本文将详细介绍换手机后苹果游戏数据还有吗,以及换iPhone怎么转移游戏数据,确保你能无缝继续你的游戏体…...
正则表达式:电子邮件地址的格式详解,及常见正则表达式符号的详细解释和匹配方式
一、第一部分是对该段电子邮件的详解 var Regex /^(?:\w\.?)*\w(?:\w\.)*\w$/; 1.^:这个符号表示匹配输入字符串的开始位置。 2.(?:...):这是一个非捕获组(non-capturing group),用于将正则表达式的一部分组合在…...
AWS全服务历史年表:发布日期、GA和服务概述一览(一)
我一直在尝试从各种角度撰写关于Amazon Web Services(AWS)的信息和魅力。由于我喜欢技术历史,这次我总结了AWS服务发布的历史年表。 虽然AWS官方也通过“Whats New”发布了官方公告,但我一直希望能有一篇文章将公告日期、GA日期&…...
现场可重构CPLD芯片应用案例—蓝牙音箱
我司英尚微提供的高性能数模混合现场可重构IC、通用可配置的模数混合芯片内部集成丰富的模拟资源和数字资源,可轻松替代电路中的各种标准器件,并按照客户要求组合成最优小型ASIC,缩短开发周期,降低成本。下面介绍LS98002现场可重构…...
vue2关于Object.defineProperty实现响应式
实现步骤: 1. 初始化阶段 当 Vue 实例化时,会遍历data 选项中的属性,并使用 Object.defineProperty 将它们转换为 getter 和 setter。这样一来,每当访问或修改这些属性时, Vue就能捕获到这些操作,从而实现…...
中英双语介绍一级市场(Primary Market)和二级市场(Secondary Market)
中文版 一级市场和二级市场是金融市场中的两个主要部分,分别对应证券发行和交易的不同阶段。 一级市场(Primary Market) 定义: 一级市场,又称新发行市场,是指证券首次发行和出售的市场。在一级市场中&am…...
OpenCV 轮廓检测
在 OpenCV 中,轮廓检测是一种用于查找图像中具有相似颜色或强度的连通像素组的技术,这些像素组通常代表了图像中的物体边缘。轮廓可以用来识别和分割图像中的物体,是计算机视觉应用中的一个重要步骤,如目标识别、形状分析等。 轮…...
ubuntu源码安装Odoo
序言:时间是我们最宝贵的财富,珍惜手上的每个时分 Odoo具有非常多的安装方式,除了我最爱用的 apt-get install,我们还可以使用git拉取Odoo源码进行安装。 本次示例于ubuntu20.04 Desktop上进行操作,理论上在ubuntu14.04之后都可以用此操作。 …...
大鲸鱼docker-compose单机容器集群编排工具
目录 一、Docker-compose 概述 二、Docker-compose简介 三、YML文件格式及编写注意事项 1.yml文件是什么 2.yml问价使用注意事项 3.yml文件的基本数据结构 四、Docker-compose 配置 1.Docker-Compose 配置常用字段 2.Docker Compose常用命令 3.使用Docker-compose创建…...
Dify中的高质量索引模式实现过程
思考在什么情况下会使用到高质量索引模式呢?第1种情况是在知识库中上传文档,文档被拆分为段落后需要进行编码(增加);第2种情况是在召回测试的时候,需要对query进行编码(查询);第3种情况是当文档中的段落增加和更新时需要进行编码(增加和更新)。索引模式是针对知识库…...
GO:Socket编程
目录 一、TCP/IP协议族和四层模型概述 1.1 互联网协议族(TCP/IP) 1.2 TCP/IP四层模型 1. 网络访问层(Network Access Layer) 2. 网络层(Internet Layer) 3. 传输层(Transport Layer&#…...
wls2下的centos使用桥接模式连接宿主机网络独立静态ip
前提:wsl2已安装,可正常更新 1.在控制面板中,打开开启或关闭windows功能,将里面的 Hyper-V功能打开,此处涉及重启 2. 按一下win键,输入hy,上面可以看到Hyper-V Manager,点进去 3.选择右边的 Vi…...
R语言实现神经网络ANN
# 常用激活函数 # 自定义Sigmoid函数 sigmod <- function(x){return(1/(1exp(-x))) } # 绘制Sigmoid曲线 x <- seq(-10,10,length.out 100) plot(x,sigmod(x),type l,col blue,lwd 2,xlab NA,ylab NA,main Sigmoid函数曲线)# 自定义Tanh函数 tanh <- function(…...
实战:shell脚本练习
高效编写Bash脚本的技巧 总结了10个实用技巧,帮助提高脚本的效率和可靠性,具体包括: 多写注释:在脚本中添加注释,以帮助理解脚本的不同部分。 当运行失败时使脚本退出:使用set -o errexit或set -e&#x…...
常见排序算法总结
文章目录 比较排序冒泡排序选择排序插入排序归并排序快速排序堆排序希尔排序 非比较排序(桶排序)计数排序基数排序 比较排序 冒泡排序 嵌套循环,每次内层循环执行时,数组的每两个元素交换,将一个最大/小的数排到数组…...
网页HTTP协议 get请求和post请求区别?(HTTP中Get、Post、Put与Delete的区别)(HTTP请求方法、HTTP请求方式、HTTP方法)
文章目录 设计GET、POST、DELETE 等多种请求方法的原因1. 符合语义化设计2. 允许服务器对不同的请求方法进行优化处理3. 提高数据传输的安全性4. 遵循现有的网络架构5. 提高网络通信的效率6. 支持 RESTful API 设计 设计GET、POST、DELETE 等多种请求方法的原因 后端之所以要分…...
攻防世界 re新手模式
Reversing-x64Elf-100 64位ida打开 看if语句,根据i的不同,选择不同的数组,后面的2*i/3选择数组中的某一个元素,我们输入的是a1 直接逆向得到就行 二维字符数组写法:前一个是代表有几个字符串,后一个是每…...
Ajax是什么?如何在HTML5中使用Ajax?
Ajax是什么,它如何工作? Ajax是什么 Ajax,全称Asynchronous Javascript And XML(异步JavaScript和XML),是一种创建交互式网页应用的网页开发技术。它允许网页在不重新加载整个页面的情况下,与…...
Python+Flask+MySQL/Sqlite的个人博客系统(前台+后端管理)【附源码,运行简单】
PythonFlaskMySQL/Sqlite的个人博客系统(前台后端管理)【附源码,运行简单】 总览 1、《个人博客系统》1.1 方案设计说明书设计目标工具列表 2、详细设计2.1 管理员登录2.2 程序主页面2.3 笔记新增界面2.4 文章新增界面2.5 文章/笔记管理界面2…...
【Android性能优化】Android CPU占用率检测原理和优化方向
【Android性能优化】Android CPU占用率检测原理和优化方向 CPU相关知识 CPU占用的基本计算公式 (1 - 空闲态运行时间/总运行时间) * 100% Hz、Tick、Jiffies: Hz:Linux核心每隔固定周期会发出timer interrupt (IRQ 0),HZ是用来定义每一秒有…...
个人是否可以做网站/软件培训机构排名
EL操作操作对象的方式 l 操作变量和常量:${name}、${8}; l 操作List和数组:${list[0]}、${arr[0]}; l 操作bean的属性:${person.name}、${person[‘name’]},对应person.getName()方法; l 操…...
江苏网站建设多少钱/网站运营
简介 SymPy是一个符号计算的Python库。它的目标是成为一个全功能的计算机代数系统,同时保持代码简 洁、易于理解和扩展。它完全由Python写成,不依赖于外部库。SymPy支持符号计算、高精度计算、模式匹配、绘图、解方程、微积分、组合数学、离散 数学、几何…...
网站建设用户调查问卷/杭州seo中心
题型: 编程题 语言: G;GCC Description “丑数”是指除了质因子2,3,5,不含其它质因子的正整数,例如由小到大前10个“丑数”为 1, 2, 3, 4, 5, 6, 8, 9, 10, 12, ... 非“丑数”的前10个数为 7, 11, 13, 14, 17, 19, 21, 22, 23, 26, ... 现…...
树莓派做网站/互联网培训
本篇文章给大家带来的内容是关于MySQL如何通过实例化对象参数查询数据 ?(源代码),有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助。public static string QueryByEntity(T t) where T : new(){ string resultstr s…...
ic千库网/seo专员是指什么意思
首先感慨下 vivizhyy 现在正在看的这本书——《Flex 完全自学手册》,这本书会让你看后相当有自信心,因为一般你会发现里面的代码不是太 cuo 就是太冗余……好吧,拿书里面给出的单选控件与用户交互的例子来说,书里给的 ① 个解决方…...
响应式网站的建设/广告联盟下载app
链接: 源代码下载地址 下面展示 代码 爬取上海交通大学软科中国大学排名import requests from bs4 import BeautifulSoupif __name__ "__main__":destinationPath "html信息.txt"allUniv []# headers{User-Agent:Mozilla/5.0}url http://www.shanghai…...