웹소켓을 먼저 소개하기 전에 기존의 통신 방식인 HTTP는 영구적인 연결 없이 클라이언트 쪽에서 Request할 때만 서버가 Response하는 방식으로 진행되는 단방향 Stateless protocol이다.
하지만 웹소켓은 서버와 클라이언트 사이에 Socket Connection을 유지해서 실시간, 양방향 통신이 가능하도록 하는 기술이다. 작동원리로는 HTTP 프로토콜로부터 서버와 클라이언트의 WebSocket 연결이 이루어지고 일정 시간이 지나면 HTTP 연결은 끊어지게 된다.
먼저 다수의 클라이언트가 보낼 메세지를 처리할 Handler가 필요한데, 텍스트 기반의 채팅을 구현할 것이므로 TextWebSocketHandler를 상속받는다.
handler 또한 controller로 보기에 handler 내부의 로직은 service단으로 옮기길 추천한다.
websocket을 통한 다중 채팅방이다.
비동기를 접목했다. 순서가 보장되지 않을 수 있으므로 클라이언트에서 타임스탬프를 통해 순서를 보장해야 한다.
@Component // ChatConfig에서 의존성 주입을 위해
@EnableAsync
public class ChatHandler extends TextWebSocketHandler{
//각 방마다 현재 연결되어 있는 session 목록
private final Map<Long, Set<WebSocketSession>> chatRoomSessionMap; // thread safe를 위해 ConCurrentHashMap 사용한다.
private final RedisService redisService;
private final ChatMessageService chatMessageService;
private final ObjectMapper objectMapper;
@Override
@Transactional
//WebSocket이 연결되었을 시에 실행하는 메서드
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
//쿼리 파라미터에서 정보 추출
Long chatRoomId = Long.parseLong(session.getUri().getQuery().split("&")[0].split("=")[1]);
String oauth2Id = session.getUri().getQuery().split("&")[1].split("=")[1];
if (!chatRoomSessionMap.containsKey(chatRoomId)) {
chatRoomSessionMap.put(chatRoomId, Collections.newSetFromMap(new ConcurrentMap<>())); // 마찬가지로 thread safe한 set 생성
} else {
redisService.deleteToken(chatRoomId.toString(), oauth2Id);
List<ChatMessageDto> chatMessageDtos = chatMessageService.getAllUnreadMessage(oauth2Id);
chatMessageService.deleteAll(oauth2Id);
for (ChatMessageDto chatMessageDto : chatMessageDtos) {
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(chatMessageDto))); // sendMessage는 WebSocketSession 이 해당 메세지를 받는 메서드이다.
}
}
chatRoomSessionMap.get(chatRoomId).add(session);
}
@Override
//@Async("threadPoolTaskExecutor") websocket은 계속해서 데이터를 주고 받으므로 비동기적으로 처리하여 서버 처리량을 높이고 응답시간을 단축
//이 메소드는 요청 당 스레드가 생기므로 비동기로 처리하나 안 하나 별로 상관없다. fcm 토큰으로 알림 보내기나 메세지 저장과 같은 것을 비동기로 처리해야 한다.
//클라이언트가 텍스트 메세지를 전송할 때 호출되는 메소드
@Transactional
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String msg = message.getPayload(); //getPayload 메서드로 메세지에 담긴 텍스트를 가져올 수 있다.
Long chatRoomId = Long.parseLong(session.getUri().getQuery().split("&")[0].split("=")[1]);
String oauth2Id = session.getUri().getQuery().split("&")[1].split("=")[1];
Set<WebSocketSession> roomSessions = chatRoomSessionMap.get(chatRoomId);
for (WebSocketSession roomSession : roomSessions) {
String oauthId = roomSession.getUri().getQuery().split("&")[1].split("=")[1];
ChatMessageDto chatMessageDto = new ChatMessageDto(msg, chatRoomId, oauthId, oauth2Id);
roomSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(chatMessageDto)));
}
/*
fcm토큰으로 알림 보내기
*/
for (Map.Entry<String, String> entry : redisService.getAllDisconnectedUser(chatRoomId.toString()).entrySet()) {
String oauthId = entry.getKey();
chatMessageService.save(oauthId, msg, chatRoomId, oauth2Id);
}
}
@Override
@Transactional
//WebSocket이 끊어졌을 때 실행되는 메서드
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
Long chatRoomId = Long.parseLong(session.getUri().getQuery().split("&")[0].split("=")[1]);
String oauth2Id = session.getUri().getQuery().split("&")[1].split("=")[1];
chatRoomSessionMap.get(chatRoomId).remove(session);
chatMessageService.deleteAll(oauth2Id);
redisService.putToken(chatRoomId.toString(), oauth2Id, "1123"); // 3번째 인자에 fcm token 넣어줘야함
}
}
이후 설정파일을 만들어준다.
@Configuration
@EnableWebSocket // ws:로 들어오는 url을 받기 위한 어노테이션
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketConfigurer {
private final WebSocketHandler chatHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
// ws://localhost:9090/ws/chat 으로 요청이 들어오면 websocket 통신을 진행한다. 이후 chatHandler에서 작업함
registry.addHandler(chatHandler, "/ws/chat").setAllowedOrigins("*");//*로 설정함으로써 모든 ip에서 접근가능함
}
}
'Spring Boot' 카테고리의 다른 글
Spring Boot - properties에 있는 값 가져오기 (0) | 2024.04.10 |
---|---|
Spring Boot - STOMP를 이용한 실시간 채팅 구현 (0) | 2024.04.08 |
Spring Boot에서 selenium으로 동적 웹 크롤링하기 (0) | 2024.03.21 |
Spring Boot에서 jsoup으로 웹 크롤링하기 (0) | 2024.03.14 |
Spring Boot - WebFlux (0) | 2024.03.10 |