채팅 구현 정보 정리 - DevCamp2Flame/FlameTalk_Server GitHub 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를 사용할 것이 아니라면,
  • enableSimpleBroker → enableStompBrokerRelay 로 변경해야 한다. Simple Message Broker는 처음 시작하기에 좋지만, 일부 STOMP COMMAND만 지원한다는 단점이 있다. 구체적으로, Simple Message Broker는 acks, receipts 등의 다른 기능을 지원하지 않는다. 뿐만 아니라, 연결된 구독자들에게 메시지 전송하는 경우에 Simple Message Broker는 단순한 반복문(Loop)에 의존하기 때문에 클러스터링에 적합하지 않다는 단점이 있다. 위와 같은 단점들은 완전한 기능을 갖춘 Message Broker를 사용하므로써, 애플리케이션을 업그레이드할 수 있다. RabbitMQ, ActiveMQ 같은 외부 메시지 브로커를 선택해 설치하고 실행하면, 애플리이케션에서 STOMP broker relay를 아래와 같이 설정할 수 있다.
@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