NameServer路由注册与发现

news2024/11/17 13:39:47

        NameServer在RocketMQ中主要承担的就是路由的管理、服务注册、以及服务的发现。在RocketMQ这承担着很重要的责任。

整体架构:

        消息生产者在发送消息前需要考虑的问题就是,我需要发给谁?地址在哪儿?对于消费者也一样。那么NameServer就是用来解决这个问题的。 

        首先消息服务器(Broker)在启动的时候会向所有的NameServer注册自己的信息,然后消息生产者就会在发送消息前就会先从NameServer获取这些路由信息,然后选择一个合适的服务器去发送消息。

        在这当中NameServer与每台Broker保持的是长连接,每隔10s就会去检测Broker是否存活,超过120s没有更新路由信息,就会将这个broker的路由信息给他剔除,broker是每隔30s就会向所有NameServer发送心跳包。同样的,客户端(消息消费者和生产者)也会每隔30s从NameServer更新路由信息。

NameServer存储了Broker的那些信息?

/**topic消息队列的路由信息,消息发送时候根据路由表进行负载均衡*/
private final HashMap<String/* topic */, List<QueueData>> topicQueueTable;
/**broker基础信息*/
private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable;
/**broker集群信息,*/
private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;
/**Broker状态信息,每次收到心跳包就会替换里面的信息*/
private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
/**消息过滤要用到的一个map*/
private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;

NameServer启动之后就会初始化一些基本信息,然后就是等待broker发送心跳包,然后处理。然后就是每隔10s去检测这些broker的路由信息,然后发现已经超过120s没有更新,就会将它剔除。NameServer还提供路由发现功能,就是支持客户端通过topic名称来查找它的路由信息。

Broker发送心跳包(即注册路由)

首先看一下Broker发送心跳包:

在broker启动的时候,会创建一个线程池每隔30s去注册自己的路由信息,也就是前面所说的发送心跳包。

/**
private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl("BrokerControllerScheduledThread"));     
*/
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                try {
                    BrokerController.this.registerBrokerAll(true, false, brokerConfig.isForceRegister());
                } catch (Throwable e) {
                    log.error("registerBrokerAll Exception", e);
                }
            }
        }, 1000 * 10, Math.max(10000, Math.min(brokerConfig.getRegisterNameServerPeriod(), 60000)), TimeUnit.MILLISECONDS);
//brokerConfig.getRegisterNameServerPeriod()  默认值为:1000*30

再来看一下里面的registerBrokerAll(true, false, brokerConfig.isForceRegister())方法:

1.首先封装topic信息,是跟据自身的 topicConfigTable 来构建的,topicConfigTable是一个ConcurrentMap。

2.然后就是遍历topic信息,封装topic队列信息。

3.发送心跳包

public synchronized void registerBrokerAll(final boolean checkOrderConfig, boolean oneway, boolean forceRegister) {
//封装topic信息
  TopicConfigSerializeWrapper topicConfigWrapper = this.getTopicConfigManager().buildTopicConfigSerializeWrapper();
//判断是否具有可读可写权限
  if (!PermName.isWriteable(this.getBrokerConfig().getBrokerPermission())
      || !PermName.isReadable(this.getBrokerConfig().getBrokerPermission())) {
    ConcurrentHashMap<String, TopicConfig> topicConfigTable = new ConcurrentHashMap<String, TopicConfig>();
    //遍历topic
    for (TopicConfig topicConfig : topicConfigWrapper.getTopicConfigTable().values()) {
      //封装topic队列信息,这个对象的数据结构就跟NameServer的 QueueData 结构是一样的了
      TopicConfig tmp =
        new TopicConfig(topicConfig.getTopicName(), topicConfig.getReadQueueNums(), topicConfig.getWriteQueueNums(),
                        this.brokerConfig.getBrokerPermission());
      topicConfigTable.put(topicConfig.getTopicName(), tmp);
    }
    topicConfigWrapper.setTopicConfigTable(topicConfigTable);
  }

  if (forceRegister || needRegister(this.brokerConfig.getBrokerClusterName(),
                                    this.getBrokerAddr(),
                                    this.brokerConfig.getBrokerName(),
                                    this.brokerConfig.getBrokerId(),
                                    this.brokerConfig.getRegisterBrokerTimeoutMills())) {
    //注册
    doRegisterBrokerAll(checkOrderConfig, oneway, topicConfigWrapper);
  }
}

然后再来看一下doRegisterBrokerAll方法:

这个方法主要就是去获取本机的ip地址,以及broker的配置信息

private void doRegisterBrokerAll(boolean checkOrderConfig, boolean oneway,
        TopicConfigSerializeWrapper topicConfigWrapper) {
  			//重点是这个方法
        List<RegisterBrokerResult> registerBrokerResultList = this.brokerOuterAPI.registerBrokerAll(
            this.brokerConfig.getBrokerClusterName(),
            this.getBrokerAddr(),   //获取broker地址和端口号
            this.brokerConfig.getBrokerName(),
            this.brokerConfig.getBrokerId(),
            this.getHAServerAddr(),  //注意这里,这里就是去获取的本级的地址和IP
            topicConfigWrapper,
            this.filterServerManager.buildNewFilterServerList(),
            oneway,
            this.brokerConfig.getRegisterBrokerTimeoutMills(),
            this.brokerConfig.isCompressedRegister());

        if (registerBrokerResultList.size() > 0) {
            RegisterBrokerResult registerBrokerResult = registerBrokerResultList.get(0);
            if (registerBrokerResult != null) {
                if (this.updateMasterHAServerAddrPeriodically && registerBrokerResult.getHaServerAddr() != null) {
                    this.messageStore.updateHaMasterAddress(registerBrokerResult.getHaServerAddr());
                }

                this.slaveSynchronize.setMasterAddr(registerBrokerResult.getMasterAddr());

                if (checkOrderConfig) {
                    this.getTopicConfigManager().updateOrderTopicConfig(registerBrokerResult.getKvTable());
                }
            }
        }
    }

然后再来看最后的构建请求,以及发送心跳包的方法:

首先获取所有的NameServer地址,然后封装请求头和请求体,最后通过netty将心跳包发送出去。

 public List<RegisterBrokerResult> registerBrokerAll(
        final String clusterName,
        final String brokerAddr,
        final String brokerName,
        final long brokerId,
        final String haServerAddr,
        final TopicConfigSerializeWrapper topicConfigWrapper,
        final List<String> filterServerList,
        final boolean oneway,
        final int timeoutMills,
        final boolean compressed) {

        final List<RegisterBrokerResult> registerBrokerResultList = Lists.newArrayList();
   			//获取所有的NameServer地址呢
        List<String> nameServerAddressList = this.remotingClient.getNameServerAddressList();
        if (nameServerAddressList != null && nameServerAddressList.size() > 0) {
						//请求头
            final RegisterBrokerRequestHeader requestHeader = new RegisterBrokerRequestHeader();
            requestHeader.setBrokerAddr(brokerAddr);
            requestHeader.setBrokerId(brokerId);
            requestHeader.setBrokerName(brokerName);
            requestHeader.setClusterName(clusterName);
            requestHeader.setHaServerAddr(haServerAddr);
            requestHeader.setCompressed(compressed);
						//请求体
            RegisterBrokerBody requestBody = new RegisterBrokerBody();
            requestBody.setTopicConfigSerializeWrapper(topicConfigWrapper);
            requestBody.setFilterServerList(filterServerList);
            final byte[] body = requestBody.encode(compressed);
            final int bodyCrc32 = UtilAll.crc32(body);
            requestHeader.setBodyCrc32(bodyCrc32);
            final CountDownLatch countDownLatch = new CountDownLatch(nameServerAddressList.size());
          	//遍历所有的NameServer地址
            for (final String namesrvAddr : nameServerAddressList) {
                brokerOuterExecutor.execute(new Runnable() {
                    @Override
                    public void run() {
                        try {
                          //通过netty 发送!
                            RegisterBrokerResult result = registerBroker(namesrvAddr,oneway, timeoutMills,requestHeader,body);
                            if (result != null) {
                                registerBrokerResultList.add(result);
                            }

                            log.info("register broker[{}]to name server {} OK", brokerId, namesrvAddr);
                        } catch (Exception e) {
                            log.warn("registerBroker Exception, {}", namesrvAddr, e);
                        } finally {
                            countDownLatch.countDown();
                        }
                    }
                });
            }

            try {
                countDownLatch.await(timeoutMills, TimeUnit.MILLISECONDS);
            } catch (InterruptedException e) {
            }
        }

        return registerBrokerResultList;
    }

NameServer处理心跳包

NameServer处理请求的方法是在processRequest方法里的:

org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor#processRequest
其中的switch case下面有这样一段代码:
  case RequestCode.REGISTER_BROKER:
                Version brokerVersion = MQVersion.value2Version(request.getVersion());
                if (brokerVersion.ordinal() >= MQVersion.Version.V3_0_11.ordinal()) {
                    return this.registerBrokerWithFilterServer(ctx, request);
                } else {
                    return this.registerBroker(ctx, request);
                }

方法registerBroker:

里面有一个方法,将请求解析之后,作为参数传入到registerBroker方法

 RegisterBrokerResult result = this.namesrvController.getRouteInfoManager().registerBroker(
            requestHeader.getClusterName(),
            requestHeader.getBrokerAddr(),
            requestHeader.getBrokerName(),
            requestHeader.getBrokerId(),
            requestHeader.getHaServerAddr(),
            topicConfigWrapper,
            null,
            ctx.channel()
        );

registerBroker方法:

在这里面就可以看到更新心跳包的方法啦。可以看到更新nameServer的路由信息都在这个方法里面执行了。

更新clusterAddrTable,

更新brokerAddrTable,

更新topicQueueTable,

更新brokerLiveTable,

更新filterServerTable,

  public RegisterBrokerResult registerBroker(
        final String clusterName,
        final String brokerAddr,
        final String brokerName,
        final long brokerId,
        final String haServerAddr,
        final TopicConfigSerializeWrapper topicConfigWrapper,
        final List<String> filterServerList,
        final Channel channel) {
        RegisterBrokerResult result = new RegisterBrokerResult();
        try {
            try {
                this.lock.writeLock().lockInterruptibly();
                //根据集群名称获取老的集群
                Set<String> brokerNames = this.clusterAddrTable.get(clusterName);
                //没有就直接放进去
                if (null == brokerNames) {
                    brokerNames = new HashSet<String>();
                    this.clusterAddrTable.put(clusterName, brokerNames);
                }
                //将broker放进去
                brokerNames.add(brokerName);

                boolean registerFirst = false;
                //获取old brokerData
                BrokerData brokerData = this.brokerAddrTable.get(brokerName);
                //没有表示是新建的,直接放进去
                if (null == brokerData) {
                    registerFirst = true;
                    brokerData = new BrokerData(clusterName, brokerName, new HashMap<Long, String>());
                    this.brokerAddrTable.put(brokerName, brokerData);
                }
                Map<Long, String> brokerAddrsMap = brokerData.getBrokerAddrs();
                //Switch slave to master: first remove <1, IP:PORT> in namesrv, then add <0, IP:PORT>
                //The same IP:PORT must only have one record in brokerAddrTable
                Iterator<Entry<Long, String>> it = brokerAddrsMap.entrySet().iterator();
                while (it.hasNext()) {
                    Entry<Long, String> item = it.next();
                    if (null != brokerAddr && brokerAddr.equals(item.getValue()) && brokerId != item.getKey()) {
                        it.remove();
                    }
                }
                String oldAddr = brokerData.getBrokerAddrs().put(brokerId, brokerAddr);
                //第一次注册,或者旧的没了
                registerFirst = registerFirst || (null == oldAddr);

                if (null != topicConfigWrapper
                    && MixAll.MASTER_ID == brokerId) {
                    if (this.isBrokerTopicConfigChanged(brokerAddr, topicConfigWrapper.getDataVersion())
                        || registerFirst) {
                        ConcurrentMap<String, TopicConfig> tcTable =
                            topicConfigWrapper.getTopicConfigTable();
                        if (tcTable != null) {
                            for (Map.Entry<String, TopicConfig> entry : tcTable.entrySet()) {
                              //主节点,初次注册 更新topic理由元信息
                                this.createAndUpdateQueueData(brokerName, entry.getValue());
                            }
                        }
                    }
                }
                //更新心跳包
                BrokerLiveInfo prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr,
                    new BrokerLiveInfo(
                        System.currentTimeMillis(),
                        topicConfigWrapper.getDataVersion(),
                        channel,
                        haServerAddr));
                if (null == prevBrokerLiveInfo) {
                    log.info("new broker registered, {} HAServer: {}", brokerAddr, haServerAddr);
                }

                if (filterServerList != null) {
                    if (filterServerList.isEmpty()) {
                        this.filterServerTable.remove(brokerAddr);
                    } else {
                        this.filterServerTable.put(brokerAddr, filterServerList);
                    }
                }
                if (MixAll.MASTER_ID != brokerId) {
                    String masterAddr = brokerData.getBrokerAddrs().get(MixAll.MASTER_ID);
                    if (masterAddr != null) {
                        BrokerLiveInfo brokerLiveInfo = this.brokerLiveTable.get(masterAddr);
                        if (brokerLiveInfo != null) {
                            result.setHaServerAddr(brokerLiveInfo.getHaServerAddr());
                            result.setMasterAddr(masterAddr);
                        }
                    }
                }
            } finally {
                this.lock.writeLock().unlock();
            }
        } catch (Exception e) {
            log.error("registerBroker Exception", e);
        }

        return result;
    }

NameServer路由删除

前面说过,NameServer会每隔10s扫描心跳包,如果超过120s就会将broker给他剔除。

NameServer启动时会启动一个线程池,没隔10s执行一次。

this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
      @Override
      public void run() {
      NamesrvController.this.routeInfoManager.scanNotActiveBroker();
      }
}, 5, 10, TimeUnit.SECONDS);

再来看看scanNotActiveBroker()方法:

遍历brokerLiveTable,如果最后更新时间超过两分钟就给他移除了从brokerLiveTable。

/**
private final static long BROKER_CHANNEL_EXPIRED_TIME = 1000 * 60 * 2;
*/
public void scanNotActiveBroker() {
        Iterator<Entry<String, BrokerLiveInfo>> it = this.brokerLiveTable.entrySet().iterator();
        while (it.hasNext()) {
            Entry<String, BrokerLiveInfo> next = it.next();
            long last = next.getValue().getLastUpdateTimestamp();
            if ((last + BROKER_CHANNEL_EXPIRED_TIME) < System.currentTimeMillis()) {
                RemotingUtil.closeChannel(next.getValue().getChannel());
                it.remove();
                log.warn("The broker channel expired, {} {}ms", next.getKey(), BROKER_CHANNEL_EXPIRED_TIME);
                this.onChannelDestroy(next.getKey(), next.getValue().getChannel());
            }
        }
    }

然后是onChannelDestroy方法,这个方法就不细讲了,大致意思就是更新该broker的其他信息,将它的队列信息什么的给他删除干净。

NameServer理由发现

当topic信息发生变化后,是靠客户端自己主动拉取主题的最新信息,NameServer对的处理方法是:

org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor#getRouteInfoByTopic

 

//根据主题获取对应的路由信息,然后判断是否为顺序消息
public RemotingCommand getRouteInfoByTopic(ChannelHandlerContext ctx,
        RemotingCommand request) throws RemotingCommandException {
        final RemotingCommand response = RemotingCommand.createResponseCommand(null);
        final GetRouteInfoRequestHeader requestHeader =
            (GetRouteInfoRequestHeader) request.decodeCommandCustomHeader(GetRouteInfoRequestHeader.class);
			//获取主题路由信息
        TopicRouteData topicRouteData = this.namesrvController.getRouteInfoManager().pickupTopicRouteData(requestHeader.getTopic());

        if (topicRouteData != null) {
            if (this.namesrvController.getNamesrvConfig().isOrderMessageEnable()) {
                String orderTopicConf =
                    this.namesrvController.getKvConfigManager().getKVConfig(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG,
                        requestHeader.getTopic());
                topicRouteData.setOrderTopicConf(orderTopicConf);
            }

            byte[] content = topicRouteData.encode();
            response.setBody(content);
            response.setCode(ResponseCode.SUCCESS);
            response.setRemark(null);
            return response;
        }

        response.setCode(ResponseCode.TOPIC_NOT_EXIST);
        response.setRemark("No topic route info in name server for the topic: " + requestHeader.getTopic()
            + FAQUrl.suggestTodo(FAQUrl.APPLY_TOPIC_URL));
        return response;
    }

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

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

相关文章

软件工程 | 期末复习

一、软件与软件危机 1、软件发展经历三个阶段&#xff1a;程序设计、程序系统、软件工程 2、软件的概念&#xff1a;软件是计算机系统与硬件相互依存的另一部分&#xff0c;包括程序、数据以及相关文档的完整集合&#xff0c;软件程序数据文档 数据&#xff1a;使程序能够适…

测试人员转型是大势所趋:我的十年经验告诉我,你必须要行动起来了。

做测试十多年&#xff0c;有不少人问过我下面问题&#xff1a; 现在的手工测试真的不行了吗&#xff1f; 测试工程师&#xff0c;三年多快四年的经验&#xff0c;入门自动化测试需要多久&#xff1f; 自学自动化测试到底需要学哪些东西&#xff1f; 不得不说&#xff0c;随着行…

学习open62541 --- [76] 使用智能指针处理内存释放问题

在使用监测项时&#xff0c;一般都会加一个context&#xff0c;然后在回调函数里使用这个context&#xff0c;这就需要保证context的内存空间在执行回调函数时是有效的。往往有以下三种方法&#xff1a; 使用静态内存空间&#xff1a;使用static创建静态变量&#xff0c;然后把…

【Python 爬虫常见的报错及其解决方法】零基础也能轻松掌握的学习路线与参考资料

Python 爬虫被广泛应用于数据采集和分析。然而&#xff0c;爬虫在运行过程中常常会遇到各种问题和错误&#xff0c;降低了爬虫效率、准确性和可靠性。因此掌握爬虫常见报错及其解决方法是非常关键的。本文将介绍 Python 爬虫常见的报错及其解决方法&#xff0c;并提供参考资料和…

内存泄漏的原因,内存泄漏如何避免?内存泄漏如何定位?

1. 内存溢出 内存溢出 OOM &#xff08;out of memory&#xff09;&#xff0c;是指程序在申请内存时&#xff0c;没有足够的内存空间供其使用&#xff0c;出现out of memory&#xff1b;比如申请了一个int,但给它存了long才能存下的数&#xff0c;那就是内存溢出。 2. 内存泄…

PyTorch LSTM和LSTMP的原理及其手写复现

PyTorch LSTM和LSTMP的原理及其手写复现 0、前言全部参数的细致介绍代码实现Reference 0、前言 关于LSTM的原理以及公式其实在这篇博客一步一步详解LSTM网络【从RNN到LSTM到GRU等&#xff0c;直至attention】讲的非常清晰明了了。 这里就是写出LSTM的pytorch的实现&#xff0c;…

【随笔记】全志 T507 PF4 引脚无法被正常设置为中断模式的问题分析

相关信息 硬件平台&#xff1a;全志T507 系统版本&#xff1a;Android 10 / Linux 4.9.170 问题描述&#xff1a;PF4 无法通过标准接口设置为中断模式&#xff0c;PF1、PF2、PF3、PF5 都可以。 分析过程 一开始以为是引脚被其它驱动占用引起&#xff0c;或者该引脚不具备中断…

高光谱成像技术在果蔬品质检测中的应用

在当前市场经济背景下&#xff0c;食品安全问题是消费者最为关心的问题之一&#xff0c;尤其是果蔬产品&#xff0c;农药残留问题和品质问题直接关系着消费者的权益和人身安全。针对传统化学检测的缺陷&#xff0c;本文结合高光谱成像技术&#xff0c;对其在果蔬品质与安全无损…

【C++】多态的概念/重写/虚表/抽象类

多态 多态的概念多态的定义和实现重写抽象类多态的原理虚表的构建原理虚函数的调用原理 多态的概念 多态就是多种形态&#xff0c;传递不同的对象&#xff0c;会调用不同的方法。 多态的定义和实现 那么在C语法中&#xff0c;多态是如何实现的呢&#xff1f; 我们首先要在继承…

vue学习 - 基础篇

初始工程结构 这里我们使用script标签从cdn获取vue.js, 而不是使用脚手架vue-cli, 因为cdn比较方便一点, 也不用配置node之类的比较麻烦 index.html <!DOCTYPE html> <html><head><title>VueJS Course</title><link rel"stylesheet"…

第三篇、基于Arduino uno,用oled0.96寸屏幕显示dht11温湿度传感器的温度和湿度信息——结果导向

0、结果 说明&#xff1a;先来看看拍摄的显示结果&#xff0c;如果是你想要的&#xff0c;可以接着往下看。 1、外观 说明&#xff1a;本次使用的oled是0.96寸的&#xff0c;别的规格的屏幕不一定适用本教程&#xff0c;一般而言有显示白色、蓝色和蓝黄一起显示的&#xff0…

RabbitMQ日常使用小结

一、使用场景 削峰、解耦、异步。 基于AMQP(高级消息队列协议)协议来统一数据交互,通过channel(网络信道)传递信息。erlang语言开发&#xff0c;并发量12000&#xff0c;支持持久化&#xff0c;稳定性好&#xff0c;集群不支持动态扩展。 RabbitMQ的基本概念 二、组成及工作流…

可见性原子性有序性的+线程传参的方式+Java如何实现多个线程之间共享数据+线程间通信+死锁产生

//为了均衡CPU和内存的速度差异,增加了缓存 导致了可见性的问题; //操作系统增加了进程 线程 分时复用CPU,均衡CPU和io设备的速速差异 导致了原子性问题; //jvm指令重排序(优化指令排序) 导致了有序性的问题 可见性问题是指 线程A修改共享变量,修改后CPU缓存中的数据没有及时同…

Emacs之目前最快补全插件lsp-bridge(八十八)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

数据分析12——Pandas中数据合并方法

0、前言&#xff1a; 在pandas中进行数据合并的操作和数据库中的join操作非常类似。 1、merge横向合并&#xff1a; 前言&#xff1a;该函数只能做横向合并函数名&#xff1a;merge()函数参数&#xff1a; left: 数据类型为’DataFrame | Series’&#xff0c;需要进行合并的…

[CTF/网络安全] 攻防世界 PHP2 解题详析

[CTF/网络安全] 攻防世界 PHP2 解题详析 index.php.phps扩展名姿势 翻译&#xff1a;你能给这个网站进行身份验证吗&#xff1f; index.php index.php是一个常见的文件名&#xff0c;通常用于Web服务器中的网站根目录下。它是默认的主页文件名&#xff0c;在访问一个网站时&am…

说说计算这事儿:从开关到人工智能

目录 一 前言 二 计算历史 三 计算探秘 四 算力优化 五 未来展望 一 前言 计算本身其实是一个比较抽象的词&#xff0c;或者说比较笼统。很多场景都可能用到计算这个词&#xff0c;因此具体的含义就需要根据上下文来确定。今天我们讨论的计算&#xff0c;是比较狭义的计算…

【环境准备】在虚拟机的Ubuntu下安装VS Code并配置C/C++运行环境

1.点击进入 vscode官网 下载.deb安装包 2.启动虚拟机下的Ubuntu&#xff0c;Windows下的Xftp和Xshell Xftp&#xff1a;用于将刚刚在Windows下下载好的vscode.deb安装包传输到Ununtu中。Xshell&#xff1a;用于远程登录Ununtu&#xff0c;进行 vscode.deb 安装包安装&#xff…

算法26:递归练习

目录 题目1&#xff1a;给你一个字符串&#xff0c;要求打印打印出这个字符串的全部子序列&#xff08;子序列不能重复&#xff09; 题目2&#xff1a;打印一个字符串的全部排列。 题目3&#xff1a;针对题目2&#xff0c;要求去除重复元素 题目4&#xff1a;给定一个字符串…

ARM的读写内存指令与栈的应用

1.基础读写指令 写内存指令&#xff1a;STR MOV R1, #0xFF000000 MOV R2, #0x40000000 STR R1, [R2] 将R1寄存器中的数据写入到R2指向的内存空间 需注意&#xff0c;此命令是将R1中的数据写给R2所指向的内存空间&#xff0c;而不是直接把R1的数据赋给R2&#xff0c;R2寄存器…