Nacos 客户端服务注册源码分析-篇三

news2024/9/27 15:35:10

Nacos 客户端服务注册源码分析-篇三

版本说明:

源码版本 nacos-1.4.2

image-20230406165130271

Nacos 的核心功能点

服务注册: Nacos Client 会通过发送 REST 请求的方式向 Nacos Server 注册自己的服务,提供自身的元数据,比如 ip 地址以及端口等信息。Nacos Server 在接受到注册请求后,就会把元数据信息存储在一个双层的内存 Map 当中

服务心跳:在服务注册后,Nacos Client会维护一个定时心跳来持续通知Nacos Server,说明服务一直处于可用状态,防止被剔除。默认5s发送一次心跳。

服务健康检查:Nacos Server会开启一个定时任务用来检查注册服务实例的健康情况,对于超过15s没有收到客户端心跳的实例会将它的healthy属性置为false(客户端服务发现时不会发现),如果某个实例超过30秒没有收到心跳,直接剔除该实例(被剔除的实例如果恢复发送心跳则会重新注册)

服务发现:服务消费者(Nacos Client)在调用服务提供者的服务时,会发送一个REST请求给Nacos Server,获取上面注册的服务清单,并且缓存在Nacos Client本地,同时会在Nacos Client本地开启一个定时任务定时拉取服务端最新的注册表信息更新到本地缓存

服务同步:Nacos Server集群之间会互相同步服务实例,用来保证服务信息的一致性。

Nacos 服务端原理

image-20230408123438352

nacos 客户端原理

20190703005828565

源码分析

服务注册信息

我们在使用 nacos 的时候,其实只是在我们的项目当中引入了一个 nacos 的客户端的依赖,我们在使用的时候直接配置注入即可,然后通过 nacos 的 PC 客户端进行登录就可看到具体的实例信息,但这中间发生了什么我们是不知道的。

既然研究源码我们就从流程分析,一步一步的按照我们实际的操作去剖析,所以我们就从最基础的客户端的注册开始。

在这个目录包 package com.alibaba.nacos.client 下 nacos 提供了一套客户端注册的测试类 NamingTest 我们就从该类开始

nacos 的连接信息
//nacos连接信息
Properties properties = new Properties();
properties.put(PropertyKeyConst.SERVER_ADDR, "127.0.0.1:8848"); //服务器地址
properties.put(PropertyKeyConst.USERNAME, "nacos"); //主机名
properties.put(PropertyKeyConst.PASSWORD, "nacos"); //主机密码
  • Service地址:Nacos 服务器的地址,属性 key 为了 SERVER_ADDR = “serverAddr”
  • 用户名:连接 Nacos 服务的用户名,属性 key 为 USERNAME = “username”
  • 密码:连接 Nacos 服务的密码,属性 key 为 PASSWORD = “password”
实例信息
//注册实例信息
Instance instance = new Instance();
instance.setIp("1.1.1.1");
instance.setPort(800);
instance.setWeight(2);
Map<String, String> map = new HashMap<String, String>();
map.put("netType", "external");
map.put("version", "2.0");
instance.setMetadata(map);

注册的实例信息用 Instance 去作为基础的承载的,在 Instance 对象中可以到由两部分组成,分别是实例信息(user extended attributes.)和元数据(add meta data.

基础实例信息 - user extended attributes

private String instanceId; //unique id of this instance.实例的唯一ID;

private String ip; //instance ip.实例IP,提供给消费者进行通信的地址;

private int port; //instance port.端口,提供给消费者访问的端口;

private double weight = 1.0D; //instance weight.权重,当前实例的权限,浮点类型(默认1.0D);

private boolean healthy = true; //instance health status.健康状况,默认true;

private boolean enabled = true; //If instance is enabled to accept request.实例是否准备好接收请求,默认true;

private boolean ephemeral = true; //If instance is ephemeral.实例是否为瞬时的,默认为true;

private String clusterName; //cluster information of instance.实例所属的集群名称;

private String serviceName; //Service information of instance.实例的服务信息;

存储元数据 - add meta data

private Map<String, String> metadata = new HashMap<String, String>();

public Map<String, String> getMetadata() {
    return this.metadata;
}

public void setMetadata(final Map<String, String> metadata) {
    this.metadata = metadata;
}

/**
 * add meta data.
 *
 * @param key   meta data key
 * @param value meta data value
 */
public void addMetadata(final String key, final String value) {
    if (metadata == null) {
        metadata = new HashMap<String, String>(4);
    }
    metadata.put(key, value);
}
    

可以看到的 meta data 元数据是通过一个 Map 即可的 key-value 键值对进行维护的。

关于这两个键值具体是什么我们可以在 NamingTest 的该 Dome 中找到具体指向。

Map<String, String> map = new HashMap<String, String>();
map.put("netType", "external");
map.put("version", "2.0");
  • netType:顾名思义,网络类型,这里的值为external,也就是外网的意思;
  • version:版本,Nacos的版本,这里是2.0这个大版本。

当然在 Instance 对象当中对上述的基本信息都提供了具体的 get 方法例如:

//获取实例的心跳间隔
public long getInstanceHeartBeatInterval() {
    return getMetaDataByKeyWithDefault(PreservedMetadataKeys.HEART_BEAT_INTERVAL,
            Constants.DEFAULT_HEART_BEAT_INTERVAL);
}
//获取实例的心跳超时
public long getInstanceHeartBeatTimeOut() {
    return getMetaDataByKeyWithDefault(PreservedMetadataKeys.HEART_BEAT_TIMEOUT,
            Constants.DEFAULT_HEART_BEAT_TIMEOUT);
}
//获取 ip 的删除超时
public long getIpDeleteTimeout() {
    return getMetaDataByKeyWithDefault(PreservedMetadataKeys.IP_DELETE_TIMEOUT,
            Constants.DEFAULT_IP_DELETE_TIMEOUT);
}
//获取实例 ID 生成器
public String getInstanceIdGenerator() {
    return getMetaDataByKeyWithDefault(PreservedMetadataKeys.INSTANCE_ID_GENERATOR,
            Constants.DEFAULT_INSTANCE_ID_GENERATOR);
}

上面的get方法在需要元数据默认值时会被用到:

  • preserved.heart.beat.interval:心跳间隙的key,默认为5s,也就是默认5秒进行一次心跳;
  • preserved.heart.beat.timeout:心跳超时的key,默认为15s,也就是默认15秒收不到心跳,实例将会标记为不健康;
  • preserved.ip.delete.timeout:实例IP被删除的key,默认为30s,也就是30秒收不到心跳,实例将会被移除;
  • preserved.instance.id.generator:实例ID生成器key,默认为simple;

我们可以 getInstanceHeartBeatInterval 该方法为例看一下整个过程

  1. 当我们执行 getInstanceHeartBeatInterval() 方法的时候会默认的调用该方法下的 getMetaDataByKeyWithDefault(final String key**,** final long defaultValue) 方法

    private long getMetaDataByKeyWithDefault(final String key, final long defaultValue) {
        //返回为默认的元数据值 metadata
        if (getMetadata() == null || getMetadata().isEmpty()) {
            return defaultValue;
        }
        //根据具体的 key 获取预设的 matedata
        final String value = getMetadata().get(key);
        if (!StringUtils.isEmpty(value) && value.matches(NUMBER_PATTERN)) {
            return Long.parseLong(value);
        }
        return defaultValue;
    }
    
  2. 如果当前对象元数据 metadata 为空则返回默认值,以心跳检测为例,默认值为 DEFAULT_HEART_BEAT_INTERVAL = TimeUnit.SECONDS.toMillis(5) 也就是 5 秒,如果说不为空则会通过具体的 key (心跳间隙、心跳超时、实例 IP 被删除、实例 ID 生成器)等进行获取预设值

NamingService接口

NamingService namingService = NacosFactory.createNamingService(properties);
namingService.registerInstance("nacos.test.1", instance);

其实 namingService 是通过工厂类 NacosFactory 下的 createNamingService 创建的

/**
 * Create naming service.
 *
 * @param properties init param
 * @return Naming
 * @throws NacosException Exception
 */
public static NamingService createNamingService(Properties properties) throws NacosException {
    return NamingFactory.createNamingService(properties);
}

这里通过反射获取 com.alibaba.nacos.client.naming.NacosNamingService 包下的实体对象

/**
 * Create a new naming service.
 *
 * @param properties naming service properties
 * @return new naming service
 * @throws NacosException nacos exception
 */
public static NamingService createNamingService(Properties properties) throws NacosException {
    try {
        Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.naming.NacosNamingService");
        Constructor constructor = driverImplClass.getConstructor(Properties.class);
        NamingService vendorImpl = (NamingService) constructor.newInstance(properties);
        return vendorImpl;
    } catch (Throwable e) {
        throw new NacosException(NacosException.CLIENT_INVALID_PARAM, e);
    }
}

可以看到的 NamingService 接口提供了大量的接口

image-20230411202432897

例如以下几个重要的接口:

/**
 * register a instance to service. 将实例注册到服务
 *
 * @param serviceName name of service
 * @param ip          instance ip
 * @param port        instance port
 * @throws NacosException nacos exception
 */
void registerInstance(String serviceName, String ip, int port) throws NacosException;

/**
 * deregister instance from a service. 从服务取消一个注册实例
 *
 * @param serviceName name of service
 * @param ip          instance ip
 * @param port        instance port
 * @throws NacosException nacos exception
 */
void deregisterInstance(String serviceName, String ip, int port) throws NacosException;

/**
 * get all instances of a service. 获取服务的所有实例
 *
 * @param serviceName name of service
 * @return A list of instance
 * @throws NacosException nacos exception
 */
List<Instance> getAllInstances(String serviceName) throws NacosException;

/**
 * Get qualified instances of service. 获取合格的服务实例
 *
 * @param serviceName name of service.
 * @param healthy     a flag to indicate returning healthy or unhealthy instances
 * @return A qualified list of instance
 * @throws NacosException nacos exception
 */
List<Instance> selectInstances(String serviceName, boolean healthy) throws NacosException;

/**
 * Select one healthy instance of service using predefined load balance strategy.
 * 使用预定义的负载平衡策略选择一个正常运行的服务实例。
 * @param serviceName name of service
 * @return qualified instance
 * @throws NacosException nacos exception
 */
Instance selectOneHealthyInstance(String serviceName) throws NacosException;

/**
 * Subscribe service to receive events of instances alteration.
 * 订阅服务以接收实例变更事件。
 * @param serviceName name of service
 * @param listener    event listener
 * @throws NacosException nacos exception
 */
void subscribe(String serviceName, EventListener listener) throws NacosException;

/**
 * Unsubscribe event listener of service.
 * 取消订阅服务的事件侦听器。
 * @param serviceName name of service
 * @param listener    event listener
 * @throws NacosException nacos exception
 */
void unsubscribe(String serviceName, EventListener listener) throws NacosException;

/**
 * Get all service names from server.
 * 从服务器获取所有服务名称。
 * @param pageNo   page index
 * @param pageSize page size
 * @return list of service names
 * @throws NacosException nacos exception
 */
ListView<String> getServicesOfServer(int pageNo, int pageSize) throws NacosException;

/**
 * Shutdown the resource service.
 * 关闭资源服务。
 * @throws NacosException exception.
 */
void shutDown() throws NacosException;

可以看到在 NamingService 当中其实提供了大量的功能性的接口,我们可以在不用的场景去选择适配的功能进行调用。

既然说到这里了,不能只说一半,我们就实例服务注册方法 registerInstance 进一步,探究一下其具体的调用叭!

  1. //用户调用服务进行注册
    NamingService namingService = NacosFactory.createNamingService(properties);
    namingService.registerInstance("nacos.test.1", instance);
    
  2. //被调用的方法
    void registerInstance(String serviceName, String ip, int port) throws NacosException;
    
  3. public void registerInstance(String serviceName, String ip, int port) throws NacosException {
        //这里进行默认的初始化
        registerInstance(serviceName, ip, port, Constants.DEFAULT_CLUSTER_NAME);
    }
    
  4. public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
        //心跳检测-检查有关保持活动状态的实例参数。
        NamingUtils.checkInstanceIsLegal(instance);
        //获取分组名
        String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
        if (instance.isEphemeral()) {
            BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
            beatReactor.addBeatInfo(groupedServiceName, beatInfo);
        }
        //注册服务
        serverProxy.registerService(groupedServiceName, groupName, instance);
    }
    
    1. /**
       * <p>Check instance param about keep alive.</p>
       *
       * <pre>
       * heart beat timeout must > heart beat interval
       * ip delete timeout must  > heart beat interval
       * </pre>
       *
       * @param instance need checked instance
       * @throws NacosException if check failed, throw exception
       */
      public static void checkInstanceIsLegal(Instance instance) throws NacosException {
          if (instance.getInstanceHeartBeatTimeOut() < instance.getInstanceHeartBeatInterval()
                  || instance.getIpDeleteTimeout() < instance.getInstanceHeartBeatInterval()) {
              //实例“心跳间隔”必须小于“心跳超时”和“IP 删除超时”。
              throw new NacosException(NacosException.INVALID_PARAM,
                      "Instance 'heart beat interval' must less than 'heart beat timeout' and 'ip delete timeout'.");
          }
      }
      
    2. /**
       * Returns a combined string with serviceName and groupName. serviceName can not be nil.
       *
       * <p>In most cases, serviceName can not be nil. In other cases, for search or anything, See {@link
       * com.alibaba.nacos.api.naming.utils.NamingUtils#getGroupedNameOptional(String, String)}
       *
       * <p>etc:
       * <p>serviceName | groupName | result</p>
       * <p>serviceA    | groupA    | groupA@@serviceA</p>
       * <p>nil         | groupA    | threw IllegalArgumentException</p>
       *
       * @return 'groupName@@serviceName'
       */
      public static String getGroupedName(final String serviceName, final String groupName) {
          if (StringUtils.isBlank(serviceName)) {
              //参数“服务名称”是非法的,服务名称为空
              throw new IllegalArgumentException("Param 'serviceName' is illegal, serviceName is blank");
          }
          if (StringUtils.isBlank(groupName)) {
              //参数“组名称”是非法的,组名称为空
              throw new IllegalArgumentException("Param 'groupName' is illegal, groupName is blank");
          }
          final String resultGroupedName = groupName + Constants.SERVICE_INFO_SPLITER + serviceName;
          return resultGroupedName.intern();
      }
      
    3. package com.alibaba.nacos.client.naming.net;
      /**
       * register a instance to service with specified instance properties.
       * 使用指定的实例属性将实例注册到服务。
       * @param serviceName name of service
       * @param groupName   group of service
       * @param instance    instance to register
       * @throws NacosException nacos exception
       */
      //代理类 NamingProxy 下的注册服务方法
      public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
          
          NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance: {}", namespaceId, serviceName,
                  instance);
          
          final Map<String, String> params = new HashMap<String, String>(16);
          params.put(CommonParams.NAMESPACE_ID, namespaceId);
          params.put(CommonParams.SERVICE_NAME, serviceName);
          params.put(CommonParams.GROUP_NAME, groupName);
          params.put(CommonParams.CLUSTER_NAME, instance.getClusterName());
          params.put("ip", instance.getIp());
          params.put("port", String.valueOf(instance.getPort()));
          params.put("weight", String.valueOf(instance.getWeight()));
          params.put("enable", String.valueOf(instance.isEnabled()));
          params.put("healthy", String.valueOf(instance.isHealthy()));
          params.put("ephemeral", String.valueOf(instance.isEphemeral()));
          params.put("metadata", JacksonUtils.toJson(instance.getMetadata()));
          
          reqApi(UtilAndComs.nacosUrlInstance, params, HttpMethod.POST);
          
      }
      
    4. public String reqApi(String api, Map<String, String> params, String method) throws NacosException {
              return reqApi(api, params, Collections.EMPTY_MAP, method);
      }
      
    5. public String reqApi(String api, Map<String, String> params, Map<String, String> body, String method)
              throws NacosException {
          return reqApi(api, params, body, getServerList(), method);
      }
      
    6. /**
       * Request api.
       *
       * @param api     api
       * @param params  parameters
       * @param body    body
       * @param servers servers
       * @param method  http method
       * @return result
       * @throws NacosException nacos exception
       */
      public String reqApi(String api, Map<String, String> params, Map<String, String> body, List<String> servers,
              String method) throws NacosException {
          
          params.put(CommonParams.NAMESPACE_ID, getNamespaceId());
          
          if (CollectionUtils.isEmpty(servers) && StringUtils.isBlank(nacosDomain)) {
              throw new NacosException(NacosException.INVALID_PARAM, "no server available");
          }
          
          NacosException exception = new NacosException();
          
          if (StringUtils.isNotBlank(nacosDomain)) {
              for (int i = 0; i < maxRetry; i++) {
                  try {
                      return callServer(api, params, body, nacosDomain, method);
                  } catch (NacosException e) {
                      exception = e;
                      if (NAMING_LOGGER.isDebugEnabled()) {
                          NAMING_LOGGER.debug("request {} failed.", nacosDomain, e);
                      }
                  }
              }
          } else {
              Random random = new Random(System.currentTimeMillis());
              int index = random.nextInt(servers.size());
              
              for (int i = 0; i < servers.size(); i++) {
                  String server = servers.get(index);
                  try {
                      return callServer(api, params, body, server, method);
                  } catch (NacosException e) {
                      exception = e;
                      if (NAMING_LOGGER.isDebugEnabled()) {
                          NAMING_LOGGER.debug("request {} failed.", server, e);
                      }
                  }
                  index = (index + 1) % servers.size();
              }
          }
          
          NAMING_LOGGER.error("request: {} failed, servers: {}, code: {}, msg: {}", api, servers, exception.getErrCode(),
                  exception.getErrMsg());
          
          throw new NacosException(exception.getErrCode(),
                  "failed to req API:" + api + " after all servers(" + servers + ") tried: " + exception.getMessage());
      

NacosNamingService的实现

前面提到过在 NacosService 中是通过 registerInstance 方法进行服务层的注册的,而对于该方法只需要提过两个参数服务名称 serviceName 与实例信息对象 instance

@Override
public void registerInstance(String serviceName, Instance instance) throws NacosException {
    registerInstance(serviceName, Constants.DEFAULT_GROUP, instance);
}

对于这两个参数其实最大的作用就是设置了当前实例的分组信息。在 Nacos 中是通过Namespace、group、Service、Cluster 进行一级一级的隔离的,在不指定的前提下一般都是默认调用 String DEFAULT_GROUP = “DEFAULT_GROUP” 来实现的。

image-20230411214042881

而在 registerInstance 方法中有重要的两个方法

  • 检查心跳时间设置的对不对(心跳默认为5秒)
  • 通过 NamingClientProxy 这个代理来执行服务注册操作
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
    //检查有关保持活动状态的实例参数。
    NamingUtils.checkInstanceIsLegal(instance);
    String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
    if (instance.isEphemeral()) {
        BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
        beatReactor.addBeatInfo(groupedServiceName, beatInfo);
    }
    //接口代理
    serverProxy.registerService(groupedServiceName, groupName, instance);
}

其实在第一步通过反射获取 com.alibaba.nacos.client.naming.NacosNamingService 包,进行创建 NamingService 的实现类 NacosNamingService 的时候就已经通过默认的构造方法完成了初始化。

//反射获取对象
NamingService namingService = NacosFactory.createNamingService(properties);

/**
 * Create a new naming service.
 *
 * @param properties naming service properties
 * @return new naming service
 * @throws NacosException nacos exception
 */
public static NamingService createNamingService(Properties properties) throws NacosException {
    try {
        Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.naming.NacosNamingService");
        //调用 Properties 参数的构造方法
        Constructor constructor = driverImplClass.getConstructor(Properties.class);
        NamingService vendorImpl = (NamingService) constructor.newInstance(properties);
        return vendorImpl;
    } catch (Throwable e) {
        throw new NacosException(NacosException.CLIENT_INVALID_PARAM, e);
    }
}

public NacosNamingService(Properties properties) throws NacosException {
    //在默认的 NacosNamingService(Properties properties) 构造方法中调用初始化方法 init 
    init(properties);
}

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

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

相关文章

Sentinal持久化到Nacos

Springboot应用整合Sentinel实现限流、熔断、降级笔记https://blog.csdn.net/chenjian723122704/article/details/130101875 Sentinel版本 1.8.6 Nacos版本 2.2.0 下载Sentinel源码 Sentinel1.8.6&#xff1a;https://github.com/alibaba/Sentinel/releases/tag/1.8.6 拷贝源…

AtCoder Beginner Contest 295——F - substr = S

蒟蒻来讲题&#xff0c;还望大家喜。若哪有问题&#xff0c;大家尽可提&#xff01; Hello, 大家好哇&#xff01;本初中生蒟蒻讲解一下AtCoder Beginner Contest 295这场比赛的F题&#xff01; F - substr S 原题 Problem Statement You are given a string SSS consisti…

双塔模型:微软DSSM模型浅析

1.背景 DSSM是Deep Structured Semantic Model (深层结构语义模型) 的缩写&#xff0c;即我们通常说的基于深度网络的语义模型&#xff0c;其核心思想是将query和doc映射到到共同维度的语义空间中&#xff0c;通过最大化query和doc语义向量之间的余弦相似度&#xff0c;从而训…

2023好玩的解压游戏,压力大点开玩可以放松自己

你是不是经常感觉到压力大&#xff1f; 现代社会&#xff0c;竞争逐步激烈&#xff0c;不管是来自学习上&#xff0c;工作上&#xff0c;还是生活上的&#xff0c;压力都非常大&#xff01; 这时候&#xff0c;我们要学会自我减压&#xff0c;有效的放松是为了更好地前行。 …

JavaWeb开发 —— MyBatis基本操作

目录 一、环境准备 二、删除操作实现 1. 根据主键删除 2. 删除&#xff08;预编译SQL&#xff09; 2.1 SQL注入 2.2 参数占位符 三、新增操作实现 1. 新增代码实现 2. 新增&#xff08;主键返回&#xff09; 四、更新操作实现 五、查询操作实现 1. 根据ID查询 1.1…

【Python】pip 和 conda install、list的区别,是否一致

【Python】pip 和 conda install、list的区别&#xff0c;是否一致 文章目录【Python】pip 和 conda install、list的区别&#xff0c;是否一致1. 介绍2. 看效果2.1 首先&#xff0c;conda 创建环境2.2 然后&#xff0c;激活环境2.3 查看环境下已经安装包列表2.4 安装新的包&am…

【2023最新】超详细图文保姆级教程:App开发新手入门(2)

上章节我们已经成功的创建了一个 App 项目&#xff0c;接下来我们讲述一下&#xff0c;如何导入项目、编辑代码和提交项目代码。 Let’s Go! 4. 项目导入 当用户创建一个新的应用时&#xff0c;YonStudio 开发工具会自动导入模板项目的默认代码&#xff0c;不需要手动进行代…

C语言的Hello World的汇编剖析(64位 Intel架构)

C语言的Hello World的汇编剖析&#xff08;64位 Intel架构&#xff09; 文章目录C语言的Hello World的汇编剖析&#xff08;64位 Intel架构&#xff09;一. 前提准备二. C转换为汇编操作准备2.1 创建目录&复制代码2.2 C文件转换为汇编文件三. 剖析汇编文件四. 指令相关五. …

TenserRT(三)PYTORCH 转 ONNX 详解

第三章&#xff1a;PyTorch 转 ONNX 详解 — mmdeploy 0.12.0 文档 torch.onnx — PyTorch 2.0 documentation torch.onnx.export 细解 计算图导出方法 TorchScript是一种序列化和优化PyTorch模型的格式&#xff0c;将torch.nn.Module模型转换为TorchScript的torch.jit.Scr…

ERTEC200P-2 PROFINET设备完全开发手册(6-1)

6 报警和诊断 Profinet提供了强大的诊断功能&#xff0c;这是其他通讯协议所无法比拟的。PN设备检测到问题后可以向控制器发送报警信息&#xff0c;报警分为三大类&#xff1a; 诊断报警 &#xff08;PN设备本身故障触发的报警&#xff0c;例如&#xff1a;温度测量通道变送电…

Activiti学习02

这里写目录标题一、流对象简介1.1 事件1.2 活动1.3 条件二、Activiti系统服务结构图核心类:服务类:RepositoryServiceRuntimeServiceTaskServiceHistoryServiceFormServiceIdentityServiceManagementService三、Activiti数据库支持一、流对象简介 一个业务流程图有三个流对象的…

ATFX国际:中国一季度GDP同比增长4.5%,社消总额约11.5万亿元

ATFX国际&#xff1a;中国统计局发布一季度国民经济运行报告&#xff0c;其中值得关注两大数据分别为GDP同比增速、社会消费品零售总额增速。统计显示&#xff0c;一季度GDP总额28.5万亿元&#xff0c;同比增长4.5%&#xff0c;其中第一产业和第二产业的增速低于平均值&#xf…

Pyqt案例讲解(实现模拟计算器效果)

PyQt5是一个用于Python的GUI框架&#xff0c;它提供了一个简单易用的GUI工具包&#xff0c;可以用于创建各种类型的应用程序&#xff0c;包括计算器。下面是一个简单的计算器的实现&#xff0c;其中包括了一些难点和复杂的地方。 难点&#xff1a; 使用Qt的布局管理器来创建窗…

证书扫描件怎么弄?手机也能轻松扫描

现代社会中&#xff0c;证书是人们展示自己能力和经历的重要凭证。然而&#xff0c;我们有时需要将证书扫描并保存在电脑或手机中&#xff0c;以备不时之需。本文将介绍如何扫描证书以及手机上是否能进行扫描。 证书扫描的方法 将证书扫描成电子文档可以方便地将其存储在电脑或…

C++ Primer 第7章 类 - 中(零基础学习C++,精简学习笔记)

&#x1f916; 作者简介&#xff1a;努力的clz &#xff0c;一个努力编程的菜鸟 &#x1f423;&#x1f424;&#x1f425; &#x1f440; 文章专栏&#xff1a;C Primer 学习笔记 &#x1f4d4;专栏简介&#xff1a; 本专栏是博主学习 C Primer 的学习笔记&#xff0c;因为…

技巧:WIN10手动指定某个应用程序使用独立显卡

目录1. 背景2. 解决方法&#xff0c;假如要让剪映始终使用独立显卡2.1 步骤1&#xff0c;右击电脑桌面空白处&#xff0c;选择“显示设置”2.2 步骤2&#xff0c;拉到最下面&#xff0c;点击图形设置2.3 步骤3&#xff0c;选择桌面应用&#xff0c;点击浏览2.4 步骤4&#xff0…

领课在线教育系统源码 各行业都适用的分布式在线教育系统+支持讲师入驻功能

领课教育系统&#xff08;roncoo-education&#xff09;是基于领课网络多年的在线教育平台开发和运营经验打造出来的产品&#xff0c;致力于打造一个各行业都适用的分布式在线教育系统。系统采用前后端分离模式&#xff0c;前台采用vue.js为核心框架&#xff0c;后台采用Spring…

bash shell 无法使用 perl 正则

1.案例现象 前几天有一个小伙伴在群里求助&#xff0c;说他这个 shell 脚本有问题&#xff0c;让大家帮忙看看 #!/bin/bash regularExpression"^\[(\d)\].$" contentcat $1 for i in ${content} doif [[ $i ~ $regularExpression ]]thenecho -e "\033[32m 【 i…

一款多参数多合一的空气质量传感器【温湿度、TVOC甲醛CO2粉尘等】

档案馆库房专用的一款智能型空气质量云测仪 空气质量检测仪 空气质量传感器 环境集成传感器 集成/温湿度、粉尘PM2.5 PM10/甲醛/TVOC/CO2等高度集成的一款传感器/RS485信号输出 ◆温度测量参数: (1)温度测量范围: -40~80℃(2&#xff09;输出分辨率:0.1oC (3&#xff09;…

从零开始学架构——高性能负载均衡

高性能负载均衡 单服务器无论如何优化&#xff0c;无论采用多好的硬件&#xff0c;总会有一个性能天花板&#xff0c;当单服务器的性能无法满足业务需求时&#xff0c;就需要设计高性能集群来提升系统整体的处理性能。高性能集群的本质很简单——通过增加更多的服务器来提升系…