채팅 구현 정보 정리 - DevCamp2Flame/FlameTalk_Server Wiki

개념잡기

채팅방 생성 : pub / sub 구현을 위한 Topic이 생성됨

채팅방 입장 : Topic 구독(/sub)

SUBSCRIBE
destination: /topic/chat/room/5
id: sub-1

채팅방에서 메세지를 송수신 : 해당 Topic으로 메세지를 송신(pub), 메세지를 수신(sub)

SEND
destination: /pub/chat
content-type: application/json
 
{"chatRoomId": 5, "type": "MESSAGE", "writer": "clientB"} ^@
MESSAGE
destination: /topic/chat/room/5
message-id: d4c0d7f6-1
subscription: sub-1
 
{"chatRoomId": 5, "type": "MESSAGE", "writer": "clientB"} ^@

구현

  1. STOMP 에 내장된 Simple Broker를 사용할 것이 아니라면,
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/test").setAllowedOrigins("*").withSockJS();
    }
 
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.setApplicationDestinationPrefixes("/simple")
            .enableStompBrokerRelay("/topic", "/queue");
    }
}
  1. http://localhost:8080/test 에 연결하여 커넥션 수립하고, STOMP 프레임들을 해당 커넥션으로 전송하기 시작한다.
registry.addEndpoint("/test").setAllowedOrigins("*").withSockJS();
  1. 채팅 방을 만든다는 거는 ‘룸’이라는 개념을 우리가 객체로 생성하여 관리하는 것이다. /chat/room 으로 호출하면 채팅방이 만들어짐. ChatRoom, ChatRoomRepository 메시지를 보낼 때는 이제 /sub/chat/room 으로 보내게 됨.
  2. 채팅방 입장은 ‘룸’을 구독하는 것이다. subscribe 컨트롤러를 통해 /chat/enter 로 클라이언트의 요청을 받고 내부에서 stomp 컨트롤함. /sub/chat/room + 입장했습니다. 전송
  3. 실질적인 메시지 전송은 stomp를 활용하여 send, message를 통해 이루어진다.

컨트롤러를 통해 /chat/message 로 클라이언트의 요청을 받고 내부에서 stomp 컨트롤함. /sub/chat/room + message 전송 6. 사용자 인증 후 flametalk 유저인 사람만 소켓통신이 가능해야하기 때문에 토큰을 헤더에 담아 같이 요청을 보내야함.

STOMP 클라이언트는 CONNECT 프레임에 pass 인증 헤더를 추가해야 한다.

// ChannelInterceptor 사용해서 인증 헤더를 처리한다.
 
@Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(new ChannelInterceptor() {
            @Override
            public Message<?> preSend(Message<?> message, MessageChannel channel) {
                StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
                 
                // CONNECT 메시지인 경우에만 인증 처리
                if (StompCommand.CONNECT.equals(accessor.getCommand())) {
                    Authentication user = ...; // access authentication headers
                    accessor.setUser(user); // 사용자 헤더 추가
                }
 
                return message;
            }
        });
    }

STOMP broker relay

STOMP broker relay는 메시지를 외부 메시지 브로커로 포워딩하는 스프링의 MessageHandler(StompBrokerRelayMessageHandler)이다.

구체적으로, StompBrokerRelayMessageHandler는 아래 순서로 동작한다.

  1. 매 CONNECT 메시지마다 브로커와 TCP 연결을 수립하고,

(각 클라이언트마다 독립된 TCP 커넥션을 사용하는데, 이는 session-id 메시지 헤더로 식별한다.)

  1. 모든 메시지를 브로커에 전달한 다음,

  2. 브로커로부터 수신한 모든 메시지는 각각의 session-id를 메시지 헤더에 더하고, WebSocket 세션을 통해 클라이언트에게 전달된다.

위 흐름을 통해서 알 수 있듯이, StompBrokerRelayMessageHandler는 메시지를 양방향으로 전달하는 릴레이 역할을 한다.

또한, StompBrokerRelayMessageHandler는 자동으로(기본적으로) 메시지 브로커와 단 하나의 System TCP Connection을 수립한다.

System TCP Connection은 서버 애플리케이션이 메시지 브로커에게 메시지를 전달하기 위한 용도이다.

구체적으로, 메시지는 어떠한 클라이언트와도 관련이 없기 때문에 session-id 헤더도 가지고 있지 않다.

System TCP Connection은 효율적으로 공유가 가능하지만, 메시지 브로커로부터 메시지를 받는 용도로 사용하지 않는다.

이러한, StompBrokerRelayMessageHandler은 System TCP Connection의 몇 가지 설정할 수 있도록 아래와 같은 메서드를 제공한다.

스프링 STOMP Configuration에서도 아래와 같이 설정할 수 있도록 메서드를 제공한다.

@Configuration
 
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
 
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        ExecutorSubscribableChannel
        registry.addEndpoint("/test").setAllowedOrigins("*").withSockJS();
    }
 
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.setApplicationDestinationPrefixes("/simple")
            .enableStompBrokerRelay("/topic", "/queue")
            .setSystemLogin(String)
            .setSystemPasscode(String)
            .setSystemHeartbeatSendInterval(long)
            .setSystemHeartbeatReceiveInterval(long)
    }
}

뿐만 아니라, 애플리케이션의 컨트롤러, 서비스 등의 컴포넌트에서도 broker relay에 메시지를 보내어, 구독중인 WebSocket 클라이언트들에게 메시지를 브로드캐스팅할 수 있다.

실제로 broker relay는 강력하고 확장 가능한 메시지 브로드캐스팅을 지원한다.

TCP 커넥션을 관리하기 위해서는 io.projectreactor.netty:reactor-netty과 io.netty:netty-all 의존성을 추가해야 한다.

참고

STOMP

WebSocket