webrtc pacer模块(一) 平滑处理的实现
Pacer起到平滑码率的作用,使发送到网络上的码率稳定。如下的这张创建Pacer的流程图,其中PacerSender就是Pacer,其中PacerSender就是Pacer。这篇文章介绍它的核心子类PacingController及Periodic模式下平滑处理的基本流程。平滑处理流程中还有与带宽探测所关联的流程,在本篇文章中并不涉及。
从上图中可以看到,在创建Call
对象时,会创建一个RtpTransportControllerSend
,它是Call对象中发送数据的大总管,而PacerSender
也是属于它管理的对象。
一个Call
对象中一个RtpTransportControllerSend
,一个RtpTransportControllerSend
中一个PacerSender
,所以Pacer是作用于Call中所有的stream,这里并不是只处理音视频包,还有fec包,重传包,padding包,Call
对象中也发送出去的数据都会经过Pacer。
这篇文章是介绍平滑实现的基本原理和Pacer中的Periodic
模式的处理流程。Pacer的流程中还有与带宽探测所关联的流程,在本篇文章中并不涉及。
码率平滑的原理
在视频编码中,虽然编码器会将输出码流的码率控制在所设置的码率范围内。但是在编码器产生关键帧或在画面变化比较大时,码率可能超过设置的码率值。在有fec或重传包时,也可能造成实际发送的码率值超过目标值。这种突发的大码率的数据,可能就会造成网络链路拥塞。
所以引入的pacer就是平滑发送的码率值,在一段时间内,保证发送码率接近设置目标码率值。而避免突发的高码率造成网络链路拥塞。
平滑的基本原理就是**缓存队列+周期发送,将要发送的数据先缓存,在周期性的发送出去,起到平均码率的目的。那么这种周期有两种模式:**
**kPeriodic**
,周期模式,也是默认模式,以固定间隔时间发送数据。kDynamic
,动态模式,根据数据的缓存时长及数据量来计算下一次发送数据的时间点。
组成
pacer的流程都实现在PacingController
,包括两个核心类:RoundBoinPacketQueue
,IntervalBudget
。
RoundBobinPacketQueue
缓存队列,对每条流都会缓存,以ssrc做为流的唯一标识,包括:重传包,fec,padding包。IntervalBudget
根据设置的目标码率值及时间间隔计算可发送的数据量。
PacingController类
所属文件为\modules\pacing\pacing_controller.h
,如下类图:
两个核心的成员变量:
RoundRobinPakcetQueue packet_queue_
packet的缓存队列。IntervalBudget media_buget_
可发送数据量计算。
两个核心函数:
NextSendTime
,获取每次执行的时间(5毫秒,在kPeriodic
模式下)。ProcessPackets
,周期处理包的发送,确定要发送的数据量,从缓存队列中取包。
平滑逻辑的处理流程
整个pacer运行的机制就是靠PacingController
的NextSendTime
和ProcessPackets
两个方法,它们被单独的放在一个ModuleThread
线程中执行,周期性的被执行,两个方法调用的堆栈如下:
**NextSendTime**
peerconnection_client.exe!webrtc::PacingController::NextSendTime() 行 348 C++
peerconnection_client.exe!webrtc::PacedSender::TimeUntilNextProcess() 行 171 C++
peerconnection_client.exe!webrtc::PacedSender::ModuleProxy::TimeUntilNextProcess() 行 150 C++
peerconnection_client.exe!webrtc::`anonymous namespace’::GetNextCallbackTime(webrtc::Module * module, __int64 time_now) 行 30 C++
peerconnection_client.exe!webrtc::ProcessThreadImpl::Process() 行 231 C++
peerconnection_client.exe!webrtc::ProcessThreadImpl::Run(void * obj) 行 198 C++
peerconnection_client.exe!rtc::PlatformThread::Run() 行 130 C++
peerconnection_client.exe!rtc::PlatformThread::StartThread(void * param) 行 62 C++
**ProcessPackets**
peerconnection_client.exe!webrtc::PacingController::ProcessPackets() 行 408 C++
peerconnection_client.exe!webrtc::PacedSender::Process() 行 183 C++
peerconnection_client.exe!webrtc::PacedSender::ModuleProxy::Process() 行 152 C++
peerconnection_client.exe!webrtc::ProcessThreadImpl::Process() 行 226 C++
peerconnection_client.exe!webrtc::ProcessThreadImpl::Run(void * obj) 行 198 C++
peerconnection_client.exe!rtc::PlatformThread::Run() 行 130 C++
peerconnection_client.exe!rtc::PlatformThread::StartThread(void * param) 行 62 C++
核心骨架就是下面三个步骤:
- 设置目标码率,通过
SetPacingRates(...)
方法。 - 计算每个时间片可以发送的数据,在
UpdateBudgetWithElapsedTime(TimeDelta delta)
方法中。 - 用已发送的数据量来计算还剩多少数据量可以发送,在
UpdateBudgetWithSentData(DataSize size)
方法中。
详细流程:
(1). 如果媒体数据包处理模式是 kDynamic,则检查期望的发送时间和 前一次数据包处理时间 的对比,当前者大于后者时,则根据两者的差值更新预计仍在传输中的数据量,以及 前一次数据包处理时间;(Periodic模式是5ms执行一次)
(2). 从媒体数据包优先级队列中取一个数据包出来;
(3). 第 (2) 步中取出的数据包为空,但已经发送的媒体数据的量还没有达到码率探测器 webrtc::BitrateProber 建议发送的最小探测数据量,则创建一些填充数据包放入媒体数据包优先级队列,并继续下一轮处理;
(4). 发送取出的媒体数据包;
(5). 获取 FEC 数据包,并放入媒体数据包优先级队列;
(6). 根据发送的数据包的数据量,更新预计仍在传输中的数据量等信息;
(7). 如果是在码率探测期间,且发送的数据量超出码率探测器 webrtc::BitrateProber 建议发送的最小探测数据量,则结束发送过程;
(8). 如果媒体数据包处理模式是 kDynamic,则更新目标发送时间。
RoundBobinPacketQueue
RoundBobinPacketQueue
是一个缓存队列, 用于缓存数据包(音视频包,fec,padding,重传包),它有两个特征:
- 根据优先级存储包(每种类型包都有优先级)。
- 记录缓存时长(记录每个包的入队时间,用于计算缓存的总时长,避免引入过多的延迟)。
类图
上图种的Stream
类代表了一路流,QueuePacket
类代表了数据包。
RoundBobinPacketQueue
三个核心的数据结构:
std::map<uint32_t, Stream> streams_
key为ssrc。
std::multimap<StreamPrioKey, uint32_t> **stream_priorities_**
**Stream**
的优先级信息表,以**priority**
和**DataSize**
为比较的key,value是ssrc。通过优先级找Stream
,方便优先级变化的实现。越靠前,优先级越高。
Stream
类中的std::multimap<StreamPrioKey, uint32_t>::iterator priority_it;
它指向 RoundBobinPacketQueue
中的stream_priorities_
中的某项,可以快速定位到自己的优先级。
std::multiset<Timestamp> enqueue_times_
The enqueue time of every packet currently in the queue. Used to figure out the age of the oldest packet in the queue.
记录每一个包的入队时间
QueuedPacket
对象中的std::multiset<Timestamp>::iterator enqueue_time_it_;
指向enqueue_times_
中的项,可以快速定位到自己的入队时间。
Stream
,QueuePacket
,RoundBobinPacketQueue
关系图
如下是Stream
对象,QueuePacket
对象与RoundBobinPacketQueue
对象的关系图。
上图是以Stream
为中心,描绘Stream
,QueuePacket
,RoundBobinPacketQueue
的关系。
- 每个
Stream
都被记录在RoundRobinPacketQueue
的streams_
中,以ssrc为key。 - 每个
Stream
的优先级都被记录在RoundRobinPacketQueue
的stream_priorites_
中,以优先级为key,ssrc为value。 - 数据包都被封装成
QueuePacket
缓存在Stream
对象的packet_queue
中,它也是一个优先级队列,所以每个数据包都是有优先级的。 RoundRobinPacketQueue
的enqueue_times_
记录着每个rtp packet的入队时间。stream
中std::multimap<StreamPrioKey,uint32_t>::iterator priority_it
迭代器指向该stream在stream_priorites_
中的位置,便于快速检索。QueuedPacket
中的std::multiset<Timestamp>::iterator enqueue_time_it
迭代器指向该packet在enqueue_times_
中的位置,便于快速检索。
缓存队列中记录的信息有:
- 记录总的缓存包个数。
- 记录总的数据量。
- 记录包的优先级。
- 记录包的入队时间(计算包的缓存总时长,平均缓存时间,最大缓存时间)。
插入队列(push方法)的逻辑
- 从streams_中找pakcet所属的Ssrc的stream,如果没有,则在streams_中插入一项。
- 查看stream的priority_it是否等于stream_priorities_的end():如果相等,则在stream_priorities插入新的项; 否则,如果新包的优先级高,则更新其ssrc对应队列的优先级。
- 更新队列总时长。
- 入队时间减去暂停时间(一般不会有暂停)。
- 队列总包数+1。
- 队列总字节大小+包的负载大小+Padding大小(Packet的大小)。
- 插入到steam对象的packet_queue中。
push流程的注意点:
- stream的size指的是stream发送的size,在Pop中,会加上弹出的PacketSize。
- 一条stream的packet的priority值都是一样的。
- 在入队一个stream的新的packet时,并不确定优先级,触发优先级队列中没有记录或packet的优先级发生变化。
取数据(Pop方法)的逻辑
- 获得优先级最高的stream。
- 从stream的packet_queue中取出第一个Packet。
- 将stream在stream_priorites_中的项删除掉。
- 计算Packet入队后到现在的时间(不包括暂停时间)。
- 将这段时间从队列的总时间中减去。
- 从equeue_times_中将Packet的项删除。
- 总包数减一。
- 总字节数减去包的字节数。
- 将包从stream中的queue中弹出。
- 如果stream中的队列为空,则令stream的priority_it指向stream_priorities的end()。
- 否则,从stream队列头部取Packet,将该Packet的priority插入到stream_priorities_中。
缓存时间的计算
计算缓存时间的目的是控制延迟,包括如下几个方法:
- 获取缓存时间最长的包
Timestamp RoundRobinPacketQueue::OldestEnqueueTime() const {if (single_packet_queue_.has_value()) {return single_packet_queue_->EnqueueTime();}if (Empty())return Timestamp::MinusInfinity();RTC_CHECK(!enqueue_times_.empty());return *enqueue_times_.begin();
}
这个方法是用于统计,最终会被call
对象的GetStats()
方法调用。
- 计算总延时,
UpdateQueueTime
每次被调用,总时长都会被计算,累加。
void RoundRobinPacketQueue::UpdateQueueTime(Timestamp now) {RTC_CHECK_GE(now, time_last_updated_);if (now == time_last_updated_)return;TimeDelta delta = now - time_last_updated_;if (paused_) {pause_time_sum_ += delta;} else {//有n个包,每调一次UpdateQueueTime就有一个delta值,总数为size of packet乘以deltaqueue_time_sum_ += TimeDelta::Micros(delta.us() * size_packets_);}time_last_updated_ = now;
}
- 计算平均缓存时间
平均缓存时间=queue的总时间数/包数,用于判断延时(缓存时间)是否过大。
TimeDelta RoundRobinPacketQueue::AverageQueueTime() const {if (Empty())return TimeDelta::Zero();return queue_time_sum_ / size_packets_;
}
控制延时
在PacingController::ProcessPackets()
方法中,会计算包的缓存时间,如下if
分支
if (drain_large_queues_) {//限制延时TimeDelta avg_time_left =std::max(TimeDelta::Millis(1),queue_time_limit - packet_queue_.AverageQueueTime());DataRate min_rate_needed = queue_size_data / avg_time_left;if (min_rate_needed > target_rate) {target_rate = min_rate_needed;RTC_LOG(LS_VERBOSE) << "bwe:large_pacing_queue pacing_rate_kbps="<< target_rate.kbps();}
}
首先会计算缓存队列的的平均缓存时间,通过设置的缓存时间限制值减去它得出应该要在多长时间发送这些数据。
再计算发送速率,最后设置目标码率值。这个目标码率值会被设置到media_buget
中去(kPeriodic
模式下)。
快速处理
缓存队列缓存数据,肯定会引入延迟,在RonundBobinPacketQeueu
有一个absl::optional<QueuedPacket> single_packet_queue_
成员变量,它的作用就是快速处理数据包。
只有音频流时的处理
音频对延迟很敏感,需要尽量少引入延迟。在RoundRobinPacketQueue::Push
中,有一个分支,如下:
if (size_packets_ == 0) {single_packet_queue_.emplace(QueuedPacket(priority, enqueue_time, enqueue_order,enqueue_times_.end(), std::move(packet)));UpdateQueueTime(enqueue_time);single_packet_queue_->SubtractPauseTime(pause_time_sum_);size_packets_ = 1;size_ += PacketSize(*single_packet_queue_);}
在 size_packets_ == 0,会放到single_packet_queue_
。而每取一个数据包,size_packets
设置0。对音频包,一次采集周期内,20ms,只会产生一个包,而pacer的执行周期是5ms,音频包始终会走入if (size_packets_ == 0)
为0的分支。
在取数据包时,在std::unique_ptr<RtpPacketToSend> PacingController::GetPendingPacket
方法中,会有一个判断语句,判断音频是否走pacer。
bool unpaced_audio_packet =!pace_audio_ && packet_queue_.LeadingAudioPacketEnqueueTime().has_value();
LeadingAudioPacketEnqueueTime()
是判断single_packet_queue_
或streams_
是否有缓存音频包(下一个包是否是音频包)。
absl::optional<Timestamp> RoundRobinPacketQueue::LeadingAudioPacketEnqueueTime()const {if (single_packet_queue_.has_value()) {if (single_packet_queue_->Type() == RtpPacketMediaType::kAudio) {return single_packet_queue_->EnqueueTime();}return absl::nullopt;}if (stream_priorities_.empty()) {return absl::nullopt;}uint32_t ssrc = stream_priorities_.begin()->second;const auto& top_packet = streams_.find(ssrc)->second.packet_queue.top();if (top_packet.Type() == RtpPacketMediaType::kAudio) {return top_packet.EnqueueTime();}return absl::nullopt;
}
如果这个unpaced_audio_packet
变量的值为true,这不会走media_buget_
的机制,直接取出数据。
std::unique_ptr<RtpPacketToSend> PacingController::GetPendingPacket(const PacedPacketInfo& pacing_info,Timestamp target_send_time,Timestamp now) {if (packet_queue_.Empty()) {return nullptr;}// First, check if there is any reason _not_ to send the next queued packet.// Unpaced audio packets and probes are exempted from send checks.bool unpaced_audio_packet =!pace_audio_ && packet_queue_.LeadingAudioPacketEnqueueTime().has_value();bool is_probe = pacing_info.probe_cluster_id != PacedPacketInfo::kNotAProbe;//不pace audioif (!unpaced_audio_packet && !is_probe) {if (Congested()) {// Don't send anything if congested.return nullptr;}if (mode_ == ProcessMode::kPeriodic) {if (media_budget_.bytes_remaining() <= 0) {// Not enough budget.RTC_LOG(LS_INFO) << "===> media budget not enough";return nullptr;}} else {// Dynamic processing mode.if (now <= target_send_time) {// We allow sending slightly early if we think that we would actually// had been able to, had we been right on time - i.e. the current debt// is not more than would be reduced to zero at the target sent time.TimeDelta flush_time = media_debt_ / media_rate_;if (now + flush_time > target_send_time) {return nullptr;}}}}//直接取出数据return packet_queue_.Pop();
}
在std::unique_ptr<RtpPacketToSend> RoundRobinPacketQueue::Pop()
方法中,走下面这个分支。
if (single_packet_queue_.has_value()) {//音频包走这个分支RTC_DCHECK(stream_priorities_.empty());std::unique_ptr<RtpPacketToSend> rtp_packet(single_packet_queue_->RtpPacket());single_packet_queue_.reset();queue_time_sum_ = TimeDelta::Zero();size_packets_ = 0;size_ = DataSize::Zero();return rtp_packet;}
在只有音频的情况下,音频包只会入single_packet_queue_
,并且不会走media_buget_
的机制,每次时间片内都会马上取出来发送出去,起到降低延迟的作用。
音视频流的处理
PacingController::ProcessPackets
是每5ms跑一次(kPeriodic
模式)。视频数据,一次会产生一批rtp包,在间隔周期内,会有多个包进入队列。在size_packets_
为0时,包会进入single_packet_queue_
,不为0时进入包缓存队列。在这个时候media_budget_
就起作用了。
音视频流都存在的情况下,音频包也不止会进入single_pakcet_queue_
了,这时音频的加速就体现在std::unique_ptr<RtpPacketToSend> PacingController::GetPendingPacket
上了,判断为音频包时,则不走media_buget_
机制,直接取出数据。
对非音频包,则下面这个分支会起作用,限制包的发送。
if (mode_ == ProcessMode::kPeriodic) {if (media_budget_.bytes_remaining() <= 0) {// Not enough budget.RTC_LOG(LS_INFO) << "===> media budget not enough";return nullptr;}
}
IntervalBudget
原理
IntervalBudget
作用是根据当前PacedSender->Process
的调用时间间隔和当前目标码率target bitrate
来计算出本次Process
理应发送的字节数。
比如当前码率是100 000bps,本次Process
调用与上次调用间隔是20ms,则本次理应发送的字节数是100 bits per ms * 20 ms = 2000bits=250 bytes
。
250bytes为本次发送理应发送的字节数,但实际上视频RTP包差不多是一个MTU大小。我们不可能真的发送250bytes的数据,因此可能会导致理应发送的数据量多或少的问题,如何解决这个问题呢?
IntervalBudget
中引入一个bytes_remaining_
的变量来记录上次发送后,与理应发送数据量相比,多或少发了多少。其值为负表示上轮我们实际发送的比理应发送的数据量多了,我们本轮应该停止发送。其值为正表示我们上轮实际发送比理应发送的要少,还有富余。
工作原理
void set_target_rate_kbps(int target_rate_kbps);
设置总的可用量max_bytes_in_budget_
。
void IntervalBudget::set_target_rate_kbps(int target_rate_kbps) {target_rate_kbps_ = target_rate_kbps;max_bytes_in_budget_ = (kWindowMs * target_rate_kbps_) / 8;bytes_remaining_ = std::min(std::max(-max_bytes_in_budget_, bytes_remaining_),max_bytes_in_budget_);
}
target_rate_kbps
目标码率,max_bytes_in_budget_
为半秒钟可发送的码率。
void IncreaseBudget(int64_t delta_time_ms);
根据毫秒数增加预算(增加的量计入bytes_remaining
),在kPeriodic
模式下,这个delta_time_ms的值为5ms。
void IntervalBudget::IncreaseBudget(int64_t delta_time_ms) {int64_t bytes = target_rate_kbps_ * delta_time_ms / 8;if (bytes_remaining_ < 0 || can_build_up_underuse_) {// We overused last interval, compensate this interval.bytes_remaining_ = std::min(bytes_remaining_ + bytes, max_bytes_in_budget_);} else {// If we underused last interval we can't use it this interval.bytes_remaining_ = std::min(bytes, max_bytes_in_budget_);}
}
void UseBudget(size_t bytes);
使用预算(bytes_remaining_
减去bytes)。
void IntervalBudget::UseBudget(size_t bytes) {bytes_remaining_ = std::max(bytes_remaining_ - static_cast<int>(bytes),-max_bytes_in_budget_);
}
UseBudget(size_t bytes)
更新用掉的数据量(就是已发送的数据量),如下调用堆栈
如果bytes_remaining_
小于0,那么当然不能在发数据了。
**padding_budget_**
的原理也一样,它是用于计算padding的数据量。
码率平滑的实现原理
发包的流程PacingController::ProcessPackets
放在一个线程中,会被定时触发。被触发后,会计算当前时间和上次被调用时间的时间差,然后将时间差参数传入media_buget
(**IntervalBudget**
对象),media_buget_
算出当前时间片可以发送多少数据,然后从缓存队列(**RoundBobinPacketQueue**
对象)中取出数据进行发送。
**media_buget_**
计算时间片发送多少字节的公式如下:
**delta time:**上次检查时间点和这次检查时间点的时间差。
target bitrate: pacer的参考码率,是由probe模块根据网络探测带宽评估出来。
remain_bytes: 每次触发包时会减去发送报文的长度size,如果remain_bytes>0
,继续从缓存队列中取下一个报文进行发送,直到remain_bytes<=0
或者缓存队列没有更多的报文。
如果缓存队列没有更多待发的报文,但是**media_buget_**
(**IntervalBudget**
对象)计算出还可以发送更多的数据,这个时候pacer会进行padding报文补充。
四个用于控制发送码率的方法:
**bool PacingController::Congested()**
**void PacingController::OnPacketSent**
** 底层socket发送的数据量的回调。**
**void PacingController::UpdateBudgetWithSentData(DataSize size)**
**void PacingController::UpdateOutstandingData(DataSize outstanding_data)**
码率分配
数据包的优先级
前面就提到了缓存队列是一个优先级队列,对数据包会设置一个优先级,在每次插入数据时(PacingController::EnqueuePacket(...)
方法),都会调用GetPriorityForType(RtpPacketMediaType type)
,如下优先级:
int GetPriorityForType(RtpPacketMediaType type) {// Lower number takes priority over higher.switch (type) {case RtpPacketMediaType::kAudio:// Audio is always prioritized over other packet types.return kFirstPriority + 1;case RtpPacketMediaType::kRetransmission:// Send retransmissions before new media.return kFirstPriority + 2;case RtpPacketMediaType::kVideo:case RtpPacketMediaType::kForwardErrorCorrection:// Video has "normal" priority, in the old speak.// Send redundancy concurrently to video. If it is delayed it might have a// lower chance of being useful.return kFirstPriority + 3;case RtpPacketMediaType::kPadding:// Packets that are in themselves likely useless, only sent to keep the// BWE high.return kFirstPriority + 4;}RTC_CHECK_NOTREACHED();
}
在QueuedPacket
中的operator<(const RoundRobinPacketQueue::QueuedPacket& other)
会根据优先级确定QueuedPacket
在队列中顺序。priority值越小,代表优先级越高,如下,在QueuedPacket
中定义的bool operator<(const QueuedPacket& other) const
bool RoundRobinPacketQueue::QueuedPacket::operator<(const RoundRobinPacketQueue::QueuedPacket& other) const {if (priority_ != other.priority_)return priority_ > other.priority_;if (is_retransmission_ != other.is_retransmission_)return other.is_retransmission_;return enqueue_order_ > other.enqueue_order_;
}
● 优先值小的,排在前面。
● 优先级相同,非重传包在前面。
● 优先级和重传标志均相同,以入队先后顺序排列(enqueue_order_就是一个递增的值)。
相关文章:
webrtc pacer模块(一) 平滑处理的实现
Pacer起到平滑码率的作用,使发送到网络上的码率稳定。如下的这张创建Pacer的流程图,其中PacerSender就是Pacer,其中PacerSender就是Pacer。这篇文章介绍它的核心子类PacingController及Periodic模式下平滑处理的基本流程。平滑处理流程中还有…...
新能源车「大三电」与「小三电」
一、大三电系统 动力电池 作为整车的能量核心,目前主流类型包括三元锂电池(NCM/NCA)和磷酸铁锂电池(LFP),前者能量密度高,后者安全性和成本优势显著。动力电池通过模组串联形成电池包…...
xlinx GT传输器学习
xlinx有2路refclk会输入到gtbank。此2路bank输入时钟是差分输入,经过IBUFDS_GTE2源语转换为单端。如下图。 从fpga外输入高速差分时钟。 差分时钟转换为单端。 单端时钟输入到gt_common模块 下图为gt内部结构图...
C++笔记之父类引用是否可以访问到子类特有的属性?
C++笔记之父类引用是否可以访问到子类特有的属性? code review! 参考笔记 1.C++笔记之在基类和派生类之间进行类型转换的所有方法 文章目录 C++笔记之父类引用是否可以访问到子类特有的属性?1.主要原因2.示例代码3.说明4.如何访问子类特有的属性5.注意事项6.总结在 C++ 中,…...
基于SSM框架的房屋租赁小程序开发与实现
概述 一个基于SSM框架开发的微信小程序房屋租赁管理系统,该项目实现了用户管理、中介管理、房源信息管理等核心功能。 主要内容 一、管理员模块功能实现 用户管理 管理员可对通过微信小程序注册的用户信息进行修改和删除操作,确保用户数据的准…...
大模型评估论文粗读“AGIEval: A Human-Centric Benchmark for Evaluating Foundation Models“
Zhong W, Cui R, Guo Y, et al. Agieval: A human-centric benchmark for evaluating foundation models[J]. arXiv preprint arXiv:2304.06364, 2023. 摘要翻译 评估大模型在处理人类层级的任务时的通用能力,是AGI发展和应用中至关重要的一环。传统的衡量标准通常依…...
程序化广告行业(76/89):行业融资全景剖析与代码应用拓展
程序化广告行业(76/89):行业融资全景剖析与代码应用拓展 大家好!在之前的文章里,咱们一起了解了程序化广告行业的发展趋势以及PC端和移动端投放的差异。今天,咱们接着深入学习,这次聚焦在程序化…...
网工基础 | 常见英文术语注解
原创:厦门微思网络 以下是一些网工专业常见的英文术语注解: IP Address(Internet Protocol Address) 互联网协议地址,是分配给连接到互联网的设备的唯一标识符,用于在网络中定位和通信。例如,“…...
【FPGA开发技巧】Modelsim仿真中,显示状态机的名称,而非编码数字
示例 被仿真的文件名为:cmd_handle.v,其中有r_st_current和r_st_next两个状态机变量。 该模块在tb文件中,被例化的名称为cmd_handle_u0 按照如下格式写:cmd_handle_u0.r_st_current 示例tb文件: timescale 1ns / 1…...
二分答案----
二分答案 - 题目详情 - HydroOJ 问题描述 给定一个由n个数构成的序列a,你可以进行k次操作,每次操作可以选择一个数字,将其1,问k次操作以后,希望序列里面的最小值最大。问这个值是多少。 输入格式 第一行输入两个正…...
Nginx 命令大全:Linux 与 Windows 系统的全面解析
Nginx 是一个高性能的 HTTP 和反向代理服务器,广泛应用于 Web 服务、负载均衡和反向代理等场景。本文将从 Linux 和 Windows 系统的角度出发,详细解析 Nginx 的常用命令,并区分两种系统之间的差异,帮助你高效地管理和维护 Nginx 服…...
我的计算机网络(总览篇)
总览--网络协议的角度 在一个庞大的网络中,该从哪里去了解呢?我先细细的讲一下我们访问一个网站的全部流程,当我们的电脑连上网络的时候,就会启动DHCP协议,来进行IP地址,MAC地址,DNS地址的分配…...
开源CMS的模块化设计和API接口如何具体影响其扩展性?
优秀的CMS系统都有自己主打的特点,开源CMS凭借其灵活性和低成本优势占据了市场主流地位,而模块化设计与API接口正是其扩展性的两大基石。本文将深入探讨这两大技术特性是如何影响cms的扩展性的。 一、模块化设计:功能解耦与生态繁荣的引擎 …...
【网络原理】网络通信基础
目录 一. 网络发展史 (1)局域网 (2)广域网 二. 网络基础知识 (1)IP地址 (2)端口号 (3)协议 (4)五元组 一. 网络发展史 网络的发…...
Win11系统 VMware虚拟机 安装教程
Win11系统 VMware虚拟机 安装教程 一、介绍 Windows 11是由微软公司(Microsoft)开发的操作系统,应用于计算机和平板电脑等设备 。于2021年6月24日发布 ,2021年10月5日发行 。 Windows 11提供了许多创新功能,增加了新…...
新手宝塔部署thinkphp一步到位
目录 一、下载对应配置 二、加载数据库 三、添加FTP 四、上传项目到宝塔 五、添加站点 六、配置伪静态 七、其他配置 开启监控 八、常见错误 一、打开宝塔页面,下载对应配置。 二、加载数据库 从本地导入数据库文件 三、添加FTP 四、上传项目到宝塔…...
MCU如何查看系统有没有卡死
前言:在我们调试单片机的时候,经常会遇到系统非预期运行的状况。造成系统卡死的原因有很多,主要有:野指针,数组越界,死锁,还有一些while写入的一些逻辑错误。本文就如何排查这些问题,…...
有哪些反爬机制可能会影响Python爬取视频?如何应对这些机制?
文章目录 前言常见反爬机制及影响1. IP 封禁2. 验证码3. 请求头验证4. 动态加载5. 加密与混淆6. 行为分析 应对方法1. 应对 IP 封禁2. 应对验证码3. 应对请求头验证4. 应对动态加载5. 应对加密与混淆6. 应对行为分析 前言 在使用 Python 爬取视频时,会遇到多种反爬…...
【辰辉创聚生物】提供上万种单抗/多抗及其偶联物
辰辉创聚生物致力于提供高质量的一抗(Primary Antibodies)、二抗(Secondary Antibodies)和对照抗体(Control Antibodies),支持分子生物学、免疫学及生物医学科学领域的前沿研究。我们拥有超过40…...
Google Chrome Canary版官方下载及安装教程【适用于开发者与进阶用户】
谷歌浏览器(Google Chrome)以其高性能、强扩展性和良好的用户体验深受全球用户喜爱。在其多个版本中,Chrome Canary因具备最前沿的功能测试环境,成为开发者和技术探索者的首选。如果你希望第一时间体验Google Chrome最新功能&…...
BERT - BERT 模型综述
1. BERT的两个任务 (1)Pre-Training 预训练 Masked LM (MLM) - 让模型预测被隐藏(Mask)掉的token。 例如:输入“我喜欢吃[MASK]”,Label是“我喜欢吃苹果”, 模型需要预测出“苹果”。这种…...
Vue 大文件分片上传组件实现解析
Vue 大文件分片上传组件实现解析 一、功能概述 1.1本组件基于 Vue Element UI 实现,主要功能特点: 大文件分片上传:支持 2MB 分片切割上传实时进度显示:可视化展示每个文件上传进度智能格式校验:支持文件类型、大小…...
Matlab 电机激励模型和仿真
1、内容简介 Matlab 191-电机激励模型和仿真 可以交流、咨询、答疑 2、内容说明 略 3、仿真分析 略 clc close all clear Tn 165;%电机转矩 R 0.05;%定子半径 n0 1800;% 电机转速 i 2;%额定电流 Lmin 4.95;%最小电感 k 82.5; Nr 6;%转子级数 T 60/Nr/n0;%周期 b 0…...
GPU算力优化
GPU(图形处理单元)算力优化是提升计算性能的重要任务,在深度学习、科学计算等领域有重要意义。下面从硬件层面、软件层面和算法层面为你介绍一些优化方法: 硬件层面 选择合适的 GPU:不同的 GPU 在算力、显存、功耗等…...
微服务拆分的原则、时机、方法以及常见问题
前言# 在平常情况下,技术架构会随着业务规模大小、团队人数多少、技术债积累速度等动态变化。当然,引起架构变化最主要的因素还是业务发展速度。 在以前的单体架构到微服务架构演进历程 文章2 等文章中,有一张架构演进的图,如下…...
第十八天 - ELK日志体系集成 - 自定义Logstash插件 - 练习:分布式日志分析平台
前言 在分布式系统和微服务架构中,日志管理是监控系统健康、排查故障的核心环节。ELK(Elasticsearch、Logstash、Kibana)作为业界领先的日志分析解决方案,能够实现日志采集、处理、存储与可视化的全链路管理。本文将手把手带你掌…...
HBuilderX中uni-app打包Android(apk)全流程超详细打包
一、Android生成打包证书 1、Android平台签名证书(.keystore)生成指南_android 签名生成-CSDN博客(如果不上架应用商店可以跳过,可以使用云端证书) 二、打开manifest.json配置基础设置 三、配置安卓应用图标 四、配置安卓启动页图片 五、…...
CTF web入门之命令执行
web29 文件名过滤 由于flag被过滤,需要进行文件名绕过,有以下几种方法: 1.通配符绕过 fla?.* 2.反斜杠绕过 fl\ag.php 3.双引号绕过 fl’‘ag’.php 还有特殊变量$1、内联执行等 此外 读取文件利用cat函数,输出利用system、passthru 、echo echo `nl flag.php`; ec…...
安宝特案例 | 某户外机房制造企业应用AR+作业流,规范制造过程,记录施工节点,保障交付质量
行业特点:产品客制化、依赖人工,工程量大、细节多,验收困难 户外通讯机房无疑是现代工业社会的“信息心脏”,承载着信息交换、传输与处理的重任。建设一座质量过关的户外通讯机房是保障通信稳定运行的基石。 通常建设一个户外通信…...
学习MySQL的第八天
海到无边天作岸 山登绝顶我为峰 一、数据库的创建、修改与删除 1.1 引言 在经过前面七天对于MySQL基本知识的学习之后,现在我们从基本的语句命令开始进入综合性的语句的编写来实现特定的需求,从这里开始需要我们有一个宏观的思想&…...
ESP8266 采集 DHT11 和 DS18B20 温湿度数据 MQTT 上传至 Home Assistant
ESP8266 采集 DHT11 和 DS18B20 温湿度数据 MQTT 上传至 Home Assistant DHT11 采集 DHT11 温湿度数据并通过 MQTT 联网上传至 Home Assistant 智能家居平台; Arduino 代码 #include <ESP8266WiFi.h> #include <PubSubClient.h> #include <Ardui…...
三、TorchRec中的Optimizer
TorchRec中的Optimizer 文章目录 TorchRec中的Optimizer前言一、嵌入后向传递与稀疏优化器融合如下图所示:二、上述图片的关键步骤讲解:三、优势四、与传统优化器对比总结 前言 TorchRec 模块提供了一个无缝 API,用于在训练中融合后向传递和…...
操作系统 4.1-I/O与显示器
外设工作起来 操作系统让外设工作的基本原理和过程,具体来说,它概括了以下几个关键步骤: 发出指令:操作系统通过向控制器中的寄存器发送指令来启动外设的工作。这些指令通常是通过I/O指令(如out指令)来实现…...
Qt 5.14.2 入门(三)基本知识理解
目录 一、新建项目后生成的三个文件的作用和内容1、.h文件:头文件(如mainwindow.h)2、.cpp文件:源文件(如mainwindow.cpp)3、main.cpp:主程序 二、信号和槽机制1、信号(Signal)2、槽(Slot)3、连…...
Java中常见的设计模式
Java中常见的设计模式 Java 中有 23 种经典设计模式,通常被分为三大类:创建型、结构型和行为型。每个设计模式都解决了不同类型的设计问题。以下是几种常见设计模式的总结,并附带了实际应用场景、示例代码和详细的注释说明。 一、创建型设计…...
nltk 是怎么和 Transformers(比如 BERT 模型)联动来处理文本的
🧠 一句话解释nltk: nltk 是一个用于自然语言处理(NLP)的 Python 库,功能包括分词、词性标注、命名实体识别、文本分类等。 全称是:Natural Language Toolkit(自然语言工具包) &…...
项目日志配置模板示例
1.新增application.properties配置 logging.configclasspath:logback-spring.xml spring.profiles.activedev 将项目部署到服务器时需要将dev修改为test后再进行打包部署 2.新增logback-spring.xml <?xml version"1.0" encoding"UTF-8"?> <…...
sql server统计小时数据中每日最大风速及风速对应的风向
客户提出一个需求,有历年逐小时的风速、风向资料,想获取历年最大的风速及风速对应的风向值,通过sql 怎么实现,这个统计,我认为主要考虑一个问题,就是数据重复性,所以我采用以下方法实现…...
【SQL】MySql常见的性能优化方式
MySQL性能优化的常用方式及对比说明 一、引言二、MySQL性能优化的常用方式及对比说明1. 索引优化1.1 合理创建索引1.2 覆盖索引1.3 索引下推 2. SQL语法优化2.1 避免低效操作2.2 分页优化2.3 JOIN优化 3. 配置与架构优化3.1 参数调优3.2 读写分离与分库…...
Prometheus实现负载均衡并将多个实例数据汇总到一个主Prometheus
一、Prometheus实现负载均衡策略原理 要实现 Prometheus 的负载均衡并将多个 Prometheus 实例的数据汇总到一个主 Prometheus 实例中,可以结合 Prometheus 联邦(Federation) 和 负载均衡器 来进行配置。 这种方法的核心是在主 Prometheus 实例…...
力扣 — — 最长公共子序列
力扣 — — 最长公共子序列 最长公共子序列 题源:1143. 最长公共子序列 - 力扣(LeetCode) 题目: 分析: 一道经典的题目:最长公共子序列(LCS) 题目大意:求两个字符串的最长公共序列。 算法&…...
通过AWS EKS 生成并部署容器化应用
今天给大家分享一个实战例子,如何在EKS上创建容器化应用并通过ALB来发布。先介绍一下几个基本概念: IAM, OpenID Connect (OIDC) 2014 年,AWS Identity and Access Management 增加了使用 OpenID Connect (OIDC) 的联合身份支持。此功能允许…...
GNSS静态数据处理
1 安装数据处理软件:仪器之星(InStar )和 Trimble Business Center 做完控制点静态后,我们需要下载GNSS数据,对静态数据进行处理。在处理之前需要将相关软件在自己电脑上安装好: 仪器之星(InS…...
NVIDIA H100 vs A100:新一代GPU架构性能对比分析
一、核心架构演进对比 Ampere架构(A100)采用台积电7nm工艺,集成540亿晶体管,配备6,912个CUDA核心和432个第三代Tensor Core,支持FP16、TF32和INT8精度计算。其显存子系统采用HBM2e技术,80GB版本带宽可…...
AI图像生成
要通过代码实现AI图像生成,可以使用深度学习框架如TensorFlow、PyTorch或GANs等技术。下面是一个简单的示例代码,演示如何使用GANs生成手写数字图像: import torch import torchvision import torchvision.transforms as transforms import …...
计算机考研一战上岸宁波大学经验分享
目录 话不多说先上分数 个人介绍 政治 英语二 数学二 408 复试 话不多说先上分数 初试排名15/65 复试79.81分,复试排名13/65 总成绩13/65,研招网招考50人 (均为公开数据,非泄露复试信息) 个人介绍 本人山东…...
泛微ECOLOGY9 记 数据展现集成 自定义开窗测试中对SQL 的IN语法转换存在BUG
背景 搭建流程时,需将明细表1中的合同字段 供明细表2中的合同开窗查询使用。 最终实现如下图: 选择 发票号时,自动带出明细表1中的采购合同号清单,然后在明细表2中开窗采购合同号时,只跳出明细表1中有的采购合同号&am…...
【Nginx】Nginx代理Tomcat配置及404问题解决
当Tomcat返回HTTP 404未找到错误时,可以通过以下两种方式设置跳转到指定地址: ① 在Tomcat应用内部配置错误页面跳转(直接修改Tomcat的Web应用配置) ② 在Nginx反向代理层拦截404错误并跳转(无需修改Tomcat,…...
【Vue】案例——To do list:
【Vue】案例——To do list: 一、案例介绍:二、效果展示(如图)三、主要功能:四、技术要点:补充:【Vue】Vue模板语法(点击可跳转)补充:【Vue】数据绑定(单双向)…...
JVM不同环境不同参数配置文件覆盖
背景 需要在启动Java服务并且参数不同的场景,例如端口号在yml中的配置是这样的: server:port: 9100 覆盖配置对应JVM参数: java -jar xxxx.jar -Dserver.port12306 [JVM其他参数] 这样12306就会覆盖掉9100端口的配置作为启动配置 IDE…...