Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
Tags
- 작은수제거하기
- java map 출력
- java set 저장
- 항해15기
- java최솟값구하기
- java list 저장
- java참조자료형
- 코딩부트캠프후기
- javaJRE
- java set 출력
- 노베이스부트캠프
- 격파르타장점
- java list 출력
- 프로그래머스제일작은수
- java map 저장
- 컴파일
- java알고리즘문제풀이
- java map
- sqld자격증합격
- 격파르타합격후기
- java 자료구조 활용
- 격파르타비전공자
- java알고리즘
- java기본자료형
- javaJVM
- 격파르타후기
- 인터프린터언어
- 비전공자sqld
- 항해99후기
- 프로그래머스
Archives
- Today
- Total
코딩과 결혼합니다
[Game_Crew] WebSocket으로 1대1 채팅기능 구현하기(1) - 적용x 본문
728x90
게임 경험의 향상을 위하여 채팅기능을 구현하려 한다. 채팅을 통해서 대화를 나눠봄으로 자신과 잘 맞는 플레이어인지 또 한번 검증을 할 수 있다. 카카오톡과 같이 다중 채팅방이 가능한 1:1 채팅 서비스를 만들것이다.
- Spring WebSocket Stomp로 채팅 구현하기
- WebSocket 연결전 tcp handshake 과정에서 JWT 인증하기
- Spring WebSocket Exception 에러 핸들링
Spring WebSocket Stomp로 채팅 구현하기
📌의존성 추가
//websocket
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.webjars:webjars-locator-core'
implementation 'org.webjars:sockjs-client:1.5.1'
implementation 'org.webjars:stomp-websocket:2.3.4'
//mongo
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
📌프로퍼티즈 설정
#MongoDB
spring.data.mongodb.uri=mongodb://<호스트>:<포트>/<데이터베이스>
명령 프롬프트(Windows) 또는 터미널(Mac/Linux)에서 mongo 명령어를 실행하여 MongoDB 쉘에 접속하여 use 명령어를 사용하여 데이터베이스를 생성
📌 WebSocketConfig
- WebSocket을 구현하기 위한 설정 클래스
- @ Annotation
- @Configuration : 이 클래스가 스프링의 설정 클래스임을 나타냄
- @EnableWebSocketMessageBroke : WebSocket 메시지 브로커의 구성을 설정할 수 있다.
@Configuration
@EnableWebSocketMessageBroker
@RequiredArgsConstructor
public class WebSocket implements WebSocketMessageBrokerConfigurer {
private final StompHandler stompHandler;
private final StompExceptionHandler stompExceptionHandler;
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/sub");
registry.setApplicationDestinationPrefixes("/pub");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry){
registry
.setErrorHandler(stompExceptionHandler)
.addEndpoint("/websocket-endpoint")
.addInterceptors()
.setAllowedOriginPatterns("*");
}
//tcp handshake시 jwt 인증
@Override
public void configureClientInboundChannel(ChannelRegistration registration){
registration.interceptors(stompHandler);
}
}
- configureMessageBroker
브로커의 구성. "/sub" 접두사를 가진 목적지(구독자)에 메세지를 보낼 수 있도록 설정하고, "/pub"접두사를 가진 목적지(발행자)로부터의 메시지를 처리할 수 있도록 설정한다. - setApplicationDestinationPrefixes
Stomp 엔드포인트를 등록하는 메서드이다. "/websocket-endpoint" 경로의 엔드포인트를 추가로 등록하고, setErrorHandler 메서드를 통해 Stomp 예외 처리 핸들러를 설정한다. setAllowedOriginPatterns 메서드를 통해 모든 원본 주소에서의 접근을 허용한다. - configureClientInboundChannel
클라이언트의 인바운드 채널 구성을 설정한다. interceptors 메서드를 통해 stompHandler를 인터셉터로 등록 한다.
✏️메시지 브로커
프로듀서(생산자)로부터 메시지를 받아서 컨슈머(소비자)에게 전달하는 역할을 한다. 메시지 브로커는 메시지를 전달하는 동안 메시지를 보관, 라우팅, 변환 및 관리하는 기능을 수행한다. 아래의 방식을 통해 시스템간의 비동기 메시징을 가능하게 한다.
- 프로듀서는 메시지를 생성하고 메시지 브로커에게 전달한다.
- 메시지 브로커는 이메시지를 큐에 저장한다.
- 컨슈머가 메시지를 요청핳면, 메시지 브로커는 큐에서 메시지를 꺼내 컨슈머에게 전달한다.
✏️인바운드 채널
클라이언트에서 서버로 데이터를 전송하는 경로를 의미한다. WebSocket에서는 클라이언트로부터 들어오는 연결을 인바운드 채널이라고 부른다. 이 채널을 통해 들어오는 메시지는 서버에서 처리되고, 필요한 경우 다른 클라이언트에게 전달된다.
📌 Controller
- WebSocket을 사용한 실시간 채팅을 처리하는 컨트롤러
- @ Annotation
- @MessageMapping : WebSocket 요청을 처리하는 메서드임을 나타낸다.
- @SendTo : 해당 경로로 메시지가 브로드캐스트된다.
@RestController
@Slf4j
@RequiredArgsConstructor
@RequestMapping("/chat")
public class ChatController {
private final ChatService chatService;
@MessageMapping("/{roomNo}")
@SendTo("/sub/{roomNo}")
public ChatResponse broadcasting(final ChatRequest request,
@DestinationVariable(value = "roomNo") final String chatRoomNo){
log.info("{roomNo : {}, request : {}}", chatRoomNo, request);
return chatService.recordHistory(chatRoomNo, request);
}
}
- ChatResponse broadcasting
MessageMapping 애너테이션에 의해 WebSocket 요청이 들어오면 호출되는 메서드이다.
ChatService의 recordHistory() 메서드를 호출하여 채팅기록을 저장한 후, ChatResponse 객체를 반환한다.
WebSocket 연결전 tcp handshake 과정에서 JWT 인증하기
📌 StompHandler
- Stomp(WebSocket 하위 프로토콜) 메시지가 전송되기 전에 처리하는 역할을 한다.
- @Order(Ordered.HIGEST_PRECEDENCE + 99) : 해당 클래스의 우선순위를 설정한다. HIGEST_PRECEDENCE는 가장 높은 우선순위를 나타내며, + 99는 추가적인 우선순위를 설정한다.
@Component
@RequiredArgsConstructor
@Order(Ordered.HIGHEST_PRECEDENCE + 99)
public class StompHandler implements ChannelInterceptor {
private final JwtUtil jwtUtil;
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel){
final StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
if (StompCommand.CONNECTED == accessor.getCommand()){
final String authorization = jwtUtil.extractJwt(accessor);
jwtUtil.validateToken(authorization);
}
return message;
}
}
- preSend
STOMP 메시지가 전송되기 전에 호출됩니다. message와 channel을 파라미터로 받는다. - accessor
message를 StompHeaderAccessor로 감싸고, accessor 변수에 할당한다. - if()~
메서드를 사용하여 STOMP 메시지의 커맨드를 가져온다. 이 조건문은 STOMP 메시지의 커맨드가 CONNEXTED인지 확인한다. - authorization
accessor에서 JWT(Jason Web Token)를 추출한 다음 유효성을 검사한다.
📌 JwtUtil
public String extractJwt(StompHeaderAccessor accessor){
return accessor.getFirstNativeHeader("Authorization");
}
// 토큰 검증
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (SecurityException | MalformedJwtException | SignatureException e) {
log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
} catch (ExpiredJwtException e) {
log.error("Expired JWT token, 만료된 JWT token 입니다.");
} catch (UnsupportedJwtException e) {
log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
} catch (IllegalArgumentException e) {
log.error("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
}
return false;
}
- extractJwt
Stomp 프로토콜 메시지의 헤더에서 "Authorization"이라는 key 값을 가진 JWT 토큰을 추출 - validateToken
전달받은 JWT 토큰이 유효한지 검증
Spring WebSocket Exception 에러 핸들링
📌 StompExceptionHandler
STOMP 프로토콜에서 발생하는 다양한 예외 사항을 처리하여 클라이언트에게 적절한 에러 메시지를 전달하도록 설계
@Component
public class StompExceptionHandler extends StompSubProtocolErrorHandler {
private static final byte[] EMPTY_PAYLOAD = new byte[0];
public StompExceptionHandler() {
super();
}
@Override
public Message<byte[]> handleClientMessageProcessingError(Message<byte[]> clientMessage,
Throwable ex) {
final Throwable exception = converterTrowException(ex);
if (exception instanceof UnauthorizedException) {
return handleUnauthorizedException(clientMessage, exception);
}
return super.handleClientMessageProcessingError(clientMessage, ex);
}
private Throwable converterTrowException(final Throwable exception) {
if (exception instanceof MessageDeliveryException) {
return exception.getCause();
}
return exception;
}
private Message<byte[]> handleUnauthorizedException(Message<byte[]> clientMessage,
Throwable ex) {
return prepareErrorMessage(clientMessage, ex.getMessage(), HttpStatus.UNAUTHORIZED.name());
}
private Message<byte[]> prepareErrorMessage(final Message<byte[]> clientMessage,
final String message, final String errorCode) {
final StompHeaderAccessor accessor = StompHeaderAccessor.create(StompCommand.ERROR);
accessor.setMessage(errorCode);
accessor.setLeaveMutable(true);
setReceiptIdForClient(clientMessage, accessor);
return MessageBuilder.createMessage(
message != null ? message.getBytes(StandardCharsets.UTF_8) : EMPTY_PAYLOAD,
accessor.getMessageHeaders()
);
}
private void setReceiptIdForClient(final Message<byte[]> clientMessage,
final StompHeaderAccessor accessor) {
if (Objects.isNull(clientMessage)) {
return;
}
final StompHeaderAccessor clientHeaderAccessor = MessageHeaderAccessor.getAccessor(
clientMessage, StompHeaderAccessor.class);
final String receiptId =
Objects.isNull(clientHeaderAccessor) ? null : clientHeaderAccessor.getReceipt();
if (receiptId != null) {
accessor.setReceiptId(receiptId);
}
}
//2
@Override
protected Message<byte[]> handleInternal(StompHeaderAccessor errorHeaderAccessor,
byte[] errorPayload, Throwable cause, StompHeaderAccessor clientHeaderAccessor) {
return MessageBuilder.createMessage(errorPayload, errorHeaderAccessor.getMessageHeaders());
}
}
'코딩과 매일매일♥ > Game_Crew' 카테고리의 다른 글
[Game_Crew] WebSocket으로 1대1 채팅기능 구현하기(2) (0) | 2023.11.12 |
---|---|
[Game_Crew] 트러블슈팅 : S3 + 이미지 리사이징 적용하기 (0) | 2023.11.11 |
[Game_Crew] 리팩토링 + 트러블슈팅 : 이메일 인증 코드 리팩토링 (0) | 2023.10.31 |
[Game_Crew] 트러블슈팅 : 이메일 인증 완성 (0) | 2023.10.27 |
[Game_Crew] 유저 평점 기능 (0) | 2023.10.27 |