深入浅出WebRTC—NACK

news2024/11/22 22:20:35

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 对细节的掌控和重视。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1936265.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【VUE】v-if和v-for的优先级

v-if和v-for v-if 用来显示和隐藏元素 flag为true时&#xff0c;dom元素会被删除达到隐藏效果 <div class"boxIf" v-if"flag"></div>v-for用来进行遍历&#xff0c;可以遍历数字对象数组&#xff0c;会将整个元素遍历指定次数 <!-- 遍…

《数据结构:C语言实现双链表》

文章目录 一、链表的分类二、双向链表1、概念与结构 三、双向链表实现1、双向链表要实现的功能2、哨兵位初始化3、双链表头插数据4、判断链表是否为空5、打印链表数据6、尾插数据7、头删数据8、尾删数据9、寻找数据所在结点10、在任意结点之后插入数据11、删除任意结点12、销毁…

debian 更新源

前言 实现一键替换在线源 一键更新源 Debian 全球镜像站以下支持现有debian 11 12 echo "Delete the default source" rm -rf /etc/apt/sources.listecho "Build a new source" cat <<EOF>>/etc/apt/sources.list.d/debian.sources Types:…

Shell的正确使用

目录 shell 介绍 变量名的定义规则 变量名定义&#xff1a; 删除变量 特殊的变量名&#xff1a; 算术运算符 逻辑运算符&#xff1a; (1)整数之间比较 (2)按照文件权限进行判断 (3)按照文件类型判断 (4)多条件判断 常用的特殊字符&#xff1a; 条件选择、判断 if判…

PHP上门按摩专业版防东郊到家系统源码小程序

&#x1f486;‍♀️【尊享级体验】上门按摩专业版&#xff0c;告别东郊到家&#xff0c;解锁全新放松秘籍&#xff01;&#x1f3e0;✨ &#x1f525;【开篇安利&#xff0c;告别传统束缚】&#x1f525; 亲们&#xff0c;是不是厌倦了忙碌生活中的疲惫感&#xff1f;想要享…

NET 语言识别,语音控制操作、语音播报

System.Speech. 》》System.Speech.Synthesis; 语音播报 》》System.Speech.Recognition 语音识别 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Speech.Recog…

[Redis]典型应用——分布式锁

什么是分布式锁&#xff1f; 在一个分布式系统中&#xff0c;也会涉及到多个节点访问同一个公共资源的情况。此时就需要通过锁来做互斥控制&#xff0c;避免出现类似于"线程安全"的问题 举个例子&#xff0c;在平时抢票时&#xff0c;多个用户可能会同时买票&#…

Linux——多路复用之poll

目录 前言 一、poll的认识 二、poll的接口 三、poll的使用 前言 前面我们学习了多路复用的select&#xff0c;知道多路复用的原理与select的使用方法&#xff0c;但是select也有许多缺点&#xff0c;导致他的效率不算高。今天我们来学习poll的使用&#xff0c;看看poll较于…

利用AI与数据分析优化招聘决策

一、引言 在竞争激烈的职场环境中&#xff0c;招聘是组织获取人才、实现战略目标的关键环节。然而&#xff0c;传统的招聘方式往往依赖人力资源部门的主观经验和直觉&#xff0c;难以准确预测招聘效果&#xff0c;评估招聘渠道的效率。随着人工智能&#xff08;AI&#xff09;…

Windows右键新建Markdown文件类型配置 | Typora | VSCode

&#x1f64b;大家好&#xff01;我是毛毛张! &#x1f308;个人首页&#xff1a; 神马都会亿点点的毛毛张 今天毛毛张分享的是如何在右键的新建菜单中添加新建MarkdownFile文件&#xff0c;这是毛毛张分享的关于Typora软件的相关知识的第三期 文章目录 1.前言&#x1f3dd;…

Android车载MCU控制音量和ARM控制音量的区别和优缺点—TEF6686 FM/AM芯片

不要嫌前进的慢&#xff0c;只要一直在前进就好 文章目录 前言一、系统架构图1.MCU控制音量的架构图&#xff08;老方法&#xff09;2.ARM控制音量的架构图&#xff08;新方法&#xff09; 二、为啥控制音量不是用AudioManager而是执着去直接控制TDA7729&#xff1f;三、MCU控制…

[数据集][目标检测]婴儿车检测数据集VOC+YOLO格式1073张5类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;1073 标注数量(xml文件个数)&#xff1a;1073 标注数量(txt文件个数)&#xff1a;1073 标注…

Matlab基础语法篇(下)

Matlab基础语法&#xff08;下&#xff09; 一、逻辑基础&#xff08;一&#xff09;逻辑运算符&#xff08;二&#xff09;all、any、find函数&#xff08;三&#xff09;练习 二、结构基础&#xff08;一&#xff09;条件结构&#xff08;1&#xff09;if-elseif-else-end&am…

通过albumentation对目标检测进行数据增强(简单直接)

albumentation官方文档看不懂&#xff1f;xml文件不知道如何操作&#xff1f;下面只需要修改部分代码即可上手使用 要使用这个方法之前需要按照albumentation这个库还有一些辅助库,自己看着来安装就行 pip install albumentation pip install opencv-python pip install json…

<数据集>蛋壳裂缝检测数据集<目标检测>

数据集格式&#xff1a;VOCYOLO格式 图片数量&#xff1a;2520张 标注数量(xml文件个数)&#xff1a;2520 标注数量(txt文件个数)&#xff1a;2520 标注类别数&#xff1a;2 标注类别名称&#xff1a;[crack, egg] 序号类别名称图片数框数1crack245128352egg25142514 使…

揭秘饲料制粒机:生产颗粒料加工的利器

随着现代畜牧业的发展&#xff0c;饲料的质量和加工效率成为了养殖业者关注的焦点。在这个背景下&#xff0c;饲料制粒机——这一饲料加工设备的核心&#xff0c;凭借其稳定、环保的特点&#xff0c;逐渐加入养殖行业中。 一、饲料制粒机的工作原理 饲料制粒机主要通过挤压、切…

msyql (8.4,9.0) caching_sha2_password 转换 mysql_native_password用户认证

mysql 前言 caching_sha2_password 主要特性 用于增强用户账户密码的存储和验证安全性。这种插件利用 SHA-256 散列算法的变体来存储和验证密码 安全的密码散列&#xff1a; caching_sha2_password 使用基于 SHA-256 的算法来生成密码的散列值。这意味着即使数据库被未授权访…

【JS特效之手风琴效果】基于jquery实现手风琴网页特效(附源码)

HTMLCSSJS手风琴效果目录 &#x1f354;涉及知识&#x1f964;写在前面&#x1f367;一、网页主题&#x1f333;二、网页效果&#x1f40b;三、网页架构与技术3.1 脑海构思3.2 实现原理 &#x1f308;四、网页源码4.1 手风琴模块4.2 完整源码获取方式 &#x1f305; 作者寄语 &…

机械学习—零基础学习日志(高数05——函数概念与特性)

零基础为了学人工智能&#xff0c;真的开始复习高数 本小节讲解隐函数&#xff0c;有点神奇&#xff0c;我竟然完全没有隐函数记忆了。 隐函数 隐函数&#xff0c;我个人通俗理解就是&#xff0c;在复杂的环境里&#xff0c;发现纯净天地。例如&#xff0c;在外太空的某个大陆…

《JavaSE》---14.<面向对象系列之(附:this和super关键字)>

目录 系列文章目录 前言 一、为什么要有this引用 1. 用代码看有this与无this的区别 1.1 代码示例 1.2 输出结果&#xff1a; 1.3 代码示例&#xff1a; 1.4 输出结果&#xff1a; 2. this深度理解 3. 什么是this引用 3.1 this引用的概念 4. this引用的特性 二、th…