在构建实时通信运用时,WebSocket 无疑是一个强壮的东西。Spring Boot 供给了对 WebSocket 的支撑,使得完成实时功用变得更加简略。但是,一个常见的应战是怎么有用地办理 WebSocket 会话。本文旨在探讨怎么在 Spring Boot 运用中完成 WebSocket 会话办理,咱们将经过一个模仿的场景一步步展开评论。

场景设定

假定咱们正在开发一个在线谈天运用,该运用需求完成以下功用:

  1. 用户能够经过 WebSocket 实时发送和接收音讯。
  2. 系统需求跟踪用户的会话状况,以便在用户从头衔接时恢复状况。
  3. 为了进步功率和安全性,咱们需求监控闲暇衔接并及时封闭它们。

根据这个场景,咱们将探讨四种完成 WebSocket 会话办理的策略:

1. 运用现有的会话标识符

一种常见的做法是运用 HTTP 会话(例如,经过 cookies)来办理 WebSocket 会话。

完成办法

  • 在 WebSocket 握手阶段,从 HTTP 请求中提取会话标识符。
  • 将 WebSocket 会话与提取的会话标识符关联。
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
import javax.servlet.http.HttpSession;
import java.util.Map;
public class MyHandshakeInterceptor extends HttpSessionHandshakeInterceptor {
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
        if (request instanceof ServletServerHttpRequest) {
            ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
            HttpSession session = servletRequest.getServletRequest().getSession();
            attributes.put("HTTP_SESSION_ID", session.getId());
        }
        return super.beforeHandshake(request, response, wsHandler, attributes);
    }
}

这个拦截器需求在 WebSocket 的装备类中注册。例如,在 WebSocketConfig 类中,你能够这样注册拦截器:

import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new MyWebSocketHandler(), "/ws")
                .addInterceptors(new MyHandshakeInterceptor())
                .setAllowedOrigins("*");
        // 你也能够增加 .withSockJS() 假如你需求SockJS支撑
    }
    // ...其他装备...
}

2. 自界说协议音讯

另一种办法是在 WebSocket 衔接中界说自己的音讯格式,包括会话办理信息。

完成办法

  • 界说音讯格式(如 JSON),包括会话信息。
  • 在衔接建立后,经过 WebSocket 发送和接收这些自界说音讯。
@Controller
public class WebSocketController {
    @Autowired
    private WebSocketSessionManager sessionManager;
    @MessageMapping("/sendMessage")
    public void handleSendMessage(ChatMessage message, SimpMessageHeaderAccessor headerAccessor) {
        String sessionId = (String) headerAccessor.getSessionAttributes().get("HTTP_SESSION_ID");
        // 运用 sessionId 处理音讯
        // 能够经过 sessionManager 获取用户信息
    }
    // ...其他音讯处理办法...
}

3. 衔接映射

将每个 WebSocket 衔接映射到特定的用户会话。

完成办法

  • 在衔接建立时,从 WebSocket 握手信息中获取用户身份。
  • 维护一个映射,关联 WebSocket 会话 ID 和用户会话。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler; 
import java.util.Iterator; 
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
@Component
public class WebSocketSessionManager extends TextWebSocketHandler {
    @Autowired
    private WebSocketHandler webSocketHandler;
    private Map<String, String> sessionMap = new ConcurrentHashMap<>();
    private Map<String, Long> lastActiveTimeMap = new ConcurrentHashMap<>();
    public void registerSession(String websocketSessionId, String userSessionId) {
        sessionMap.put(websocketSessionId, userSessionId);
        lastActiveTimeMap.put(websocketSessionId, System.currentTimeMillis());
    }
    public String getUserSessionId(String websocketSessionId) {
        return sessionMap.get(websocketSessionId);
    }
    public void updateLastActiveTime(String websocketSessionId) {
        lastActiveTimeMap.put(websocketSessionId, System.currentTimeMillis());
    }
    public Long getLastActiveTime(String websocketSessionId) {
        return lastActiveTimeMap.get(websocketSessionId);
    }
    public void checkAndCloseInactiveSessions(long timeout) {
        long currentTime = System.currentTimeMillis();
        lastActiveTimeMap.entrySet().removeIf(entry -> {
            String sessionId = entry.getKey();
            long lastActiveTime = entry.getValue();
            if (currentTime - lastActiveTime > timeout) {
                closeSession(sessionId);  // 封闭会话
                sessionMap.remove(sessionId);  // 从用户会话映射中移除
                return true;  // 从活泼时刻映射中移除
            }
            return false;
        });
    }
    private void closeSession(String websocketSessionId) {
        // 逻辑来封闭 WebSocket 会话
        // 可能需求与 webSocketHandler 交互
    }
    public void unregisterSession(String websocketSessionId) {
        sessionMap.remove(websocketSessionId);
    }
    // 能够增加刊出会话的办法等
}

4. 心跳和超时机制

完成心跳音讯和超时机制,以办理会话的生命周期。

完成办法

  • 客户端守时发送心跳音讯。
  • 服务端监听这些音讯,并完成超时逻辑。
function sendHeartbeat() {
    if (stompClient && stompClient.connected) {
        stompClient.send("/app/heartbeat", {}, JSON.stringify({ timestamp: new Date() }));
    }
}
setInterval(sendHeartbeat, 10000); // 每10秒发送一次心跳
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.stereotype.Controller;
@Controller
public class HeartbeatController {
    @Autowired
    private WebSocketSessionManager sessionManager;
    @MessageMapping("/heartbeat")
    public void handleHeartbeat(HeartbeatMessage message, SimpMessageHeaderAccessor headerAccessor) {
        String websocketSessionId = headerAccessor.getSessionId();
        sessionManager.updateLastActiveTime(websocketSessionId);
        // 根据需求处理其他逻辑
    }
}

运用 Spring 的守时使命功用来定期履行会话超时查看,ScheduledTasks 类中的 checkInactiveWebSocketSessions 办法每5秒履行一次,调用 WebSocketSessionManagercheckAndCloseInactiveSessions 办法来查看和封闭超时的会话。

import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@EnableScheduling
@Component
public class ScheduledTasks {
    @Autowired
    private WebSocketSessionManager sessionManager;
    // 界说超时阈值,例如30分钟
    private static final long TIMEOUT_THRESHOLD = 30 * 60 * 1000;
    @Scheduled(fixedRate = 5000) // 每5秒履行一次
    public void checkInactiveWebSocketSessions() {
        sessionManager.checkAndCloseInactiveSessions(TIMEOUT_THRESHOLD);
    }
}

弥补:在 WebSocket 衔接封闭或用户刊出时,能够调用 unregisterSession 办法来整理会话信息。当 WebSocket 衔接封闭时,afterConnectionClosed 办法会被调用,这时咱们能够经过 sessionManager 移除对应的会话信息。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
public class MyWebSocketHandler extends TextWebSocketHandler {
    @Autowired
    private WebSocketSessionManager sessionManager;
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        String websocketSessionId = session.getId();
        sessionManager.unregisterSession(websocketSessionId);
        // 进行其他整理工作
    }
    // 完成其他必要的办法
}

总结

完成 WebSocket 会话办理需求综合考虑运用的需求和架构特点。Spring Boot 供给了完成这些功用的强壮支撑,但正确地运用这些东西和策略是成功的关键。经过本文的评论,咱们看到了怎么在一个实际场景中一步步地考虑和完成有用的 WebSocket 会话办理。