Soul 网关源码分析(六)使用websocket同步数据到网关

10 篇文章 0 订阅

Soul 提供了多种数据同步的策略,默认且推荐的方式是使用 websocket。为了追求极致的响应速度,所有的配置没有选择存到redis,MySQL或是其他存储方式中,而是直接在HashMap中缓存,可以达到极低的延迟。

下图是三种策略下,Soul 网关与 soul-admin 数据同步的流程:
在这里插入图片描述
借助websocket的特性,DataChangedEventDispatcher 得以主动向 Soul 网关推送数据变更。当 soul-admin 与 soul-bootstrap 首次建立连接时,会向soul-bootstrap 推送一次全量的配置数据,增量的数据也可以由 soul-admin 主动推送给 soul-bootstrap。

soul-admin 会根据配置加载以下的 bean:

/**
 * The WebsocketListener(default strategy).
 */
@Configuration
@ConditionalOnProperty(name = "soul.sync.websocket.enabled", havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(WebsocketSyncProperties.class)
static class WebsocketListener {

    /**
     * 监听数据变化的listener.
     *
     * @return the data changed listener
     */
    @Bean
    @ConditionalOnMissingBean(WebsocketDataChangedListener.class)
    public DataChangedListener websocketDataChangedListener() {
        return new WebsocketDataChangedListener();
    }

    /**
     * websocket服务端,负责websocket 数据的 OnOpen,OnMessage,OnClose,OnError等生命周期的处理和数据推送。
     *
     * @return the websocket collector
     */
    @Bean
    @ConditionalOnMissingBean(WebsocketCollector.class)
    public WebsocketCollector websocketCollector() {
        return new WebsocketCollector();
    }

    /**
     * Server endpoint exporter server endpoint exporter.
     *
     * @return the server endpoint exporter
     */
    @Bean
    @ConditionalOnMissingBean(ServerEndpointExporter.class)
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

其中,WebsocketDataChangedListener 封装了对一系列数据的监听,以插件变更监听为例:

@Override
public void onPluginChanged(final List<PluginData> pluginDataList, final DataEventTypeEnum eventType) {
	// 先封装 websocket 数据,再使用 WebsocketCollector 来发送json序列化后的数据到 soul-bootstrap.
    WebsocketData<PluginData> websocketData =
            new WebsocketData<>(ConfigGroupEnum.PLUGIN.name(), eventType.name(), pluginDataList);
    WebsocketCollector.send(GsonUtils.getInstance().toJson(websocketData), eventType);
}
/**
 * The type Websocket data changed listener.
 *
 * @author xiaoyu(Myth)
 * @author huangxiaofeng
 * @since 2.0.0
 */
@Slf4j
@ServerEndpoint("/websocket")
public class WebsocketCollector {

    private static final Set<Session> SESSION_SET = new CopyOnWriteArraySet<>();

    private static final String SESSION_KEY = "sessionKey";

    /** 略 **/

    /**
     * On message.
     *
     * @param message the message
     * @param session the session
     */
    @OnMessage
    public void onMessage(final String message, final Session session) {
    	// 只有 message 的类型是 MYSELF时才处理
        if (message.equals(DataEventTypeEnum.MYSELF.name())) {
            try {
            	//将 websocket session 保存到 ThreadLocal 中。
                ThreadLocalUtil.put(SESSION_KEY, session);
                
                SpringBeanUtils.getInstance().getBean(SyncDataService.class).syncAll(DataEventTypeEnum.MYSELF);
            } finally {
                ThreadLocalUtil.clear();
            }
        }
    }
    
    /**
     * Send.
     *
     * @param message the message
     * @param type    the type
     */
    public static void send(final String message, final DataEventTypeEnum type) {
        if (StringUtils.isNotBlank(message)) {
            if (DataEventTypeEnum.MYSELF == type) {
                try {
                    Session session = (Session) ThreadLocalUtil.get(SESSION_KEY);
                    if (session != null) {
                        session.getBasicRemote().sendText(message);
                    }
                } catch (IOException e) {
                    log.error("websocket send result is exception: ", e);
                }
                return;
            }
            //向所有存在的session 发送消息
            for (Session session : SESSION_SET) {
                try {
                    session.getBasicRemote().sendText(message);
                } catch (IOException e) {
                    log.error("websocket send result is exception: ", e);
                }
            }
        }
    }
}

我们可以看到当 DataEventTypeEnum 是 MYSELF 时,也就是客户端发来 MYSELF 消息时,服务端会把所有的配置数据同步给客户端,具体的代码实现在 WebsocketSyncDataConfiguration 这个类加载、创建 WebsocketSyncDataService bean 时,在 WebsocketSyncDataService 类中:

@Slf4j
public class WebsocketSyncDataService implements SyncDataService, AutoCloseable {

    private final List<WebSocketClient> clients = new ArrayList<>();

    private final ScheduledThreadPoolExecutor executor;
    
	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 {
	            	//遍历所有url,将 SoulWebsocketClient 实例加到 clients中
	                clients.add(new SoulWebsocketClient(new URI(url), Objects.requireNonNull(pluginDataSubscriber), metaDataSubscribers, authDataSubscribers));
	            } catch (URISyntaxException e) {
	                log.error("websocket url({}) is error", url, e);
	            }
	        }
        /** 略 **/

而在实例化 SoulWebsocketClient时,会调用如下的函数:

@Override
 public void onOpen(final ServerHandshake serverHandshake) {
     if (!alreadySync) {
         send(DataEventTypeEnum.MYSELF.name());
         alreadySync = true;
     }
 }

client会在 websocket open 的时候 发送 MYSELF 消息到服务端,验证了上面我们的想法。其他类型的 DataEventTypeEnum 发送则是通过 onMessage 处理。

总结

soul 巧妙得运用了 websocket 进行数据同步,将数据直接放到JVM的方法也体现了对性能的极致追求。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值