@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를 이용해서 메시지를 주고받는 것에 대해 공부할 예정인데 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
[소스코드]
'web > websocket' 카테고리의 다른 글
Redis pub/sub 기능을 이용한 여러 서버 메시지 관리 (2) | 2021.08.23 |
---|---|
Spring WebSocket Stomp 메시지 보내기 - 2. specific user (0) | 2021.08.15 |
SimpleBroker로 알아보는 WebSocket Massage Broker (0) | 2021.08.01 |
Spring webSocket with stomp 기본 개념 정리 (1) | 2021.07.31 |