Spring WebSocket Stomp 메시지 보내기 - 1. broadcast
web/websocket

Spring WebSocket Stomp 메시지 보내기 - 1. broadcast

반응형
@Controller
public class GreetingController {

    @MessageMapping("/hello")
    // simple broker를 사용하려면 Broker 설정에서 사용한 config에 맞는 값을 사용해야한다.
    @SendTo("/topic/greetings")
    public Greeting greeting(HelloMessage message) {
        return new Greeting("Hello! " + HtmlUtils.htmlEscape(message.getName()));
    }

}

이전 공부에서 SimpleBroker관련해서 알아봤다.(https://wedul.site/693)

 

SimpleBroker로 알아보는 WebSocket Massage Broker

Simple Message Broker simple broker는 클라이언트에서 전달 받은 subscription을 메모리에 저장하고 연결된 client에게 메시지를 보내는 역할을 한다. 하지만 문서에 보면 알수 있듯이 제공하는 일부의 기능

wedul.site

 

이제 SimpleBroker를 이용해서 메시지를 주고받는 것에 대해 공부할 예정인데 broker channel을 subscribe함으로써 모두 데이터를 동일하게 받을 수 있는 것과 specific한 유저에게만 메시지가 전달될 수 있도록 하는 기능 두가지에 대해 정리해볼 예정이다. 이 중 오늘은 broadcast에 대해 정리해보겠다.

 

 

설정 

simpleBroker에 연결할 prefix에 /topic, /direct를 연결하고 applicationDestinationPrefix에 /app을 지정한다.

 

simpleBroker에 추가된 엔드포인트 prefix를 달고 들어오는 요청들은 브로커 채널에 전달된다. 이중 /topic은 아래에 추가한 별도 엔드포인트에서 브로커 채널로 보내는 @SendTo 애노테이션 기능을 테스트하기 위해 사용할 예정이고 /direct는 별도 컨트롤러나 MessageMapping작업을 거치지 않고 바로 브로커 채널로 전달하여 브로커 채널과 동일한 엔드포인트를 구독하고 있는 모든 사용자에게 메시지가 전달되는지 확인해볼 예정이다.

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketBrokerConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
	    config.enableSimpleBroker("/topic", "/direct");
    	config.setApplicationDestinationPrefixes("/app");
    }
}

 

그럼 첫번째 endpoint /topic을 테스트하기 위해서 클래스를 선언해보자. 우선 @Controller를 클래스 레벨에서 선언하고 매핑될 엔드포인트를 @MessageMapping에 value를 지정하고 (setApplicationDestinationPrefixes 값이 지정되어 있을 경우 실제 경로는 prefix + value) @SendTo로 브로커 채널에 전달할 엔드포인트를 생성한다. 메소드에 반환되는 메시지 정보가 브로커를 통해 전달되게 한다.

 

@Controller
public class GreetingController {

    @MessageMapping("/hello")
    // simple broker를 사용하려면 Broker 설정에서 사용한 config에 맞는 값을 사용해야한다.
    @SendTo("/topic/greetings")
    public Greeting greeting(HelloMessage message) {
        return new Greeting("Hello! " + HtmlUtils.htmlEscape(message.getName()));
    }

}

 

그럼 이제 클라이언트를 설정해보자. sockeJS를 이용해서 spring websocket에 연결하면 session이 연결되고 sessionId가 부여된다. 이 sessionId별로 subscribe한 Subscription 객체가 생성되어 관리된다.

 

그럼 위에서 설정한 브로커 채널 두개를 subscribe해보자.

let socket = new SockJS('/gs-guide-websocket');
let client = Stomp.over(socket);
client.connect({
	user
}, function (frame) {
  setConnected(true);
	console.log('Connected: ' + frame);
	client.subscribe('/topic/greetings', function (greeting) {
		console.log(greeting)
		showGreeting("[hello] JSON.parse(greeting.body).content);
	});

	// direct로 topic에게 보내기
	client.subscribe('/direct/people', function (greeting) {
		console.log(greeting)
		showGreeting("[direct] JSON.parse(greeting.body).content);
	});
});
2021-08-12 21:03:51.772 DEBUG 19566 [http-nio-8080-exec-3] --- s.w.s.h.LoggingWebSocketHandlerDecorator : New WebSocketServerSockJsSession[id=3zkxrai2]

2021-08-12 21:03:51.801 DEBUG 19566 [clientInboundChannel-2] --- o.s.m.s.b.SimpleBrokerMessageHandler : Processing CONNECT session=3zkxrai2 2021-08-12 21:03:51.810 DEBUG 19566 [clientInboundChannel-5] --- o.s.m.s.b.SimpleBrokerMessageHandler : Processing SUBSCRIBE /topic/greetings id=sub-0 session=3zkxrai2

2021-08-12 21:03:51.811 DEBUG 19566 [clientInboundChannel-8] --- o.s.m.s.b.SimpleBrokerMessageHandler : Processing SUBSCRIBE /direct/people id=sub-1 session=axkeshfr3zkxrai2

connect로 인해 3zkxrai2 새로운 sessionId가 생성되었고 /topic/greedings와 /dicrec/people subscription 객체가 해당 sessionId에 배정되었다.

 

 

 

클라이언트에서 name을 입력하고 send 버튼을 누르면 /app/hello를 호출하고 미리 설정한 @MessageMapping과 @SendTo에 의해서 /topic/greetings에 값이 전달되어 구독하고 있는 사용자에게 값이 전달될 것이고 Direct Send를 누를경우 /direct/people를 브로커 채널에 전달하고 구독하고 있는 사용자에게 메시지가 전달되게 된다.

function sendName(client) {
    // app prefix를 달고 있고 MessageMapping에 hello로 되어있는곳에 추가
    client.send("/app/hello", {}, JSON.stringify({'name': $("#name").val()}));
}

function sendDirect(client) {
    // 특정 토픽에 다이렉트로 보내기
    client.send("/direct/people", {}, JSON.stringify({'content': $("#name").val()}));
}

 

그럼 먼저 /app/hello로 메시지를 보내서 설정한 /topic/greetings으로 메시지가 잘가고 구독한 사용자들에게 broadcast message가 잘 도착하는지 확인해보자.

 

duri라는 메시지와 함께 요청을 보내면 선언한 컨트롤러로 요청이 전달되고 @SendTo 애노테이션에 추가한 경로로 메소드에서 반환되는 값과 함께 SendToMethodReturnValueHandler로 값이 전달되고 handleReturnValue 메소드에서 처리가 된다.

@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, Message<?> message)
		throws Exception {

	// 1
	if (returnValue == null) {
		return;
	}

	MessageHeaders headers = message.getHeaders();
	String sessionId = SimpMessageHeaderAccessor.getSessionId(headers);
	DestinationHelper destinationHelper = getDestinationHelper(headers, returnType);

	SendToUser sendToUser = destinationHelper.getSendToUser();
	if (sendToUser != null) {
		boolean broadcast = sendToUser.broadcast();
		String user = getUserName(message, headers);
		if (user == null) {
			if (sessionId == null) {
				throw new MissingSessionUserException(message);
			}
			user = sessionId;
			broadcast = false;
		}
		String[] destinations = getTargetDestinations(sendToUser, message, this.defaultUserDestinationPrefix);
		for (String destination : destinations) {
			destination = destinationHelper.expandTemplateVars(destination);
			if (broadcast) {
				this.messagingTemplate.convertAndSendToUser(
						user, destination, returnValue, createHeaders(null, returnType));
			}
			else {
				this.messagingTemplate.convertAndSendToUser(
						user, destination, returnValue, createHeaders(sessionId, returnType));
			}
		}
	}

	// 2
	SendTo sendTo = destinationHelper.getSendTo();
	if (sendTo != null || sendToUser == null) {
		String[] destinations = getTargetDestinations(sendTo, message, this.defaultDestinationPrefix);
		for (String destination : destinations) {
			destination = destinationHelper.expandTemplateVars(destination);
			this.messagingTemplate.convertAndSend(destination, returnValue, createHeaders(sessionId, returnType));
		}
	}
}

1. header에서 요청을 보낸 sessionId를 꺼내고 다음 영역에서 다룰 특정 user에게만 보내는 메시지 인지 확인한다.

2. 메시지가 전달되어야할 최종 destination정보를 추출하고 messagingTemplate에 convertAndSend를 통해 메시지를 전달한다. 필요에 의해서는 @SendTo를 사용하지 않고 greeting메소드에서 바로 messagingTemplate를 통해 메시지를 보내도 된다. (추후 specific user에게 메시지를 보낼 때 이부분을 다룰예정)

 

이렇게 전달된 메시지는 최종적으로 SimpleBrokerMessageHandler에 sendMessageToSubscribers를 통해 해당 endpoint subscription을 보유하고 있는 세션들에게 전달된다.

2021-08-13 21:44:12.454 DEBUG 23387 [clientInboundChannel-36] --- o.s.m.s.b.SimpleBrokerMessageHandler : Processing MESSAGE destination=/topic/greetings session=n2lyk2xx payload={"content":"Hello! dfas"}
2021-08-13 21:44:28.349 DEBUG 23387 [clientInboundChannel-36] --- o.s.m.s.b.SimpleBrokerMessageHandler : Broadcasting to 2 sessions.

 

그리고 두번째로 /direct로 시작하는 엔드포인트로 별도 매핑없이 다이렉트로 메시지 채널에 메시지를 전송해보자. 이렇게 전송 시 위와는 다르게 별도의 SendTo를 거치지 않고 바로 broker channel로 전송되기 때문에 SimpleBrokerMessageHandler에 sendMessageToSubscribers를 통해 해당 endpoint subscription을 보유하고 있는 세션들에게 전달된다.

2021-08-13 21:49:26.503 DEBUG 23387 [clientInboundChannel-45] --- o.s.m.s.b.SimpleBrokerMessageHandler : Processing SEND /direct/people session=n2lyk2xx
2021-08-13 21:50:00.370 DEBUG 23387 [clientInboundChannel-45] --- o.s.m.s.b.SimpleBrokerMessageHandler : Broadcasting to 2 sessions.

로그는 별도의 메시지 매핑이 없기 때문에 바로 메시지가 전달된 부분만 보인다.

 

그럼 다음 글에서 specific한 유저에게 메시지를 보내는 실습을 해보자.

 

[참고]

https://www.slideshare.net/monthlyslide/201904-websocket

 

201904 websocket

이번 월간 슬라이드 4월호는 2019 스프링에서 발표한 슬라이드입니다.

www.slideshare.net

[소스코드]

https://github.com/weduls/websocket_study

반응형