前面的几篇文章,我们分析了 soul-admin 模块使用 http 数据同步方式之后,soul-admin 做了哪些工作。

但是从分析结果来看,都只是对 soul-admin 自身的数据同步,也就是数据库数据和内存数据保持一致。

今天我们看看 soul-bootstrap 是如何通过 http 方式跟 soul-admin 模块保持数据同步的。

做一个猜测

我们都知道 http 会涉及到「请求」和「响应」两个过程,在这两个过程中,一般会有两个对象,一个是客户端,一个是服务端。由客户端向服务端发起请求,然后服务端接收到请求之后进行一系列处理之后把结果返回给客户端,这就是响应。

上面的内容就是 http 的工作方式,至于建立 tcp 连接等过程这里就不多提了。

换句话说就是,如果使用 http 做数据同步方式,必须由客户端主动发起请求,然后交给服务端响应返回结果。

在这个前提下,我们结合 soul 网关来推测下,soul-admin 和 soul-bootstrap 模块之间的 http 数据同步应该怎么做?

首先要确定客户端和服务端分别是谁,因为在 soul 里面所有的数据都会在 soul-admin 模块,而真正起网关的作用却是 soul-bootstrap 模块,它的数据全部来自于 soul-admin,所以 soul-admin 是服务端,soul-bootstrap 是客户端。

明确了这一点,也就可以猜测在 soul 里面进行 http 数据同步的过程是从 soul-bootstrap 对 soul-admin 发起 http 请求,然后根据返回值去更新数据。

soul-bootstrap 里面的 http 数据同步

soul-bootstrap 里面需要首先开启 http 数据同步方式,在配置文件里面:

  1. soul :
  2. sync:
  3. http:
  4. url : http://localhost:9095

一旦设置了这个属性,soul-bootstrap 就会从依赖的 soul-spring-boot-starter-sync-data-http 模块里面去读取配置,然后为 http 数据同步做准备。我们一起来看看代码:

  1. @Configuration
  2. @ConditionalOnClass(HttpSyncDataService.class)
  3. @ConditionalOnProperty(prefix = "soul.sync.http", name = "url")
  4. @Slf4j
  5. public class HttpSyncDataConfiguration {
  6. @Bean
  7. public SyncDataService httpSyncDataService(final ObjectProvider<HttpConfig> httpConfig, final ObjectProvider<PluginDataSubscriber> pluginSubscriber,
  8. final ObjectProvider<List<MetaDataSubscriber>> metaSubscribers, final ObjectProvider<List<AuthDataSubscriber>> authSubscribers) {
  9. log.info("you use http long pull sync soul data");
  10. return new HttpSyncDataService(Objects.requireNonNull(httpConfig.getIfAvailable()), Objects.requireNonNull(pluginSubscriber.getIfAvailable()),
  11. metaSubscribers.getIfAvailable(Collections::emptyList), authSubscribers.getIfAvailable(Collections::emptyList));
  12. }
  13. @Bean
  14. @ConfigurationProperties(prefix = "soul.sync.http")
  15. public HttpConfig httpConfig() {
  16. return new HttpConfig();
  17. }
  18. }

我们可以看到这里面的主要内容就 new 了一个 HttpSyncDataService 的实例,以及加载了一系列的参数,剩下的内容不重要。

我们继续看这个 HttpSyncDataService 类,我们想要的答案应该也会在里面的。

  1. public class HttpSyncDataService implements SyncDataService, AutoCloseable {
  2. public HttpSyncDataService(final HttpConfig httpConfig, final PluginDataSubscriber pluginDataSubscriber,
  3. final List<MetaDataSubscriber> metaDataSubscribers, final List<AuthDataSubscriber> authDataSubscribers) {
  4. this.factory = new DataRefreshFactory(pluginDataSubscriber, metaDataSubscribers, authDataSubscribers);
  5. this.httpConfig = httpConfig;
  6. this.serverList = Lists.newArrayList(Splitter.on(",").split(httpConfig.getUrl()));
  7. this.httpClient = createRestTemplate();
  8. this.start();
  9. }
  10. private void start() {
  11. // It could be initialized multiple times, so you need to control that.
  12. if (RUNNING.compareAndSet(false, true)) {
  13. // fetch all group configs.
  14. this.fetchGroupConfig(ConfigGroupEnum.values());
  15. int threadSize = serverList.size();
  16. this.executor = new ThreadPoolExecutor(threadSize, threadSize, 60L, TimeUnit.SECONDS,
  17. new LinkedBlockingQueue<>(),
  18. SoulThreadFactory.create("http-long-polling", true));
  19. // start long polling, each server creates a thread to listen for changes.
  20. this.serverList.forEach(server -> this.executor.execute(new HttpLongPollingTask(server)));
  21. } else {
  22. log.info("soul http long polling was started, executor=[{}]", executor);
  23. }
  24. }
  25. }

简单解释下这个类的作用:

  1. 接收传递进来的参数,这些参数主要是针对 http 数据同步的处理类,http 数据同步服务端的配置类
  2. 构造 http 客户端请求
  3. 调用 start 方法处理(建立 http 长连接,拉取数据)

这里面复杂点的内容就是 start 方法,这个方法里面一开始会从 soul-admin 拉取需要的数据,比如「插件」「选择器」「规则」「元数据」等信息。

然后开启一个线程池去跟 soul-admin 建立 http 长连接,这时候也就代表 http 请求已经从 soul-bootstrap 发送到了 soul-admin。

soul-admin 怎么处理请求

现在我们又回到 soul-admin 模块来看它是怎么处理请求的。

我们可以从 soul-bootstrap 模块的请求部分,也就是上面那块代码,找到 soul-admin 模块的接口类:

  1. @ConditionalOnBean(HttpLongPollingDataChangedListener.class)
  2. @RestController
  3. @RequestMapping("/configs")
  4. @Slf4j
  5. public class ConfigController {
  6. @PostMapping(value = "/listener")
  7. public void listener(final HttpServletRequest request, final HttpServletResponse response) {
  8. longPollingListener.doLongPolling(request, response);
  9. }
  10. }

可以看到这就是一个简单的接口类,里面还有我们熟悉的老朋友,但是请注意,它没有返回值。

是不是觉得很奇怪?明明需要返回数据,为啥没有返回值呢?

我们继续看 HttpLongPollingDataChangedListener 这个类的 doLongPolling 方法。

  1. public class HttpLongPollingDataChangedListener extends AbstractDataChangedListener {
  2. public void doLongPolling(final HttpServletRequest request, final HttpServletResponse response) {
  3. // compare group md5
  4. List<ConfigGroupEnum> changedGroup = compareChangedGroup(request);
  5. String clientIp = getRemoteIp(request);
  6. // response immediately.
  7. if (CollectionUtils.isNotEmpty(changedGroup)) {
  8. this.generateResponse(response, changedGroup);
  9. log.info("send response with the changed group, ip={}, group={}", clientIp, changedGroup);
  10. return;
  11. }
  12. // listen for configuration changed.
  13. final AsyncContext asyncContext = request.startAsync();
  14. // AsyncContext.settimeout() does not timeout properly, so you have to control it yourself
  15. asyncContext.setTimeout(0L);
  16. // block client's thread.
  17. scheduler.execute(new LongPollingClient(asyncContext, clientIp, HttpConstants.SERVER_MAX_HOLD_TIMEOUT));
  18. }
  19. }

可以看到这里出现了立即返回方式和异步返回方式两种,原来上面的接口不是没有返回值,而只是没有在 controller 类里面返回。

立即返回方式太简单了,我们直接看异步返回的处理,看着是不是很像新开启了一个线程去执行?

我们继续看 LongPollingClient 类。

  1. class LongPollingClient implements Runnable {
  2. LongPollingClient(final AsyncContext ac, final String ip, final long timeoutTime) {
  3. this.asyncContext = ac;
  4. this.ip = ip;
  5. this.timeoutTime = timeoutTime;
  6. }
  7. @Override
  8. public void run() {
  9. this.asyncTimeoutFuture = scheduler.schedule(() -> {
  10. clients.remove(LongPollingClient.this);
  11. List<ConfigGroupEnum> changedGroups = compareChangedGroup((HttpServletRequest) asyncContext.getRequest());
  12. sendResponse(changedGroups);
  13. }, timeoutTime, TimeUnit.MILLISECONDS);
  14. clients.add(this);
  15. }
  16. }

果然不出所料,的确是一个线程类,主要作用就是根据请求里面的参数去处理返回值,最后异步返回出去。

到了这里,soul-admin 和 soul-bootstrap 间的 http 数据同步就已经完成了,剩下的返回值的处理其实我们在上面已经提到过了——还记得上面 HttpSyncDataService 类实例化时传递进去的几个参数类么,它们会分别对不同的数据类型进行处理。

总结

我们简单总结一下,这篇文章我们先分析了 http 的工作方式,需要客户端发起请求给服务端,然后服务端返回数据给客户端。

然后以此推断 soul 网关使用 http 数据同步也是一样的过程,所以我们从客户端 soul-bootstrap 模块开始,找到它是如何发起请求的。

然后顺着请求找到 soul-admin 模块的接口,再一步步去看它是怎么处理请求的。

最后返回的数据又会回到 soul-bootstrap 模块,交给对应的类去处理。

那么,这是不是一个完整的 http 数据同步的过程呢?

其实不是的,因为请求是从 soul-bootstrap 开始的,即便在 soul-bootstrap 建立的 http 长连接里面加上了定时任务,也还是会有延迟的。

如果是 soul-admin 这边的数据有变更呢?

这就需要用到我们上一篇文章里面那个我们记录的问题了,我们下篇文章分析。