从零开始读RocketMq源码(一)生产者启动

news2024/10/6 2:58:23

目录

前言

获取源码

总概论

生产者实例

源码

A-01:设置生产者组名称

A-02:生产者服务启动

B-01:初始化状态

B-02:该方法再次对生产者组名称进行校验

B-03:判断是否为默认生产者组名称

B-04: 该方法是为了实例化MQClientInstance对象,mq客户端对象实例

B-05: 该方法就是将当前生产者对象注册到mqClientInstance中的producerTable集合中,并且生产者组名称作为key

B-06: 启动相关核心服务以及开启一系列定时任务(核心逻辑)

1. 开启请求-响应通道- this.mQClientAPIImpl.start();

2. 开启拉动式服务- this.mQClientAPIImpl.start();

3. 开启负载均衡服务- this.rebalanceService.start();

4. 开启推送服务- this.defaultMQProducer.getDefaultMQProducerImpl().start(false);

5. 启动各种计划任务- this.startScheduledTask();

a. 启动定时任务获取MQ注册中心nameServer的地址- MQClientInstance.this.mQClientAPIImpl.fetchNameServerAddr();

b. 定时从nameServer拉取topic信息到本地存储 -                                    MQClientInstance.this.updateTopicRouteInfoFromNameServer();

c. 定时清除离线的broker服务并给所有在线的broker发送心跳

d. 定时持久化消费偏移量数据

e. 定时调整消费者消息的线程池数量

B-07:初始化topic路由信息、topic訂閲信息以及topic端点映射信息

B-08:开启定时监测broker故障信息任务

B-09:发送心跳给所有的broker服务

B-10:开启定时扫描异步请求响应任务

A-03:开启监控和处理同步发送和异步发送操作的守护线程

A-04:开启消息轨迹和发送机制

总结

展望


前言

大概一年半前自己写了一篇《云原生》一文搞懂RocketMQ队列概述,这篇对rocketmq的相关概念和使用方法进行了整理概述,就像结尾说的都太局限于表面,简单使用还能将就,但一出现问题自己也很难排查,为了迈向技能的下一个阶段,还得是要读源码,学习大佬们的编码风格和技巧,对于使用mq以及排除问题也会更得心应手。因为最近一年自己的工作充满了波折,让自己没法静下心来学习整理,虽然现在也好不了多少,但可算能回归本心。本章篇幅比较长,将近万字,博主也是自己读源码一步一步跟踪的,所以尽量想描述得通俗易懂一些。

获取源码

首先我们从github上拉取rocketmqd的源码链接到本地,使用idea打开。

源码地址:https://github.com/apache/rocketmq

目前最新版本为:5.2.0

那么我们在idea上切换分支为 release-5.2.0

注:rocketmq5.x与4.x官方改动的东西比较多,尽量使用一直的版本,具体差异可查看官网,这里只对源码逻辑进行分析

总概论

我们知道rocketmq的组成需要四大模块构成,缺一不可

  • nameserver mq注册中心(状态管理)
  • broker mq的服务端(核心)
  • producer 生产者
  • consumer 消费者

本章我们先从应该大家接触最多的生产者开始学习源码吧。

生产者实例

  1. 在idea的rocketmq源码中找到 example 模块,这个模块中都是官方给出的简单案例
  2. 然后找到simple 包下面的 Producer类打开
  3. 然后在producer类中配置自己的mq的地址,topic以及tag就能成功启动生产者并且发送消息

注意:这里成功启动的前提是必须提前启动了mq的nameserver服务和broker服务才能成功,若没有可不用启动,直接跳过看下面源码

源码

根据上面简单生产者实例可知,生产者端的两大核心就是,启动生产者发送消息,分别对应下面两行代码。看似简单的两行其实里面的功能逻辑很强大。

  • producer.start();
    
  • producer.send(msg);

生产者包含4中状态:

  • CREATE_JUST 服务刚刚创建,尚未启动
  • RUNNING 服务运行中
  • SHUTDOWN_ALREADY 服务已关闭
  • START_FAILED 启动出错

按照顺序,我们从 生产者的启动开始

public void start() throws MQClientException {
    //A-01:设置生产者组名称
    this.setProducerGroup(withNamespace(this.producerGroup));
    //A-02:生产者服务启动
    this.defaultMQProducerImpl.start();
    //A-03:开启监控和处理同步发送和异步发送操作的守护线程
    if (this.produceAccumulator != null) {
        this.produceAccumulator.start();
    }
    //A-04:开启消息轨迹和发送机制
    if (null != traceDispatcher) {
        try {
            traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel());
        } catch (MQClientException e) {
            logger.warn("trace dispatcher start failed ", e);
        }
    }
}

A-01:设置生产者组名称

  1. 该方法中顾名思义主要用于设置生产者组的名称
  2. withNamespace()进入该方法发现,其实对生产者组的名称就行各种非空校验和长度校验,最后根据固定格式拼接名称后返回。(对于开源组件大佬,校验方式也是和我们无异的)

A-02:生产者服务启动

该方法为本次的启动核心方法,我们直接深入了解下其内部实现。

方法逻辑太长我们进行分段拆分来解析

public void start(final boolean startFactory) throws MQClientException {
    switch (this.serviceState) {
            //B-01:初始化状态
        case CREATE_JUST:
            this.serviceState = ServiceState.START_FAILED;
            //B-02:校验
            this.checkConfig();
            //B-03:生产者组名设置
            if (!this.defaultMQProducer.getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP)) {
                this.defaultMQProducer.changeInstanceNameToPID();
            }
        //...
B-01:初始化状态

因为现在还是正在启动中,所以状态还是默认未启动状态,那么直接进入第一个case逻辑中,进入后里面把状态至为启动失败,我认为这是一种防御性编码,并且防止未成功启动的生产者被重复启动

B-02:该方法再次对生产者组名称进行校验
B-03:判断是否为默认生产者组名称

        前面可知我们已经成功设置自定义名称,所以直接进入if中

  • changeInstanceNameToPID(),该方法就设置实例名称,进入方法可以看到名称的生成规则,this.instanceName = UtilAll.getPid() + "#" + System.nanoTime();
    当前运行的虚拟机的名称截取拼接上当前纳米时间戳,保证唯一性
public void start(final boolean startFactory) throws MQClientException {
    /....../
    //B-04:该方法是为了实例化MQClientInstance对象,mq客户端对象实例
    this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQProducer, rpcHook);
    //B-05:注册生产者
    boolean registerOK = mQClientFactory.registerProducer(this.defaultMQProducer.getProducerGroup(), this);
    /....../
B-04: 该方法是为了实例化MQClientInstance对象,mq客户端对象实例
  • 内部首先生成一个唯一的clientId,其组成包含ip地址与之前生成的实例名称instanceName组成,然后new 了一个MQClientInstance对象并设置对应属性。
  • 将clientId作为key维护到一个Map对象中,private final ConcurrentMap<String/* clientId */, MQClientInstance> factoryTable;

注:MQClientInstance对象,该对象非常重要,因为生产者和消费者都在使用

进入该对象我们可以发现,里面维护了两个Map集合,就是分别存储当前客户端的生产者和消费者的对象数据

private final ConcurrentMap<String, MQProducerInner> producerTable
private final ConcurrentMap<String, MQConsumerInner> consumerTable

B-05: 该方法就是将当前生产者对象注册到mqClientInstance中的producerTable集合中,并且生产者组名称作为key
public void start(final boolean startFactory) throws MQClientException {
    switch (this.serviceState) {
            /..../
            //B-06: 启动相关核心服务以及开启一系列定时任务
            if (startFactory) {
                mQClientFactory.start();
            }
            /.../
B-06: 启动相关核心服务以及开启一系列定时任务(核心逻辑)
1. 开启请求-响应通道- this.mQClientAPIImpl.start();
2. 开启拉动式服务- this.mQClientAPIImpl.start();
3. 开启负载均衡服务- this.rebalanceService.start();
4. 开启推送服务- this.defaultMQProducer.getDefaultMQProducerImpl().start(false);
  • 这个方法是否有点眼熟,没错这就是我们最开始调用的启动方法A-2,参数传的false,说明上面if代码块中startFactory=false,则不进入B-06的代码块中
  • 并且A-2代码块方法中,因为第一次进入时状态已经从CREATE_JUST变更为START_FAILED,所以也不会再次进入第一个case中
  • 阅读后续代码可知,核心就是调用了 this.mQClientFactory.sendHeartbeatToAllBrokerWithLock(); 向所有Broker服务发送一次心跳(具体后面会详解)
5. 启动各种计划任务- this.startScheduledTask();

所有任务都是使用Executors线程池创建一个单独的的单线程定时任务实现,如下格式

private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "MQClientFactoryScheduledThread"));
//....
this.scheduledExecutorService.scheduleAtFixedRate(() -> {
    try {
        //业务逻辑
    } catch (Exception e) {
        log.error("ScheduledTask fetchNameServerAddr exception", e);
    }
}, 1000 * 10, 1000 * 60 * 2, TimeUnit.MILLISECONDS);
a. 启动定时任务获取MQ注册中心nameServer的地址- MQClientInstance.this.mQClientAPIImpl.fetchNameServerAddr();

首次启动延迟时间:2s

定时间隔时间:2m

mQClientAPIImpl对象是否眼熟,没错就是上面B-06-1启动的服务,所以该服务必须在任务执行之前启动,查看源码如此

  • 深入方法中会发现其实就是获取地址处理后存储在一个List集合中,为什么使用集合,我认为如果是集群那就就会有多条地址存在。 private final AtomicReference<List<String>> namesrvAddrList = new AtomicReference<>();
  • 继续深入会发现有Netty的身影,用于服务间远程通信,这里不再研究。
  • private final ConcurrentMap<String /* addr */, ChannelWrapper> channelTables;
  • 该Map就是用nameserver地址作为key,而value为ChannelWrapper对象,该对象内部就使用了netty框架 包中的对象,一个地址对应一个通道封装器。但是该逻辑中并没有使用put操作,只是get获取。
b. 定时从nameServer拉取topic信息到本地存储 -                                    MQClientInstance.this.updateTopicRouteInfoFromNameServer();

首次启动延迟时间:10ms

定时间隔时间:30s

  • 深入方法内部可知,其实就是分别对producerTableconsumerTable的map进行操作遍历,取出对象里面的topic名称,由前面B-04中可知,分别用于存储生产者对象消费者对象信息
  • 再将topic名称的set集合进行遍历去远程获取nameserver中的topic的路由详细信息,并将信息存储在另一个map对象中。作用: 用于管理和查询主题的路由信息,帮助生产者和消费者确定消息的发送和接收路径。
  • private final ConcurrentMap<String/* Topic */, TopicRouteData> topicRouteTable = new ConcurrentHashMap<>();
c. 定时清除离线的broker服务并给所有在线的broker发送心跳

        MQClientInstance.this.cleanOfflineBroker(); 清除离线的broker

        MQClientInstance.this.sendHeartbeatToAllBrokerWithLock(); 给所有的broker发送心跳

首次启动延迟时间:1s

定时间隔时间:30s

  • 清除离线的broker,查看源码可知道,大概意思为首先从private final ConcurrentMap<String, HashMap<Long, String>> brokerAddrTable = new ConcurrentHashMap<>(); map中
    • 获取所有broker的地址数据,然后进行遍历,
    • 在遍历中取出 topicRouteTable,该map存放的是topic的对象信息
    • 再对topic map的values进行遍历,取出topic信息对象中存储的对应broker集合,
    • 判断上面的brokerAddrTable中的broker是否在topic维护的broker集合中,没有则清除
d. 定时持久化消费偏移量数据

     MQClientInstance.this.persistAllConsumerOffset();

首次启动延迟时间:10s

定时间隔时间:5s

同样的维护了一个Map对象:

private ConcurrentMap<MessageQueue, ControllableOffset> offsetTable;

key则为消息队列对象

  • 深入源码可知,它的消费者持久化实现方式有三种
    • lite pull
    • mp pull
    • mp push
e. 定时调整消费者消息的线程池数量

    MQClientInstance.this.adjustThreadPool();

首次启动延迟时间:1m

定时间隔时间:1m

public void start(final boolean startFactory) throws MQClientException {
    switch (this.serviceState) {
            //...
            //B-07:初始化topic路由信息、topic訂閲信息以及topic端点映射信息
            this.initTopicRoute();
            //B-08:开启定时监测broker故障信息任务
            this.mqFaultStrategy.startDetector();
            //...
B-07:初始化topic路由信息、topic訂閲信息以及topic端点映射信息
  • 深入源码可知,首先获取开发者自定义的topic集合,然后分别处理成MQ要求的格式newTopic,然后创建TopicPublishInfo对象,用于存储topic订阅信息newTopic作为key,同样最后放入map中

private final ConcurrentMap<String/* topic */, TopicPublishInfo> topicPublishInfoTable;

  • 查看TopicPublishInfo对象可知,对象里面包含了TopicRouteData对象,我们知道这个对象在上面定时器B-06-5-b中出现过用于存储topic路由信息,并且存储在topicRouteTable map中
  • 所以在本方法中也会通过newTopic去远程从nameserver中拉去TopicRouteData信息,设置到TopicPublishInfo对象中,同样也会对比topic新获取的TopicRouteData与原来定时器存储的topicRouteTable中的是否有变化,有则更新
  • 有变化同时还会更新,上面定时器B-06-5-c中出现的brokerAddrTable map,更新broker的地址信息
  • 同时更新topic端点映射信息-记录每个主题的消息队列与 Broker 之间的映射 
  • private final ConcurrentMap<String/* Topic */, ConcurrentMap<MessageQueue, String/*brokerName*/>> topicEndPointsTable;这是一个嵌套map,因为一个topic可能对应多个broker,那么消息队列也会是对应多个broker, 可以帮助管理和均衡负载,确保消息被分布到不同的 Broker 上
B-08:开启定时监测broker故障信息任务

深入源码可知,里面维护了一个定时任务,定时监测 Broker 的故障详细信息

首次启动延迟时间:3s

定时间隔时间:3s

  • 同时也维护了一个map,用于存储每一个broker的 故障详细信息,包括故障时间、故障持续时间和可用状态

        private final ConcurrentHashMap<String, FaultItem> faultItemTable;

  • 逻辑处理中还会去查询brokerAddrTable中是否还存在当前broker地址信息,不存在则从faultItemTable中移除,然后再去监测broker服务是否可用,若可用则将可用状态 设置为true
public void start(final boolean startFactory) throws MQClientException {
    //...
    //B-09:发送心跳给所有的broker服务
    this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
    //B-10:开启定时扫描异步请求响应任务
    RequestFutureHolder.getInstance().startScheduledTask(this);

}
B-09:发送心跳给所有的broker服务

        发送心跳其实在上面定时器B-06-5-c中已经出现过了,但是没有深入了解,那么定时器中既然已经在发送心跳了,为什么生产者启动最后还要发送呢?

  • 定时任务的作用:定时任务确保客户端在运行过程中定期发送心跳,保持与 Broker 的连接。
  • 启动时的心跳:生产者在启动完成时立即发送心跳,以确保初始化成功、快速检测连接状态并更新路由信息。
  • 同样的心跳机制中也维护了一个map, 用于记录和管理每个 Broker 的心跳信息,private final ConcurrentMap<String, Integer> brokerAddrHeartbeatFingerprintTable;
  • 其中value值称为心跳指纹, MQ通过比较当前心跳指纹和上次记录的指纹,可以判断 Broker 是否正常工作
B-10:开启定时扫描异步请求响应任务

        深入源码可知,里面维护了一个定时任务,定时扫描MQ存储的生产者发布的异步请求以及响应的信息,帮助MQ实现异步请求的超时、回调和状态管理,增强系统的异步处理能力。

次启动延迟时间:3s

定时间隔时间:1s

同样的是维护了一个map数据用于存储异步请求以及响应的信息:

private ConcurrentHashMap<String, RequestResponseFuture> requestFutureTable

那么key为请求时生成的唯一标识,value为RequestResponseFuture对象则记录了请求信息、超时时间、响应信息、回调信息等,mq根据记录的信息做出响应的处理。

  • 源码内部逻辑有一个地方就判断了isTimeout是否请求超时,为true则抛出异常

该map requestFutureTable在本次启动中只是使用,具体在什么地方存储的,应该会在后续的生产者发送消息源码中再次出现,本次启动使用到的requestFutureTable应该都是没数据的。日常开发看似只是简单的调用了发送消息的api方法,而mq内部则做了许多复杂的处理来保证消息的可靠性和高可用性

A-03:开启监控和处理同步发送和异步发送操作的守护线程

  • guardThreadForSyncSend.start();
  • guardThreadForAsyncSend.start();

        这些线程中,可以实现具体的监控和处理逻辑,例如检测发送超时、重试失败的发送操作等。 并且这些线程在 JVM 退出时会自动终止

A-04:开启消息轨迹和发送机制

通过收集消息轨迹信息,可以了解消息在 RocketMQ 中的流转路径,帮助系统监控和故障排查。

总结

对于RocketMQ我们都知道生产者会从nameserver中拉取数据,并且会在本地存储,就算nameserver服务意外离线了,也能通过本地保存的数据进行消息通信。那么如何远程拉取数据以及心跳监测如何在本地存储,我想大家通过对上面start启动源码的学习,疑惑都解开了吧。

  • 数据更新以及心跳无非就是通过一系列的定时器在不断请远程请求
  • 本地存储则是使用已 table为后缀命名的Map集合来存储的

对本章源码中遇到的定时器和table进行了整理,方便大家快速记忆


展望

本章内容比较多,博主也是肝了几天才完成,希望对大家都有所收获,下一章我们继续对生产者send消息源码进行学习!

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

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

相关文章

机器学习第四十六周周报 FMP

文章目录 week46 FMP摘要Abstract1. 题目2. Abstract3. FMP3.1 优化框架3.2 优化器 4. 文献解读4.1 Introduction4.2 创新点4.3 实验过程 5. 结论6.代码复现1. FMP2. fairGNN小结参考文献 week46 FMP 摘要 本周阅读了题为Chasing Fairness in Graphs: A GNN Architecture Per…

永磁同步电机控制算法--最大转矩电流比控制(虚拟信号注入法)

目前&#xff0c;国内外相关学者对 MTPA 控制方法进行了一系列的理论研究与仿真分析。通过研究取得的成果综合来看&#xff0c;该控制方法主要有&#xff1a;直接公式计算法、曲线拟合法、查表法、搜索法、高频信号注入法以及参数辨识法等。 之前的文章中已经介绍了直接公式计…

ASP.NET Core----基础学习03----开发者异常页面 MVC工作原理及实现

文章目录 1. 开发者异常页面(1)Startup.cs 页面的基础配置(2)自定义显示报错代码的前后XX行 2. MVC 的原理3. MVC 的实现4.默认路由路径5.返回Json字符串 1. 开发者异常页面 (1)Startup.cs 页面的基础配置 namespace ASP.Net_Blank {public class Startup{private readonly IC…

Matlab协方差矩阵分解法生成随机场

Matlab协方差矩阵分解法生成随机场 相关系数矩阵 % function outcohesion(x,y,mu,theta) % end % xyload(F:\Research-OUC\基于机器许学习模型的海底斜坡可靠度研究\基于comsol的斜坡稳定性分析\comsol网格操作\grid_operate-matlab.mphtxt); % xxy(:,1); % yxy(:,2); Xlinspac…

前端JS特效第22集:html5音乐旋律自定义交互特效

html5音乐旋律自定义交互特效&#xff0c;先来看看效果&#xff1a; 部分核心的代码如下(全部代码在文章末尾)&#xff1a; <!DOCTYPE html> <html lang"en" > <head> <meta charset"UTF-8"> <title>ChimeTime™</title…

Spring源码十二:事件发布源码跟踪

上一篇我们在Spring源码十一&#xff1a;事件驱动中&#xff0c;介绍了spring refresh方法的initMessageSource方法与initApplicationEventMulticaster方法&#xff0c;举了一个简单的例子进行简单的使用的Spring为我们提供的事件驱动发布的示例。这一篇我们将继续跟踪源码&…

谷粒商城学习笔记-使用renren-fast-vue框架时安装依赖包遇到的问题及解决策略

文章目录 1&#xff0c;npm error Class extends value undefined is not a constuctor or null2&#xff0c;npm warn cli npm v10.8.1 does not support Node.js v16.20.2.3&#xff0c;npm error code CERT_HAS_EXPIRED学习心得 这篇文章记录下使用renren-fast-vue&#xff…

花所Flower非小号排名20名下载花所Flower

1、Flower花所介绍 Flower花所是一家新兴的数字货币交易平台&#xff0c;致力于为全球用户提供安全、便捷的交易体验。平台以其强大的技术支持和丰富的交易产品闻名&#xff0c;为用户提供多样化的数字资产交易服务&#xff0c;涵盖了主流和新兴数字货币的交易需求。 2. Flowe…

SQL之delete、truncate和drop区别

MySQL删除数据的方式都有哪些&#xff1f; 常用的三种删除方式&#xff1a;通过 delete、truncate、drop 关键字进行删除&#xff1b;这三种都可以用来删除数据&#xff0c;但场景不同。 一、从执行速度上来说 drop > truncate >> DELETE;二、从原理上讲 1、DELET…

Guitar Pro8.2让你的吉他弹奏如虎添翼!

亲爱的音乐爱好者们&#xff0c;今天我要跟大家安利一个让我彻底沉迷其中的神器——Guitar Pro8.2&#xff01;这可不是一般的软件&#xff0c;它简直是吉他手们的福音。不管你是初学者还是老鸟&#xff0c;这个打谱软件都能给你带来前所未有的便利和价值。 让我们来聊聊Guita…

原理图设计工作平台:capture和capture CIS的区别在于有没有CIS模块

1环境:design entry CIS 2.参数设置命令options——preference&#xff08;7个选项卡colors/print&#xff0c;grid display&#xff0c;miscellaneous&#xff0c;pan and zoom&#xff0c;select&#xff0c;text editor和board simulation&#xff09; 1)颜色设置colors/p…

12 电商高并发缓存实战

序章 项目代码缓存的数据一致性 延时双删 淘汰缓存写数据库休眠1s,再次淘汰缓存缺点:如果mysql是主从复制,去从库中拿去数据,此时同步数据还未完成,拿到的数据是旧数据。 先更新 DB,后删除缓存 采用异步延时删除策略. ①利用消息队列进行删除的补偿。②Mysql 数据库更新操…

深入理解并发、线程与等待通知机制

目录 一、基础概念 进程和线程 进程 线程 Java 线程的无处不在 进程间的通信 进程间通信有几种方式&#xff1f; CPU 核心数和线程数的关系 上下文切换&#xff08;Context switch&#xff09; 并行和并发 二、认识 Java 里的线程 Java 程序天生就是多线程的 线程的…

python语句性能分析

1、for语句性能优于while import timeif __name__ __main__:start_time time.time()for i in range(10 ** 8):passend_time time.time()run_time end_time - start_timeprint(run_time)i 0start_time time.time()while i < 10 ** 8:i 1end_time time.time()run_tim…

【Spring Cloud】微服务的简单搭建

文章目录 &#x1f343;前言&#x1f384;开发环境安装&#x1f333;服务拆分的原则&#x1f6a9;单一职责原则&#x1f6a9;服务自治&#x1f6a9;单向依赖 &#x1f340;搭建案例介绍&#x1f334;数据准备&#x1f38b;工程搭建&#x1f6a9;构建父子工程&#x1f388;创建父…

Spring Boot的无缝衔接:深入解析与实践

欢迎来到 破晓的历程的 博客 ⛺️不负时光&#xff0c;不负己✈️ &#x1f680;The begin&#x1f697;点点关注&#xff0c;收藏不迷路&#x1f6a9; 引言 在快速迭代的软件开发环境中&#xff0c;无缝衔接是提升开发效率、降低维护成本、增强系统稳定性的关键。Spring Boo…

嵌入式c语言5——位运算符

<<与>>是c语言中两个移位运算符&#xff0c;分别有乘以2与除以2的意义 位运算符还包括&#xff0c;与&&#xff0c;或|&#xff0c;均进行按位操作 同时&#xff0c;还可以进行取反以及异或操作

【C++】cout.self()函数

&#x1f4e2;博客主页&#xff1a;https://blog.csdn.net/2301_779549673 &#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01; &#x1f4e2;本文作为 JohnKi 学习笔记&#xff0c;借鉴了部分大佬案例 &#x1f4e2;未来很长&#…

web前端开发——开发环境和基本知识

今天我来针对web前端开发讲解一些开发环境和基本知识 什么是前端 前端通常指的是网站或者Web应用中用户可以直接与之交互的部分&#xff0c;包括网站的结构、设计、内容和功能。它是软件开发中的一个专业术语&#xff0c;特别是指Web开发领域。前端开发涉及的主要技术包括HTML…

C++语言相关的常见面试题目(一)

1. const关键字的作用 答&#xff1a; 省流&#xff1a;&#xff08;1&#xff09;定义变量&#xff0c;主要为了防止修改 (2) 修饰函数参数&#xff1a;防止在函数内被改变 &#xff08;3&#xff09;修饰函数的返回值 &#xff08;4&#xff09;修饰类中的成员函数 2. Sta…