webrtc源码阅读之P2P流程分析
P2P从宏观原理上其实就是:
- 收集本地Candidates
- 设置远程Candidates
- 连通性测试及排序
本文我们从Offer端的角度进行源码分析,学习webrtc是如何进行P2P连接的。版本m98。
一、收集本地Candidates
-
examples/peerconnection中,
CreateOffer以后,会调用PeerConnection::SetLocalDescription,继而调用到SdpOfferAnswerHandler::SetLocalDescription>SdpOfferAnswerHandler::DoSetLocalDescription>JsepTransportController::MaybeStartGathering>P2PTransportChannel::MaybeStartGathering。
void P2PTransportChannel::MaybeStartGathering() {RTC_DCHECK_RUN_ON(network_thread_);if (ice_parameters_.ufrag.empty() || ice_parameters_.pwd.empty()) {RTC_LOG(LS_ERROR)<< "Cannot gather candidates because ICE parameters are empty"" ufrag: "<< ice_parameters_.ufrag << " pwd: " << ice_parameters_.pwd;return;}// Start gathering if we never started before, or if an ICE restart occurred.//正常第一次收集Candidates的话,allocator_sessions_应该是空的if (allocator_sessions_.empty() ||IceCredentialsChanged(allocator_sessions_.back()->ice_ufrag(),allocator_sessions_.back()->ice_pwd(),ice_parameters_.ufrag, ice_parameters_.pwd)) {if (gathering_state_ != kIceGatheringGathering) {gathering_state_ = kIceGatheringGathering; //gathering_state_设置为正在kIceGatheringGatheringSignalGatheringState(this);}if (!allocator_sessions_.empty()) { //更新ICE状态,第一次收集应该是空IceRestartState state;if (writable()) {state = IceRestartState::CONNECTED;} else if (IsGettingPorts()) {state = IceRestartState::CONNECTING;} else {state = IceRestartState::DISCONNECTED;}RTC_HISTOGRAM_ENUMERATION("WebRTC.PeerConnection.IceRestartState",static_cast<int>(state),static_cast<int>(IceRestartState::MAX_VALUE));}for (const auto& session : allocator_sessions_) {if (session->IsStopped()) {continue;}session->StopGettingPorts();}// Time for a new allocator.//从当前的Session中寻找对应的ice,如果是第一次收集,则pooled_session为空。std::unique_ptr<PortAllocatorSession> pooled_session =allocator_->TakePooledSession(transport_name(), component(),ice_parameters_.ufrag,ice_parameters_.pwd);if (pooled_session) {AddAllocatorSession(std::move(pooled_session));PortAllocatorSession* raw_pooled_session =allocator_sessions_.back().get();// Process the pooled session's existing candidates/ports, if they exist.OnCandidatesReady(raw_pooled_session,raw_pooled_session->ReadyCandidates());for (PortInterface* port : allocator_sessions_.back()->ReadyPorts()) {OnPortReady(raw_pooled_session, port);}if (allocator_sessions_.back()->CandidatesAllocationDone()) {OnCandidatesAllocationDone(raw_pooled_session);}} else {//创建Session并添加到allocator_sessions_AddAllocatorSession(allocator_->CreateSession(transport_name(), component(), ice_parameters_.ufrag,ice_parameters_.pwd));allocator_sessions_.back()->StartGettingPorts();}}
}
- 接下来做了一系列调用,直到真正的
DoAllocate:BasicPortAllocatorSession::StartGettingPorts>BasicPortAllocatorSession::GetPortConfigurations>BasicPortAllocatorSession::ConfigReady>BasicPortAllocatorSession::OnConfigReady>BasicPortAllocatorSession::AllocatePorts>BasicPortAllocatorSession::OnAllocate>BasicPortAllocatorSession::DoAllocate
void BasicPortAllocatorSession::DoAllocate(bool disable_equivalent) {RTC_DCHECK_RUN_ON(network_thread_);bool done_signal_needed = false;std::vector<rtc::Network*> networks = GetNetworks(); //获取到所有的网络设备if (networks.empty()) {RTC_LOG(LS_WARNING)<< "Machine has no networks; no ports will be allocated";done_signal_needed = true;} else {RTC_LOG(LS_INFO) << "Allocate ports on " << networks.size() << " networks";PortConfiguration* config =configs_.empty() ? nullptr : configs_.back().get();for (uint32_t i = 0; i < networks.size(); ++i) { //遍历所有网络设备进行端口分配uint32_t sequence_flags = flags();if ((sequence_flags & DISABLE_ALL_PHASES) == DISABLE_ALL_PHASES) {// If all the ports are disabled we should just fire the allocation// done event and return.done_signal_needed = true;break;}if (!config || config->relays.empty()) {// ice config里没有relay服务sequence_flags |= PORTALLOCATOR_DISABLE_RELAY;}//跳过IPV6相关配置if (!(sequence_flags & PORTALLOCATOR_ENABLE_IPV6) &&networks[i]->GetBestIP().family() == AF_INET6) {// Skip IPv6 networks unless the flag's been set.continue;}if (!(sequence_flags & PORTALLOCATOR_ENABLE_IPV6_ON_WIFI) &&networks[i]->GetBestIP().family() == AF_INET6 &&networks[i]->type() == rtc::ADAPTER_TYPE_WIFI) {// Skip IPv6 Wi-Fi networks unless the flag's been set.continue;}if (disable_equivalent) {// Disable phases that would only create ports equivalent to// ones that we have already made.DisableEquivalentPhases(networks[i], config, &sequence_flags);if ((sequence_flags & DISABLE_ALL_PHASES) == DISABLE_ALL_PHASES) {// New AllocationSequence would have nothing to do, so don't make it.continue;}}AllocationSequence* sequence =new AllocationSequence(this, networks[i], config, sequence_flags,[this, safety_flag = network_safety_.flag()] {if (safety_flag->alive())OnPortAllocationComplete();});sequence->Init(); //初始化AllocationSequencesequence->Start(); //开始分配端口sequences_.push_back(sequence);done_signal_needed = true;}}if (done_signal_needed) {network_thread_->PostTask(webrtc::ToQueuedTask(network_safety_, [this] { OnAllocationSequenceObjectsCreated(); }));}
}
- AllocationSequence::Start之后会循环执行AllocationSequence::Process,直到state() != kRunning,而在进行TCP端口分配以后state_ = kCompleted,也就是说如果不出错的情况下,AllocationSequence::Process会一直循环,直到TCP端口分配完成。
void AllocationSequence::Process(int epoch) {RTC_DCHECK(rtc::Thread::Current() == session_->network_thread());const char* const PHASE_NAMES[kNumPhases] = {"Udp", "Relay", "Tcp"};if (epoch != epoch_)return;// Perform all of the phases in the current step.RTC_LOG(LS_INFO) << network_->ToString()<< ": Allocation Phase=" << PHASE_NAMES[phase_];switch (phase_) {case PHASE_UDP:CreateUDPPorts(); //分配UDP端口CreateStunPorts(); //分配Stun端口break;case PHASE_RELAY:CreateRelayPorts(); //分配Relay端口break;case PHASE_TCP:CreateTCPPorts();state_ = kCompleted; //state设置为完成,可以跳出Process循环了break;default:RTC_DCHECK_NOTREACHED();}if (state() == kRunning) { //在状态为kRunning时,一直循环执行Process++phase_; //执行完一种类型的端口分配后,执行另外一种,顺序为 PHASE_UDP > PHASE_RELAY > PHASE_TCPsession_->network_thread()->PostDelayedTask(webrtc::ToQueuedTask(safety_,[this, epoch = epoch_] { Process(epoch); }),session_->allocator()->step_delay());} else {// No allocation steps needed further if all phases in AllocationSequence// are completed. Cause further Process calls in the previous epoch to be// ignored.++epoch_;port_allocation_complete_callback_(); //分配完成,回调}
}
1、PHASE_UDP
在UDP阶段,会收集两种candidate:host和srflx。对应函数CreateUDPPorts和CreateStunPorts。
CreateUDPPorts比较简单,就是从可用端口中选择一个端口创建udp socket,并通过AddAllocatedPort函数添加到session中。
AddAllocatedPort具体流程为:BasicPortAllocatorSession::AddAllocatedPort>UDPPort::PrepareAddress>UDPPort::OnLocalAddressReady>Port::AddAddress>Port::FinishAddingAddress>SignalCandidateReadysrflx使用的是STUN协议,也就是通过NAT穿透的方式传输数据。对应webrtc源码中,如果srflx与host端口复用的话,在UDPPort::OnLocalAddressReady阶段就会进行srflx收集,默认是复用的。srflx的原理是:client向stun server发送一包数据,stun server会把client对应的外网地址和端口返回给client,这样client就获取到了自己的外网地址和端口,以及与之对应的内网端口。- 发送流程:
UDPPort::OnLocalAddressReady>UDPPort::MaybePrepareStunCandidate>UDPPort::SendStunBindingRequests>UDPPort::SendStunBindingRequest>StunRequestManager::Send>StunRequestManager::SendDelayed>StunRequest::OnMessage>UDPPort::OnSendPacket>AsyncUDPSocket::SendTo>PhysicalSocket::SendTo - 接收流程:
PhysicalSocketServer::WaitSelect>ProcessEvents>SocketDispatcher::OnEvent>SignalReadEvent>AsyncUDPSocket::OnReadEvent>SignalReadPacket>AllocationSequence::OnReadPacket>UDPPort::HandleIncomingPacket>UDPPort::OnReadPacket>StunRequestManager::CheckResponse>StunBindingRequest::OnResponse>UDPPort::OnStunBindingRequestSucceeded>Port::AddAddress>Port::FinishAddingAddress>SignalCandidateReady
2、PHASE_RELAY
在RELAY阶段,会收集relay类型的candidate,也就是在NAT打洞不通的情况下,需要用中继的方式传输数据,使用的是TURN协议,TURN协议是STUN协议的扩展,所以在流程上与STUN类似。
relay类型的candidate,默认也是与udp端口复用的,TURN 协议的具体流程如下:
- 客户端发送 Allocate request 到 server,server 返回 401 未授权错误(带有 realm 和 nonce),客户端再发送带上认证信息的 Allocate request,server 返回成功分配的 relay address。分配成功后,客户端需要通过发送机制(Send Mechanism)或信道机制(Channels)在 server 上配置和其他 peer 的转发信息。此外 allocation 和 channel 都需要保活。
- 发送流程:
AllocationSequence::CreateRelayPorts>AllocationSequence::CreateTurnPort>BasicPortAllocatorSession::AddAllocatedPort>TurnPort::PrepareAddress>TurnPort::SendRequest>StunRequestManager::Send>StunRequestManager::Send>StunRequestManager::SendDelayed>StunRequest::OnMessage>UDPPort::OnSendPacket>AsyncUDPSocket::SendTo>PhysicalSocket::SendTo - 接收流程1:
PhysicalSocketServer::WaitSelect>ProcessEvents>SocketDispatcher::OnEvent>SignalReadEvent>AsyncUDPSocket::OnReadEvent>SignalReadPacket>AllocationSequence::OnReadPacket>UDPPort::HandleIncomingPacket>UDPPort::OnReadPacket>StunRequestManager::CheckResponse>TurnAllocateRequest::OnErrorResponse
然后发送认证信息 - 接收流程2:
PhysicalSocketServer::WaitSelect>ProcessEvents>SocketDispatcher::OnEvent>SignalReadEvent>AsyncUDPSocket::OnReadEvent>SignalReadPacket>AllocationSequence::OnReadPacket>UDPPort::HandleIncomingPacket>UDPPort::OnReadPacket>StunRequestManager::CheckResponse>TurnAllocateRequest::OnResponse>TurnPort::OnAllocateSuccess>Port::AddAddress>Port::FinishAddingAddress>SignalCandidateReady
3、PHASE_TCP
基本不使用
4、SignalCandidateReady
收集完成Candidate会发出SignalCandidateReady信号,进而触发BasicPortAllocatorSession::OnCandidateReady。在BasicPortAllocatorSession::OnCandidateReady中会发出两个重要的信号SignalPortReady 和 SignalCandidatesReady。
- 其中
SignalPortReady会触发P2PTransportChannel::OnPortReady,将收集到的candidate对应的端口保存下来,待远程candidate收到以后就尝试ICE连接。 SignalCandidatesReady会触发P2PTransportChannel::OnCandidatesReady>JsepTransportController::OnTransportCandidateGathered_n>PeerConnection::OnTransportControllerCandidatesGathered>PeerConnection::OnIceCandidate>Observer()->OnIceCandidate
这个observer就是examples/peerconnection的Conductor。按这个流程就可以把收集到的candidate通知到上层应用了。
二、 设置远程Candidates
有两种方式设置远程Candidates,一种是通过解析远程的sdp获取到Candidate,然后调用PeerConnection::AddRemoteCandidate来设置,另一种是通过信令服务器接收到对端发送过来的Candidate后,直接调用PeerConnection::AddIceCandidate来设置。
- SDP解析设置Candidate的流程:
PeerConnection::SetRemoteDescription>SdpOfferAnswerHandler::SetRemoteDescription>SdpOfferAnswerHandler::DoSetRemoteDescription>SdpOfferAnswerHandler::ApplyRemoteDescription>SdpOfferAnswerHandler::UseCandidatesInSessionDescription>SdpOfferAnswerHandler::UseCandidate - 直接通过
PeerConnection::AddIceCandidate设置Candidate的流程:
PeerConnection::AddIceCandidate>SdpOfferAnswerHandler::AddIceCandidate>SdpOfferAnswerHandler::AddIceCandidateInternal>SdpOfferAnswerHandler::UseCandidate - 共同部分
SdpOfferAnswerHandler::UseCandidate>PeerConnection::AddRemoteCandidate>JsepTransportController::AddRemoteCandidates>JsepTransport::AddRemoteCandidates>P2PTransportChannel::AddRemoteCandidate>P2PTransportChannel::FinishAddingRemoteCandidate
三、连通性测试及排序
在收集到本地Candidates及远程Candidates后,会调用P2PTransportChannel::FinishAddingRemoteCandidate函数,在这个函数里主要做了两件事,一是创建连接(P2PTransportChannel::CreateConnections),也就是创建每个本地candidate与远程candidate的连接;另一件事是对创建的连接进行排序和连通性测试(P2PTransportChannel::SortConnectionsAndUpdateState)。
- 创建连接
P2PTransportChannel::CreateConnections>P2PTransportChannel::CreateConnection>UDPPort::CreateConnection - 连通性测试和排序
void P2PTransportChannel::SortConnectionsAndUpdateState(IceControllerEvent reason_to_sort) {RTC_DCHECK_RUN_ON(network_thread_);// Make sure the connection states are up-to-date since this affects how they// will be sorted.UpdateConnectionStates(); //根据ping的结果更新connection的读写状态,即连接状态// Any changes after this point will require a re-sort.sort_dirty_ = false;// If necessary, switch to the new choice. Note that `top_connection` doesn't// have to be writable to become the selected connection although it will// have higher priority if it is writable.//MaybeSwitchSelectedConnection 会循环执行,对连接进行排序MaybeSwitchSelectedConnection(reason_to_sort, ice_controller_->SortAndSwitchConnection(reason_to_sort));// The controlled side can prune only if the selected connection has been// nominated because otherwise it may prune the connection that will be// selected by the controlling side.// TODO(honghaiz): This is not enough to prevent a connection from being// pruned too early because with aggressive nomination, the controlling side// will nominate every connection until it becomes writable.if (ice_role_ == ICEROLE_CONTROLLING ||(selected_connection_ && selected_connection_->nominated())) { //如果已经有了选中的连接,而且连接对应的网络设备已经是最优了,就把网络设备对应的其他连接清理掉PruneConnections();}// Check if all connections are timedout.bool all_connections_timedout = true;for (const Connection* conn : connections()) {//剩余的连接是否都已经超时了if (conn->write_state() != Connection::STATE_WRITE_TIMEOUT) {all_connections_timedout = false;break;}}// Now update the writable state of the channel with the information we have// so far.if (all_connections_timedout) { HandleAllTimedOut(); //如果剩余的连接都已经超时了,就清理掉}// Update the state of this channel.UpdateState(); //更新channel state,通知上层是否可以连接等状态// Also possibly start pinging.// We could start pinging if:// * The first connection was created.// * ICE credentials were provided.// * A TCP connection became connected.MaybeStartPinging(); //连通性测试
}
1、连通性测试
-
发送流程
P2PTransportChannel::MaybeStartPinging>P2PTransportChannel::CheckAndPing>P2PTransportChannel::PingConnection>Connection::Ping>StunRequestManager::Send>StunRequestManager::Send>StunRequestManager::SendDelayed>StunRequest::OnMessage>UDPPort::OnSendPacket>AsyncUDPSocket::SendTo>PhysicalSocket::SendTo -
接收流程
PhysicalSocketServer::WaitSelect>ProcessEvents>SocketDispatcher::OnEvent>SignalReadEvent>AsyncUDPSocket::OnReadEvent>SignalReadPacket>AllocationSequence::OnReadPacket>UDPPort::HandleIncomingPacket>UDPPort::OnReadPacket>StunRequestManager::CheckResponse>ConnectionRequest::OnResponse>Connection::OnConnectionRequestResponse>Connection::ReceivedPingResponse -
在
Connection::ReceivedPingResponse中会更新Connection的读写状态及rtt。供后续connection排序和选择使用。
2、排序和选择
2.1 排序
- 首先是通过
BasicIceController::CompareConnectionStates函数进行排序:
如果a writable b不是则a排在前面,反之b排前面;
如果a b writable是一样的,则比较a b 的write_state,小的排前面;
如果a b 的write_state也一样,则比较receiving,逻辑上与write一样;
如果a b的receiving状态也一样,且a b都是STATE_WRITABLE,则比较connected,connected的排前面; - 如果
BasicIceController::CompareConnectionStates比较不出结果,则通过BasicIceController::CompareConnectionCandidates比较。 - 如果还是比较不出结果,则通过
rtt来比较。
2.2选择
- 首先必须是可以发送的,才可能被选择;
- 如果当前没有被选择的连接,且新连接是可发送的,就选择新连接;
- 如果当前已经有了选择的连接,则通过network cost来比较,如果新连接不如已经选择的,则维持不变
- 否则通过排序原则进行比较,如果排序结果新连接不如已经选择的,则维持不变,如果新连接比已经选择的优先级高,则选择新连接。
- 如果排序的结果是新连接与已经选择的优先级相同,就比较
rtt,如果新连接的rtt小于已经连接的rtt减去一个阈值,则选择新连接,否则维持不变。
当ICE连通性和排序完成后,就可以进行正常的连接使用了。
相关文章:
webrtc源码阅读之P2P流程分析
P2P从宏观原理上其实就是: 收集本地Candidates设置远程Candidates连通性测试及排序 本文我们从Offer端的角度进行源码分析,学习webrtc是如何进行P2P连接的。版本m98。 一、收集本地Candidates examples/peerconnection中,CreateOffer以后&…...
vscode 快速修复(quick fix) 快捷键(Ctrl + .)被占用问题解决方法
vscode 快速修复(quick fix) 快捷键(Ctrl .)被占用 微软拼音的中/英文标点切换的快捷键为Ctrl .,与 vscode 快速修复(quick fix)快捷键冲突。修复方法如下: 切换到微软拼音,在输入法中或英字上,点击右键。 再点设置 - 按键。 …...
阿里云——扩展Linux系统盘
扩展分区和文件系统_Linux系统盘 {#concept_ocb_htw_dhb .concept} 本文提供了如何使用growpart和resize2fs工具完成Linux系统盘分区扩容及文件系统扩展的操作指导。 适用范围 {#section_u9c_3g5_ljs .section} 本文的操作步骤适用于以下分区和文件系统格式的云盘࿱…...
TypeScript ~ 掌握基本类型 ②
作者 : SYFStrive 博客首页 : HomePage 📜: TypeScript ~ TS 📌:个人社区(欢迎大佬们加入) 👉:社区链接🔗 📌:觉得文章不错可以点点关注 &…...
【Zookeeper】win安装随笔
目录 下载地址下载目标解压后目录结构配置文件配置文件详情伪分布式安装LinuxZooKeeper audit is disabled启动解决报错:SLF4J: Class path contains multiple SLF4J bindings. _ 下载地址 https://zookeeper.apache.org/releases.html 下载目标 记住选择带bin的…...
Unity 之 最新原生广告Ads接入 -- 助力增长游戏收益
Unity 之 最新Ads原生广告接入流程详解和工具类分享 一,注册 Unity Ads 广告 SDK二,下载 Unity Ads 广告 SDK三,配置 Unity Ads 广告 SDK3.1 广告位展示流程3.2 代码初始化 四,集成 Unity Ads 广告 SDK4.1 相关介绍4.2 代码分享 五…...
ChatGPT是否可以进行逻辑推理?
ChatGPT在逻辑推理方面的能力存在一定的限制。虽然它可以处理一些简单的逻辑问题,但由于其基于统计模型和语言模式的生成方式,它在复杂的逻辑推理和推断任务上可能会遇到挑战。以下是对ChatGPT在逻辑推理方面能力的详细分析: 1. 基于统计模型…...
TP6在composer包里写控制器
前提:首先要了解下如何自建composer包。 1.先建一个空包,加一个文件:composer.json {"name": "test/ctrs","type": "library","license": "MIT","autoload": {&quo…...
Java面试Day11
1. MySQL 事务有哪些隔离级别、分别有什么特点,以及 MySQL 的默认隔离级别是什么? 在MySQL中事务的隔离级别是为了解决常见的并发问题,在保证数据库性能的同时保持事务的隔离性,常见的并发问题有: 脏读:如果…...
python生成日报
目录 一:日报生成工具二:日报工具使用方式三:最终日报生成展示 一:日报生成工具 #!/usr/bin/python # coding:utf8class GetHtml(object):def __init__(self):self._html_head """<html><body style&qu…...
【机器学习】——续上:卷积神经网络(CNN)与参数训练
目录 引入 一、CNN基本结构 1、卷积层 2、下采样层 3、全连接层 二、CNN参数训练 总结 引入 卷积神经网络(CNN)是一种有监督深度模型框架,尤其适合处理二维数据问题,如行人检测、人脸识别、信号处理等领域,是带…...
鲸鱼算法WOA优化VMD参数,最小包络熵、样本熵、信息熵、排列熵(适应度函数可自行选择,一键修改)包含MATLAB源代码...
鲸鱼优化算法(Whale optimization algorithm, WOA)是Mirjalili根据座头鲸的捕食行为而提出来的,算法对座头鲸的狩猎行为进行模仿,通过对猎物的寻找,然后攻击进行觅食,以此来达到优化的目的,已有很多学者将算法用于实际…...
ELK日志收集系统集群实验
ELK日志收集系统集群实验 目录 一、实验拓扑 二、环境配置 三、 安装node1与node2节点的elasticsearch 1. 安装 2.配置 3.启动elasticsearch服务 4.查看节点信息 四、在node1安装elasticsearch-head插件 1.安装node 2.拷贝命令 3.安装elasticsearch-head 4.修改el…...
用Python写了一个下载网站所有内容的软件,可见即可下
目录标题 前言效果展示环境介绍:代码实战获取数据获取视频采集弹幕采集评论 GUI部分尾语 前言 嗨喽~大家好呀,这里是魔王呐 ❤ ~! 今天我们分享一个用Python写下载视频弹幕评论的代码。 顺便把这些写成GUI,把这些功能放到一起让朋友用起来更方便~ 效果…...
gin使用embed打包html
embed 使用类似的注释打包html文件 //go:embed pages/dist/* 打包的代码如下 package mainimport ("embed""io/fs""net/http""github.com/gin-gonic/gin" )//go:embed pages/dist/* var embedFs embed.FSfunc main() {e : gin.Defau…...
Android启动优化实践
作者:95分技术 启动优化是Android优化老生常谈的问题了。众所周知,android的启动是指用户从点击 icon 到看到首帧可交互的流程。 而启动流程 粗略的可以分为以下几个阶段 fork创建出一个新的进程创建初始化Application类、创建四大组件等 走Applicatio…...
ROS:通信机制实操
目录 ROS:通信机制一、话题发布实操1.1需求1.2分析1.3实现流程1.4实现代码1.4.1C版1.4.2Python版 1.5执行 二、话题订阅实操2.1需求2.2分析2.3流程2.4实现代码2.4.1启动无辜GUI与键盘控制节点2.4.2C版 ROS:通信机制 一、话题发布实操 1.1需求 编码实现…...
C/C++内存管理(内存分布、动态内存分配、动态内存分配与释放、内存泄漏等)
喵~ 内存之5大区(栈区、堆区、静态区、常量区、代码区)C/C中各自的内存分配操作符内存泄露?内存泄漏检测方法 内存之5大区(栈区、堆区、静态区、常量区、代码区) 1、栈区(stack):由编译器自动分…...
【云原生】软件架构的演进以及各个架构的优缺点
文章目录 1. 什么是软件架构?2. 单机架构3. 应用数据分离架构4. 应用服务集群架构5. 读写分离架构6. 冷热分离架构7.垂直分库架构8. 微服务架构9. 容器编排架构10. 小结 1. 什么是软件架构? 软件架构是指在设计和构建软件系统时,对系统的组织结构、组件、模块、接…...
力扣刷题笔记——二叉树
首先定义二叉树节点的结构体 struct TreeNode{TreeNode* left;TreeNode* right;int val;TreeNode():val(0),left(nullptr),right(nullptr){}TreeNode(int val):val(val),left(nullptr),right(nullptr){}TreeNode(int val,TreeNode* l,TreeNode* R):val(val),left(l),right(R){…...
Vim 调用外部命令学习笔记
Vim 外部命令集成完全指南 文章目录 Vim 外部命令集成完全指南核心概念理解命令语法解析语法对比 常用外部命令详解文本排序与去重文本筛选与搜索高级 grep 搜索技巧文本替换与编辑字符处理高级文本处理编程语言处理其他实用命令 范围操作示例指定行范围处理复合命令示例 实用技…...
MPNet:旋转机械轻量化故障诊断模型详解python代码复现
目录 一、问题背景与挑战 二、MPNet核心架构 2.1 多分支特征融合模块(MBFM) 2.2 残差注意力金字塔模块(RAPM) 2.2.1 空间金字塔注意力(SPA) 2.2.2 金字塔残差块(PRBlock) 2.3 分类器设计 三、关键技术突破 3.1 多尺度特征融合 3.2 轻量化设计策略 3.3 抗噪声…...
微信小程序之bind和catch
这两个呢,都是绑定事件用的,具体使用有些小区别。 官方文档: 事件冒泡处理不同 bind:绑定的事件会向上冒泡,即触发当前组件的事件后,还会继续触发父组件的相同事件。例如,有一个子视图绑定了b…...
Cesium1.95中高性能加载1500个点
一、基本方式: 图标使用.png比.svg性能要好 <template><div id"cesiumContainer"></div><div class"toolbar"><button id"resetButton">重新生成点</button><span id"countDisplay&qu…...
服务器硬防的应用场景都有哪些?
服务器硬防是指一种通过硬件设备层面的安全措施来防御服务器系统受到网络攻击的方式,避免服务器受到各种恶意攻击和网络威胁,那么,服务器硬防通常都会应用在哪些场景当中呢? 硬防服务器中一般会配备入侵检测系统和预防系统&#x…...
跨链模式:多链互操作架构与性能扩展方案
跨链模式:多链互操作架构与性能扩展方案 ——构建下一代区块链互联网的技术基石 一、跨链架构的核心范式演进 1. 分层协议栈:模块化解耦设计 现代跨链系统采用分层协议栈实现灵活扩展(H2Cross架构): 适配层…...
解决本地部署 SmolVLM2 大语言模型运行 flash-attn 报错
出现的问题 安装 flash-attn 会一直卡在 build 那一步或者运行报错 解决办法 是因为你安装的 flash-attn 版本没有对应上,所以报错,到 https://github.com/Dao-AILab/flash-attention/releases 下载对应版本,cu、torch、cp 的版本一定要对…...
2023赣州旅游投资集团
单选题 1.“不登高山,不知天之高也;不临深溪,不知地之厚也。”这句话说明_____。 A、人的意识具有创造性 B、人的认识是独立于实践之外的 C、实践在认识过程中具有决定作用 D、人的一切知识都是从直接经验中获得的 参考答案: C 本题解…...
【Java学习笔记】BigInteger 和 BigDecimal 类
BigInteger 和 BigDecimal 类 二者共有的常见方法 方法功能add加subtract减multiply乘divide除 注意点:传参类型必须是类对象 一、BigInteger 1. 作用:适合保存比较大的整型数 2. 使用说明 创建BigInteger对象 传入字符串 3. 代码示例 import j…...
LINUX 69 FTP 客服管理系统 man 5 /etc/vsftpd/vsftpd.conf
FTP 客服管理系统 实现kefu123登录,不允许匿名访问,kefu只能访问/data/kefu目录,不能查看其他目录 创建账号密码 useradd kefu echo 123|passwd -stdin kefu [rootcode caozx26420]# echo 123|passwd --stdin kefu 更改用户 kefu 的密码…...
