Nacos注册中心——源码解析客户端自动注册

Nacos 阿里巴巴开发的一款产品为了服务微服务而诞生 支持服务注册 心跳检测 配置中心 支持AP+CP切换 一般情况下满足AP

采用SpringCloudAlibaba构建的服务可以知道在项目启动的时候会自动将当前服务注册到Nacos服务端中。

SpringCloudAlibaba–Nacos自动注册实现流程图:
图文
图片链接:https://www.processon.com/view/link/6068719df346fb0aa98425c2

1.Nacos客户端项目启动完成自动注册流程

在springboot项目中启动项目spring容器初始化之后spring会发布一个事件(WebServerInitializedEvent) SpringCloud-common包当中有一个监听器AbstractAutoServiceRegistration.class一旦事件发布就会被这个类监听到但是这个类是抽象的具体响应监听交给子类实现这就是典型的模板方法设计模式

public abstract class AbstractAutoServiceRegistration<R extends Registration>
		implements AutoServiceRegistration, ApplicationContextAware,
		ApplicationListener<WebServerInitializedEvent>

可以看到AbstractAutoServiceRegistration实现了ApplicationListener
WebServerInitializedEvent事件进行监听
那么来看这个事件是在哪里什么时候被发布可以看到在spring源码当中的调用链:

ApplicationContext>WebServerApplicationContext>ConfigurableWebServerApplicationContext>ServletWebServerApplicationContext

ServletWebServerApplicationContext:该类是自springboot诞生就衍生出来的,是spring框架的应用上下文Application的子类。

protected void finishRefresh() {
        super.finishRefresh();
        WebServer webServer = this.startWebServer();
        if (webServer != null) {
            this.publishEvent(new ServletWebServerInitializedEvent(webServer, this));
        }

    }

在这个类当中有一个finishRefresh方法的钩子方法()
在Springboot启动tomcat初始化容器执行到refresh方法我都知道finishRefreshs在spring容器初始化refresh方法中 bean初始化完成之后执行的一个方法ServletWebServerApplicationContext中的finishRefresh在该方法中publish了一个ServletWebServerInitializedEvent事件

public class ServletWebServerInitializedEvent extends WebServerInitializedEvent {
    private final ServletWebServerApplicationContext applicationContext;

    public ServletWebServerInitializedEvent(WebServer webServer, ServletWebServerApplicationContext applicationContext) {
        super(webServer);
        this.applicationContext = applicationContext;
    }

    public ServletWebServerApplicationContext getApplicationContext() {
        return this.applicationContext;
    }
}

看源码得知ServletWebServerInitializedEvent继承了WebServerInitializedEvent
到此我们可以得知在Spring初始化Bean之后会发布一些事件 而当发布WebServerInitializedEvent事件的时候AbstractAutoServiceRegistration自动注册类就可以监听到这个事件继而响应事件。

当AbstractAutoServiceRegistration监听到事件就会执行ApplicationListener的实现方法onApplicationEvent

@Override
	@SuppressWarnings("deprecation")
	public void onApplicationEvent(WebServerInitializedEvent event) {
		bind(event);
	}

	@Deprecated
	public void bind(WebServerInitializedEvent event) {
		ApplicationContext context = event.getApplicationContext();
		if (context instanceof ConfigurableWebServerApplicationContext) {
			if ("management".equals(((ConfigurableWebServerApplicationContext) context)
					.getServerNamespace())) {
				return;
			}
		}
		this.port.compareAndSet(0, event.getWebServer().getPort());
		this.start();
	}

onApplicationEvent()调用了bind()而bind方法又调用start()看源码得知start中调用了注册的方法register() 要注意的是在start中调的register方法是执行子类NacosAutoServiceRegistration中register方法 做了一些验证等…
AbstractAutoServiceRegistration>NacosAutoServiceRegistration

protected void register() {
        if (!this.registration.getNacosDiscoveryProperties().isRegisterEnabled()) {
            log.debug("Registration disabled.");
        } else {
            if (this.registration.getPort() < 0) {
                this.registration.setPort(this.getPort().get());
            }

            super.register();
        }
    }

可以看到在子类中的register()方法里面又调用了父类的register()
而父类的register当中调用的是serviceRegistry接口实现类中的register()

在springboot项目启动的时候会去扫描META-INF下的spring.factories文件在这个文件当中nacos配置了一个类NacosDiscoveryAutoConfiguration当这个类被扫描到的时候进行实例化初始化成为一个Bean 在初始过程这个类当中由注入了两个对象
@Bean NacosServiceRegistry和NacosAutoServiceRegistration
所以NacosServiceRegistry和NacosAutoServiceRegistration 都会被spring实例化初始化后成为一个Bean存在容器当中

public class NacosDiscoveryAutoConfiguration {
    public NacosDiscoveryAutoConfiguration() {
    }

    @Bean
    public NacosServiceRegistry nacosServiceRegistry(NacosDiscoveryProperties nacosDiscoveryProperties) {
        return new NacosServiceRegistry(nacosDiscoveryProperties);
    }

    @Bean
    @ConditionalOnBean({AutoServiceRegistrationProperties.class})
    public NacosRegistration nacosRegistration(NacosDiscoveryProperties nacosDiscoveryProperties, ApplicationContext context) {
        return new NacosRegistration(nacosDiscoveryProperties, context);
    }

    @Bean
    @ConditionalOnBean({AutoServiceRegistrationProperties.class})
    public NacosAutoServiceRegistration nacosAutoServiceRegistration(NacosServiceRegistry registry, AutoServiceRegistrationProperties autoServiceRegistrationProperties, NacosRegistration registration) {
        return new NacosAutoServiceRegistration(registry, autoServiceRegistrationProperties, registration);
    }
  • NacosAutoServiceRegistration继承了AbstractAutoServiceRegistration而AbstractAutoServiceRegistration又实现ApplicationListener ApplicationListene是一个监听器 java多态的体现子类继承了父类的特性所以NacosAutoServiceRegistration也是一个监听器

  • NacosServiceRegistry实现了ServiceRegistry
    ServiceRegistry是spring提供的一个接口类 当中定义了注册服务,关闭服务,撤销等方法 实现类NacosServiceRegistry就实现了注册等方法完成了注册服务等功能

NacosServiceRegistry ——register()

public void register(Registration registration) {
        if (StringUtils.isEmpty(registration.getServiceId())) {
            log.warn("No service to register for nacos client...");
        } else {
            String serviceId = registration.getServiceId();
            Instance instance = this.getNacosInstanceFromRegistration(registration);

            try {
                this.namingService.registerInstance(serviceId, instance);
                log.info("nacos registry, {} {}:{} register finished", new Object[]{serviceId, instance.getIp(), instance.getPort()});
            } catch (Exception var5) {
                log.error("nacos registry, {} register failed...{},", new Object[]{serviceId, registration.toString(), var5});
            }

        }
    }

在实现类NacosServiceRegistry中的register方法首先会拿到我们在yml中配置好的当前注册服务名称 再拿到当前实例所有信息可以知道Registration的底层肯定是用到相关的读取文件配置信息的一些技术 拿到信息之后封装成某一个对象 最后可以在Registration当中拿到这些信息之后赋给了Instanc实例对象
调用注册实例的方法registerInstance传入了当前注册的信息实例Instanc

public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
        /*
        * 客户端注册信息    beatInfo封装客户端信息 心跳包
        * */
        if (instance.isEphemeral()) {
            BeatInfo beatInfo = new BeatInfo();
            beatInfo.setServiceName(NamingUtils.getGroupedName(serviceName, groupName));
            beatInfo.setIp(instance.getIp());
            beatInfo.setPort(instance.getPort());
            beatInfo.setCluster(instance.getClusterName());
            beatInfo.setWeight(instance.getWeight());
            beatInfo.setMetadata(instance.getMetadata());
            beatInfo.setScheduled(false);
            beatInfo.setPeriod(instance.getInstanceHeartBeatInterval());
            //开启心跳续约 延迟执行 发送心跳信息 自己调自己
            beatReactor.addBeatInfo(NamingUtils.getGroupedName(serviceName, groupName), beatInfo);
        }
        //发送Http 服务注册
        serverProxy.registerService(NamingUtils.getGroupedName(serviceName, groupName), groupName, instance);
    }

registerInstance()中首先对当前注册服务进行验证 判断是不是一个临时节点 当前是一个临时节点则将注册信息包装成心跳信息调用addBeatInfo传入注册服务名字和心跳信息

public void addBeatInfo(String serviceName, BeatInfo beatInfo) {
        NAMING_LOGGER.info("[BEAT] adding beat: {} to beat map.", beatInfo);
        String key = buildKey(serviceName, beatInfo.getIp(), beatInfo.getPort());
        BeatInfo existBeat = null;
        //fix #1733
        if ((existBeat = dom2Beat.remove(key)) != null) {
            existBeat.setStopped(true);
        }
        dom2Beat.put(key, beatInfo);
        //线程池  延迟任务
        executorService.schedule(new BeatTask(beatInfo), beatInfo.getPeriod(), TimeUnit.MILLISECONDS);
        MetricsMonitor.getDom2BeatSizeMonitor().set(dom2Beat.size());
    }

该方法当中使用了线程池ScheduledExecutorService的延迟任务 开启一个BeatTask(实现了Runnable)线程传入包装好的心跳信息设置延迟执行时间默认5秒 来看run重写的run方法

public void run() {
            if (beatInfo.isStopped()) {
                return;
            }
            long nextTime = beatInfo.getPeriod();
            try {
                JSONObject result = serverProxy.sendBeat(beatInfo, BeatReactor.this.lightBeatEnabled);
            } catch (NacosException ne) {
                NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: {}, code: {}, msg: {}",
                    JSON.toJSONString(beatInfo), ne.getErrCode(), ne.getErrMsg());

            }
            //在子线程内 又一次的使用延迟任务 传入的又是当前线程
            //nextTime = 5
            executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS);
        }

①【注意当前流程是子线程】
执行run方法中的sendBeat的时候 根据方法名我们就可以知道这个方法就是实现注册服务的功能 发送心跳信息
而sendBeat当中其实就是在封装请求参数 封装之后调用reqAPI()
reqAPI最终在return的时候调用了callServer方法

callServer:

public String callServer(String api, Map<String, String> params, String body, String curServer, String method)
        throws NacosException {
        long start = System.currentTimeMillis();
        long end = 0;
        injectSecurityInfo(params);
        List<String> headers = builderHeaders();
        // url拼接
        String url;
        if (curServer.startsWith(UtilAndComs.HTTPS) || curServer.startsWith(UtilAndComs.HTTP)) {
            url = curServer + api;
        } else {
            if (!curServer.contains(UtilAndComs.SERVER_ADDR_IP_SPLITER)) {
                curServer = curServer + UtilAndComs.SERVER_ADDR_IP_SPLITER + serverPort;
            }
            url = HttpClient.getPrefix() + curServer + api;
        }
        //http 请求发送心跳数据
        HttpClient.HttpResult result = HttpClient.request(url, headers, params, body, UtilAndComs.ENCODING, method);
        end = System.currentTimeMillis();

        MetricsMonitor.getNamingRequestMonitor(method, url, String.valueOf(result.code))
            .observe(end - start);

        if (HttpURLConnection.HTTP_OK == result.code) {
            return result.content;
        }

        if (HttpURLConnection.HTTP_NOT_MODIFIED == result.code) {
            return StringUtils.EMPTY;
        }

        throw new NacosException(result.code, result.content);
    }

本文将主线程外的线程称为子线程
【注意当前流程是子线程】
在callServer方法当中 url拼接 url当中很多的信息都是配置文件中的信息和一些默认值 最后将包装好的数据通过JDK提供的java.net包下面提供的http技术把客户端信息根据拼接好的url调用服务端的API发送到服务端 完成客户端心跳续约

当子线程发送完请求之后回到run方法代码往下执行

public void run() {
            if (beatInfo.isStopped()) {
                return;
            }
            long nextTime = beatInfo.getPeriod();
            try {
                //发送心跳
                JSONObject result = serverProxy.sendBeat(beatInfo, BeatReactor.this.lightBeatEnabled);
            } catch (NacosException ne) {
                NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: {}, code: {}, msg: {}",
                    JSON.toJSONString(beatInfo), ne.getErrCode(), ne.getErrMsg());

            }
            //在子线程内 又一次的使用延迟任务 传入的又是当前线程
            //nextTime = 5
            executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS);
        }

【注意当前流程是子线程】
送完请求之后执行到run方法的最后 又一次的使用到线程池中的延迟任务 而执行的线程任务就是当前new BeatTask线程其实就是自己 然后延迟五秒 在执行一遍BeatTask线程中的run方法 发送心跳,发送完后,再创建一个定时任务,这个心跳检测就能一直不间断进行下去,直到nacos服务销毁

总结:在registerInstance当中将注册信息包装成心跳信息调用addBeatInfo方法 addBeatInfo方法内部使用线程池开启一个延迟任务 这个时候主线程在开启线程池之后就回到了registerInstance方法当中执行下面的代码 而延迟五秒之后线程池开启一个线程我们称为子线程 子线程执行过程中发送请求之后在当前线程run方法中线程池又开启延迟任务而这个延迟5秒执行的线程就是当前子线程 至此就可以知道子线程在addBeatInfo方法当中每隔五秒就会自己调自己发送请求到服务端 就是为了让服务端知道当前客户端注册服务一直可用 直至销毁 服务停止。

回到主线程 registerInstance()中

public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
        /*
        * 客户端注册信息    beatInfo封装客户端信息 心跳包
        * */
        if (instance.isEphemeral()) {
            BeatInfo beatInfo = new BeatInfo();
            beatInfo.setServiceName(NamingUtils.getGroupedName(serviceName, groupName));
            beatInfo.setIp(instance.getIp());
            beatInfo.setPort(instance.getPort());
            beatInfo.setCluster(instance.getClusterName());
            beatInfo.setWeight(instance.getWeight());
            beatInfo.setMetadata(instance.getMetadata());
            beatInfo.setScheduled(false);
            beatInfo.setPeriod(instance.getInstanceHeartBeatInterval());
            //开启心跳续约 延迟执行 发送心跳信息 自己调自己
            beatReactor.addBeatInfo(NamingUtils.getGroupedName(serviceName, groupName), beatInfo);
        }
        //发送Http 服务注册
        serverProxy.registerService(NamingUtils.getGroupedName(serviceName, groupName), groupName, instance);
    }

执行完beatReactor.addBeatInfo代码往下执行调用serverProxy.registerService()

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>(9);
        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", JSON.toJSONString(instance.getMetadata()));
        reqAPI(UtilAndComs.NACOS_URL_INSTANCE, params, HttpMethod.POST);

    }

看源码得知 将当前服务实例对象信息put到Map中 调用reqAPI传入封装好的数据 和请求方式 reqAPI内部调用函数callServer

public static HttpResult request(String url, List<String> headers, Map<String, String> paramValues, String body, String encoding, String method) {
        HttpURLConnection conn = null;
        try {
            String encodedContent = encodingParams(paramValues, encoding);
            url += (StringUtils.isEmpty(encodedContent)) ? "" : ("?" + encodedContent);

            conn = (HttpURLConnection) new URL(url).openConnection();

            setHeaders(conn, headers, encoding);
            conn.setConnectTimeout(CON_TIME_OUT_MILLIS);
            conn.setReadTimeout(TIME_OUT_MILLIS);
            conn.setRequestMethod(method);
            conn.setDoOutput(true);
            if (StringUtils.isNotBlank(body)) {
                byte[] b = body.getBytes();
                conn.setRequestProperty("Content-Length", String.valueOf(b.length));
                conn.getOutputStream().write(b, 0, b.length);
                conn.getOutputStream().flush();
                conn.getOutputStream().close();
            }
            conn.connect();
            if (NAMING_LOGGER.isDebugEnabled()) {
                NAMING_LOGGER.debug("Request from server: " + url);
            }
            return getResult(conn);

callServer方法:拼接URL 通过JDK提供的java.net包下面提供的http技术把客户端信息根据拼接好的url发送到服务端 完成注册

两次发送请求都是一样的方式 不同的是封装的URL不一样 调用服务端的api不一样

至此客户端服务自动注册完成 客户端心跳续约完成。。。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值