Nacos源码系列—关于服务注册的那些事
点赞再看,养成习惯,微信搜索【牧小农】关注我获取更多资讯,风里雨里,小农等你,很高兴能够成为你的朋友。
项目源码地址:公众号回复 nacos,即可免费获取源码
简介
首先我们在看Nacos源码之前,要先想想为什么我们要读源码?是为了装杯?还是为了在心仪的女神面前给她娓娓道来展示自己的代码功底?当然不全是!
这都不是我们读源码的最终目的。作为一名技术人,上面的都是浮云,真正激励我们的应该是能够提升我们技术功底和整体技术大局观。此乃大道也!闲言少叙,接下来我们就来看一看,看源码究竟有什么好处
- 提升技术功底: 当我们去看源码的时候,能够学习源码里面优秀的设计思想,还含有设计模式和并发编程技术,解决问题的思路,能够知其所以然
- 新技术学习能力: 当我们看多了源码,对于一个新技术或者框架的掌握速度会有大幅度提升,能根据经验或官网资料快速掌握底层的实现,技术更新迭代也可以更快的入手
- 快速解决问题能力: 遇到问题,尤其是框架源码的问题,能够更快速的定位
- 面试获取更高成功率: 现在出去面试,一般中高级一点的,都会问到框架源码级别的实现,如果能够说出来,可以提升面试的成功率和薪资待遇,源码面试是区别程序员水平另一面镜子
- 认识更多圈子: 多活跃开源社区,熟读源码后多思考,发现问题或需求主动参与开源技术研发,与圈内大牛成为为朋友
阅读源码的方法
- 搭建入门demo: 我们可以先看一下官网提供的文档,搭建Demo,快速掌握框架的基本使用
- 看核心代码: 对于初次看源码的同学,不要太过于关注源码的细枝末节,先把主要核心流程梳理出来,找到其入口,分析静态代码,如果遇到问题,可以进行断点调试。
- 绘图和笔记: 梳理好核心功能后,可以用流程图记录下来,好记性不如烂笔头,同时对关键的源码部分可以进行备注,分析参数的变化。同时要善于用Debug,来观看源码的执行过程
- 复习总结: 当我们把框架的所有功能点的源码都分析完成后,回到主流程在梳理一遍,最后在自己脑袋中形成一个闭环,这样源码的核心内容和主流程就基本上理解了。
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源码下载
首先我们需要将Nacos的源码下载下来,下载地址:https://github.com/alibaba/nacos

我们将源码下下来以后,导入到idea中
proto编译
当我们导入成功以后,会出现程序包com.alibaba.nacos.consistency.entity不存在的错误提示,这是因为Nacos底层的数据通信会基于protobuf对数据做序列化和反序列化,需要先将proto文件编译为对应的Java代码。

最简单的 不安装任何的东西 idea2021.2已经捆绑安装了这个。

可以通过mvn copmpile来在target自动生成他们。

Nacos缺少Istio依赖问题解决
我们只需要在文件根目录下执行以下命令即可:
mvn clean package -Dmaven.test.skip=true -Dcheckstyle.skip=true
做完以上两步,我们就可以启动Nacos的了
启动Nacos
首先我们找到 nacos-console这个模块,这个就是我们的管理后台,找到它的启动类,因为Nacos默认为集群启动,所以我们要设置它为单机启动,方便演示
设置命令:
-Dnacos.standalone=true -Dnacos.home=E:\test\nacos

启动成功后,账号密码:nacos/nacos

到这里我们Nacos的源码启动就完成了。
开启源码
我们先从客户端服务的注册开始说起,我们可以先想一想如果Nacos客户端要注册,会把什么信息传递给服务器?
这里我们可以看到在 nacos-client下的NamingTest有这么一些信息
@Ignore
public class NamingTest {
@Test
public void testServiceList() throws Exception {
//Nacos Server连接信息
Properties properties = new Properties();
//Nacos服务器地址
properties.put(PropertyKeyConst.SERVER_ADDR, "127.0.0.1:8848");
//连接Nacos服务的用户名
properties.put(PropertyKeyConst.USERNAME, "nacos");
//连接Nacos服务的密码
properties.put(PropertyKeyConst.PASSWORD, "nacos");
//实例信息
Instance instance = new Instance();
//实例IP,提供给消费者进行通信的地址
instance.setIp("1.1.1.1");
//端口,提供给消费者访问的端口
instance.setPort(800);
//权重,当前实例的权限,浮点类型(默认1.0D)
instance.setWeight(2);
Map<String, String> map = new HashMap<String, String>();
map.put("netType", "external");
map.put("version", "2.0");
instance.setMetadata(map);
//关键代码 创建自己的实例
NamingService namingService = NacosFactory.createNamingService(properties);
namingService.registerInstance("nacos.test.1", instance);
ThreadUtils.sleep(5000L);
List<Instance> list = namingService.getAllInstances("nacos.test.1");
System.out.println(list);
ThreadUtils.sleep(30000L);
// ExpressionSelector expressionSelector = new ExpressionSelector();
// expressionSelector.setExpression("INSTANCE.metadata.registerSource = 'dubbo'");
// ListView<String> serviceList = namingService.getServicesOfServer(1, 10, expressionSelector);
}
}
上面就是客户端注册的一个测试类,模仿了真实的服务注册到Nacos的过程,包括NacosServer连接、实例的创建、实例属性的赋值、注册实例,所以在这个其中包含了服务注册的核心代码,从这里我们可以大致看出,它包含了两个类的信息:Nacos Server连接信息和实例信息
Nacos Server连接信息:
从上述中我们可以看到有关于Nacos Server连接信息是存储在Properties中,
- Server地址:Nacos服务器地址,属性的key为serverAddr;
- 用户名:连接Nacos服务的用户名,属性key为username,默认值为nacos;
- 密码:连接Nacos服务的密码,属性key为password,默认值为nacos;
实例信息:
从上述测试中我们可以看到注册实例信息用instance进行承载,而实例信息又分为两部分,一个是基础实例信息,一个是元数据信息
实例基础信息:
- instanceId:实例的唯一ID;
- ip:实例IP,提供给消费者进行通信的地址;
- port: 端口,提供给消费者访问的端口;
- weight:权重,当前实例的权限,浮点类型(默认1.0D);
- healthy:健康状况,默认true;
- enabled:实例是否准备好接收请求,默认true;
- ephemeral:实例是否为瞬时的,默认为true;
- clusterName:实例所属的集群名称;
- serviceName:实例的服务信息;
元数据:
元数据类型为HashMap,从当前Demo我们能够看到的数据只有两个
- netType:网络类型,这里设置的值为external(外网)
- version Nacos版本,这里为2.0
除此之外,我们在Instance类中还可以看到一些默认信息,这些方法都是通过get方法进行提供的
//心跳间隙的key,默认为5s,也就是默认5秒进行一次心跳
public long getInstanceHeartBeatInterval() {
return getMetaDataByKeyWithDefault(PreservedMetadataKeys.HEART_BEAT_INTERVAL,
Constants.DEFAULT_HEART_BEAT_INTERVAL);
}
//心跳超时的key,默认为15s,也就是默认15秒收不到心跳,实例将会标记为不健康;
public long getInstanceHeartBeatTimeOut() {
return getMetaDataByKeyWithDefault(PreservedMetadataKeys.HEART_BEAT_TIMEOUT,
Constants.DEFAULT_HEART_BEAT_TIMEOUT);
}
//实例IP被删除的key,默认为30s,也就是30秒收不到心跳,实例将会被移除;
public long getIpDeleteTimeout() {
return getMetaDataByKeyWithDefault(PreservedMetadataKeys.IP_DELETE_TIMEOUT,
Constants.DEFAULT_IP_DELETE_TIMEOUT);
}
//实例ID生成器key,默认为simple;
public String getInstanceIdGenerator() {
return getMetaDataByKeyWithDefault(PreservedMetadataKeys.INSTANCE_ID_GENERATOR,
Constants.DEFAULT_INSTANCE_ID_GENERATOR);
}
为什么要说这个呢?从这些参数中我们就可以了解到,我们服务的心跳间隙是多少以及超时时间,传递什么参数配置什么参数,以此来了解我们的实例是否健康。同时我们也可以看到一个比较关键且核心的类,是真正创建实例的类 ——NamingService
NamingService
NamingService是Nacos对外提供的一个统一的接口,当我们点进去查看,可以看到大概一下几个方法,这些方法提供了不同的重载方法,方便我们用于不同的场景。

//服务实例注册
void registerInstance(...) throws NacosException;
//服务实例注销
void deregisterInstance(...) throws NacosException;
//获取服务实例列表
List<Instance> getAllInstances(...) throws NacosException;
//查询健康服务实例
List<Instance> selectInstances(...) throws NacosException;
//查询集群中健康的服务实例
List<Instance> selectInstances(....List<String> clusters....)throws NacosException;
//使用负载均衡策略选择一个健康的服务实例
Instance selectOneHealthyInstance(...) throws NacosException;
//订阅服务事件
void subscribe(...) throws NacosException;
//取消订阅服务事件
void unsubscribe(...) throws NacosException;
//获取所有(或指定)服务名称
ListView<String> getServicesOfServer(...) throws NacosException;
//获取所有订阅的服务
List<ServiceInfo> getSubscribeServices() throws NacosException;
//获取Nacos服务的状态
String getServerStatus();
//主动关闭服务
void shutDown() throws NacosException;
NamingService的实例化是通过NacosFactory.createNamingService(properties);实现的,内部源码是通过反射来实现实例化过程
NamingService namingService = NacosFactory.createNamingService(properties);
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);
return (NamingService) constructor.newInstance(properties);
} catch (Throwable e) {
throw new NacosException(NacosException.CLIENT_INVALID_PARAM, e);
}
}
接下来我们就来看一看NamingService的具体实现
//调用registerInstance方法
namingService.registerInstance("nacos.test.1", instance);
@Override
public void registerInstance(String serviceName, Instance instance) throws NacosException {
//默认的分组为“DEFAULT_GROUP”
registerInstance(serviceName, Constants.DEFAULT_GROUP, instance);
}
@Override
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
//检查心跳时间是否正常
NamingUtils.checkInstanceIsLegal(instance);
//通过代理注册服务
clientProxy.registerService(serviceName, groupName, instance);
}
心跳检测代码
//心跳间隙超过限制 返回错误
if (instance.getInstanceHeartBeatTimeOut() < instance.getInstanceHeartBeatInterval()
|| instance.getIpDeleteTimeout() < instance.getInstanceHeartBeatInterval()) {
throw new NacosException(NacosException.INVALID_PARAM,
"Instance 'heart beat interval' must less than 'heart beat timeout' and 'ip delete timeout'.");
}
通过代理注册服务,我们了解到clientProxy代理接口是通过NamingClientProxyDelegate来完成,我们可以在init构造方法中得出,具体的实例对象
private void init(Properties properties) throws NacosException {
//使用NamingClientProxyDelegate来完成
this.clientProxy = new NamingClientProxyDelegate(this.namespace, serviceInfoHolder, properties, changeNotifier);
}

NamingClientProxyDelegate实现
在NamingClientProxyDelegate中,真正调用注册服务的并不是代理实现类,而且先判断当前实例是否为瞬时对象后,来选择对应的客户端代理来进行请求。
@Override
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
getExecuteClientProxy(instance).registerService(serviceName, groupName, instance);
}
如果当前实力是瞬时对象,则采用gRPC协议(NamingGrpcClientProxy)进行请求,否则采用Http协议(NamingHttpClientProxy),默认为瞬时对象,在2.0版本中默认采用gRPC协议进行与Nacos服务进行交互
//判断当前实例是否为瞬时对象
private NamingClientProxy getExecuteClientProxy(Instance instance) {
return instance.isEphemeral() ? grpcClientProxy : httpClientProxy;
}
NamingGrpcClientProxy中的实现
@Override
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance {}", namespaceId, serviceName,
instance);
//数据的缓存
redoService.cacheInstanceForRedo(serviceName, groupName, instance);
//gRPC进行服务调用
doRegisterService(serviceName, groupName, instance);
}
大体关系图如下所示:

Nacos客户端在项目的应用
- 我们想要让某一个服务注册到Nacos中,首先要引入一个依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
- 在依赖中,去查看SpringBoot自动装配文件自动装配文件
META-INF/spring.factories

- 通过SpringBoot的自动装配来加载
EnableAutoConfiguration对应的类,这里我们可以看到很多有关于Nacos相关的类,怎么知道哪个是我们真正需要关心的类,服务在注册的时候走的是哪个,一般自动装配,我们都会找到带有“Auto”关键字的文件进行查看,然后在结合我们需要找的,我们是客户端注册服务,所以我们大体可以定位到NacosServiceRegistryAutoConfiguration这个文件

- 查看
NacosServiceRegistryAutoConfiguration源码,在这里我们只需要关注最核心的nacosAutoServiceRegistration方法


而我们真正关心的只有三个类NacosAutoServiceRegistration类是注册的核心,我们来看一下它的继承关系

@Bean
@ConditionalOnBean(AutoServiceRegistrationProperties.class)
public NacosAutoServiceRegistration nacosAutoServiceRegistration(
NacosServiceRegistry registry,
AutoServiceRegistrationProperties autoServiceRegistrationProperties,
NacosRegistration registration) {
return new NacosAutoServiceRegistration(registry,
autoServiceRegistrationProperties, registration);
}
- 从上述内容中我们可以知道,Nacos服务自动注册是从
NacosServiceRegistryAutoConfiguration类开始的,并自动注册到NacosAutoServiceRegistration类中。
在下图中我们可以看到,主要是调用了super 方法,所以我们继续查看该类的构造方法:AbstractAutoServiceRegistration
public class NacosAutoServiceRegistration
extends AbstractAutoServiceRegistration<Registration> {
public NacosAutoServiceRegistration(ServiceRegistry<Registration> serviceRegistry,
AutoServiceRegistrationProperties autoServiceRegistrationProperties,
NacosRegistration registration) {
super(serviceRegistry, autoServiceRegistrationProperties);
this.registration = registration;
}
}
AbstractAutoServiceRegistration实现了ApplicationListener接口,用来监听Spring容器启动过程中WebServerInitializedEvent事件,一般如果我们实现这个类的时候,会实现一个方法onApplicationEvent(),这个方法会在我们项目启动的时候触发

@Override
@SuppressWarnings("deprecation")
public void onApplicationEvent(WebServerInitializedEvent event) {
bind(event);
}
由此我们可以看到bind里面的这个方法
@Deprecated
public void bind(WebServerInitializedEvent event) {
//获取 ApplicationContext对象
ApplicationContext context = event.getApplicationContext();
//判断服务的 Namespace
if (context instanceof ConfigurableWebServerApplicationContext) {
if ("management".equals(((ConfigurableWebServerApplicationContext) context).getServerNamespace())) {
return;
}
}
//记录当前服务的端口
this.port.compareAndSet(0, event.getWebServer().getPort());
//【核心】启动注册流程
this.start();
}
start()方法调用register();方法来注册服务
public void start() {
if (!isEnabled()) {
if (logger.isDebugEnabled()) {
logger.debug("Discovery Lifecycle disabled. Not starting");
}
return;
}
// only initialize if nonSecurePort is greater than 0 and it isn't already running
// because of containerPortInitializer below
//如果服务是没有运行状态时,进行初始化
if (!this.running.get()) {
//发布服务开始注册事件
this.context.publishEvent(new InstancePreRegisteredEvent(this, getRegistration()));
//【核心】注册服务
register();
if (shouldRegisterManagement()) {
registerManagement();
}
//发布注册完成事件
this.context.publishEvent(new InstanceRegisteredEvent<>(this, getConfiguration()));
//服务状态设置为运行状态
this.running.compareAndSet(false, true);
}
}
NacosServiceRegistry.register()方法,如下所示:
@Override
public void register(Registration registration) {
//判断ServiceId是否为空
if (StringUtils.isEmpty(registration.getServiceId())) {
log.warn("No service to register for nacos client...");
return;
}
//获取Nacos的服务信息
NamingService namingService = namingService();
//获取服务ID和分组
String serviceId = registration.getServiceId();
String group = nacosDiscoveryProperties.getGroup();
//构建instance实例(IP/Port/Weight/clusterName.....)
Instance instance = getNacosInstanceFromRegistration(registration);
try {
//向服务端注册此服务
namingService.registerInstance(serviceId, group, instance);
log.info("nacos registry, {} {} {}:{} register finished", group, serviceId,
instance.getIp(), instance.getPort());
}
catch (Exception e) {
if (nacosDiscoveryProperties.isFailFast()) {
log.error("nacos registry, {} register failed...{},", serviceId,
registration.toString(), e);
rethrowRuntimeException(e);
}
else {
log.warn("Failfast is false. {} register failed...{},", serviceId,
registration.toString(), e);
}
}
}
NacosNamingService.registerInstance()方法,如下:
@Override
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
//检查超时参数是否异常,心跳超时时间(15s)必须大于心跳间隙(5s)
NamingUtils.checkInstanceIsLegal(instance);
//拼接服务名,格式:groupName@@serviceName
String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
//判断是否为临时实例,默认为true
if (instance.isEphemeral()) {
//临时实例,定时向Nacos服务发送心跳
BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
beatReactor.addBeatInfo(groupedServiceName, beatInfo);
}
//【核心】发送注册服务实例请求
serverProxy.registerService(groupedServiceName, groupName, instance);
}
在registerService中我们可以看到Nacos服务注册接口需要的完整参数
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());
//当前实例IP
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);
}
补充
在这里我们会发现我们请求实例接口的地址为/nacos/v1/ns/instance,其实这个在官网中也有提供对应的地址给我们,并且是对应的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eWNVqilf-1651761976327)(https://files.mdnice.com/user/29961/1675a7d6-b364-418d-9d97-87bc7180863c.png)]


客户端注册流程图

总结
以上就是Nacos的客户端注册流程,如果您对文中有疑问或者问题,欢迎在下方留言,小农看见了会第一时间回复,大家加油~
Nacos源码系列—关于服务注册的那些事的更多相关文章
- Nacos源码系列—服务端那些事儿
点赞再看,养成习惯,微信搜索[牧小农]关注我获取更多资讯,风里雨里,小农等你,很高兴能够成为你的朋友. 项目源码地址:公众号回复 nacos,即可免费获取源码 前言 在上节课中,我们讲解了客户端注册服 ...
- Nacos源码系列—订阅机制的前因后果(下)
点赞再看,养成习惯,微信搜索[牧小农]关注我获取更多资讯,风里雨里,小农等你,很高兴能够成为你的朋友. 项目源码地址:公众号回复 nacos,即可免费获取源码 事件发布 在上一节中我们讲解了在Noti ...
- Nacos源码系列—订阅机制的前因后果(上)
点赞再看,养成习惯,微信搜索[牧小农]关注我获取更多资讯,风里雨里,小农等你,很高兴能够成为你的朋友. 项目源码地址:公众号回复 nacos,即可免费获取源码 前因 我们在了解Nacos订阅机制之前, ...
- Spring Cloud Eureka源码分析之服务注册的流程与数据存储设计!
Spring Cloud是一个生态,它提供了一套标准,这套标准可以通过不同的组件来实现,其中就包含服务注册/发现.熔断.负载均衡等,在spring-cloud-common这个包中,org.sprin ...
- Nacos源码结构和AP模式注册中心实现介绍
前言 NacosAP模式源码分析目录 微服务下的注册中心如何选择 Nacos使用和注册部分源码介绍 Nacos服务心跳和健康检查源码介绍 Nacos服务发现 Nacos源码结构介绍 Nacos版本基于 ...
- hbase源码系列(十二)Get、Scan在服务端是如何处理
hbase源码系列(十二)Get.Scan在服务端是如何处理? 继上一篇讲了Put和Delete之后,这一篇我们讲Get和Scan, 因为我发现这两个操作几乎是一样的过程,就像之前的Put和Del ...
- SpringCloud 源码系列(6)—— 声明式服务调用 Feign
SpringCloud 源码系列(1)-- 注册中心 Eureka(上) SpringCloud 源码系列(2)-- 注册中心 Eureka(中) SpringCloud 源码系列(3)-- 注册中心 ...
- 11 hbase源码系列(十一)Put、Delete在服务端是如何处理
hbase源码系列(十一)Put.Delete在服务端是如何处理? 在讲完之后HFile和HLog之后,今天我想分享是Put在Region Server经历些了什么?相信前面看了<HTab ...
- Spring源码系列 — Bean生命周期
前言 上篇文章中介绍了Spring容器的扩展点,这个是在Bean的创建过程之前执行的逻辑.承接扩展点之后,就是Spring容器的另一个核心:Bean的生命周期过程.这个生命周期过程大致经历了一下的几个 ...
随机推荐
- Leetcode26——删除有序数组中的重复项(双指针法)
Leetcode26--删除有序数组中的重复项(双指针法) 1. 题目简述 给你一个升序排列的数组 nums ,请你原地 删除重复出现的元素,使每个元素只出现一次 ,返回删除后数组的新长度.元素的相对 ...
- 遇到MyBatis-Plus的错误之“Table 'mybatis_plus.user' doesn't exist”
一.问题 Table 'mybatis_plus.user' doesn't exist 二.原因 表中没有user表 三.解决方案 生成user表既可 四.结果图 运行后显示查询出来的数据 五.总结 ...
- Matlab解析LQR与MPC的关系
mathworks社区中的这个资料还是值得一说的. 1 openExample('mpc/mpccustomqp') 我们从几个角度来解析两者关系,简单的说就是MPC是带了约束的LQR. 在陈虹模型预 ...
- PCB中加入任意LOGO图文说明 精心制作
防静电图 首先我们要对下载下来的图片进行处理否则Altium designer6.9会提示装载的图片不是单色的,用Photoshop CS打开开始下载的图片 选择 图像→模式→灰度 在选择 图像→模式 ...
- 从零开始画自己的DAG作业依赖图(四)--节点连线优化版
概述 上个版本简单的连线在一些复杂场景,尤其层级比较多,连线跨层级比较多的情况下,会出现线条会穿过矩形的情况,这一讲就是在这个基础上,去优化这个连线. 场景分析 在下面几种情况下,简单版本的画法已经没 ...
- ffmpeg将视频生成gif
1.安装ffmpeg 2.cmd中输入 ffmpeg -i 0.mp4 -f gif 0.gif 即可将视频转为gif
- python-成绩转换
本题要求编写程序将一个百分制成绩转换为五分制成绩.转换规则: 大于等于90分为A: 小于90且大于等于80为B: 小于80且大于等于70为C: 小于70且大于等于60为D: 小于60为E. 输入样例: ...
- 【uniapp 开发】Date.parse Firefox返回Nan的解决办法
- 【Android开发】通过 style 设置状态栏,导航栏等的颜色
<style name="test"> <!--状态栏颜色--> <item name="colorPrimaryDark"> ...
- 安卓性能测试之 adb shell 常用命令
pm list packages 列出包名adb shell pm list packages:列出所有的包名.adb shell dumpsys package:列出所有的安装应用的信息adb sh ...