[Nacos] Nacos Client获取所有服务和定时更新Client端的注册表 (三)

news2024/11/26 4:37:44

文章目录

      • 1.Nacos Client获取所有服务
        • 1.1 Client如何获取所有服务
        • 1.2 Client获取服务方法getServices()详解
      • 2.Nacos定时更新Client端的注册表
        • 2.1 Nacos和Eureka定时更新Client端的注册表的区别
        • 2.2 Client定时更新本地服务过程
        • 2.3 updateServiceNow方法解析
        • 2.4 定时更新本地注册表中的当前服务

Nacos的服务发现功能: 获取所有服务, 定时更新Client端的注册表

1.Nacos Client获取所有服务

1.1 Client如何获取所有服务

 <dependency>
     <groupId>com.alibaba.cloud</groupId>
     <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
 </dependency>

在这里插入图片描述

在这里插入图片描述

NacosDiscoveryClientAutoConfiguration.java

@Configuration
@ConditionalOnNacosDiscoveryEnabled
@AutoConfigureBefore({ SimpleDiscoveryClientAutoConfiguration.class,
		CommonsClientAutoConfiguration.class })
public class NacosDiscoveryClientAutoConfiguration {

	@Bean
	@ConditionalOnMissingBean
	public NacosDiscoveryProperties nacosProperties() {
		return new NacosDiscoveryProperties();
	}

	@Bean
	public DiscoveryClient nacosDiscoveryClient(
			NacosDiscoveryProperties discoveryProperties) {
		return new NacosDiscoveryClient(discoveryProperties);
	}

	@Bean
	@ConditionalOnMissingBean
	@ConditionalOnProperty(value = "spring.cloud.nacos.discovery.watch.enabled", matchIfMissing = true)
	public NacosWatch nacosWatch(NacosDiscoveryProperties nacosDiscoveryProperties) {
		return new NacosWatch(nacosDiscoveryProperties);
	}
}

NacosDiscoveryClient#getServices()

    @Override
    public List<String> getServices() {

        try {
            ListView<String> services = discoveryProperties.namingServiceInstance()
                    .getServicesOfServer(1, Integer.MAX_VALUE);
            return services.getData();
        } catch (Exception e) {
            log.error("get service name from nacos server fail,", e);
            return Collections.emptyList();
        }
    }

在这里插入图片描述

这里的discoveryProperties为上面的NacosDiscoveryClientAutoConfiguration自动注入了。

DiscoveryClientHealthIndicator为SpringBoot的actuator自带的监控功能。
DiscoveryClientHealthIndicator#health()调用了这个getServices()方法。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

1.2 Client获取服务方法getServices()详解

NacosMaingService#getServicesOfServer()

在这里插入图片描述

NamingProxy#getServiceList()

在这里插入图片描述

最后还是通过向Server端发送get请求。

在这里插入图片描述

result为两个服务名称。

2.Nacos定时更新Client端的注册表

还是自动注册类NacosDiscoreryClientAutoConfiguration.java, 会自动注入NaocsWatch类, 这个类在Nacos源码不存在, 只是自动注入SpringBoot的类。

在这里插入图片描述

2.1 Nacos和Eureka定时更新Client端的注册表的区别

Eureka:

Nacos:

2.2 Client定时更新本地服务过程

NacosNamingService#subscribe()

在这里插入图片描述

通过hostReactor.getServiceInfo()更新本地服务过程

HostReactor#getServiceInfo()

    public ServiceInfo getServiceInfo(final String serviceName, final String clusters) {

        NAMING_LOGGER.debug("failover-mode: " + failoverReactor.isFailoverSwitch());
        // 构建key,其格式为  groupId@@微服务名称@@clusters名称
        String key = ServiceInfo.getKey(serviceName, clusters);
        if (failoverReactor.isFailoverSwitch()) {
            return failoverReactor.getService(key);
        }

        // 从当前client的本地注册表中获取当前服务
        ServiceInfo serviceObj = getServiceInfo0(serviceName, clusters);

        if (null == serviceObj) {  // 若本地注册表中没有该服务,则创建一个
            // 创建一个空的服务(没有任何提供者实例instance的ServiceInfo)
            serviceObj = new ServiceInfo(serviceName, clusters);

            // serviceInfoMap即为Client端的本地注册表
            serviceInfoMap.put(serviceObj.getKey(), serviceObj);

            // updatingMap是一个临时缓存,其主要是使用这个缓存map的key,
            // 使用map的key不能重复这个特性
            // 只要有服务名称出现在这个缓存map中,就表示当前这个服务正在被更新
            // 准备要更新serviceName的服务了,就先将其名称写入到这个临时缓存map
            updatingMap.put(serviceName, new Object());
            // 更新本地注册表中的serviceName的服务
            updateServiceNow(serviceName, clusters);
            // 更新完毕,将该serviceName服务从临时缓存map中干掉
            updatingMap.remove(serviceName);

            // 若当前注册表中已经有了这个服务,那么查看一下临时缓存map中
            // 是否存在该服务。若存在,则说明这个服务正在被更新,所以本次
            // 操作先等待wait一会儿
        } else if (updatingMap.containsKey(serviceName)) {

            if (UPDATE_HOLD_INTERVAL > 0) {
                // hold a moment waiting for update finish
                synchronized (serviceObj) {
                    try {
                        serviceObj.wait(UPDATE_HOLD_INTERVAL);
                    } catch (InterruptedException e) {
                        NAMING_LOGGER
                                .error("[getServiceInfo] serviceName:" + serviceName + ", clusters:" + clusters, e);
                    }
                }
            }
        }

        // 启动一个定时任务,定时更新本地注册表中的当前服务
        scheduleUpdateIfAbsent(serviceName, clusters);

        return serviceInfoMap.get(serviceObj.getKey());
    }

    private ServiceInfo getServiceInfo0(String serviceName, String clusters) {

        String key = ServiceInfo.getKey(serviceName, clusters);
        // serviceInfoMap即为Client端的本地注册表,
        // 其key为 groupId@@微服务名称@@clusters名称
        // value为ServiceInfo
        return serviceInfoMap.get(key);
    }
  • 通过Key, Value的模式去serviceInfoMap即为Client端的本地注册表获取服务, Key: groupId@@微服务名称@@clusters名称, Value:ServiceInfo
  • 先从当前client的本地注册表中获取当前服务, 若本地注册表中没有该服务,则创建一个, 首先创建一个空的服务, 然后填充属性。
  • updatingMap: updatingMap是一个临时缓存,其主要是使用这个缓存map的key, 使用map的key不能重复这个特性, 只要有服务名称出现在这个缓存map中,就表示当前这个服务正在被更新, 准备要更新serviceName的服务了,就先将其名称写入到这个临时缓存map。
  • updateServiceNow(serviceName, clusters): 更新本地注册表中的serviceName的服务, 更新完毕后从updatingMap删除serviceName服务
  • 若当前注册表中已经有了这个服务,那么查看一下临时缓存map中, 是否存在该服务。若存在,则说明这个服务正在被更新, 操作先等待wait一会儿
  • 最后, 启动一个定时任务,定时更新本地注册表中的当前服务

2.3 updateServiceNow方法解析

更新本地注册表中的serviceName的服务

    private void updateServiceNow(String serviceName, String clusters) {
        try {
            // 更新本地注册表中的serviceName的服务
            updateService(serviceName, clusters);
        } catch (NacosException e) {
            NAMING_LOGGER.error("[NA] failed to update serviceName: " + serviceName, e);
        }
    }

在这里插入图片描述

在这里插入图片描述

processServiceJson方法去将 将来自于Server的ServiceInfo更新到本地注册表, 这里的ServerInfo为向server提交一个GTE请求, 是JSON字符串的形式。

HostReactor#processServiceJson(): 将来自Server的ServerInfo更新至本地注册表, 有一些情况具体分析。

    public ServiceInfo processServiceJson(String json) {
        // 将来自于Server的JSON转换为ServiceInfo
        ServiceInfo serviceInfo = JacksonUtils.toObj(json, ServiceInfo.class);
        // 获取注册表中当前服务的ServiceInfo
        ServiceInfo oldService = serviceInfoMap.get(serviceInfo.getKey());
        if (serviceInfo.getHosts() == null || !serviceInfo.validate()) {
            //empty or error push, just ignore
            return oldService;
        }

        boolean changed = false;

        // 若当前注册表中存在当前服务,则想办法将来自于server的数据更新到本地注册表
        if (oldService != null) {

            // 为了安全起见,这种情况几乎是不会出现的
            if (oldService.getLastRefTime() > serviceInfo.getLastRefTime()) {
                NAMING_LOGGER.warn("out of date data received, old-t: " + oldService.getLastRefTime() + ", new-t: "
                        + serviceInfo.getLastRefTime());
            }

            // 将来自于Server的serviceInfo替换掉注册表中的当前服务
            serviceInfoMap.put(serviceInfo.getKey(), serviceInfo);

            // 遍历本地注册表中当前服务的所有instance实例
            Map<String, Instance> oldHostMap = new HashMap<String, Instance>(oldService.getHosts().size());
            for (Instance host : oldService.getHosts()) {
                // 将当前遍历的instance主机的ip:port作为key,instance作为value,
                // 写入到一个新的map
                oldHostMap.put(host.toInetAddr(), host);
            }

            // 遍历来自于server的当前服务的所有instance实例
            Map<String, Instance> newHostMap = new HashMap<String, Instance>(serviceInfo.getHosts().size());
            for (Instance host : serviceInfo.getHosts()) {
                // 将当前遍历的instance主机的ip:port作为key,instance作为value,
                // 写入到一个新的map
                newHostMap.put(host.toInetAddr(), host);
            }

            // 该set集合中存放的是,两个map(oldHostMap与newHostMap)中都有的ip:port,
            // 但它们的instance不相同,此时会将来自于server的instance写入到这个set
            Set<Instance> modHosts = new HashSet<Instance>();
            // 只有newHostMap中存在的instance,即在server端新增的instance
            Set<Instance> newHosts = new HashSet<Instance>();
            // 只有oldHostMap中存在的instance,即在server端被删除的instance
            Set<Instance> remvHosts = new HashSet<Instance>();

            List<Map.Entry<String, Instance>> newServiceHosts = new ArrayList<Map.Entry<String, Instance>>(
                    newHostMap.entrySet());
            // 遍历来自于server的主机
            for (Map.Entry<String, Instance> entry : newServiceHosts) {
                Instance host = entry.getValue();
                // ip:port
                String key = entry.getKey();
                // 在注册表中存在该ip:port,但这两个instance又不同,则将这个instance写入到modHosts
                if (oldHostMap.containsKey(key) && !StringUtils
                        .equals(host.toString(), oldHostMap.get(key).toString())) {
                    modHosts.add(host);
                    continue;
                }

                // 若注册表中不存在该ip:port,说明这个主机是新增的,则将其写入到newHosts
                if (!oldHostMap.containsKey(key)) {
                    newHosts.add(host);
                }
            }

            // 遍历来自于本地注册表的主机
            for (Map.Entry<String, Instance> entry : oldHostMap.entrySet()) {
                Instance host = entry.getValue();
                String key = entry.getKey();
                if (newHostMap.containsKey(key)) {
                    continue;
                }

                // 注册表中存在,但来自于server的serviceInfo中不存在,
                // 说明这个instance被干掉了,将其写入到remvHosts
                if (!newHostMap.containsKey(key)) {
                    remvHosts.add(host);
                }

            }

            if (newHosts.size() > 0) {
                changed = true;
                NAMING_LOGGER.info("new ips(" + newHosts.size() + ") service: " + serviceInfo.getKey() + " -> "
                        + JacksonUtils.toJson(newHosts));
            }

            if (remvHosts.size() > 0) {
                changed = true;
                NAMING_LOGGER.info("removed ips(" + remvHosts.size() + ") service: " + serviceInfo.getKey() + " -> "
                        + JacksonUtils.toJson(remvHosts));
            }

            if (modHosts.size() > 0) {
                changed = true;
                // 变更心跳信息BeatInfo
                updateBeatInfo(modHosts);
                NAMING_LOGGER.info("modified ips(" + modHosts.size() + ") service: " + serviceInfo.getKey() + " -> "
                        + JacksonUtils.toJson(modHosts));
            }

            serviceInfo.setJsonFromServer(json);

            // 只要发生了变更,就将这个发生变更的serviceInfo记录到一个缓存队列
            if (newHosts.size() > 0 || remvHosts.size() > 0 || modHosts.size() > 0) {
                eventDispatcher.serviceChanged(serviceInfo);
                DiskCache.write(serviceInfo, cacheDir);
            }

            // 若本地注册表中就没有当前服务,则直接将来自于server的serviceInfo写入到注册表
        } else {
            changed = true;
            NAMING_LOGGER.info("init new ips(" + serviceInfo.ipCount() + ") service: " + serviceInfo.getKey() + " -> "
                    + JacksonUtils.toJson(serviceInfo.getHosts()));
            // 将来自于server的serviceInfo写入到注册表
            serviceInfoMap.put(serviceInfo.getKey(), serviceInfo);
            // 将这个发生变更的serviceInfo记录到一个缓存队列
            eventDispatcher.serviceChanged(serviceInfo);
            serviceInfo.setJsonFromServer(json);
            DiskCache.write(serviceInfo, cacheDir);
        }

        MetricsMonitor.getServiceInfoMapSizeMonitor().set(serviceInfoMap.size());

        if (changed) {
            NAMING_LOGGER.info("current ips:(" + serviceInfo.ipCount() + ") service: " + serviceInfo.getKey() + " -> "
                    + JacksonUtils.toJson(serviceInfo.getHosts()));
        }

        return serviceInfo;
    }
  • 来自于Server的数据是最新的数据, oldService为当前注册表中的服务SericeInfo, 为旧的服务。
  • 若当前注册表中存在当前服务, 将来自于server的数据更新到本地注册表
    • 将来自于Server的serviceInfo替换掉注册表中的当前服务, 保持至serviceInfoMap
    • 遍历本地注册表中当前服务的所有instance实例, 将当前遍历的instance主机的ip:port作为key,instance作为value,写入到一个新的map, oldHostMap
    • 遍历来自于server的当前服务的所有instance实例, 将当前遍历的instance主机的ip:port作为key,instance作为value,入到一个新的map, newHostMap
    • 创建3个Set集合, modHosts存放的是两个map(oldHostMap与newHostMap)中都有的ip:port, 但它们的instance不相同,此时会将来自于server的instance写入到这个set, newHosts存放的是只有newHostMap中存在的instance,即在server端新增的instance, remvHosts存放的是只有oldHostMap中存在的instance,即在server端被删除的instance
    • 遍历来自于server的主机, 如果在注册表中存在该ip:port,但这两个instance又不同,则将这个instance写入到modHosts, 如果若注册表中不存在该ip:port,说明这个主机是新增的,则将其写入到newHosts
    • 遍历来自于本地注册表的主机, 如果注册表中存在,但来自于server的serviceInfo中不存在, 说明这个instance被干掉了,将其写入到remvHosts
    • 如果modHosts中存在对象, 那么变更心跳信息BeatInfo, updateBeatInfo()
    • 只要发生了变更,就将这个发生变更的serviceInfo记录到一个缓存队列, DiskCache
  • 若本地注册表中就没有当前服务,则直接将来自于server的serviceInfo写入到注册表, 将这个发生变更的serviceInfo记录到一个缓存队列, DiskCache

2.4 定时更新本地注册表中的当前服务

HostReactor#scheduleUpdateIfAbsent()
发起一个定时任务, 定时更新本地注册表中的当前服务

在HostReator#getServiceInfo()最后有一个定时任务的代码

在这里插入图片描述

    public void scheduleUpdateIfAbsent(String serviceName, String clusters) {
        // futureMap是一个缓存map,其key为 groupId@@微服务名称@@clusters
        // value是一个定时异步操作对象
        // 这种结构称之为:双重检测锁,DCL,Double Check Lock
        // 该结构是为了避免在并发情况下,多线程重复写入数据
        // 该结构的特征:
        // 1)有两个不为null的判断
        // 2)有共享集合
        // 3)有synchronized代码块
        if (futureMap.get(ServiceInfo.getKey(serviceName, clusters)) != null) {
            return;
        }

        synchronized (futureMap) {
            if (futureMap.get(ServiceInfo.getKey(serviceName, clusters)) != null) {
                return;
            }

            // 创建一个定时异步操作对象,并启动这个定时任务
            ScheduledFuture<?> future = addTask(new UpdateTask(serviceName, clusters));
            // 将这个定时异步操作对象写入到缓存map
            futureMap.put(ServiceInfo.getKey(serviceName, clusters), future);
        }
    }

这个是一个DCL结构, 结构是为了避免在并发情况下,多线程重复写入数据。

UpdateTask#run() 启动定时任务

        @Override
        public void run() {
            long delayTime = DEFAULT_DELAY;

            try {
                // 从本地注册表中获取当前服务
                ServiceInfo serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters));

                // 若本地注册表中不存在该服务,则从server获取到后,更新到本地注册表
                if (serviceObj == null) {
                    // 从server获取当前服务,并更新到本地注册表
                    updateService(serviceName, clusters);
                    return;
                }

                // 处理本地注册表中存在当前服务的情况
                // 1)serviceObj.getLastRefTime() 获取到的是当前服务最后被访问的时间,这个时间
                // 是来自于本地注册表的,其记录的是所有提供这个服务的instance中最后一个instance
                // 被访问的时间
                // 2)缓存lastRefTime 记录的是当前instance最后被访问的时间
                // 若1)时间 小于 2)时间,说明当前注册表应该更新的
                if (serviceObj.getLastRefTime() <= lastRefTime) {
                    updateService(serviceName, clusters);
                    serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters));
                } else {
                    // if serviceName already updated by push, we should not override it
                    // since the push data may be different from pull through force push
                    refreshOnly(serviceName, clusters);
                }

                // 将来自于注册表的这个最后访问时间更新到当前client的缓存
                lastRefTime = serviceObj.getLastRefTime();

                if (!eventDispatcher.isSubscribed(serviceName, clusters) && !futureMap
                        .containsKey(ServiceInfo.getKey(serviceName, clusters))) {
                    // abort the update task
                    NAMING_LOGGER.info("update task is stopped, service:" + serviceName + ", clusters:" + clusters);
                    return;
                }
                if (CollectionUtils.isEmpty(serviceObj.getHosts())) {
                    incFailCount();
                    return;
                }
                delayTime = serviceObj.getCacheMillis();
                resetFailCount();
            } catch (Throwable e) {
                incFailCount();
                NAMING_LOGGER.warn("[NA] failed to update serviceName: " + serviceName, e);
            } finally {
                // 开启下一次的定时任务
                executor.schedule(this, Math.min(delayTime << failCount, DEFAULT_DELAY * 60), TimeUnit.MILLISECONDS);
            }
        }
  1. 从本地注册表中获取当前服务, 若本地注册表中不存在该服务, Server获取后, 更新到本地注册表ServiceInfo
    在这里插入图片描述
    这里的updateService()在之前分析过。

  2. 处理本地注册表中存在当前服务的情况, 判断当前服务最后被访问的时间和当前instance最后被访问的时间的大小
    如果当前服务最后被访问的时间小于等于当前instance最后被访问的时间的话, 那么说明当前注册表应该更新的
    在这里插入图片描述
    这里的updateService()在之前分析过。

  3. 最后开启下一次的定时任务
    在这里插入图片描述

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

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

相关文章

DrissionPage学习(一)

一、下载适合mac环境的chromedriver.exe 1.查找chrome浏览的版本 方法二&#xff1a;查看chrome版本信息 2.确定版本后在下载chrome对应的chromedriver.exe 下载地址&#xff1a;http://chromedriver.storage.googleapis.com/index.html 3.在文件夹中找到下载的文件chro…

MyBatis-Plus or() equls() 连用的格式为 a AND (b OR c)

正确示范&#xff1a; userInterfaceQueryWrapper.and(qw -> qw .eq("status", UserInterfaceInfoStatusEnum.OFFLINE) .or() .eq("status", UserInterfaceInfoStatusEnum.TRYOUT));错误示范&#xff1a; userInterfaceQueryWrapper.eq("status&…

分布式项目10.jsonp 使用js中script的属性src进行服务器之间的访问

一般使用ajax来访问不同服务器的数据&#xff0c;可行吗&#xff1f; 做个实验&#xff1a; 第一步&#xff1a;在本服务器中使用ajax技术访问本服务器数据 <!DOCTYPE html> <html> <head> <meta charset"UTF-8"> <title>测试JSON跨域…

NFT游戏Mythical Beings将参加NFT Polygon 在线展会

Mythical Beings神秘生物是由Tarasca Art & Games 开发的基于区块链的卡牌收集游戏。游戏中每张卡牌所拥有的属性和背后的故事都是独一无二的&#xff0c;Mythical Beings不仅具有游戏属性&#xff0c;还兼具故事的传承。 作为一款跨链Polygon的NFT游戏&#xff0c;Mythic…

Transformer架构解析

1.Transformer架构图 本文主要来自&#xff1a;http://nlp.seas.harvard.edu/annotated-transformer/#hardware-and-schedule 论文地址: https://arxiv.org/pdf/1810.04805.pdf 1.1 Transformer模型的作用 基于seq2seq架构的transformer模型可以完成NLP领域研究的典型任务,…

ThinkPHP6模型中的获取器,修改器及搜索器的简单使用

ThinkPHP6模型中的获取器&#xff0c;修改器及搜索器的简单使用 1. 获取器2. 修改器3. 搜索器 1. 获取器 获取器的作用是对模型实例的&#xff08;原始&#xff09;数据做出自动处理。一个获取器对应模型的一个特殊方法&#xff08;该方法必须为public类型&#xff09;&#x…

MySQL(用户管理)

文章目录 1 用户1.1 用户信息1.2 创建用户1.3 删除用户1.4 修改用户密码 2 数据库的权限2.1 给用户授权2.2 回收权限 1 用户 1.1 用户信息 MySQL中的用户&#xff0c;都存储在系统数据库mysql的user表中 host&#xff1a; 表示这个用户可以从哪个主机登陆&#xff0c;如果是l…

GPT专业应用:如何让GPT策划方案

身为一名职场打工人&#xff0c;或多或少会面临需要写策划案的难题。 不管是策划一场线下活动&#xff0c;还是策划业务发展的方向&#xff1b; 甚至到生活中还需要策划婚礼&#xff0c;策划房屋装修&#xff0c;策划和朋友的聚会等等。那么如何快速积累经验&#xff0c;找准…

项目管理基础:什么是项目管理?

一、项目管理的缘起与发展 项目管理并非基础学科&#xff0c;它是人类在生产实践中不断的经验积累所总结归纳的一门学科&#xff0c;在人们不断的完善与修订下逐步形成了现有的项目管理知识体系。 项目经理是随着人们对项目管理要求的提升而逐渐分化出的一个专业职位&#xf…

SNAT与DNAT的应用

目录 一、SNAT概述 1.SNAT应用环境——局域网共享上网 2. SNAT工作原理 2.1未作SNAT转换时 2.2进行SNAT转换后 二、配置SNAT策略 1.开启IP路由转发 2.SNAT转换 3.模拟实验 3.1服务端 3.2网关服务器 3.3网关服务器设置iptables规则 3.4客户端 3.5实验结果 三、DNAT概…

Tauri应用开发(二):创建第一个Tauri应用

创建tauri应用 推荐参考官方文档&#xff1a;https://tauri.app/v1/guides/ 创建命令&#xff1a; npm create tauri-applatest&#x1f4a1;注意&#xff1a;请确保Node.js和Rust已经正确安装 在创建过程中&#xff0c;需要根据提示选择配置项。 主要配置有&#xff1a; 项目…

在Postman接口工具,脚本中发送请求(pm.sendRequest)

Postman的Collection(集合)/Folder(集合的子文件夹)/Request(请求)都有Pre-request script和Tests两个脚本区域, 分别可以在发送请求前和请求后使用脚本(基于Javascript实现各种操作) 在遇到有依赖的接口时,比如需要登录或者需要从前一个接口的结果中获取参数时,我们往往需要在…

vue+element实现美观大方好看的音乐网站,仿照咪咕音乐网

目录 一、前言 二、实现及效果图 1.效果图 2.项目结构、设计说明 3.顶部菜单 4.首页轮播图 5.歌单推荐 三、总结 一、前言 咪咕音乐网&#xff0c;最近看到其官网&#xff0c;第一感觉真的很美观大方&#xff0c;有被它惊艳到。所以作者忍不住做了一版仿照咪咕音乐的demo。…

庄懂的TA笔记(十七)<特效:屏幕UV + 屏幕扰动>

庄懂的TA笔记&#xff08;十七&#xff09;&#xff1c;特效&#xff1a;屏幕UV 屏幕扰动&#xff1e; 大纲&#xff1a; 目录 庄懂的TA笔记&#xff08;十七&#xff09;&#xff1c;特效&#xff1a;屏幕UV 屏幕扰动&#xff1e; 大纲&#xff1a; 正文&#xff1a; 一…

UNIX环境高级编程——线程控制

12.1 引言 本章讲解控制线程行为方面的详细内容&#xff0c;介绍线程属性和同步原语属性。 12.2 线程限制 12.3 线程属性 线程属性对象用pthread_attr_t结构表示&#xff0c;可以用这个结构修改线程默认属性&#xff0c;并把这些属性与创建的线程联系起来。 #include <p…

腾讯C++二面,全程2小时追问基础!

今天给大家分享星球一位同学腾讯面经&#xff0c;主要摘取了部分一二面经&#xff0c;然后部分问题我做了补充说明~ 星球原文&#xff1a;https://t.zsxq.com/0eO4O13HV&#xff0c;已获授权 一面 1、C11有哪些新特性&#xff0c;有哪些新关键字 2、C中结构体占多少字节&…

51单片机——I2C-EEPROM实验,小白讲解,相互学习

I2C介绍 I2C&#xff08;Inter&#xff0d;Integrated Circuit&#xff09;总线是由 PHILIPS 公司开发的两线式 串行总线&#xff0c;用于连接微控制器及其外围设备。是微电子通信控制领域广泛采用的 一种总线标准。它是同步通信的一种特殊形式&#xff0c;具有接口线少&#x…

【逆向工程核心原理:SEH】

SEH SEH是Windows操作系统提供的异常处理机制&#xff0c;在程序源代码中使用__ try、 __except、__finally关键字来具体实现。主要用在反调试中。 注&#xff1a; SEH与C中的try. catch 异常处理具有不同结构。从时间上看&#xff0c;与C的try、catch异常处理相比&#xff0…

uni-app--》uView组件库:提升您的uni-app开发体验

&#x1f3cd;️作者简介&#xff1a;大家好&#xff0c;我是亦世凡华、渴望知识储备自己的一名在校大学生 &#x1f6f5;个人主页&#xff1a;亦世凡华、 &#x1f6fa;系列专栏&#xff1a;uni-app &#x1f6b2;座右铭&#xff1a;人生亦可燃烧&#xff0c;亦可腐败&#xf…

MMDet3d样本均衡

MMDet3d样本均衡 文章目录 MMDet3d样本均衡CBGSDataset训练时数据是200帧&#xff0c;后面处理时&#xff0c;dataloader中数据变成了460帧&#xff0c;怎么均衡的&#xff1f;思考抽帧数计算某个类别帧数为0 Reference欢迎关注公众号【三戒纪元】 CBGSDataset **CBGS &#x…