
Я разрабатываю многопользовательскую игру как личный проект (приложение Spring Boot + React). Большая часть активности пользователя в игре происходит через веб-сокет, поэтому мне нужно, чтобы сеанс Spring сохранялся до тех пор, пока существует какая-либо активность веб-сокета.
Я выполнил конфигурацию, описанную в разделе документация:
https://docs.spring.io/spring-session/r ... ocket.html
и там сказано, что Spring должен автоматически обрабатывать обновление сеанса - именно то, что я хочу, но это просто не работает. Сообщения веб-сокета не обновляют весенний сеанс, что приводит к тайм-ауту.
Вот мой класс конфигурации веб-сокета:
Код: Выделить всё
package com.myapp.guess_who.configuration;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.session.Session;
import org.springframework.session.web.socket.config.annotation.AbstractSessionWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
@RequiredArgsConstructor
@EnableWebSocketMessageBroker
@Configuration
public class WebSocketConfig extends AbstractSessionWebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic", "/queue");
registry.setApplicationDestinationPrefixes("/app");
}
@Override
protected void configureStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").setAllowedOrigins("http://localhost:3000");
}
}
Код: Выделить всё
spring:
session:
timeout: 15s
redis:
repository-type: indexed # needed to be able to listen for redis session events
Код: Выделить всё
package com.myapp.guess_who.listener;
import com.myapp.guess_who.room.RoomManager;
import lombok.RequiredArgsConstructor;
import org.springframework.context.event.EventListener;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.session.Session;
import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDestroyedEvent;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.UUID;
@RequiredArgsConstructor
@Component
public class RedisSessionListener {
private final SimpMessagingTemplate messagingTemplate;
private final RoomManager roomManager;
@EventListener
public void sessionCreated(SessionCreatedEvent event) {
System.out.printf("Session %s created%n", event.getSession().getId());
System.out.printf("%s - creation time%n%n", event.getSession().getCreationTime().atZone(ZoneId.systemDefault()).toLocalTime());
}
@EventListener
public void sessionDestroyed(SessionDestroyedEvent event) {
System.out.printf("Session %s destroyed%n", event.getSession().getId());
System.out.printf("%s - creation time%n", event.getSession().getCreationTime().atZone(ZoneId.systemDefault()).toLocalTime());
System.out.printf("%s - last accessed time%n", event.getSession().getLastAccessedTime().atZone(ZoneId.systemDefault()).toLocalTime());
System.out.printf("%s - current time%n%n", Instant.now().truncatedTo(ChronoUnit.MILLIS).atZone(ZoneId.systemDefault()).toLocalTime());
Session session = event.getSession();
UUID roomId = session.getAttribute("roomId");
UUID playerId = session.getAttribute("playerId");
if (roomId == null || playerId == null || !roomManager.roomExists(roomId)) {
return;
}
roomManager.removePlayer(roomId, playerId);
messagingTemplate.convertAndSend("/topic/room/%s/player/%s/sessionInvalidate".formatted(roomId, playerId), "timeout");
}
}
Код: Выделить всё
package com.myapp.guess_who.player;
import com.myapp.guess_who.room.RoomManager;
import com.myapp.guess_who.team.Team;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
import java.time.Instant;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.Map;
import java.util.UUID;
@Slf4j
@RequiredArgsConstructor
@Controller
public class PlayerController {
private final RoomManager roomManager;
private final PlayerService playerService;
@MessageMapping("/room/{roomId}/player/{playerId}/changeTeam")
@SendTo("/topic/room/{roomId}/players")
public Map changePlayerTeam(
@DestinationVariable("roomId") UUID roomId,
@DestinationVariable("playerId") UUID playerId,
@Payload Team newTeam
) {
Map players = roomManager.getRoom(roomId).getPlayers();
System.out.printf(
"%s - changePlayerTeam called%n%n",
Instant.now().truncatedTo(ChronoUnit.MILLIS).atZone(ZoneId.systemDefault()).toLocalTime()
);
playerService.changePlayerTeam(players, playerId, newTeam);
return players;
}
}
Постоянная смена команды во время ожидания в комнате не возобновляет сеанс и приводит к тайм-ауту через 15 секунд. Вот вывод консоли:
Код: Выделить всё
Session 428df918-f84a-4203-b9c6-c9fc63e9eed9 created
17:15:05.029 - creation time
17:15:14.978 - changePlayerTeam called
17:15:17.884 - changePlayerTeam called
Session 428df918-f84a-4203-b9c6-c9fc63e9eed9 destroyed
17:15:05.029 - creation time
17:15:05.549 - last accessed time
17:15:20.605 - current time
Я попробовал обновить весеннюю сессию вручную в моем специальном перехватчике:
Код: Выделить всё
package com.myapp.guess_who.interceptor;
import jakarta.servlet.http.HttpSession;
import org.springframework.lang.NonNull;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.stereotype.Component;
import java.util.Objects;
@Component
public class WebSocketSessionInterceptor implements ChannelInterceptor {
@Override
public Message preSend(@NonNull Message message, @NonNull MessageChannel channel) {
SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.wrap(message);
System.out.println(headerAccessor.getSessionAttributes());
// Access HttpSession from the message headers
HttpSession httpSession = (HttpSession) Objects.requireNonNull(headerAccessor.getSessionAttributes()).get("HTTP_SESSION");
if (httpSession != null) {
// Manually update the session to renew it
httpSession.setAttribute("lastAccessedTime", System.currentTimeMillis());
}
return message;
}
}
Код: Выделить всё
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(webSocketSessionInterceptor);
}
Код: Выделить всё
System.out.println(headerAccessor.getSessionAttributes());
{}
Похоже, что автоконфигурация Spring неправильно сопоставляет сеанс http с сеансом веб-сокета.
Я искал решение на форумах и в документации уже 2 дня, но не нашел ни одного, которое бы работало. Я что-то пропустил?
Я знаю, что могу реализовать это вручную, но мне бы очень хотелось знать, как заставить его работать с механизмом автоконфигурации Spring и избежать ненужного кода.
Подробнее здесь: https://stackoverflow.com/questions/791 ... tive-users