Configure Spring for STMOP messaging
package com.lguplus.chumi.common.config
import com.lguplus.chumi.common.message.websocket.StompEventHandler
import org.springframework.context.annotation.Configuration
import org.springframework.messaging.simp.config.ChannelRegistration
import org.springframework.messaging.simp.config.MessageBrokerRegistry
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker
import org.springframework.web.socket.config.annotation.StompEndpointRegistry
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer
import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration
@Configuration
@EnableWebSocketMessageBroker
class StompWebSocketConfig(
private val corsProperties: CorsProperties,
private val stompEventHandler: StompEventHandler,
) : WebSocketMessageBrokerConfigurer {
override fun configureMessageBroker(registry: MessageBrokerRegistry) {
registry
.setPreservePublishOrder(true)
.setApplicationDestinationPrefixes("/app") // pub
.enableSimpleBroker("/topic") // sub
.setTaskScheduler(taskScheduler())
.setHeartbeatValue(longArrayOf(50000, 50000)) // inbound, outbound (50s)
}
override fun registerStompEndpoints(registry: StompEndpointRegistry) {
registry
.addEndpoint("/ws")
.setAllowedOrigins(*corsProperties.allowedOrigins.toTypedArray())
}
override fun configureClientInboundChannel(registration: ChannelRegistration) {
registration.interceptors(stompEventHandler)
}
// STOMP 에서 64KB 이상의 데이터 전송을 못하는 문제 해결
override fun configureWebSocketTransport(registration: WebSocketTransportRegistration) {
registration
.setMessageSizeLimit(160 * 64 * 1024)
.setSendTimeLimit(100 * 10000)
.setSendBufferSizeLimit(3 * 512 * 1024)
}
private fun taskScheduler(): ThreadPoolTaskScheduler {
val taskScheduler = ThreadPoolTaskScheduler()
taskScheduler.initialize()
return taskScheduler
}
}
@EnableWebSocketMessageBroker
: WebSocket 메시지 handling을 가능케 함, backed by message broker
- configureMessageBroker() :
enableSimpleBroker()
: 단순한 메모리 기반의 메시지 브로커를 작동시킴. destination
의 prefix가 /topic
인 경우에 대해 message를 다시 클라이언트로 돌려보냄setApplicationDestinationPrefixes()
: /app 으로 시작하는 메시지는 @MessageMapping 으로 bound 시킴
@Controller
public class GreetingController {
@MessageMapping("/hello")
@SendTo("/topic/greetings")
public Greeting greeting(HelloMessage message) throws Exception {
Thread.sleep(1000); // simulated delay
return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!");
}
}
/app/hello
엔드포인트는 GreetingController.greeting()
메서드로 매핑되어 작동하게 됨@SendTo
: /topic/greetings 를 구독하고 있는 모든 구독자에게 response를 보내게 됨
STOMP Client와 Server 사이 Sample 코드
const stompClient = new StompJs.Client({
brokerURL: 'ws://localhost:8081/ws'
});
stompClient.onConnect = (frame) => {
setConnected(true);
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/greetings/greetingId', (greeting) => {
showGreeting(greeting.body);
});
};
stompClient.onWebSocketError = (error) => {
console.error('Error with websocket', error);
};
stompClient.onStompError = (frame) => {
console.error('Broker reported error: ' + frame.headers['message']);
console.error('Additional details: ' + frame.body);
};
function setConnected(connected) {
$("#connect").prop("disabled", connected);
$("#disconnect").prop("disabled", !connected);
if (connected) {
$("#conversation").show();
}
else {
$("#conversation").hide();
}
$("#greetings").html("");
}
function connect() {
stompClient.connectHeaders = {
"Test-Header": "test-value"
}
stompClient.activate();
}
function disconnect() {
stompClient.deactivate();
setConnected(false);
console.log("Disconnected");
}
function sendName() {
stompClient.publish({
destination: "/app/agent/ping",
body: $("#name").val()
});
}
function showGreeting(message) {
$("#greetings").append("<tr><td>" + message + "</td></tr>");
}
$(function () {
$("form").on('submit', (e) => e.preventDefault());
$( "#connect" ).click(() => connect());
$( "#disconnect" ).click(() => disconnect());
$( "#send" ).click(() => sendName());
});
@Controller
@MessageMapping("/agent")
class AgentMessageController( // TODO: 사용 여부 확인 후 제거
private val messagingTemplate: SimpMessagingTemplate,
) {
private val log = logger()
/**
*! /app/agent
*/
@MessageMapping("/ping")
@SendTo("/topic/greetings/greetingId")
private fun ping(agentId: String): String {
val pongMessage = SanrioMessage(AgentPongStompMessage(agentId = agentId))
messagingTemplate.convertAndSend(pongMessage.data.endpoint, pongMessage)
log.info("Sent agent pong. User ID: ${pongMessage.data.agentId}")
return agentId
}
}