我正在尝试在spring-boot(1.5.13)的基础上实现websocket。 消息传递正常,但在大约30分钟后,服务器终止连接(原因1008)。我试图设置不同的超时但似乎没有任何影响。
WebSocketHandler
@Service
@RequiredArgsConstructor
@Slf4j
public class OCPPSocketHandler extends TextWebSocketHandler {
@Override
public void handleTextMessage(WebSocketSession webSocketSession, TextMessage textMessage)
throws IOException {
...
}
}
WebSocketConfig
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
public static final String ENDPOINT = "/pp/v2.0";
@Autowired
private CustomSocketHandler socketHandler;
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new CustomExceptionWebSocketHandlerDecorator(socketHandler),
ENDPOINT)
.setAllowedOrigins("*");
}
}
application.properties:
#6h as milliseconds
server.connection-timeout=3600000
server.servlet.session.timeout=6h
每30分钟发送一次TextMessage(WebSocket)以保持连接活动。
我在Spring websocket timeout settings上看过帖子,但我看不到解决方案。
答案 0 :(得分:1)
如果服务器或客户端之间的连接关闭,则可以关闭连接,如果它注意到已建立的连接上没有活动,则可以通过防火墙关闭。
因此您还需要在客户端检查超时。可以接受的是,30分钟是此类连接的默认超时,因此它在客户端连接它使用默认值。
此外,定期检查连接状态是一个很好的设计,例如发送一种ping(来自客户端的消息)/ pong(来自服务器的响应)消息。如果您每分钟都这样做,例如您已了解连接状态,并且永远不会因为不活动而关闭连接。
答案 1 :(得分:0)
我也发现30分钟后我的WebSocket也关闭了。
根本原因是默认情况下,http会话将在SpringBoot中关闭30分钟。
在SpringBoot中,配置属性server.servlet.session.timeout
将更改默认行为,但可能会有some limit。
此外,WebSocket连接具有保持有效的乒乓消息,因此在乒乓停止之前,永远不要关闭该连接。
经过一些跟踪,我找到了解决此问题的方法:
io.undertow.server.session.InMemorySessionManager.SessionImpl
。io.undertow.server.session.InMemorySessionManager.SessionImpl#setMaxInactiveInterval
将重置计时器。javax.servlet.http.HttpSession#setMaxInactiveInterval
将调用此方法。setMaxInactiveInterval
在每次超时之前被调用,连接将永远不会关闭。这是我的实现方式:
HandshakeRequest
存储到配置器javax.websocket.EndpointConfig#getUserProperties
中的javax.websocket.server.ServerEndpointConfig.Configurator#modifyHandshake
onMessage
方法中,从HandshakeRequest
获取javax.websocket.Session#getUserProperties
,然后HttpSession httpSession = (HttpSession) handshakeRequest.getHttpSession();
httpSession.setMaxInactiveInterval((int) (session.getMaxIdleTimeout() / 1000));
仅此而已,希望对您有所帮助。
答案 2 :(得分:0)
就我而言,SpringBoot 中 HttpSession 的超时设置为
server.servlet.session.timeout: 1
(1 分钟)
将 Spring Security 配置中的 websocket 端点设置为 httpBasic
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.antMatcher("/ocpp/**")
.authorizeRequests()
.anyRequest().permitAll()//.hasAnyRole("CS","SYSTEM_ADMIN")
.and().httpBasic();
}
在 websocket 客户端中使用基本身份验证
String id = "testuser";
String pw = "1234";
String idpw = new String(Base64.getEncoder().encode((id+":"+pw).getBytes()));
System.out.println("id["+id+"]pw["+pw+"]idpw["+idpw+"]");
;
Map<String, String> httpHeaders = new HashMap<String, String>();
httpHeaders.put("authorization", "Basic "+idpw);
WebSocketClient webSocketClient = new WebSocketClient(new URI("ws://localhost:9112/ocpp/testuser")
, new Draft_6455(Collections.emptyList(), Collections.singletonList(new Protocol("ocpp2.0.1")))
, httpHeaders
) {
...
在这种情况下,如果端点是受保护的资源,则在 HttpSession 终止时会发生 websocket close 1008。
Reference:阅读第 7.2 段
**解决方案是在websocket addInterceptor中直接实现http基础处理。 (Spring Security httpBasic 不使用) **
在 addInterceptor
中添加 WebSocketConfig.java
:
.addInterceptors(new HandshakeInterceptor() {
@Override
public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
log.debug("===============================================================");
log.debug("beforeHandshake[]"+attributes);
ServletServerHttpRequest ssreq = (ServletServerHttpRequest)serverHttpRequest;
ServletServerHttpResponse ssres = (ServletServerHttpResponse)serverHttpResponse;
HttpServletRequest req = ssreq.getServletRequest();
HttpServletResponse res = ssres.getServletResponse();
HttpSession session = req.getSession();
log.debug("session["+session.getId());
log.debug("session["+session.getMaxInactiveInterval());
//authentication
try {
String header = req.getHeader("Authorization");
log.debug("header[" + header + "]");
if (header == null) {
log.debug("The Authorization header is empty");
// throw new BadCredentialsException("The Authorization header is empty");
} else {
header = header.trim();
if (!StringUtils.startsWithIgnoreCase(header, "Basic")) {
log.debug("The Authorization header does not start with Basic.");
} else if (header.equalsIgnoreCase("Basic")) {
throw new BadCredentialsException("Empty basic authentication token");
} else {
byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8);
byte[] decoded;
try {
decoded = Base64.getDecoder().decode(base64Token);
} catch (IllegalArgumentException var8) {
throw new BadCredentialsException("Failed to decode basic authentication token");
}
String token = new String(decoded, "UTF-8");
int delim = token.indexOf(":");
if (delim == -1) {
throw new BadCredentialsException("Invalid basic authentication token");
} else {
log.info("TOKEN [" +token+"]");
String principal = token.substring(0, delim);
String credencial = token.substring(delim + 1);
//your
if(principal.equals("testuser") && credencial.equals("1234")){
log.debug("login OK");
}else{
throw new BadCredentialsException("Invalid basic authentication token");
}
}
}
}
}catch(Exception e){
log.error("Basic Authentication error", e);
res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
res.addHeader("WWW-Authenticate", "Basic realm=" + "Realm" + "");
PrintWriter pw = res.getWriter();
pw.println("Invalid status code received: 401 Status line: HTTP/1.1 401");
return false;
}
return true;
}