如何使Zuul支持WebSocket

·  阅读 396

问题背景

近期项目遇到一个场景,需要使用到WebSocket来推送数据到客户端,即浏览器,项目的架构大概如下:

image.png

项目框架:SpringBoot2.x,SpringCloud Zuul 1.x,对于这次问题来说,Nginx不影响,所以可以忽略,Nacos为服务发现框架。

我们想在此架构下,使用WebSocket把实时数据从App推送到前端浏览器,但查阅资料后发现,Zuul 1.x根本不支持WebSocket协议,具体原因没有去研究,但大概情况在GitHub的Issue有提到:Zuul - Websocket proxy #163,Zuul 2.x支持WebSocket,但没被集成进Spring Cloud。

解决方案

方案1: Sock.js + STOMP

这篇文章主要是介绍了使用Sock.js库、STOMP协议、Spring Messaging框架来达到WebSocket的双向通信效果。Sock.js是一个js库,它提供了一个类似于网络的对象,在浏览器和web服务器之间创建了一个低延迟、全双工、跨域通信通道。Spring 4开始就支持SockJS了。STOMP是一种消息传递的文本格式的通信协议,因为如果直接使用WebSocket API开发,其实就跟使用套接字编程差不多的风格,所以使用STOMP提供了更简化的接口,Spring Messaging框架就支持得很好。

使用这个方案对我来说,并不算很好,问题在于这个方案在浏览器或者服务端不支持WebSocket的情况下,就会退化为轮询的方式,当然这种退化对使用者来说完全是透明的,所以好处还是有的,省了这部分代码。

即然都是轮询,那就用处不大,就不考虑了。

方案2: Zuul框架切换Spring Gateway

Spring Gateway是支持WebSocket,但是对我的项目来说,不太现实,主要原因还在于切换成本,要切换就要所有项目组都要配合,这既不是公司强推的技术方案,也没经过架构的评审,反正是不太可能,所以不考虑。

方案3: mthizo247 / spring-cloud-netflix-zuul-websocket

这个项目是用一个叫Ronald Mthombeni的老外写的,18年的时候开源,它就是一个jar包,maven中央仓库可以下载,注意的是下载的是1.0.0-RELEASE版本,使用的是SpringBoot 1.x,因为我的项目使用的是SpringBoot 2.x,所以下载的包基本不可用。

spring-cloud-netflix-zuul-websocket的master分支已经更新到1.0.7.RELEASE,我是使用这个版本的,但还是会出现版本问题,还是因为我的项目使用的是SpringBoot 2.x,在使用的时候WebSocket一直无法建立连接,也不报错,通过阅读一下spring-cloud-netflix-zuul-websocket的源码,大致了解一下,再加上调试过程断点,结果发现在com.github.mthizo247.cloud.netflix.zuul.web.proxytarget.AbstractProxyTargetResolver这个类报错了

protected URI resolveUri(ServiceInstance serviceInstance) {
        Map<String, Object> metadata = new HashMap<>();
        for (Map.Entry<String, String> entry : serviceInstance.getMetadata().entrySet()) {
            metadata.put(entry.getKey(), entry.getValue());
        }

        UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUri(serviceInstance.getUri());
        RelaxedPropertyResolver propertyResolver = new RelaxedPropertyResolver(new MapPropertyResolver(metadata));
        String configPath = propertyResolver.getProperty("configPath");
        if (configPath != null) {
            uriBuilder.path(configPath);
        }

        return uriBuilder.build().toUri();
    }
复制代码

具体在这个方法里,RelaxedPropertyResolver类在SpringBoot 2.x已经被移除了,所以得把这个类去掉,RelaxedPropertyResolver就是对Map的封装,所以可以改成以下这样:

protected URI resolveUri(ServiceInstance serviceInstance) {
        Map<String, Object> metadata = new HashMap<>();
        for (Map.Entry<String, String> entry : serviceInstance.getMetadata().entrySet()) {
            metadata.put(entry.getKey(), entry.getValue());
        }

        UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUri(serviceInstance.getUri());
        String configPath = metadata.get("configPath") == null ? null : String.valueOf(metadata.get("configPath"));
        if (configPath != null) {
            uriBuilder.path(configPath);
        }

        return uriBuilder.build().toUri();
    }
复制代码

然后install到本地,再引进到项目中,就可以使Zuul 1.x支持WebSocket了

Ronald Mthombeni还提供了个demo,mthizo247 / zuul-websocket-support-demo,这个demo的前端依赖的sockjs、stomp库都是引用自webjar这个库的,反正就是按自己项目来即可,一番折腾后基本不成问题,无非即使版本问题。

前端代码和后端代码照着来,就能跑起来了。到这里还是要感叹一下Ronald Mthombeni的代码能力。

总结

这样总结一下,感觉也没啥,但是自己也折腾了好几天。其实一开始还不太相信Zuul 1.x不支持WebSocket,主要是想找到不支持的原因,所以折腾了一番,在Zuul网关,我写了个Zuul Filter

public Object run(){
        RequestContext context = RequestContext.getCurrentContext();
        HttpServletRequest request = context.getRequest();
        String upgradeHeader = request.getHeader("Upgrade");
        if (null == upgradeHeader) {
            upgradeHeader = request.getHeader("upgrade");
        }
        if (null != upgradeHeader && "websocket".equalsIgnoreCase(upgradeHeader)) {
            context.addZuulRequestHeader("connection", "Upgrade");
        }
        return null;
}
复制代码

在Zuul转发到App的时候,发现有个奇怪的问题,这里的context.addZuulRequestHeader("connection", "Upgrade")已经把请求头的connection改为Upgrade,但是到App拿到的请求头的connection确实Keep-Alive,所以App端一直报Handshake失败,大概是这个原因吧,有时间再研究一下Zuul的源码。

分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改