源码篇--Nacos服务--中章(5):Nacos客户端启动-实例注册-grpc连接建立

news2024/11/18 1:50:41

文章目录

  • 前言
  • 一、 前奏:
  • 二、客户端连接的建立:
    • 2.1 NacosNamingService 创建:
    • 2.2 NacosNamingService 初始化:
    • 2.3 NamingClientProxyDelegate 长连接建立:
      • 2.3.1 grpc 代理对象创建:
      • 2.3.2 NamingGrpcClientProxy grpc:
        • 2.3.2.1 createClient 客户端的创建:
        • 2.3.2.2 start 长连接建立:
  • 三、客户端实例的注册:
    • 3.1 NamingGrpcClientProxy# registerService:
    • 3.2 客户端发送注册请求:
  • 总结


前言

本文对Nacos 客户端启动时,同服务端建立长连接的过程进行介绍。环境:客户端版本2.2.1,服务端版本 3.0.13;


一、 前奏:

实际客户端同服务端进行grpc 通道的建立,是在客户端实例注册过程中进行的,因为注册肯定要向服务端发送请求,所以要先通过grpc 完成通道的建立 ;一下对客户端实例的注册流程进行简单介绍。
流程图:
在这里插入图片描述
流程步骤解释:

  • 客户端所在web 应用启动完成,发布 WebServiceInitializedEvent 事件;
  • AbstractAutoServiceRegistration ,onApplicationEvent 方法接收事件 并调用start 方法;
public void onApplicationEvent(WebServerInitializedEvent event) {
  ApplicationContext context = event.getApplicationContext();
   if (!(context instanceof ConfigurableWebServerApplicationContext) || !"management".equals(((ConfigurableWebServerApplicationContext)context).getServerNamespace())) {
   	  // 本机web 端口 
       this.port.compareAndSet(0, event.getWebServer().getPort());
       // 初始化方法调用
       this.start();
   }
}

start() 方法:

public void start() {
    if (!this.isEnabled()) {
        if (logger.isDebugEnabled()) {
            logger.debug("Discovery Lifecycle disabled. Not starting");
        }

    } else {
        if (!this.running.get()) {
        	// 发布 实例注册 前事件
            this.context.publishEvent(new InstancePreRegisteredEvent(this, this.getRegistration()));
            this.registrationLifecycles.forEach((registrationLifecycle) -> {
                registrationLifecycle.postProcessBeforeStartRegister(this.getRegistration());
            });
            // 实例注册方法调用
            this.register();
            this.registrationLifecycles.forEach((registrationLifecycle) -> {
                registrationLifecycle.postProcessAfterStartRegister(this.getRegistration());
            });
            if (this.shouldRegisterManagement()) {
                this.registrationManagementLifecycles.forEach((registrationManagementLifecycle) -> {
                    registrationManagementLifecycle.postProcessBeforeStartRegisterManagement(this.getManagementRegistration());
                });
                this.registerManagement();
                this.registrationManagementLifecycles.forEach((registrationManagementLifecycle) -> {
                    registrationManagementLifecycle.postProcessAfterStartRegisterManagement(this.getManagementRegistration());
                });
            }
			 // 实例注册完成事件
            this.context.publishEvent(new InstanceRegisteredEvent(this, this.getConfiguration()));
            this.running.compareAndSet(false, true);
        }

    }
}
  • AbstractAutoServiceRegistration ,register() 方法 调用到 NacosServiceRegistry 的 register 方法;
  • namingService.registerInstance 进行客户端的注册;
 public void register(Registration registration) {
    if (StringUtils.isEmpty(registration.getServiceId())) {
        log.warn("No service to register for nacos client...");
    } else {
    	// 客户端长连接的建立
        NamingService namingService = this.namingService();
        String serviceId = registration.getServiceId();
        String group = this.nacosDiscoveryProperties.getGroup();
        // 构建客户端实例对象
        Instance instance = this.getNacosInstanceFromRegistration(registration);

        try {
             // grpc 发送 客户端实例注册请求
            namingService.registerInstance(serviceId, group, instance);
            log.info("nacos registry, {} {} {}:{} register finished", new Object[]{group, serviceId, instance.getIp(), instance.getPort()});
        } catch (Exception var7) {
            if (this.nacosDiscoveryProperties.isFailFast()) {
                log.error("nacos registry, {} register failed...{},", new Object[]{serviceId, registration.toString(), var7});
                ReflectionUtils.rethrowRuntimeException(var7);
            } else {
                log.warn("Failfast is false. {} register failed...{},", new Object[]{serviceId, registration.toString(), var7});
            }
        }

    }
}

二、客户端连接的建立:

NamingService namingService = this.namingService(); 这行代码做了很多事情,其中需要重点关注的时 客户端与服务端连接的建立,以及客户端的故障转移机制,下文先对连接的建立进行介绍;

2.1 NacosNamingService 创建:

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

2.2 NacosNamingService 初始化:

public NacosNamingService(Properties properties) throws NacosException {
init(properties);
}

private void init(Properties properties) throws NacosException {
    PreInitUtils.asyncPreLoadCostComponent();
    // 自定义属性
    final NacosClientProperties nacosClientProperties = NacosClientProperties.PROTOTYPE.derive(properties);
    ValidatorUtils.checkInitParam(nacosClientProperties);
    // 命名空间
    this.namespace = InitUtils.initNamespaceForNaming(nacosClientProperties);
    InitUtils.initSerialization();
    InitUtils.initWebRootContext(nacosClientProperties);
    // 日志名称属性设置
    initLogName(nacosClientProperties);

    this.notifierEventScope = UUID.randomUUID().toString();
    //  InstancesChangeNotifier extends Subscriber<InstancesChangeEvent>
    // 订阅者,订阅InstancesChangeEvent 实例变更事件 ,出现变更调用InstancesChangeNotifier onchange 方法
    this.changeNotifier = new InstancesChangeNotifier(this.notifierEventScope);
    // 注册事件发布器
    NotifyCenter.registerToPublisher(InstancesChangeEvent.class, 16384);
    // 注册订阅者
    NotifyCenter.registerSubscriber(changeNotifier);
    // 服务信息获取(故障转移)
    this.serviceInfoHolder = new ServiceInfoHolder(namespace, this.notifierEventScope, nacosClientProperties);
    // 客户端代理
    this.clientProxy = new NamingClientProxyDelegate(this.namespace, serviceInfoHolder, nacosClientProperties,
            changeNotifier);
}

2.3 NamingClientProxyDelegate 长连接建立:

2.3.1 grpc 代理对象创建:

 public NamingClientProxyDelegate(String namespace, ServiceInfoHolder serviceInfoHolder,
            NacosClientProperties properties, InstancesChangeNotifier changeNotifier) throws NacosException {
 // 服务更新
    this.serviceInfoUpdateService = new ServiceInfoUpdateService(properties, serviceInfoHolder, this,
            changeNotifier);
    // 服务端地址管理器
    this.serverListManager = new ServerListManager(properties, namespace);
    this.serviceInfoHolder = serviceInfoHolder;
    this.securityProxy = new SecurityProxy(this.serverListManager.getServerList(),
            NamingHttpClientManager.getInstance().getNacosRestTemplate());
    initSecurityProxy(properties);
    // http 代理
    this.httpClientProxy = new NamingHttpClientProxy(namespace, securityProxy, serverListManager, properties);
    // grpc 代理
    this.grpcClientProxy = new NamingGrpcClientProxy(namespace, securityProxy, serverListManager, properties,
            serviceInfoHolder);
}

2.3.2 NamingGrpcClientProxy grpc:

public NamingGrpcClientProxy(String namespaceId, SecurityProxy securityProxy, ServerListFactory serverListFactory,
        NacosClientProperties properties, ServiceInfoHolder serviceInfoHolder) throws NacosException {
    super(securityProxy);
    this.namespaceId = namespaceId;
    this.uuid = UUID.randomUUID().toString();
    // 请求超时时间
    this.requestTimeout = Long.parseLong(properties.getProperty(CommonParams.NAMING_REQUEST_TIMEOUT, "-1"));
    Map<String, String> labels = new HashMap<>();
    // 资源是sdk
    labels.put(RemoteConstants.LABEL_SOURCE, RemoteConstants.LABEL_SOURCE_SDK);
    // 模式是注册
    labels.put(RemoteConstants.LABEL_MODULE, RemoteConstants.LABEL_MODULE_NAMING);
    labels.put(Constants.APPNAME, AppNameUtils.getAppName());
    // rpc  客户端的创建
    this.rpcClient = RpcClientFactory.createClient(uuid, ConnectionType.GRPC, labels,
            RpcClientTlsConfig.properties(properties.asProperties()));
    this.redoService = new NamingGrpcRedoService(this, properties);
    NAMING_LOGGER.info("Create naming rpc client for uuid->{}", uuid);
    start(serverListFactory, serviceInfoHolder);
}
2.3.2.1 createClient 客户端的创建:
public static RpcClient createClient(String clientName, ConnectionType connectionType, Integer threadPoolCoreSize,
            Integer threadPoolMaxSize, Map<String, String> labels, RpcClientTlsConfig tlsConfig) {
        // 不是grpc 抛出异常
   if (!ConnectionType.GRPC.equals(connectionType)) {
        throw new UnsupportedOperationException("unsupported connection type :" + connectionType.getType());
    }
    // 客户端创建 Map<String, RpcClient> CLIENT_MAP
    return CLIENT_MAP.computeIfAbsent(clientName, clientNameInner -> {
        LOGGER.info("[RpcClientFactory] create a new rpc client of " + clientName);

        return new GrpcSdkClient(clientNameInner, threadPoolCoreSize, threadPoolMaxSize, labels, tlsConfig);
    });
}
2.3.2.2 start 长连接建立:
 private void start(ServerListFactory serverListFactory, ServiceInfoHolder serviceInfoHolder) throws NacosException {
  // 服务地址的工厂
   rpcClient.serverListFactory(serverListFactory);
   // 监听器放入
   rpcClient.registerConnectionListener(redoService);
   // 请求处理器
   rpcClient.registerServerRequestHandler(new NamingPushRequestHandler(serviceInfoHolder));
   // 客户端启动
   rpcClient.start();
   NotifyCenter.registerSubscriber(this);
}

rpcClient.start():注意做了3件事 (具体的实现细节在后续文章进行介绍)

  • 客户端与服务端的通道建立:
    1)和nacos 服务端建立通信的channel 管道;建立双向流的grpc 通信存根;
    2)发送服务检查请求,从nacos 服务端获取到连接的connectId ;
    3) 发送给服务端客户端和服务端完成连接建立的请求;

  • 客户端与服务端的心跳监测:
    1) 在while(true) 循环中,发送healthCheck() 请求,得到true 则保持心跳(继续下一次循环),false 则失去心跳;
    2)如果失去心跳,则将客户端从健康状态标记为不健康状态;
    3)通过reconnect 方法尝试与nacos 服务端重新建立通信连接;

  • 客户端与服务端的断线重连:

    1. 通过 connectToServer 尝试与nacos 服务端重新建立通信连接;
      2)建立成功,则将原有的连接置为不可用,并关闭原有连接,释放资源;发布新的连接建立事件到 eventLinkedBlockingQueue 队列中;
      3)如果建立不成功则进行增大通nacos 服务端建立连接请求的时间间隔;
/**
 * Start this client.
 */
public final void start() throws NacosException {
    // cas 状态转换: 乐观锁实现
    boolean success = rpcClientStatus.compareAndSet(RpcClientStatus.INITIALIZED, RpcClientStatus.STARTING);
    if (!success) {
        return;
    }
    // 创建 clientEventExecutor  线程池,池子中设置了2个线程
    clientEventExecutor = new ScheduledThreadPoolExecutor(2, r -> {
        Thread t = new Thread(r);
        t.setName("com.alibaba.nacos.client.remote.worker");
        t.setDaemon(true);
        return t;
    });
    
    // connection event consumer.
    // 线程池提交任务: 客户端和服务端 连接重置;当nacos 服务端重启,客户端在心跳监测
    clientEventExecutor.submit(() -> {
        while (!clientEventExecutor.isTerminated() && !clientEventExecutor.isShutdown()) {
            ConnectionEvent take;
            try {
                take = eventLinkedBlockingQueue.take();
                if (take.isConnected()) {
                    notifyConnected();
                } else if (take.isDisConnected()) {
                    notifyDisConnected();
                }
            } catch (Throwable e) {
                // Do nothing
            }
        }
    });
    
    clientEventExecutor.submit(() -> {
        while (true) {
            try {
                if (isShutdown()) {
                    break;
                }
                // reconnectionSignal 重连接队列
                ReconnectContext reconnectContext = reconnectionSignal
                        .poll(rpcClientConfig.connectionKeepAlive(), TimeUnit.MILLISECONDS);
                if (reconnectContext == null) {
                	// 重连接队列是null 则表示 客户端与服务端没有发生断线重连的情况
                    // check alive time. 超过心跳的间隔时间,则重新发送healthCheck 监控检查请求
                    if (System.currentTimeMillis() - lastActiveTimeStamp >= rpcClientConfig.connectionKeepAlive()) {
                        boolean isHealthy = healthCheck();
                        if (!isHealthy) {
                        	// 如果 健康检测失败
                            if (currentConnection == null) {
                                continue;
                            }
                            LoggerUtils.printIfInfoEnabled(LOGGER,
                                    "[{}] Server healthy check fail, currentConnection = {}",
                                    rpcClientConfig.name(), currentConnection.getConnectionId());
                            
                            RpcClientStatus rpcClientStatus = RpcClient.this.rpcClientStatus.get();
                            if (RpcClientStatus.SHUTDOWN.equals(rpcClientStatus)) {
                                break;
                            }
                            // 标记客户端为 UNHEALTHY
                            boolean statusFLowSuccess = RpcClient.this.rpcClientStatus
                                    .compareAndSet(rpcClientStatus, RpcClientStatus.UNHEALTHY);
                            if (statusFLowSuccess) {
                            	// 服务端有可能发生了故障,则将服务端信息 ServerInfo 置为null 
                                reconnectContext = new ReconnectContext(null, false);
                            } else {
                                continue;
                            }
                            
                        } else {
                            lastActiveTimeStamp = System.currentTimeMillis();
                            continue;
                        }
                    } else {
                        continue;
                    }
                    
                }
                
                if (reconnectContext.serverInfo != null) {
                	// 发送连接重置时,检查 nacos 服务端的ip 和端口
                    // clear recommend server if server is not in server list.
                    boolean serverExist = false;
                    for (String server : getServerListFactory().getServerList()) {
                        ServerInfo serverInfo = resolveServerInfo(server);
                        if (serverInfo.getServerIp().equals(reconnectContext.serverInfo.getServerIp())) {
                            serverExist = true;
                            reconnectContext.serverInfo.serverPort = serverInfo.serverPort;
                            break;
                        }
                    }
                    if (!serverExist) {
                        LoggerUtils.printIfInfoEnabled(LOGGER,
                                "[{}] Recommend server is not in server list, ignore recommend server {}",
                                rpcClientConfig.name(), reconnectContext.serverInfo.getAddress());
                        
                        reconnectContext.serverInfo = null;
                        
                    }
                }
                // 发送重新连接服务端的请求
                reconnect(reconnectContext.serverInfo, reconnectContext.onRequestFail);
            } catch (Throwable throwable) {
                // Do nothing
            }
        }
    });
    
    // connect to server, try to connect to server sync retryTimes times, async starting if failed.
    // 客户端启动时 第一次进行同nacos 服务端的连接建立
    Connection connectToServer = null;
    rpcClientStatus.set(RpcClientStatus.STARTING);
    
    int startUpRetryTimes = rpcClientConfig.retryTimes();
    // 重试次数判断
    while (startUpRetryTimes > 0 && connectToServer == null) {
        try {
            startUpRetryTimes--;
            // 随机获取一个nacos 服务端的地址
            ServerInfo serverInfo = nextRpcServer();
            
            LoggerUtils.printIfInfoEnabled(LOGGER, "[{}] Try to connect to server on start up, server: {}",
                    rpcClientConfig.name(), serverInfo);
            // 服务端的连接
            connectToServer = connectToServer(serverInfo);
        } catch (Throwable e) {
            LoggerUtils.printIfWarnEnabled(LOGGER,
                    "[{}] Fail to connect to server on start up, error message = {}, start up retry times left: {}",
                    rpcClientConfig.name(), e.getMessage(), startUpRetryTimes, e);
        }
        
    }
    
    if (connectToServer != null) {
        LoggerUtils
                .printIfInfoEnabled(LOGGER, "[{}] Success to connect to server [{}] on start up, connectionId = {}",
                        rpcClientConfig.name(), connectToServer.serverInfo.getAddress(),
                        connectToServer.getConnectionId());
         // 连接建立成功,则将连接成功时间放入到 eventLinkedBlockingQueue 队列中进行消费
        this.currentConnection = connectToServer;
        rpcClientStatus.set(RpcClientStatus.RUNNING);
        eventLinkedBlockingQueue.offer(new ConnectionEvent(ConnectionEvent.CONNECTED));
    } else {
    	// 连接失败则 将失败时间放入到reconnectionSignal 队列中,消费改队列时 进入重连的逻辑
        switchServerAsync();
    }
    // 注册连接重置 处理器
    registerServerRequestHandler(new ConnectResetRequestHandler());
    
    // register client detection request.
    registerServerRequestHandler(request -> {
        if (request instanceof ClientDetectionRequest) {
            return new ClientDetectionResponse();
        }
        
        return null;
    });
    
}

三、客户端实例的注册:

在完成与服务端的通信channel 建立之后,就可以通过 namingService.registerInstance(serviceId, group, instance) 进行nacos 客户端实例的注册;

3.1 NamingGrpcClientProxy# registerService:

@Override
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
    NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance {}", namespaceId, serviceName,
            instance);
     //  ConcurrentMap<String, InstanceRedoData> registeredInstance map 中放入实例信息 key:分组名@@服务名
    redoService.cacheInstanceForRedo(serviceName, groupName, instance);
    // 向nacos 服务端发送注册请求,然后修改 InstanceRedoData 的实例信息为注册成功状态
    doRegisterService(serviceName, groupName, instance);
}
public void cacheInstanceForRedo(String serviceName, String groupName, Instance instance) {
//  key:分组名@@服务名
 String key = NamingUtils.getGroupedName(serviceName, groupName);
 	// 客户端服务对象创建,然后放入到  registeredInstance map 缓存(注册状态是未注册)
    InstanceRedoData redoData = InstanceRedoData.build(serviceName, groupName, instance);
    synchronized (registeredInstances) {
        registeredInstances.put(key, redoData);
    }
}

3.2 客户端发送注册请求:

public void doRegisterService(String serviceName, String groupName, Instance instance) throws NacosException {
    InstanceRequest request = new InstanceRequest(namespaceId, serviceName, groupName,
            NamingRemoteConstants.REGISTER_INSTANCE, instance);
     // 发送注册请求到服务端
    requestToServer(request, Response.class);
    // 请求发送成功 ,将当前服务实例的注册状态改为已注册
    redoService.instanceRegistered(serviceName, groupName);
}

requestToServer(request, Response.class);

private <T extends Response> T requestToServer(AbstractNamingRequest request, Class<T> responseClass)
            throws NacosException {
try {
       request.putAllHeader(
               getSecurityHeaders(request.getNamespace(), request.getGroupName(), request.getServiceName()));
       // 通过 rpcClient 获取通道 发送 InstanceRequest 类型的 request 请求
       Response response =
               requestTimeout < 0 ? rpcClient.request(request) : rpcClient.request(request, requestTimeout);
       if (ResponseCode.SUCCESS.getCode() != response.getResultCode()) {
           throw new NacosException(response.getErrorCode(), response.getMessage());
       }
       if (responseClass.isAssignableFrom(response.getClass())) {
           return (T) response;
       }
       NAMING_LOGGER.error("Server return unexpected response '{}', expected response should be '{}'",
               response.getClass().getName(), responseClass.getName());
   } catch (NacosException e) {
       throw e;
   } catch (Exception e) {
       throw new NacosException(NacosException.SERVER_ERROR, "Request nacos server failed: ", e);
   }
   throw new NacosException(NacosException.SERVER_ERROR, "Server return invalid response");
}

总结

客户端在启动成功之后发布 WebServiceInitializedEvent 事件,nacos 客户端同服务端创建通信通道,发送nacos 服务端的服务检查请求,正常返回后得到通道的id,创建双向流 grpc 的通信存根,发送连接确定建立的请求后;发起客户端实例的注册请求到nacos 服务端进行注册。

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

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

相关文章

3月魅力彩妆行业数据分析:某国产品牌彩妆产品销额将近30亿!

彩妆行业发展多年&#xff0c;经历了多重红利期和激烈的市场竞争后&#xff0c;进入到缓慢发展时期。 根据鲸参谋数据显示&#xff0c;今年3月在线上电商平台&#xff08;淘宝天猫京东&#xff09;彩妆产品销量累计超过6700万件&#xff0c;同比去年下降了29%&#xff1b;销售…

【智能算法】吉萨金子塔建造算法(GPC)原理及实现

目录 1.背景2.算法原理2.1算法思想2.2算法过程 3.结果展示4.参考文献 1.背景 2021年&#xff0c;S Harifi等人受到观古代遗迹构造启发&#xff0c;提出了吉萨金子塔建造算法&#xff08;Giza Pyramids Construction, GPC&#xff09;。 2.算法原理 2.1算法思想 GPC模拟了古埃…

通配符(泛域名)SSL证书在使用上有什么优势?

通配符证书在使用上具有以下显著优势&#xff1a; 1. 安全性&#xff1a; - 统一加密保护&#xff1a;通配符证书基于SSL/TLS协议&#xff0c;为一个主域名及其所有子域名提供相同的高强度数据加密服务。无论用户访问的是mail.example.com、shop.example.com还是其他任何以exam…

jackson.dataformat.xml 反序列化 对象中包含泛型

重点&#xff1a; JacksonXmlProperty localName 指定本地名称 JacksonXmlRootElement localName 指定root的根路径的名称&#xff0c;默认值为类名 JsonIgnoreProperties(ignoreUnknown true) 这个注解写在类上&#xff0c;用来忽略在xml中有的属性但是在类中没有的情况 Jack…

SQL Server指南:从入门到进阶实战

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

代码随想录算法训练营DAY36|C++贪心算法Part.5|435.无重叠区间、763.划分字母区间、56. 合并区间

文章目录 435.无重叠区间按右边界排序CPP代码 按左边界排序如何判断相邻区间是否重叠如何判断一下一个区间与当前相邻区间是否重叠总结CPP代码 763.划分字母区间思路伪代码实现CPP代码 56. 合并区间思路CPP代码 435.无重叠区间 力扣题目链接 文章链接&#xff1a;435.无重叠区间…

[笔试训练](五)

013 游游的you__牛客网 (nowcoder.com) 题目&#xff1a; 题解&#xff1a; 组成一个you需要一个o且能得2分&#xff0c;而组成相邻字母oo需要两个o&#xff0c;只能得1分。优先考虑组成尽可能多的you&#xff0c;再考虑剩下的o&#xff0c;放一起。 #include <iostream…

VSCode通过跳板机免密连接远程服务器的解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

JS -正则表达式

正则表达式 关于正则表达式&#xff0c;其实我写过几篇了&#xff0c;但是真正的正则表达式其实主要用于定义一些字符串的规则&#xff0c;计算机根据给出的正则表达式&#xff0c;来检查一个字符串是否符合规则。 我们来看一下&#xff0c;在JS中如何创建正则表达式对象。 语…

北京筑龙当选中招协第二届招标采购数字化专业委员会执行主任单位

4月18-19日&#xff0c;中国招标投标协会&#xff08;以下简称中招协&#xff09;2024年年会在宁波召开&#xff0c;北京筑龙作为中招协理事会员单位受邀出席会议。会议期间举行了“电子招标采购专业委员会换届会议暨第二届第一次工作会议”&#xff0c;北京筑龙当选第二届招标…

JavaScript 计算颜色的相对亮度,并确定相应的文本颜色

JavaScript 计算颜色的相对亮度&#xff0c;并确定相应的颜色 一、需求内容 需求点&#xff1a;给出一组颜色列表&#xff0c;渲染对应的颜色以及颜色值&#xff0c;但是要保证文本颜色和背景色不冲突&#xff0c;文本颜色保持 black 和 white 两种即可 示例如下&#xff1a…

误差的一阶和二阶——MSE/MAE

variance和bias MSE之前&#xff0c;先看两个更为朴素的指标&#xff1a;variance和bias。 在打靶中&#xff0c;有的人所有的子弹都离靶心很远&#xff0c;偏差显然过高&#xff0c;但是很稳定地维持在某一点附近&#xff1b;有的人平均环数更高&#xff0c;但是分布太过分散…

电磁兼容(EMC):静电放电(ESD)抗扰度试验深度解读(六)

目录 1. 静电测试干扰方式 2. 案例一 3. 案例二 4. 案例三 5. 案例四 6. 总结 静电放电测试的复杂性决定了这项测试对产品的主要影响方式也是多样的。标准里介绍了几种常见的影响方式&#xff1a; 1. 静电测试干扰方式 在静电放电试验中&#xff0c;测试了受试设备对于…

自然语言处理 (NLP) 的技术演变史

一、简述 本文的目标是了解自然语言处理 (NLP) 的历史&#xff0c;包括 Transformer 体系结构如何彻底改变该领域并帮助我们创建大型语言模型 (LLM)。 基础模型&#xff08;如 GPT-4&#xff09;是最先进的自然语言处理模型&#xff0c;旨在理解、生成人类语言并与之交互。 要理…

倍思、南卡、Cleer开放式耳机怎么样?三大网红真实数据测评PK

​作为一名在数码产品评测领域耕耘五载的专业人士&#xff0c;我有幸涉足各类蓝牙耳机的深度测评&#xff0c;涉猎范围广泛&#xff0c;从崭露头角的新锐品牌直至业界巨擘的旗舰之作&#xff0c;无一不在我的评测之列。鉴于近期对开放式耳机类咨询热度不减&#xff0c;我决定开…

Kafka 可视化管理工具 CMAK 启动错误 -- 命令行太长 问题解决

一、安装环境描述&#xff1a; Kafka版本&#xff1a;kafka_2.13-2.8.1cmak 版本&#xff1a;cmak-3.0.0.6安装环境&#xff1a;windows 11 二、问题描述 当我们在 命令行启动 cmak.bat 命令时&#xff0c;会报如下错误&#xff1a; 命令行太长三、解决办法 修改 cmak.bat…

C#设计树形程序界面的方法:创建特殊窗体

目录 1.TreeView控件 2.实例 &#xff08;1&#xff09;Resources.Designer.cs &#xff08;2&#xff09;Form1.Designer.cs &#xff08;3&#xff09;Form1.cs &#xff08;4&#xff09;生成效果 以树形来显示程序的菜单&#xff0c;可以更直观、更快捷地对窗体进行…

Python 面向对象——5.多态

本章学习链接如下&#xff1a; Python 面向对象——1.基本概念 Python 面向对象——2.类与对象实例属性补充解释&#xff0c;self的作用等 Python 面向对象——3.实例方法&#xff0c;类方法与静态方法 Python 面向对象——4.继承 1.基本概念 多态是面向对象编程&#x…

【智慧园区、低碳园区】工业园区综合能源管理系统解决方案

安科瑞薛瑶瑶18701709087 ◆行业特点 产业园区是国民经济发展的重要载体, 同时也是重点用能企业聚集地。园区面积大&#xff0c;供电距离远&#xff0c;建筑多且分散&#xff0c;用电负荷种类多&#xff0c;用电负荷不均匀&#xff0c;园区配电结构应整体规划&#xff0c;统一…

Xshell7免费版下载及安装(详细教程)

Xshell7免费版下载及安装&#xff08;详细教程&#xff09; 一、下载及安装 1.打开官网下载 https://www.xshell.com/zh/free-for-home-school/ 2.选择合适的下载路径 点击下载按钮 开始下载 3.下载完成后 我们双击打开.exe文件 点击下一步 4.点击我同意 点击下一步 5.选择合…