Nacos 注册中心核心能力以及现实原理解析

news2024/10/2 1:25:17

Nacos注册中心主要分两方面解析:动态服务发现和Nacos实现动态服务发现的原理;

动态服务发现

服务发现是指使用一个注册中心来记录分布式系统中的全部服务的信息,以便其他服务能够快速的找到这些已注册的服务。

在单体应用中,DNS+Nginx 可以满足服务发现的要求,此时服务的IP列表配置在 nginx 上。在微服务架构中,由于服务粒度变的更细,服务的上下线更加频繁,注册中心可以动态感知服务的上下线,并且推送IP列表变化给服务消费者,架构如下图。

 Nacos 实现动态服务发现的原理

Nacos实现动态服务发现的核心原理如下图:

2.1 通讯协议

      1.x的 Nacos 版本中服务端只支持 http 协议,2.x版本引入了谷歌的 grpc,grpc 是一款长连接协议,极大的减少了 http 请求频繁的连接创建和销毁过程,能大幅度提升性能 ,Nacos服务端 grpc 版本,相比 http 版本的性能提升了9倍以上。

2.2 Nacos 服务注册

     服务注册的目的就是客户端将自己的ip端口等信息上报给 Nacos 服务端,过程如下:

  • 创建长连接:Nacos SDK 通过Nacos服务端域名解析出服务端ip列表,选择其中一个ip创建 grpc 连接,并定时检查连接状态,当连接断开,则自动选择服务端ip列表中的下一个ip进行重连。

  • 健康检查请求:在正式发起注册之前,Nacos SDK 向服务端发送一个空请求,服务端回应一个空请求,若Nacos SDK 未收到服务端回应,则认为服务端不健康,并进行一定次数重试,如果都未收到回应,则注册失败。

  • 发起注册:当你查看Nacos java SDK的注册方法时,你会发现没有返回值,这是因为Nacos SDK做了补偿机制,在真实给服务端上报数据之前,会先往缓存中插入一条记录表示开始注册,注册成功之后再从缓存中标记这条记录为注册成功,当注册失败时,缓存中这条记录是未注册成功的状态,Nacos SDK开启了一个定时任务,定时查询异常的缓存数据,重新发起注册。

Nacos SDK注册失败时的自动补偿机制时序图。

 相关源码如下:

@Override
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
    NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance {}", namespaceId, serviceName,
            instance);
        //添加redo日志
    redoService.cacheInstanceForRedo(serviceName, groupName, instance);

    doRegisterService(serviceName, groupName, instance);
}
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);
}

执行补偿定时任务RedoScheduledTask。

@Override
public void run() {
    if (!redoService.isConnected()) {
        LogUtils.NAMING_LOGGER.warn("Grpc Connection is disconnect, skip current redo task");
        return;
    }
    try {
        redoForInstances();
        redoForSubscribes();
    } catch (Exception e) {
        LogUtils.NAMING_LOGGER.warn("Redo task run with unexpected exception: ", e);
    }
}
  private void redoForInstances() {
    for (InstanceRedoData each : redoService.findInstanceRedoData()) {
        try {
            redoForInstance(each);
        } catch (NacosException e) {
            LogUtils.NAMING_LOGGER.error("Redo instance operation {} for {}@@{} failed. ", each.getRedoType(),
                    each.getGroupName(), each.getServiceName(), e);
        }
    }
}
  • 服务端数据同步(Distro协议):Nacos SDK只会与服务端某个节点建立长连接,当服务端接受到客户端注册的实例数据后,还需要将实例数据同步给其他节点。Nacos自己实现了一个一致性协议名为Distro,服务注册的时候会触发Distro一次同步,每个Nacos节点之间会定时互相发送Distro数据,以此保证数据最终一致。

  • 服务实例上线推送:Nacos服务端收到服务实例数据后会将服务的最新实例列表通过grpc推送给该服务的所有订阅者。

  • 服务注册过程源码时序图:整理了一下服务注册过程整体时序图,对源码实现感兴趣的可以按照根据这个时序图view一下源码。

2.3 Nacos 心跳机制 

       目前主流的注册中心,比如Consul、Eureka、Zk都是通过心跳机制来感知服务的下线,Nacos也是通过心跳机制来实现的。

Nacos目前SDK维护了两个分支的版本(1.x、2.x),这两个版本心跳机制的实现不一样。其中1.x版本的SDK通过http协议来定时向服务端发送心跳维持自己的健康状态,2.x版本的SDK则通过grpc自身的心跳机制来保活,当Nacos服务端接受不到服务实例的心跳,会认为实例下线。如下图:

grpc监测到连接断开事件,发送ClientDisconnectEvent。

public class ConnectionBasedClientManager extends ClientConnectionEventListener implements ClientManager {
  //连接断开,发送连接断开事件
    public boolean clientDisconnected(String clientId) {
       Loggers.SRV_LOG.info("Client connection {} disconnect, remove instances and 
    subscribers", clientId);
       ConnectionBasedClient client = clients.remove(clientId);
       if (null == client) {
           return true;
       }
       client.release();
       NotifyCenter.publishEvent(new ClientEvent.ClientDisconnectEvent(client));
       return true;
    }
}

 移除客户端注册的服务实例

public class ClientServiceIndexesManager extends SmartSubscriber {

  @Override
    public void onEvent(Event event) {
    //接收失去连接事件
        if (event instanceof ClientEvent.ClientDisconnectEvent) {
            handleClientDisconnect((ClientEvent.ClientDisconnectEvent) event);
        } else if (event instanceof ClientOperationEvent) {
            handleClientOperation((ClientOperationEvent) event);
        }
    }
    private void handleClientDisconnect(ClientEvent.ClientDisconnectEvent event) {
        Client client = event.getClient();
        for (Service each : client.getAllSubscribeService()) {
            removeSubscriberIndexes(each, client.getClientId());
        }
        //移除客户端注册的服务实例
        for (Service each : client.getAllPublishedService()) {
            removePublisherIndexes(each, client.getClientId());
        }
    }
    
    //移除客户端注册的服务实例
    private void removePublisherIndexes(Service service, String clientId) {
        if (!publisherIndexes.containsKey(service)) {
            return;
        }
        publisherIndexes.get(service).remove(clientId);
        NotifyCenter.publishEvent(new ServiceEvent.ServiceChangedEvent(service, true));
    }
}

2.4 Nacos 服务订阅

当一个服务发生上下线,Nacos如何知道要推送给哪些客户端?

Nacos SDK 提供了订阅和取消订阅方法,当客户端向服务端发起订阅请求,服务端会记录发起调用的客户端为该服务的订阅者,同时将服务的最新实例列表返回。当客户端发起了取消订阅,服务端就会从该服务的订阅者列表中把当前客户端移除。

当客户端发起订阅时,服务端除了会同步返回最新的服务实例列表,还会异步的通过grpc推送给该订阅者最新的服务实例列表,这样做的目的是为了异步更新客户端本地缓存的服务数据。

当客户端订阅的服务上下线,该服务所有的订阅者会立刻收到最新的服务列表并且将服务最新的实例数据更新到内存。

相关源码中,服务端接收到订阅数据,首先保存到内存中 :

@Override
public void subscribeService(Service service, Subscriber subscriber, String clientId) {
    Service singleton = ServiceManager.getInstance().getSingletonIfExist(service).orElse(service);
    Client client = clientManager.getClient(clientId);
    //校验长连接是否正常
    if (!clientIsLegal(client, clientId)) {
        return;
    }
    //保存订阅数据
    client.addServiceSubscriber(singleton, subscriber);
    client.setLastUpdatedTime();
    //发送订阅事件
    NotifyCenter.publishEvent(new ClientOperationEvent.ClientSubscribeServiceEvent(singleton, clientId));
}

    private void handleClientOperation(ClientOperationEvent event) {
    Service service = event.getService();
    String clientId = event.getClientId();
    if (event instanceof ClientOperationEvent.ClientRegisterServiceEvent) {
        addPublisherIndexes(service, clientId);
    } else if (event instanceof ClientOperationEvent.ClientDeregisterServiceEvent) {
        removePublisherIndexes(service, clientId);
    } else if (event instanceof ClientOperationEvent.ClientSubscribeServiceEvent) {
    //处理订阅操作
        addSubscriberIndexes(service, clientId);
    } else if (event instanceof ClientOperationEvent.ClientUnsubscribeServiceEvent) {
        removeSubscriberIndexes(service, clientId);
    }
}

然后发布订阅事件。

private void addSubscriberIndexes(Service service, String clientId) {
    //保存订阅数据
    subscriberIndexes.computeIfAbsent(service, (key) -> new ConcurrentHashSet<>());
    // Fix #5404, Only first time add need notify event.
    if (subscriberIndexes.get(service).add(clientId)) {
    //发布订阅事件
        NotifyCenter.publishEvent(new ServiceEvent.ServiceSubscribedEvent(service, clientId));
    }
}

服务端自己消费订阅事件,并且推送给订阅的客户端最新的服务实例数据。

@Override
public void onEvent(Event event) {
    if (!upgradeJudgement.isUseGrpcFeatures()) {
        return;
    }
    if (event instanceof ServiceEvent.ServiceChangedEvent) {
        // If service changed, push to all subscribers.
        ServiceEvent.ServiceChangedEvent serviceChangedEvent = (ServiceEvent.ServiceChangedEvent) event;
        Service service = serviceChangedEvent.getService();
        delayTaskEngine.addTask(service, new PushDelayTask(service, PushConfig.getInstance().getPushTaskDelay()));
    } else if (event instanceof ServiceEvent.ServiceSubscribedEvent) {
        // If service is subscribed by one client, only push this client.
        ServiceEvent.ServiceSubscribedEvent subscribedEvent = (ServiceEvent.ServiceSubscribedEvent) event;
        Service service = subscribedEvent.getService();
        delayTaskEngine.addTask(service, new PushDelayTask(service, PushConfig.getInstance().getPushTaskDelay(),
                subscribedEvent.getClientId()));
    }
}

2.5 Nacos 推送

服务的注册和订阅都会发生推送(服务端->客户端),那推送到底是如何实现的呢?

早期Nacos版本,当服务实例变化,服务端会通过udp协议将最新的数据发送给客户端,后来发现udp推送有一定的丢包率,于是新版本的Nacos支持了grpc推送。Nacos服务端会自动判断客户端的版本来选择哪种方式来进行推送,如果你使用1.4.2以前的SDK进行注册,那Nacos服务端会使用udp协议来进行推送,反之则使用grpc。 

推送失败处理:当发送推送时,客户端可能正在重启,或者连接不稳定导致推送失败,这个时候Nacos会进行重试。Nacos将每个推送都封装成一个任务对象,放入到队列中,再开启一个线程不停的从队列取出任务执行,执行之前会先删除该任务,如果执行失败则将任务重新添加到队列,该线程会记录任务执行的时间,如果超过1秒,则会记录到日志。

推送部分源码,添加推送任务到执行队列中。

private static class PushDelayTaskProcessor implements NacosTaskProcessor {

    private final PushDelayTaskExecuteEngine executeEngine;

    public PushDelayTaskProcessor(PushDelayTaskExecuteEngine executeEngine) {
        this.executeEngine = executeEngine;
    }

    @Override
    public boolean process(NacosTask task) {
        PushDelayTask pushDelayTask = (PushDelayTask) task;
        Service service = pushDelayTask.getService();
        NamingExecuteTaskDispatcher.getInstance()
                .dispatchAndExecuteTask(service, new PushExecuteTask(service, executeEngine, pushDelayTask));
        return true;
    }
}

推送任务PushExecuteTask 的执行。

public class PushExecuteTask extends AbstractExecuteTask {

//..省略

@Override
public void run() {
    try {
        //封装要推送的服务实例数据
        PushDataWrapper wrapper = generatePushData();
        ClientManager clientManager = delayTaskEngine.getClientManager();
        //如果是服务上下线导致的推送,获取所有订阅者
        //如果是订阅导致的推送,获取订阅者
        for (String each : getTargetClientIds()) {
            Client client = clientManager.getClient(each);
            if (null == client) {
                // means this client has disconnect
                continue;
            }
            Subscriber subscriber = clientManager.getClient(each).getSubscriber(service);
            //推送给订阅者
            delayTaskEngine.getPushExecutor().doPushWithCallback(each, subscriber, wrapper,
                    new NamingPushCallback(each, subscriber, wrapper.getOriginalData(), delayTask.isPushToAll()));
        }
    } catch (Exception e) {
        Loggers.PUSH.error("Push task for service" + service.getGroupedServiceName() + " execute failed ", e);
        //当推送发生异常,重新将推送任务放入执行队列
        delayTaskEngine.addTask(service, new PushDelayTask(service, 1000L));
    }
}

 //如果是服务上下线导致的推送,获取所有订阅者
        //如果是订阅导致的推送,获取订阅者
    private Collection<String> getTargetClientIds() {
    return delayTask.isPushToAll() ? delayTaskEngine.getIndexesManager().getAllClientsSubscribeService(service)
            : delayTask.getTargetClients();
}

执行推送任务线程InnerWorker 的执行。

/**
  * Inner execute worker.
  */
private class InnerWorker extends Thread {

    InnerWorker(String name) {
        setDaemon(false);
        setName(name);
    }

    @Override
    public void run() {
        while (!closed.get()) {
            try {
            //从队列中取出任务PushExecuteTask 
                Runnable task = queue.take();
                long begin = System.currentTimeMillis();
                //执行PushExecuteTask 
                task.run();
                long duration = System.currentTimeMillis() - begin;
                if (duration > 1000L) {
                    log.warn("task {} takes {}ms", task, duration);
                }
            } catch (Throwable e) {
                log.error("[TASK-FAILED] " + e.toString(), e);
            }
        }
    }
}

2.6 Nacos SDK 查询服务实例

       服务消费者首先需要调用Nacos SDK的接口来获取最新的服务实例,然后才能从获取到的实例列表中以加权轮询的方式选择出一个实例(包含ip,port等信息),最后再发起调用。

由于Nacos服务发生上下线、订阅的时候都会推送最新的服务实例列表到当客户端,客户端再更新本地内存中的缓冲数据,所以调用Nacos SDK提供的查询实例列表的接口时,不会直接请求服务端获取数据,而是会优先使用内存中的服务数据,只有内存中查不到的情况下才会发起订阅请求服务端数据。

Nacos SDK内存中的数据除了接受来自服务端的推送更新之外,自己本地也会有一个定时任务定时去获取服务端数据来进行兜底。Nacos SDK在查询的时候也了容灾机制,即从磁盘获取服务数据,而这个磁盘的数据其实也是来自于内存,有一个定时任务定时从内存缓存中获取然后加载到磁盘。Nacos SDK的容灾机制默认关闭,可通过设置环境变量failover-mode=true来开启。

架构图:

用户查询流程:

查询服务实例部分源码:

private final ConcurrentMap<String, ServiceInfo> serviceInfoMap;
 @Override
public List<Instance> getAllInstances(String serviceName, String groupName, List<String> clusters,

        boolean subscribe) throws NacosException {
    ServiceInfo serviceInfo;
    String clusterString = StringUtils.join(clusters, ",");
    //这里默认传过来是true
    if (subscribe) {
    //从本地内存获取服务数据,如果获取不到则从磁盘获取
        serviceInfo = serviceInfoHolder.getServiceInfo(serviceName, groupName, clusterString);
        if (null == serviceInfo || !clientProxy.isSubscribed(serviceName, groupName, clusterString)) {
      //如果从本地获取不到数据,则调用订阅方法
            serviceInfo = clientProxy.subscribe(serviceName, groupName, clusterString);
        }
    } else {
     //适用于不走订阅,直接从服务端获取数据的情况
        serviceInfo = clientProxy.queryInstancesOfService(serviceName, groupName, clusterString, 0, false);
    }
    List<Instance> list;
    if (serviceInfo == null || CollectionUtils.isEmpty(list = serviceInfo.getHosts())) {
        return new ArrayList<Instance>();
    }
    return list;
}
}
  //从本地内存获取服务数据,如果开启了故障转移则直接从磁盘获取,因为当服务端挂了,本地启动时内存中也没有数据
public ServiceInfo getServiceInfo(final String serviceName, final String groupName, final String clusters) {
    NAMING_LOGGER.debug("failover-mode: {}", failoverReactor.isFailoverSwitch());
    String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
    String key = ServiceInfo.getKey(groupedServiceName, clusters);
    //故障转移则直接从磁盘获取
    if (failoverReactor.isFailoverSwitch()) {
        return failoverReactor.getService(key);
    }
    //返回内存中数据
    return serviceInfoMap.get(key);
}

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

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

相关文章

MINE: Towards Continuous Depth MPI with NeRF for Novel View Synthesis

MINE: Towards Continuous Depth MPI with NeRF for Novel View Synthesis&#xff1a;利用NeRF实现新视图合成的连续深度MPI 摘要&#xff1a;在论文中&#xff0c;提出了MINE&#xff0c;通过从单个图像进行密集3D重建来执行新的视图合成和深度估计。通过引入神经辐射场&…

05-Oracle中的对象(视图,索引,同义词,系列)

本章主要内容&#xff1a; 1.视图管理&#xff1a;视图新增&#xff0c;修改&#xff0c;删除&#xff1b; 2.索引管理&#xff1a;索引目的&#xff0c;创建&#xff0c;修改&#xff0c;删除&#xff1b; 3.同义词管理&#xff1a;同义词的作用&#xff0c;创建&#xff0…

如何通过websoket实现即时通讯+断线重连?

本篇博客只是一个demo&#xff0c;具体应用还要结合项目实际情况&#xff0c;以下是目录结构&#xff1a; 1.首先通过express搭建一个本地服务器 npm install express 2.在serve.js中自定义测试数据 const express require(express); const app express(); const http req…

详细stm32驱动SDRAM的注意事项以及在keil中的使用

SDRAM的主要参数&#xff1a; 容量&#xff1a;SDRAM的容量是指其可以存储的数据量&#xff0c;通常以兆字节&#xff08;MB&#xff09;或千兆字节&#xff08;GB&#xff09;为单位。 时钟频率&#xff1a;SDRAM的时钟频率指的是其内部时钟的速度&#xff0c;通常以兆赫&…

94. 二叉树的中序遍历

94. 二叉树的中序遍历 给定一个二叉树的根节点 root &#xff0c;返回 它的 中序 遍历 (左根右)。 首先我们需要了解什么是二叉树的中序遍历&#xff1a;按照访问左子树——根节点——右子树的方式遍历这棵树&#xff0c;而在访问左子树或者右子树的时候我们按照同样的方式遍历…

MQTT协议-订阅主题和订阅确认

MQTT协议-订阅主题和订阅确认 SUBSCRIBE——订阅主题 订阅是客户端向服务端订阅 订阅报文 订阅报文与CONNECT报文类似&#xff0c;都是由固定报头可变报头有效载荷组成 固定报头比较简单&#xff0c;也是由两个字节组成&#xff0c;第一个字节为82&#xff0c;第二个字节是…

像素密度提升33%,Quest Pro动态注视点渲染原理详解

在Connect 2022上&#xff0c;Meta发布了Quest Pro&#xff0c;并首次在VR中引入动态注视点渲染&#xff08;ETFR&#xff09;功能&#xff0c;这是一种新型图形优化技术&#xff0c;特点是以用户注视点为中心&#xff0c;动态调节VR屏幕的清晰度&#xff08;注视点中心最清晰、…

Oracle VM VirtualBox6.1.36导入ova虚拟机文件报错,代码: E_INVALIDARG (0x80070057)

问题 运维人员去客户现场部署应用服务&#xff0c;客户是windows server 服务器&#xff08;客户不想买新机器&#xff09;&#xff0c;我们程序是在linux系统里运行&#xff08;其实windows也可以&#xff0c;主要是为了保持各地环境一致方便更新和排查问题&#xff09;我们使…

吐血整理学习方法,2年多功能测试成功进阶自动化测试,月薪23k+......

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 测试进阶方向 测试进…

[Gin]框架底层实现理解(三)

1.engine.Run(port string) 这个就是gin框架的启动语句&#xff0c;看看就好了&#xff0c;下面我们解析一下那个engine.Handler() listenandserve 用于启动http包进行监听&#xff0c;获取链接conn // ListenAndServe listens on the TCP network address addr and then ca…

【SOP 】配电网故障重构方法研究【IEEE33节点】(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

Java中wait和sleep区别

文章目录1. Java中wait和sleep区别2. wait和sleep所属方法的不同3. wait的Demo3.1 没有synchronized同步代码块异常3.2 wait使用Demo4. sleep的Demo1. Java中wait和sleep区别 sleep属于Thread类中的static方法&#xff1b;wait属于Object类的方法sleep时线程状态进入TIMED_WAI…

java 如何实现在线日志

如何采集springboot日志至web页面查看 实现方案 基于Filter方式,在日志输出至控制台前,LoggerFitler 拦截日志通过websocket推送至前台页面 实现逻辑&#xff1a; LoggerFilter采集日志添加至LoggerQueue队列, LoggerConsumer 从LoggerQueue中采集推送至前台页面 #mermaid-s…

LearnOpenGL-光照-3.材质

本人刚学OpenGL不久且自学&#xff0c;文中定有代码、术语等错误&#xff0c;欢迎指正 我写的项目地址&#xff1a;https://github.com/liujianjie/LearnOpenGLProject 文章目录材质例子1代码相关光照太强了例子2例子3: 不同的光源颜色材质 引出材质 如果我们想要在OpenGL中模拟…

TTS | 语音合成常见数据集及数据格式详情

linkLJSpeech网址 &#xff1a; The LJ Speech Dataset (keithito.com)数据集描述&#xff1a;数据集大小&#xff1a;2.6GB这是一个公共领域的语音数据集&#xff0c;由 13&#xff0c;100 个简短的音频剪辑组成 一位演讲者阅读 7 本非小说类书籍的段落。为每个剪辑提供转录。…

删除的文件怎么恢复?恢复方法在这里(支持Win和Mac)

案例&#xff1a;文件永久删除还能找回来吗&#xff1f;关于Win和Mac系统的恢复方法 “前几天我在清理电脑垃圾&#xff0c;不小心误删了重要的文件。有没有什么比较全面的方法&#xff0c;可以帮助我恢复删除的文件啊&#xff1f;在线急等回复&#xff01;” 随着电脑使用的…

Golang 中 Slice的分析与使用(含源码)

文章目录1、slice结构体2、slice初始化3、append操作4、slice截取5、slice深拷贝6、值传递还是引用传递参考文献众所周知&#xff0c;在golang中&#xff0c;slice&#xff08;切片&#xff09;是我们最常使用到的一种数据结构&#xff0c;是一种可变长度的数组&#xff0c;本篇…

三维人脸实践:基于Face3D的渲染、生成与重构 <二>

face3d: Python tools for processing 3D face git code: https://github.com/yfeng95/face3d paper list: PaperWithCode 3DMM方法&#xff0c;基于平均人脸模型&#xff0c;可广泛用于基于关键点的人脸生成、位姿检测以及渲染等&#xff0c;能够快速实现人脸建模与渲染。推…

MySQL基础篇3

第一章 多表关系实战 1.1 实战1&#xff1a;省和市 方案1&#xff1a;多张表&#xff0c;一对多 方案2&#xff1a;一张表&#xff0c;自关联一对多 id1 name‘北京’ p_id null; id2 name‘昌平’ p_id1 id3 name‘大兴’ p_id1 id3 name‘上海’ p_idnull id4 name‘浦东’…

中国人工智能企业中集飞瞳全球港航人工智能领军者,箱况残缺检测视频流动态感知智能闸口,自动化港口码头数字化智慧港航中国人工智能企业

中国人工智能企业中集飞瞳全球港航人工智能领军者&#xff0c;箱况残缺检测视频流动态感知超级智能闸口&#xff0c;自动化港口码头数字化智慧港航。CIMCAI已完成全球250万人工智能集装箱箱况检验&#xff0c;完成全球上亿集装箱信息&#xff0c;先进产品在全球各港区及集装箱枢…