背景概述
因为项目中存在频繁的由服务器发起的数据交换,相比使用Ajax轮训的方式,websocket长连接和双向保持的特点能够较好的提升数据交换的性能。
为了简便,直接使用spring boot + shiro + stomp和socketJs作为构建的工具。
但是由于使用时,主要是由服务端进行数据的推送,通过stomp自行保持心跳,就会存在session过期导致连接断开的情况,而且由于stomp本身不会提示错误原因,导致排查起来比较麻烦,因此记录下整个纠错过程以备忘。
原因排查
1. 问题现象:
在一开始打开时,一切正常,数据能推送成功,但是在大约10分钟左右(时长不定)能通过chrome的前端调试工具发现stomp提示连接断开。服务器端无任何异常提示。
2. 原因排查:
因为服务器端无任何异常,且断连时长不定,因此推测为可能是session导致的自动断链或者心跳丢失导致的。因为stomp本身不会报错误原因,因此想要找到具体的错误原因比较麻烦。后来发现可以通过断点到stomp的onclose时间就能够获取到具体的错误码,这就为我们定位问题提供了很好的帮助(具体断点位置如下图所示)
3. 问题确定
最后通过错误码发现,提示的错误码是1008,reason部分提示的是http session被关闭,因此问题就比较清楚了,是由于session释放导致的断链。这一点就很有意思,因为在使用过程中为了避免不必要的数据传输就一直没有发起websocket的send事件,只是一直在subscribe监听服务器的推送。因此导致了服务器的session一直没有被touch(),从而释放。(其实个人觉得这个可能也是一个比较有意思的问题,为什么心跳没有被处理会触发到session,具体没有试验是不是和我使用了shiro的session作为管理有关,有兴趣的同学们可以尝试一下)
这里也顺便贴上对于错误码的描述和相关参考资料,便于大家解决其它的错误码问题:
- 首先关于1008 错误错误码的描述如下 Java WebSocket 规范:
如果用户退出了包含的web应用,或如果身份验证超时,或由于其他一些原因无效的。在这种情况下,websocket实现必须立即使用websocket关闭状态码1008关闭连接
- 关于错误码的具体原因 WebSocket Protocol
4. 问题解决
那么问题找到了响应的解决方案就比较好处理了,但是究竟哪种方案比较好,还是得看具体的业务需求和对利弊的取舍。
- 可以通过修改session的过期时间来控制连接的时长(但是可能没有那么精准),如果为了简便以可以设置为永远不过期(但是永远不过期有很多潜在问题)。笔者主要使用了shiro作为session的管理工具,设置session不过期的代码如下:
Subject currentUser = SecurityUtils.getSubject();
Session session = currentUser.getSession();
//注意这里单位为ms,且会向s向下取整
session.setTimeout(-1000);
- 通过服务器端主动发送send前,手动调用一下session的touch()函数,但是这种方法感觉比较不合理,而且需要注意send和touch的顺序关系,不然容易报response已经commit后再重建session的异常
Subject currentUser = SecurityUtils.getSubject();
Session session = currentUser.getSession();
session.touch();
//注意顺序
messagingTemplate.convertAndSend(desUrl, value);
- 周期性通过客户端发送一个send消息到服务器,维持session的不过期。这个方案实现比较简单,而且对服务器的影响较小,唯一不爽的一点是感觉有点违背了当时服务器推送的初衷(毕竟已经发送心跳保持激活了,还再发送send有点多此一举的感觉),不过综合考虑到实际处理中笔者可以通过send来帮助控制及时取消掉一些observer的订阅,因此最后选择了这个方案。
function listenInfo(){
var listenUrl = "/queue/xxx/" ;
stompClient.subscribe(listenUrl, function (data) {
setChangedAccount(data);
var currentDate = new Date();
//50s send once(server expire is 60s)
if((currentDate.getTime()-lastSendDate.getTime())>=50000){
sendInfo(userID);
lastSendDate = new Date();
}
});
}
结语
问题本身倒是不复杂,就是对stomp不熟悉,确定具体的错误原因上花了很多时间,进行了很多白费的尝试,所以希望能记录下,做个以后的备忘,防止再走很多弯路。