弹簧websocket在30分钟后关闭(超时)

时间:2018-05-29 15:03:47

标签: java spring-boot websocket

我正在尝试在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上看过帖子,但我看不到解决方案。

3 个答案:

答案 0 :(得分:1)

如果服务器或客户端之间的连接关闭,则可以关闭连接,如果它注意到已建立的连接上没有活动,则可以通过防火墙关闭。

因此您还需要在客户端检查超时。可以接受的是,30分钟是此类连接的默认超时,因此它在客户端连接它使用默认值。

此外,定期检查连接状态是一个很好的设计,例如发送一种ping(来自客户端的消息)/ pong(来自服务器的响应)消息。如果您每分钟都这样做,例如您已了解连接状态,并且永远不会因为不活动而关闭连接。

答案 1 :(得分:0)

我也发现30分钟后我的WebSocket也关闭了。

根本原因是默认情况下,http会话将在SpringBoot中关闭30分钟。

在SpringBoot中,配置属性server.servlet.session.timeout将更改默认行为,但可能会有some limit

此外,WebSocket连接具有保持有效的乒乓消息,因此在乒乓停止之前,永远不要关闭该连接。

经过一些跟踪,我找到了解决此问题的方法:

  1. 在我的情况下,关闭连接的计时器在io.undertow.server.session.InMemorySessionManager.SessionImpl
  2. 我们可以看到io.undertow.server.session.InMemorySessionManager.SessionImpl#setMaxInactiveInterval将重置计时器。
  3. javax.servlet.http.HttpSession#setMaxInactiveInterval将调用此方法。
  4. 因此,一旦setMaxInactiveInterval在每次超时之前被调用,连接将永远不会关闭。

这是我的实现方式:

  1. HandshakeRequest存储到配置器javax.websocket.EndpointConfig#getUserProperties中的javax.websocket.server.ServerEndpointConfig.Configurator#modifyHandshake
  2. 我们的客户将发送String消息以保持活动状态,,因此,在onMessage方法中,从HandshakeRequest获取javax.websocket.Session#getUserProperties,然后
HttpSession httpSession = (HttpSession) handshakeRequest.getHttpSession();
httpSession.setMaxInactiveInterval((int) (session.getMaxIdleTimeout() / 1000));

仅此而已,希望对您有所帮助。

答案 2 :(得分:0)

  1. 就我而言,SpringBoot 中 HttpSession 的超时设置为 server.servlet.session.timeout: 1(1 分钟)

  2. 将 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();
    }
    
  3. 在 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;
    }