前面三篇文章探讨了以下几个问题:
- 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 后台项目。
源码探秘
网关启动
依据前面经验,在后台项目WebsocketDataChangedListener
的onSelectorChanged
方法中打上断点。然后启动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
进行了实例化。