Overview
Stomp 메시지에 대해서 간!단!히! 해보자 저번에 했던것들을 보면서 아 잘못적은것도 많고 아직 안되는 부분도 많지만 딱 핵심적인 부분만 보자 코드위주로 보게 될것이다.
- config
- handler
- controller
- service
JDK : 17
Springboot : 3.2.~
// websocket
implementation 'org.springframework.boot:spring-boot-starter-websocket'
1. Config
첫번째 설정파일이다.
따로 이야기를 할 것은 WebSocketMessageBrokerConfigurer 을 상속받아 사용한다.
1. registerStompEndpoints 는 엔드포인트를 지정한다. 한마디로 http://localhost:포트번호/ws 로 엔드포인트 ws 를 사용했다.
2. configureMessageBroker 는 구독과 발행의 포인트를 지정한다. 나는 구독은 /sub 발행은 /pub 로 하였다.
3. configureClientInboundChannel 는 channelRegistry 로 인터셉터를 지정할 수 있다.
@Configuration
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
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 com.store.goguma.chat.handler.StompHandler;
import lombok.RequiredArgsConstructor;
@EnableWebSocketMessageBroker
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private final StompHandler stompHandler;
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/sub");
registry.setApplicationDestinationPrefixes("/pub");
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(stompHandler);
}
}
2. Handler
handler 는 앞서 설명한 것 처럼 인터셉터를 지정하는 부분인데 나는 세션이 들어왔는지만 확인한다. 따로 더 많은 기능들이 있으니 참조하자
이전 ChannelInterceptorAdeptor 는 Springboot 버전 3.~ 부터 deprecated 되었으므로 ChannelInterceptor 를 implement 하여 사용한다.
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.stereotype.Component;
@Component
public class StompHandler implements ChannelInterceptor {
@Override
public void postSend(Message message, MessageChannel channel, boolean sent) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
String sessionId = accessor.getSessionId();
switch (accessor.getCommand()) {
case CONNECT:
System.out.println("세션 들어옴 => " + sessionId);
break;
case DISCONNECT:
System.out.println("세션 끊음 => " + sessionId);
break;
default:
break;
}
}
}
3. 이벤트 리스너
말 그대로 이벤트 리스너를 지정해줬다 세션이 들어오거나 나가는 것만 확인 용도로 했지만 코드를 추가해서 세션에서 나가면 채팅방에서 나갔다는 뭔가를 넣어주거나 가능하다. 여러 기능을 추가해보자!
import org.springframework.context.event.EventListener;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.socket.messaging.SessionConnectEvent;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;
import lombok.RequiredArgsConstructor;
@RestController
@RequiredArgsConstructor
public class SocketController {
private final SimpMessageSendingOperations simpleMessageSendingOperations;
@EventListener
public void handleWebSocketConnectListener(SessionConnectEvent event) {
System.out.println("Received a new web socket connection");
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
String sessionId = headerAccessor.getSessionId();
System.out.println("Session Connected: " + sessionId);
}
@EventListener
public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
String sessionId = headerAccessor.getSessionId();
System.out.println("Session Disconnected: " + sessionId);
// 세션 해제 시 필요한 로직을 추가할 수 있습니다.
// 예를 들어, 채팅방에서 사용자가 나갔을 때 해당 사용자를 처리하는 등의 작업을 수행할 수 있습니다.
// 이 부분에 필요한 로직을 추가하세요.
}
}
4. Controller
컨트롤러에서 사실상 필요한것은 @MessageMapping("/chat/sendMessage") 이 부분이다. 궁금한게 많을 것이다. 왜 /sub /pub 를 해놓고서 쓰이질 않느냐고 그 부분은 나중에 html 에서 확인하자
@SubscribeMapping("/sub/chat/{roomId}") 이 부분이 안되는 것은 인터셉터가 잡고 있어서 그런건지 아니면 뭔지 안들어온다. 나중에 알아봐야 겠다.. ( 사실 안되도 상관없다 )
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.simp.annotation.SubscribeMapping;
import org.springframework.web.bind.annotation.RestController;
import com.store.goguma.entity.ChatMessage;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RestController
public class CahtController {
@Autowired
ChatService chatService;
@MessageMapping("/chat/sendMessage")
public void sendMessage(@Payload ChatMessage message) {
System.out.println(message);
chatService.handleChatMessage(message);
}
// 현재 이부분이 안됨 딱히 상관은 없는데 짜증나네 ㅋㅋㅋㅋㅋㅋㅋ
@SubscribeMapping("/sub/chat/{roomId}")
// public ChatRoom joinRoom(@DestinationVariable String roomId) {
public void joinRoom(@DestinationVariable String roomId) {
System.out.println("구독 룸넘버 | ");
// chatService.handleUserJoin(roomId , "USER");
// return chatService.getChatRoom(roomId);
}
}
5. Service
서비스단이다.
서비스 단에서는 /sub 한마디로 발행을 한다. SimpleMessageSendingOperations 의 convertAndSend 로 간단하게 메시지를 보낼 수 있다.
/pub/chat/{방번호} 를 구독한 사람들에게 /sub/chat/{방번호} 컨텐츠를 쏴주자
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.stereotype.Service;
import com.store.goguma.entity.ChatMessage;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class ChatService {
private final SimpMessageSendingOperations messagingTemplate;
public void handleChatMessage(ChatMessage message) {
messagingTemplate.convertAndSend("/sub/chat/" + message.getRoomId() , message.getContent());
}
}
마무리?
사실 이거면 가능하냐라면 가능하다. 어렵게 생각하지 않아도 되더라고요
마무리로 html 만 설명하고 마무리 짓죠
간단하게 꾸며(?) 봤습니다만.. 저는 프론트 고자이기 때문에 f12 관리자 모드를 열어 확인하죠 ㅋㅋ..
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Chat Room</title>
<style>
body {
font-family: "Arial", sans-serif;
margin: 0;
padding: 0;
background-color: #f0f2f5;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
#chat-room-container {
background-color: #ffffff;
border-radius: 15px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
width: 80%;
max-width: 600px;
}
.chat-header {
background-color: #009688;
color: #ffffff;
font-weight: bold;
padding: 15px;
text-align: center;
border-bottom: 1px solid #00796b;
border-radius: 15px 15px 0 0;
}
.message-container {
padding: 10px;
border-bottom: 1px solid #e0e0e0;
}
.message-input-container {
display: flex;
align-items: center;
border-top: 1px solid #e0e0e0;
padding: 10px;
}
.message-input {
flex: 1;
padding: 8px;
border-radius: 20px;
border: 1px solid #ccc;
outline: none;
}
.file-upload-btn {
background-color: #009688;
color: #ffffff;
border: none;
padding: 8px 15px;
border-radius: 20px;
cursor: pointer;
margin-right: 10px;
}
.file-upload-btn:hover {
background-color: #00796b;
}
.send-btn {
background-color: #009688;
color: #ffffff;
border: none;
padding: 8px 15px;
border-radius: 20px;
cursor: pointer;
}
.send-btn:hover {
background-color: #00796b;
}
/* 스타일은 이전과 동일합니다. */
#messages-container {
max-height: 300px;
overflow-y: auto;
}
</style>
</head>
<body>
<div id="chat-room-container">
<div class="chat-header">Chat Room</div>
<input type="text" id="messageInput"/>
<button onclick="sendTextMessage()"></button>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.5.2/sockjs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script>
// 소켓
const socket = new SockJS("http://localhost:8080/ws");
const stompClient = Stomp.over(socket);
stompClient.connect({}, (frame) => {
console.log("Connected to WebSocket");
console.log("Session ID: " + frame.headers["user-name"]);
stompClient.subscribe(`/sub/chat/1`, (message) => {
console.log("Received message: " + message);
console.log("메시지: " + message.body);
});
}
);
socket.addEventListener("close", (event) => {
console.log("WebSocket connection closed.");
});
// 일반 텍스트 메시지
function sendTextMessage() {
const messageInput = document.getElementById("messageInput");
const message = messageInput.value;
stompClient.send(
"/pub/chat/sendMessage",
{},
JSON.stringify({
roomId: 1,
content: message,
type: "CHAT",
})
);
messageInput.value = "";
}
</script>
</body>
</html>
하나씩 뜯어서 볼까요
소켓 지정입니다. 앞서 설명했다싶히 http://localhost:포트번호/엔드포인트 로 연결이 가능합니다.
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.5.2/sockjs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
// 소켓
const socket = new SockJS("http://localhost:8080/ws");
const stompClient = Stomp.over(socket);
stompClient 의 커넥트 (연결 connect) 과 구독 (subscribe) 입니다
앞서 지정한 /sub 구독을 시켜줄수 있습니다. 위의 Springboot 에서는 registry.enableSimpleBroker("/sub"); 로 /sub 만 되어있다고 /sub/1 로만 할 수 있는것이 아니라 /sub/room/1 뭐 , /sub/chat/1 이런식으로 아무거나 중간에 지정이 가능해요..
저희는 /chat 채팅이라는 것과 방번호 1번 을 지정했습니다.
그리고 subscribe 에서 서버에서 넘어오는 데이터를 받는 곳입니다. !!
stompClient.connect({}, (frame) => {
console.log("Connected to WebSocket");
console.log("Session ID: " + frame.headers["user-name"]);
stompClient.subscribe(`/sub/chat/1`, (message) => {
console.log("Received message: " + message);
console.log("메시지: " + message.body);
});
}
);
이벤트 리스너로 close 즉 소켓이 닫혔을때 이벤트 지정입니다. 소켓이 닫히면 즉, 세션이 끊기면 알려주지요 ㅎㅎ
socket.addEventListener("close", (event) => {
console.log("WebSocket connection closed.");
});
sendTextMessage 함수를 자른 부분입니다.
딱 보면 알수 있겠지만 /pub 즉 발행부분 입니다.
앞서 매핑했던. @MessageMapping("/chat/sendMessage") 이 부분으로 받을 수 있습니다.
JSON 으로 문자열 처리 하여 보내면 됩니다.
나머지는 서버에서 다시 convertAndSend 로 보내줄수 있도록 방번호 1번과 메시지를 보내주었습니다.
stompClient.send(
"/pub/chat/sendMessage",
{},
JSON.stringify({
roomId: 1,
content: message,
type: "CHAT",
})
);
real 마무리
끝입니다. 와~~ stomp 는 여러 설정이 가능합니다. 그 중 딱 그냥 보내고 받고 정도만 넣어봤어요 ㅎㅎ.. 일단 이것부터 알아야 진도가 나가기 때문에 중요 부분만 넣어놯습니다.
끝으로 바이너리 데이터 타입도 보낼 수 있지만, 효율적이지 않기 때문에 바이너리 타입 인 경우 ajax 로 보내서 convertAndSend 로 보내는 정도만 해도 무방할 겁니다.
이전 stomp 에서 딱 이정도면 된다만 딱 떼와서 넣었습니다.
추가적으로 여러 구독을 하고 싶다면 반복문 돌리면 됩니다.
var socket = new SockJS('/websocket');
var stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
console.log('Connected: ' + frame);
// 구독할 방의 번호 목록
var roomNumbers = [1, 2, 3];
roomNumbers.forEach(function(roomNumber) {
var topic = '/topic/room' + roomNumber;
stompClient.subscribe(topic, function (message) {
console.log('Room ' + roomNumber + ': ' + message.body);
});
});
});
'SpringBoot > code' 카테고리의 다른 글
WebSocket 을 사용해보자 - 2. Stomp Springboot 부분 (2) | 2024.01.04 |
---|---|
WebSocket 을 사용해보자 - 2. Stomp 프론트 부분 (1) | 2024.01.04 |
Springboot - OAuth2.0 Google API , kakao API , Naver API 로그인 (0) | 2023.12.14 |
SpringBoot - MyBatis - 다이나믹 SQL 작성 (0) | 2023.10.16 |
SpringBoot - SSR에서 페이징 처리에 필요한 객체 PageReq , PageRes (0) | 2023.10.12 |