1. 背景
在公司业务中,客户端和服务器之间通过websocket进行消息传递,服务器是用C开发的,并且使用了libwebsockets
库。
由于系统架构升级,新建立信令服务模块,使用Java语言开发,作为新的websocket服务器。
2. 问题及分析
信令服务器作为websocket服务,web客户端连接服务器没有问题,但是Linux sdk总是无法和信令服务器建立websocket连接。Linux sdk的websocket客户端也使用了libwebsockets库,该库在发起websocket连接请求是携带的Connection如下:
Connection: Upgrade,close
其中Upgrade
表示要将http升级为websocket。作者称其中close
是用于将http升级成websocket失败时对应的处理,描述如下:
但实际上许多服务器并不支持这样(旧服务器支持因为也使用了libwebsockets
库),比如Spring websocket在request头部中包含close时,会返回如下,并断开连接。
Connection: close
附:
github上case讨论:
https://github.com/warmcat/libwebsockets/issues/1435
libwebsockets库修复:
https://github.com/warmcat/libwebsockets/commit/bc394b0680ba4b0a1789d549f9464ad9ae6425a5#diff-277c228a17322dfa446081eac59cbd2d
3. 解决方案
3.1 思路
由于时Connection
中包含close
引起的,所以思考在收到请求时去掉close
。
尝试了以下几种方案,均不可行:
- 在WebSocketConfigurer实现类中注册了HttpSessionHandshakeInterceptor子类,在beforeHandshake方法中处理头部。
- 使用拦截器,在拦截器中处理头部。
可以说测试到近乎崩溃,明明修改了request的Connection header,但是发现服务response的Connection header值总是close,然后直接关闭了连接。
3.2 断点调试
走投无路,开始打断点一步一步调试,发现在tomcat内置的Http11Processor
类中有prepareRequest()
和prepareResponse()
方法。在prepareResponse()
方法中有这么一段代码:
keepAlive
值为false时,则在Connection header中添加 close
,这时候已经看到曙光了,接下来就要寻找keepalive什么时候会被修改成false。
然后在 prepareRequest()
中发现这么一段代码
如果request Connection头中包含close
,就会将keepAlive
置为false,这使得在prepareResponse()
方法中给response Connection header添加了close
值。并且调试发现,Http11Processor
类的prepareRequest()
在拦截器以及beforeHandshake()方法都更先执行,所以在拦截器和beforeHandshake()方法中修改request header都无效。
3.3 重写MimeHeaders类
根据前面的调试分析,已经知道了response Connection header中添加close的原因,通过阅读代码可知,request Connection value的值来源于这两行:
MimeHeaders headers = request.getMimeHeaders();
// Check connection header
MessageBytes connectionValueMB = headers.getValue(Constants.CONNECTION);
那么,只需要重写MimeHeaders.getValue()
方法即可,建立同名package,再建立同名class,根据JVM类加载机制,会优先加载工程目录下的类文件,重写主要代码如下:
package org.apache.tomcat.util.http;
public MessageBytes getValue(String name) {
for(int i = 0; i < this.count; ++i) {
if (this.headers[i].getName().equalsIgnoreCase(name)) {
return getHeader(i);
}
}
return null;
}
// 重写header方法
private MessageBytes getHeader(int i) {
if (this.headers[i].getName().equalsIgnoreCase("connection")) {
String originValue = this.headers[i].getValue().getString();
if ((originValue.contains("close") || originValue.contains("Close"))
&& (originValue.contains("upgrade") || originValue.contains("Upgrade"))) {
MessageBytes messageBytes = MessageBytes.newInstance();
messageBytes.setString(convertConnectionHeader(originValue));
return messageBytes;
}
}
return this.headers[i].getValue();
}
public static String convertConnectionHeader(String oldValue) {
String[] array = oldValue.split(",");
Set<String> headerSet = new HashSet<>();
for (int i = 0; i < array.length; i++) {
headerSet.add(array[i].trim());
}
headerSet.remove("close");
headerSet.remove("Close");
return StringUtil.concat(",", headerSet.stream().toArray());
}