Soul网关第6天:使用WebSocket同步数据到网关

1,124 阅读5分钟

一、介绍

Soul 通过 soul-admin 将用户和接入服务数据同步到网关服务的本地缓存,目前的数据同步方式有四种,本周就学习这四种数据同步方式,最后一天来研究下网关服务的高可用是怎么实现的。

  • websocket
  • zookeeper
  • http 长轮训
  • nacos

今天体验一下 websocket 数据同步方式,贴一张Soul官网数据同步的图。

二、什么是 websocket?

摘自廖雪峰的官方网站的一段

WebSocket是一种基于HTTP的长链接技术。传统的HTTP协议是一种请求-响应模型,如果浏览器不发送请求,那么服务器无法主动给浏览器推送数据。如果需要定期给浏览器推送数据,例如股票行情,或者不定期给浏览器推送数据,例如在线聊天,基于HTTP协议实现这类需求,只能依靠浏览器的JavaScript定时轮询,效率很低且实时性不高。

因为HTTP本身是基于TCP连接的,所以,WebSocket在HTTP协议的基础上做了一个简单的升级,即建立TCP连接后,浏览器发送请求时,附带几个头:

GET /chat HTTP/1.1 
Host: www.example.com
Upgrade: websocket
Connection: Upgrade

就表示客户端希望升级连接,变成长连接的WebSocket,服务器返回升级成功的响应:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade

收到成功响应后表示WebSocket“握手”成功,这样,代表WebSocket的这个TCP连接将不会被服务器关闭,而是一直保持,服务器可随时向浏览器推送消息,浏览器也可随时向服务器推送消息。双方推送的消息既可以是文本消息,也可以是二进制消息,一般来说,绝大部分应用程序会推送基于JSON的文本消息。

三、WebSocket方式数据同步

今天不演示操作了,通过代码来看看如何实现 WebSocket方式数据同步

首先可以我们可以猜测出 WebSocket 通信中 soul-admin 是服务端,网关是客户端。然后,可以看到在调用 WebSocket 进行通信之前,使用的是 Spring 事件驱动模式去发布监听事件。

1、从服务端监听事件往下看

我们从中间层事件监听类 DataChangedEventDispatcher 入口,先从它自顶向下走一遍。

@Component
public class DataChangedEventDispatcher implements ApplicationListener<DataChangedEvent>, InitializingBean {

    private ApplicationContext applicationContext;

    private List<DataChangedListener> listeners;

    public DataChangedEventDispatcher(final ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    @Override
    @SuppressWarnings("unchecked")
    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());
            }
        }
    }

    @Override
    public void afterPropertiesSet() {
        Collection<DataChangedListener> listenerBeans = applicationContext.getBeansOfType(DataChangedListener.class).values();
        this.listeners = Collections.unmodifiableList(new ArrayList<>(listenerBeans));
    }
}

可以看到想要同步的事件也就是数据有五大类(顾名思义就可以了):

  • APP_AUTH 权限
  • PLUGIN 插件
  • SELECTOR 选择器
  • RULE 路由规则
  • META_DATA 元数据

我们先不关心业务,重点先放在通信上,可以看出 DataChangedListener 才是具体干活的,看一些这个接口的实现类中有一个叫做 WebsocketDataChangedListener 类

public class WebsocketDataChangedListener implements DataChangedListener {

    @Override
    public void onPluginChanged(final List<PluginData> pluginDataList, final DataEventTypeEnum eventType) {
        WebsocketData<PluginData> websocketData =
                new WebsocketData<>(ConfigGroupEnum.PLUGIN.name(), eventType.name(), pluginDataList);
        WebsocketCollector.send(GsonUtils.getInstance().toJson(websocketData), eventType);
    }

    @Override
    public void onSelectorChanged(final List<SelectorData> selectorDataList, final DataEventTypeEnum eventType) {
        WebsocketData<SelectorData> websocketData =
                new WebsocketData<>(ConfigGroupEnum.SELECTOR.name(), eventType.name(), selectorDataList);
        WebsocketCollector.send(GsonUtils.getInstance().toJson(websocketData), eventType);
    }

    @Override
    public void onRuleChanged(final List<RuleData> ruleDataList, final DataEventTypeEnum eventType) {
        WebsocketData<RuleData> configData =
                new WebsocketData<>(ConfigGroupEnum.RULE.name(), eventType.name(), ruleDataList);
        WebsocketCollector.send(GsonUtils.getInstance().toJson(configData), eventType);
    }

    @Override
    public void onAppAuthChanged(final List<AppAuthData> appAuthDataList, final DataEventTypeEnum eventType) {
        WebsocketData<AppAuthData> configData =
                new WebsocketData<>(ConfigGroupEnum.APP_AUTH.name(), eventType.name(), appAuthDataList);
        WebsocketCollector.send(GsonUtils.getInstance().toJson(configData), eventType);
    }

    @Override
    public void onMetaDataChanged(final List<MetaData> metaDataList, final DataEventTypeEnum eventType) {
        WebsocketData<MetaData> configData =
                new WebsocketData<>(ConfigGroupEnum.META_DATA.name(), eventType.name(), metaDataList);
        WebsocketCollector.send(GsonUtils.getInstance().toJson(configData), eventType);
    }
}

看得出都是使用 WebsocketCollector#send

    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;
            }
            for (Session session : SESSION_SET) {
                try {
                    session.getBasicRemote().sendText(message);
                } catch (IOException e) {
                    log.error("websocket send result is exception: ", e);
                }
            }
        }
    }

虽然没写过 Java WebSocket 代码,但是不难猜出来,客户端和服务端建立链接后,服务端会存储 session 列表,维护关系。通过 sesson 发送数据。

2、从服务端事件发布者往上看

Spring 框架中的事件发布类 ApplicationEventPublisher,看一下有哪些调用者。

如何查看调用方法,mac 是 ctrl + option + h,windows 应该类似

看的出调用类很多很多,谁会调,也就是谁会发布事件呢?发布事件为了更新数据,猜测两种情况需要更新数据“

  • soul-admin 管理台台更新数据需要通知网关服务,所以点开调用栈发现大部分都是 soul-admin 的 controller 方法,说明是管理台接口。
  • 用 WebSocket 就是为了客户端可以主动向服务端发起请求,此处就是网关主动发出请求给 soul-admin 更新数据,何时发的呢,【剧透,是客户端在第一次建立链接的时候】。

发布事件方法 SyncDataServiceImpl#syncAll 代码,顾名思义像是全量更新数据

    @Override
    public boolean syncAll(final DataEventTypeEnum type) {
        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 服务端接口客户端请求的方法 WebsocketCollector#onMessage

    @OnMessage
    public void onMessage(final String message, final Session session) {
        if (message.equals(DataEventTypeEnum.MYSELF.name())) {
            try {
                ThreadLocalUtil.put(SESSION_KEY, session);
                SpringBeanUtils.getInstance().getBean(SyncDataService.class).syncAll(DataEventTypeEnum.MYSELF);
            } finally {
                ThreadLocalUtil.clear();
            }
        }
    }

其实可以看到 MYSELF 参数就是网关获取数据给自己,使用ThreadLocal存储自己的session,最后再调用发送数据给请求数据的客户端。

3、从客户端看 WebSocket 通信

Soul网关是依赖 Spring 项目的,很多自定义的模块的依赖也适用 springboot-starter 的形式引入的,我们从 soul-bootstrap 依赖的 soul-spring-boot-starter-sync-data-websocket 再追踪到 soul-sync-data-websocket 可以看到 WebSocket 客户端依赖的库是

  <dependency>
      <groupId>org.java-websocket</groupId>
      <artifactId>Java-WebSocket</artifactId>
      <version>${java-websocket.version}</version>
  </dependency>

网关服务 WebSocket 客户端启动类 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 {
                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) {
                boolean success = client.connectBlocking(3000, TimeUnit.MILLISECONDS);
                if (success) {
                    log.info("websocket connection is successful.....");
                } else {
                    log.error("websocket connection is error.....");
                }
                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 客户端和服务端如何做到断开重链

在建立链接的时候,初始化 SoulWebsocketClient 类,看一下它的代码

@Slf4j
public final class SoulWebsocketClient extends WebSocketClient {
    
    private volatile boolean alreadySync = Boolean.FALSE;
    
    private final WebsocketDataHandler websocketDataHandler;
    
    /**
     * Instantiates a new Soul websocket client.
     *
     * @param serverUri             the server uri
     * @param pluginDataSubscriber the plugin data subscriber
     * @param metaDataSubscribers   the meta data subscribers
     * @param authDataSubscribers   the auth data subscribers
     */
    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) {
            send(DataEventTypeEnum.MYSELF.name());
            alreadySync = true;
        }
    }
    
    @Override
    public void onMessage(final String result) {
        handleResult(result);
    }
    
    @Override
    public void onClose(final int i, final String s, final boolean b) {
        this.close();
    }
    
    @Override
    public void onError(final Exception e) {
        this.close();
    }
    
    @SuppressWarnings("ALL")
    private void handleResult(final String result) {
        WebsocketData websocketData = GsonUtils.getInstance().fromJson(result, WebsocketData.class);
        ConfigGroupEnum groupEnum = ConfigGroupEnum.acquireByName(websocketData.getGroupType());
        String eventType = websocketData.getEventType();
        String json = GsonUtils.getInstance().toJson(websocketData.getData());
        websocketDataHandler.executor(groupEnum, json, eventType);
    }
}

可以看到在建立链接时会调用SoulWebsocketClient#onOpen方法,执行send(DataEventTypeEnum.MYSELF.name()),参数是 MYSELF ,我们明白了,这里和 WebSocket 服务端代码对应上了,在客户端第一次建立链接后给服务端发送请求,服务端推送全量数据,客户端接收后将数据存储在本地JVM里。