问题背景
近期项目遇到一个场景,需要使用到WebSocket来推送数据到客户端,即浏览器,项目的架构大概如下:
项目框架: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的源码。