ShenYu网关源码解析(二十)Websocket数据同步-Admin端

873 阅读5分钟

简介

    本篇文章探索下Soul网关Admin的Websocket数据同步流程

概览

    首先使用Websocket同步方式启动下示例

    根据前面Zookeeper和Nacos数据同步分析的经验,找到Websocket的事件监听处理的类,在其上打上断点,调试查看初始化流程

    然后在Admin后台修改插件状态,调试查看数据变更处理流程

    发现初始化都是从熟悉的syncAll开始,而事件变更处理都是从Controllers入口开始的,具体详情记录情况在源码Debug环节

示例运行

    启动数据库:

docker run --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 -d mysql:latest

    首先配置运行Soul-Admin,设置数据同步方式为Websocket

soul:
  sync:
    websocket:
      enabled: true

    配置Soul-Bootstrap,配置websocket同步方式,运行Soul-Bootstrap,大致如下:

soul :
    # 把 websocket 数据同步打开
    sync:
        websocket :
             urls: ws://localhost:9095/websocket

    运行Soul-Example-HTTP,注册一些数据用于Debug测试

源码Debug

初始化流程

    首先根据前面的经验在Soul-Admin模块,listener.websocket 目录包下找到相应的Websocket事件监听处理类:WebsocketDataChangedListener

    我们找到插件变更处理的函数,在其上打上端口,重启Admin

    成功进入断点,我们可以看到下面的函数大意是封装了Websocket格式的数据,然后用Websocket发送出去,细节后面再看,我们先看看调用栈

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);
    }
}

    PS:这个有个小细节,如果没有任何一台Bootstrap或者事件发送,那这个断点不会进入

    我们跟踪调用栈来到前面文章中熟悉的事件处理分发,这里继续跟下去

public class DataChangedEventDispatcher implements ApplicationListener<DataChangedEvent>, InitializingBean {

    @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;
                // Plugin 触发
                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());
            }
        }
    }
}

    来到又是非常熟悉的全量数据同步函数:从数据库中读取所有的数据,然后发布事件,进行同步

public class SyncDataServiceImpl implements SyncDataService {

    @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相关的,下面这个函数如果是写过Websocket的,那一定非常熟悉了,就是接收到消息,然后调用处理逻辑

    在调试中,我们看到message的值是MYSELF,猜测Websocket的初始化通信约定应该是收到MYSELF,则同步全量数据给Bootstrap

public class WebsocketCollector {

    @OnMessage
    public void onMessage(final String message, final Session session) {
        // message == MYSELF
        if (message.equals(DataEventTypeEnum.MYSELF.name())) {
            try {
                // 将客户端Session信息进行保存
                ThreadLocalUtil.put(SESSION_KEY, session);
                SpringBeanUtils.getInstance().getBean(SyncDataService.class).syncAll(DataEventTypeEnum.MYSELF);
            } finally {
                ThreadLocalUtil.clear();
            }
        }
    }
}

    我们回过头去看看Websocket的发送函数,有两个点需要注意一下:

    1.是当为MYSELF时,消息只发送给一个特定的客户端

    在上面的函数中,使用ThreadLocal进行了session的保存,然后取出来,然后利用session进行消息发送

    这种应该是针对新建立连接的Bootstrap的,发送全量的数据给它

    2.不是MYSELF,则发送消息给所有的客户端

    这个应该是事件变更,然后同步数据给所有的客户端

public class WebsocketCollector {

    public static void send(final String message, final DataEventTypeEnum type) {
        if (StringUtils.isNotBlank(message)) {
            if (DataEventTypeEnum.MYSELF == type) {
                Session session = (Session) ThreadLocalUtil.get(SESSION_KEY);
                if (session != null) {
                    sendMessageBySession(session, message);
                }
            } else {
                SESSION_SET.forEach(session -> sendMessageBySession(session, message));
            }
        }
    }
}

    在这里看到使用ThreadLocal进行标识获取,但SESSION_KEY都是一样的,它是原理好像自己还有点迷糊,后面再研究下

    以前我们使用时候,都是在OnMessage中直接进行这种针对特定客户端的处理;或者客户端连接的时候自带ID

    看了这个,感觉又学到了一手

数据变更

    数据同步走完了,我们在Admin后台管理界面,修改限流插件的状态,然后触发进入了最开始我们打上断点的函数:

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);
    }
}

    跟踪调用栈事件,事件分发跳过,又来到熟悉的:PluginServiceImpl,这里面进行插件数据的更新,然后发布事件

public class PluginServiceImpl implements PluginService {

    @Override
    @Transactional(rollbackFor = Exception.class)
    public String createOrUpdate(final PluginDTO pluginDTO) {
        final String msg = checkData(pluginDTO);
        if (StringUtils.isNoneBlank(msg)) {
            return msg;
        }
        PluginDO pluginDO = PluginDO.buildPluginDO(pluginDTO);
        DataEventTypeEnum eventType = DataEventTypeEnum.CREATE;
        if (StringUtils.isBlank(pluginDTO.getId())) {
            pluginMapper.insertSelective(pluginDO);
        } else {
            eventType = DataEventTypeEnum.UPDATE;
            pluginMapper.updateSelective(pluginDO);
        }

        // publish change event.
        eventPublisher.publishEvent(new DataChangedEvent(ConfigGroupEnum.PLUGIN, eventType,
                Collections.singletonList(PluginTransfer.INSTANCE.mapToData(pluginDO))));
        return StringUtils.EMPTY;
    }
}

    继续跟踪来到属性的Controllers接口,从这里触发事件更新

@RestController
@RequestMapping("/plugin")
public class PluginController {

    @PutMapping("/{id}")
    public SoulAdminResult updatePlugin(@PathVariable("id") final String id, @RequestBody final PluginDTO pluginDTO) {
        Objects.requireNonNull(pluginDTO);
        pluginDTO.setId(id);
        final String result = pluginService.createOrUpdate(pluginDTO);
        if (StringUtils.isNoneBlank(result)) {
            return SoulAdminResult.error(result);
        }
        return SoulAdminResult.success(SoulResultMessage.UPDATE_SUCCESS);
    }
}

总结

    本篇文章进行了初步探索的Admin端的Websocket数据同步的处理流程,大致可以分为初始化和数据更新(包括删除)的处理流程

  • 数据初始化:Boostrap发送MYSELF消息,触发所有数据同步

  • 数据处理流程(监听类ZookeeperDataChangedListener)

    • HTTP接口调用:可以是管理后台;也可以是服务注册Client
    • Service调用:更新数据库中的数据,调用发布事件接口
    • 发布事件:发布事件到数据同步监听中
    • 数据更新:接收到事件后,进行更新(Websocket进行推送、Zookeeper写入、HTTP更新MD5、Nacos写入)

    这三篇:Zookeeper、Nacos、Websocket,发现处理流程都是基本相似的,前面的初始化入口、事件触发和分发都是同一个,具体的发送逻辑不一样而已

    可以看出代码的结构是非常清晰的