Soul网关源码探秘《十五》 - WebSocket同步

前面三篇文章探讨了以下几个问题:

  • soul-admin后台
    • 在服务启动时如何注册服务的
    • 探活机制是如何实现的
    • 如何同步给soul-bootstrap网关
  • soul-bootstrap网关
    • 接收到后台传来的配置信息后是如何处理流转的
    • 探活机制是如何实现的

今天主要来探究soul-admin后台和soul-bootstrap网关是如何通过websocket通信的。

准备工作

在soul-admin项目的 application.yml 中配置使用 websocket 的同步方式。

soul:
  sync:
    websocket:
      enabled: true

在soul-bootstrap项目的 application-local.yml 中配置使用 websocket 的同步方式及后台的同步配置。

soul :
    file:
      enabled: true
    corss:
      enabled: true
    dubbo :
      parameter: multi
    sync:
        websocket :
             urls: ws://localhost:9095/websocket

启动 soul-admin 后台项目。


源码探秘

网关启动

依据前面经验,在后台项目WebsocketDataChangedListeneronSelectorChanged方法中打上断点。然后启动soul-bootstrap网关项目。此时在后台项目中立即进入断点位置。先来看看调用栈。

可以找到,在后台项目中的入口是之前熟悉的WebsocketCollector

@ServerEndpoint("/websocket")
public class WebsocketCollector {
    @OnMessage
    public void onMessage(final String message, final Session session) {
        if (message.equals(DataEventTypeEnum.MYSELF.name())) {
            WebsocketCollector.session = session;
			// 如果参数值为 MYSELF,则调用SyncDataService中的syncAll方法         
            SpringBeanUtils.getInstance().getBean(SyncDataService.class).syncAll(DataEventTypeEnum.MYSELF);
        }
    }
}

后台项目接收到请求后,判断如果参数为 MYSELF,则调用SyncDataService中的syncAll方法。

public boolean syncAll(final DataEventTypeEnum type) {
		// 同步appAuth数据
        appAuthService.syncData();
        List<PluginData> pluginDataList = pluginService.listAll();
		// 发布插件变动类型事件,并广播给所有监听器       
        eventPublisher.publishEvent(new DataChangedEvent(ConfigGroupEnum.PLUGIN, type, pluginDataList));
        List<SelectorData> selectorDataList = selectorService.listAll();
        // 发布选择器变动类型事件,并广播给所有监听器  
        eventPublisher.publishEvent(new DataChangedEvent(ConfigGroupEnum.SELECTOR, type, selectorDataList));
        List<RuleData> ruleDataList = ruleService.listAll();
		// 发布规则变动类型事件,并广播给所有监听器 
        eventPublisher.publishEvent(new DataChangedEvent(ConfigGroupEnum.RULE, type, ruleDataList));
        // 同步元数据
        metaDataService.syncData();
        return true;
    }

从这个方法里,可以看出当网关启动时,会通过websocket给后台发送请求,带的参数是MYSELF。此时后台会同步全量配置(appAuth、插件、选择器、规则、元数据)信息发给网关。

后台统一处理逻辑

在后台发布事件后,会流转到监听器DataChangedEventDispatcher进行处理,并按照配置信息的类型转交给soul自定义监听器的对应方法走后面的流程。另外从DataChangedEventDispatcher这个类的英文名也可以大概猜出来,这个类是在接收到数据变更事件后,分发给对应的监听器进行处理。

	public void onApplicationEvent(final DataChangedEvent event) {
        for (DataChangedListener listener : listeners) {
            switch (event.getGroupKey()) {
                case APP_AUTH:
                    listener.onAppAuthChanged((List<AppAuthData>) event.getSource(), event.getEventType());
                    break;
                case PLUGIN:
                    listener.onPluginChanged((List<PluginData>) event.getSource(), event.getEventType());
                    break;
                case RULE:
                    listener.onRuleChanged((List<RuleData>) event.getSource(), event.getEventType());
                    break;
                case SELECTOR:
                    listener.onSelectorChanged((List<SelectorData>) event.getSource(), event.getEventType());
                    break;
                case META_DATA:
                    listener.onMetaDataChanged((List<MetaData>) event.getSource(), event.getEventType());
                    break;
                default:
                    throw new IllegalStateException("Unexpected value: " + event.getGroupKey());
            }
        }
    }

在这个方法里按照同步数据的类型不同,转发给对应的监听器方法进行后续处理。后面的逻辑在前文里已经阐述过了,最终通过WebsocketCollector的send方法将配置信息发给网关。

后台建立 WebSocket 连接

在后台的 config/DataSyncConfiguration 文件中,可以找到启动时需要加载的两个实例。

@Configuration
public class DataSyncConfiguration {
    @Configuration
    @ConditionalOnProperty(name = "soul.sync.websocket.enabled", havingValue = "true", matchIfMissing = true)
    @EnableConfigurationProperties(WebsocketSyncProperties.class)
    static class WebsocketListener {
    
        @Bean
        @ConditionalOnMissingBean(WebsocketDataChangedListener.class)
        public DataChangedListener websocketDataChangedListener() {
            return new WebsocketDataChangedListener();
        }

        @Bean
        @ConditionalOnMissingBean(WebsocketCollector.class)
        public WebsocketCollector websocketCollector() {
            return new WebsocketCollector();
        }
    }
}

WebsocketDataChangedListener: websocket监听器;
WebsocketCollector:维护连接后台的所有 session;监听 websocket 连接及接收信息;send方法发送 websocket 信息。

网关建立 WebSocket 连接

在网关项目的 pom.xml 文件中引用了以下依赖:

		<dependency>
            <groupId>org.dromara</groupId>
            <artifactId>soul-spring-boot-starter-sync-data-websocket</artifactId>
            <version>${project.version}</version>
        </dependency>

soul-spring-boot-starter-sync-data-websocket项目中只有一个配置类WebsocketSyncDataConfiguration

	@Bean
    public SyncDataService websocketSyncDataService(final ObjectProvider<WebsocketConfig> websocketConfig, final ObjectProvider<PluginDataSubscriber> pluginSubscriber,
                                           final ObjectProvider<List<MetaDataSubscriber>> metaSubscribers, final ObjectProvider<List<AuthDataSubscriber>> authSubscribers) {
        log.info("you use websocket sync soul data.......");
        return new WebsocketSyncDataService(websocketConfig.getIfAvailable(WebsocketConfig::new), pluginSubscriber.getIfAvailable(),
                metaSubscribers.getIfAvailable(Collections::emptyList), authSubscribers.getIfAvailable(Collections::emptyList));
    }

继续查找WebsocketSyncDataService类。

	public WebsocketSyncDataService(final WebsocketConfig websocketConfig,final PluginDataSubscriber pluginDataSubscriber,final List<MetaDataSubscriber> metaDataSubscribers,final List<AuthDataSubscriber> authDataSubscribers) {
        String[] urls = StringUtils.split(websocketConfig.getUrls(), ",");
        executor = new ScheduledThreadPoolExecutor(urls.length, SoulThreadFactory.create("websocket-connect", true));
        for (String url : urls) {
            try {
            	// 每一个配置的 websocket 的 url 都新建一个 SoulWebsocketClient 实例
                clients.add(new SoulWebsocketClient(new URI(url), Objects.requireNonNull(pluginDataSubscriber), metaDataSubscribers, authDataSubscribers));
            } catch (URISyntaxException e) {
                log.error("websocket url({}) is error", url, e);
            }
        }
        try {
            for (WebSocketClient client : clients) {
            	// 去连接每一个 websocket 的url 地址去判断是否可用
                boolean success = client.connectBlocking(3000, TimeUnit.MILLISECONDS);
                if (success) {
                    log.info("websocket connection is successful.....");
                } else {
                    log.error("websocket connection is error.....");
                }
				// 新建一个线程池每隔30秒去判断是否可用,不可用则尝试重新连接               
                executor.scheduleAtFixedRate(() -> {
                    try {
                        if (client.isClosed()) {
                            boolean reconnectSuccess = client.reconnectBlocking();
                            if (reconnectSuccess) {
                                log.info("websocket reconnect is successful.....");
                            } else {
                                log.error("websocket reconnection is error.....");
                            }
                        }
                    } catch (InterruptedException e) {
                        log.error("websocket connect is error :{}", e.getMessage());
                    }
                }, 10, 30, TimeUnit.SECONDS);
            }
            /* client.setProxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("proxyaddress", 80)));*/
        } catch (InterruptedException e) {
            log.info("websocket connection...exception....", e);
        }

    }

在这里,会为每一个配置的 websocket 的 url 生成一个 SoulWebsocketClient 的websocket客户端实例。并新建一个线程池每隔30秒判断websocket是否可用,不可用则再次尝试连接。

public final class SoulWebsocketClient extends WebSocketClient {
    public SoulWebsocketClient(final URI serverUri, final PluginDataSubscriber pluginDataSubscriber,
                               final List<MetaDataSubscriber> metaDataSubscribers, final List<AuthDataSubscriber> authDataSubscribers) {
        super(serverUri);
        this.websocketDataHandler = new WebsocketDataHandler(pluginDataSubscriber, metaDataSubscribers, authDataSubscribers);
    }
    
    @Override
    public void onOpen(final ServerHandshake serverHandshake) {
        if (!alreadySync) {
			// 新增websocket中url对应的  SoulWebsocketClient 客户端实例时,会对该url发送 MYSELF 类型的信息         
            send(DataEventTypeEnum.MYSELF.name());
            alreadySync = true;
        }
    }
}

可以看到,新增websocket中url对应的 SoulWebsocketClient 客户端实例时,会对该url发送 MYSELF 类型的信息。这就和后台接收到的请求信息对应上了。


总结

网关启动

网关启动时,会对应配置中的每一个websocket的url生成一个对应的SoulWebsocketClient客户端实例,同时向对应的url发送MYSELF类型的信息。

后台接收

后台接收到MYSELF类型的websocket信息后,会从数据库取出全部配置信息,发布数据变更事件广播给所有监听器。由数据变更监听器分发给监听器对应数据类型的方法执行后续逻辑。最终由WebsocketCollector发送对应配置数据给网关完成同步。

后台启动

后台启动时,将监听器WebsocketCollector进行了实例化。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

rughru

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值