프로젝트 간 구현해야될 내용이나 발생한 이슈들에 대해서의 자세다.
불편하니까 고쳐앉고 정신차리도록 하자
STOMP 활용한 실시간 채팅 구현
STOMP 가 뭐지?
- Simple Text Oriented Messaging Protocol 의 약자.
- WebSocket 위에서 동작하는 텍스트 기반 메세징 프로토콜이다.
- 클라이언트와 서버가 전송할 메세지 유형, 형식, 내용들을 정의하는 매커니즘
STOMP는 HTTP 위에서 동작하는 Frame 기반 프로토콜
Frame 은 아래와 같은 형식을 가짐
COMMAND
header1:value1
header2:value2
Body^@
Frame 이란
- Frame : 통신 상 가장 작은 단위
- Message : 다수의 Frame 으로 이루어진 구조
- Stream : 클라이언트 - 서버 맺어진 연결을 통해 주고받는 여러개의 Message
스프링에서 지원하는 STOMP 기능
- Message 를 @Controller 의 메세지 핸들링 메서드로 라우팅 함
- SimpleIn-Memory Broker 이용한 Subscribe 중인 다른 Client 에게 메세지 브로드캐스팅
- 스프링은 RabbitMQ,ActiveMQ 같은 외부 Messaging System을 STOMP Broker로 사용할 수 있도록 지원
이러한 구조 덕분에 스프링 웹 어플리케이션은 HTTP 기반의 보안설정, 공통된 검증 을 적용할 수 있음
클라이언트는 특정 경로에 대해서 Subscribe 한다면, 서버는 원할때마다 클라이언트에게 메세지 전송이 가능
또한 클라이언트는 서버에 메세지 전달할 수있는데,
서버는 @MessageMapping 된 메서드를 통해 해당 메세지를 처리할 수 있다.
서버는 Subscribe 한 클라이언트들에게 메세지를 브로드캐스팅할 수도 있다.
/topic ( one-to-many 의 pub-sub 관계 )
/queue ( one-to-one 의 message 교환 관계 )
* 현 프로젝트에서는 단순 pub-sub 관계로 표현할 예정.
서버는 MESSAGE 를 사용해 모든 Subscriber 들에게 메세지를 브로드캐스트 할 수 있다.
STOMP 브로커는 반드시 메세지 전달을 구독한 클라이언트들에게 전달해야함
웹 소켓 말고 왜 STOMP 사용하나요?
각 커넥션마다 WebSocketHandler를 구현하는 것보다 @Controller 적용된 객체를 이용해 조직적으로 관리를 할 수 있음( 메세지를 Controller 객체의 MessageMapping Annotation으로 라우팅시킬 수 있다. )
Stomp의 Destination(URI 경로)를 기반으로 Spring Security를 적용해 메세지를 보호할 수 있다.
STOMP 장점
WebSocket 만 이용할때 보다 더 풍부한 프로그래밍 모델을 제공할 수 있음
- 1. 메세지 프로토콜을 만들고, 형식을 커스터마이징 필요없음
- 2. 외부 브로커를 이용해 여러 서버를 관리할 수 있다.
- 3. 커넥션마다 WebSocketHandler 에서 관리하는 것 보다 @Controller 를 이용해 조직적 관리 가능
- 4. Spring Security 사용 가능
STOMP 사용
STOMP Message 헤더가 /pub 으로 시작하면 @MessageMapping 정보와 매핑 메서드를 호출
STOMP Message 헤더가 /sub 으로 시작하면 Message Broker 로 바로 라우팅됨.
서버
WebSocketConfig.java
@EnableWebSocketMessageBroker
@Configuration
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/stomp/chat")
//sockJs 클라이언트가 websocket handshake로 커넥션할 경로
.setAllowedOrigins("http://*:*")
//가능한 경로 설정 ( 전체 오픈 : 기호에따라 수정 )
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.setApplicationDestinationPrefixes("/pub");
// /pub 으로 시작하는 stomp 메세지의 경로는 @controller @MessageMaping 메서드로 라우팅
config.enableSimpleBroker("/sub");
// /sub 로 시작하는 stomp 메세지는 브로커로 라우팅함
}
}
StompChatController.java
@Controller
@RequiredArgsConstructor
public class StompChatController {
private final SimpMessagingTemplate template; //특정 Broker로 메세지를 전달
//Client가 SEND할 수 있는 경로
//stompConfig에서 설정한 applicationDestinationPrefixes와 @MessageMapping 경로가 병합됨
//"/pub/chat/enter"
@MessageMapping(value = "/chat/enter")
public void enter(ChatMessageDTO message){
message.setMessage(message.getWriter() + "님이 채팅방에 참여하였습니다.");
template.convertAndSend("/sub/chat/room/" + message.getRoomId(), message);
}
//"/pub/chat/message" 로 날린 데이터에 대해서 /sub/chat/room + roomId 한 구독자들에게 해당 message를 전달
@MessageMapping(value = "/chat/message")
public void message(ChatMessageDTO message){
template.convertAndSend("/sub/chat/room/" + message.getRoomId(), message);
}
}
/pub 으로 날라온 내용들에 대해서 처리하는 컨트롤러
클라이언트
<script>
// config 에서 설정한 endpoint로 websocket handshake 를 해준다.
var sockJs = new SockJS("/stomp/chat", null, {transports: ["websocket", "xhr-streaming", "xhr-polling"]});
// sockJs 를 stomp 에 넘겨준다.
var stomp = Stomp.over(sockJs);
// connect이 맺어지면 실행
stomp.connect({}, function (){
// subscribe(path, callback)으로 메세지를 받을 수 있음
// 해당 채팅방에 대해서 구독을 함 + 콜백함수로 chat 에 대한 메소드를 실행 (대충 가져온 메세지를 html 띄움)
stomp.subscribe("/sub/chat/room/" + roomId, function (chat) {
var content = JSON.parse(chat.body);
var writer = content.writer;
var dateTime = new Date().toDateString();
var str = '';
if(writer === username){
str = "<div class='col-6 offset-md-6'>";
str += "<div class='alert alert-warning'>";
str += "<b>" + writer + " : " + content.message + " : " + dateTime + "</b>";
str += "</div></div>";
$("#msgArea").append(str);
}
else{
str = "<div class='col-6'>";
str += "<div class='alert alert-success'>";
str += "<b>" + writer + " : " + content.message + "</b>";
str += "</div></div>";
$("#msgArea").append(str);
}
});
stomp.send('/pub/chat/enter', {}, JSON.stringify({roomId: roomId, writer: username}))
// send(path, header, message)로 메세지를 보낼 수 있음
// /pub 경로이므로 @MessageMapping 에 있는 endpoint로 이동
});
$("#button-send").on("click", function(e){
var msg = document.getElementById("msg");
stomp.send('/pub/chat/message', {}, JSON.stringify({roomId: roomId, message: msg.value, writer: username}));
// 버튼 클릭시 /pub 경로이므로 @MessageMapping 에 있는 endpoint 이동
// message endpoint는 해당 방을 구독한 사람들에게 메세지 전달함
});
});
</script>
Message 처리 과정
1. 클라이언트는 config에서 정의한 endpoint 에 (/stomp/chat) 커넥션을 수행하고 STOMP 프레임들을 해당 커넥션으로 전송
2. 클라이언트는 /sub/chat/room + roomId 경로의 목적지 헤더를 가지며 SUBSCRIBE 프레임을 전송
2-1. 서버는 프레임을 수신하면 디코딩하여 Message 로 변환하고 메세지를 clientInboundChannel 전송
2-2. 해당 채널에서 메세지를 Message Broker 라우팅 후 브로커는 해당 클라이언트의 구독 정보를 저장
3. 이후 클라이언트는 /pub/chat/message 경로의 목적지 헤더를 가지고 메세지를 전송
3-1. /pub 은 메세지가 @MessageMapping 메서드를 가진 컨트롤러에서 핸들링 됨
4. 핸들링된 메서드에서 반환한 값은 스프링 Message 변환
4-1. Message 에 내용을 담은채로 목적지는 /sub/chat/room + roomId 로 설정
4-2. 변환된 메세지는 BrokerChannel 로 전송되며 브로커에 의해 처리됨
5. 마지막으로 브로커는 매칭된 모든 구독자를 탐색하고 clientOutboundChannel을 통해 각 구독자들에게 Message 프레임을 보냄
간략히.
스프링의 Message > STOMP 의 Frame 인코딩 > 연결된 Websocket 커넥션으로 Frame 전송
참고자료
https://velog.io/@koseungbin/WebSocket#stomp
WebSocket
이 글은 Spring WebSocket(https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/web.htmlWebSocket 프로토콜은 표준된 방법으로 서버-클라이언트 간
velog.io
'2023년 > 멋쟁이사자처럼 팀프로젝트' 카테고리의 다른 글
[팀 프로젝트] 1명의 USER 가 n개의 모임에 참여했는지 여부를 파악하는 방법 구현하기 (0) | 2023.02.28 |
---|---|
[팀프로젝트] datepicker / Timepicker 적용하기 (2) | 2023.02.07 |
[팀프로젝트] Spring Boot에서 jwt + 쿠키를 활용한 로그인 구현과 비동기 방식 처리 (0) | 2023.02.01 |
[팀프로젝트] 비동기 방식 통신 + 스프링부트 RestController 활용한 웹 페이지 (0) | 2023.01.26 |
[팀프로젝트] 깃허브 워크플로우 (2) | 2023.01.20 |