웹소켓을 빠르게 써보기 위해 STOMP를 잡아 보았다
quick .. ?한지는 모르겠지만 일단 엄청난 오류와 싸우는 중
데모 코드 출처
STOMP을 알아보고 서버 구현해보자!
Simple Text Oriented Messaging ProtocolTCP 또는 WebSocket 같은 양방향 네트워크 프로토콜 기반으로 동작Message Payload에는 Text or Binary 데이터를 포함 할 수 있다.pub/sub 구조로 동작Spring에서
velog.io
위 출처의 코드를 받아오되 지원되지 않는 메서드 부분을 바꾸었다.
package raidu.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;
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;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketConfig.class);
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
// .withSockJS() // not allows wildcard(*) on AllowedOriginPatterns
;
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/sub");
registry.setApplicationDestinationPrefixes("/pub");
}
}
package raidu.stomp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.stereotype.Component;
@Component
public class CustomChannelInterceptor implements ChannelInterceptor {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomChannelInterceptor.class);
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
LOGGER.info("Pre Send: {}", message);
return message;
}
@Override
public void postSend(Message<?> message, MessageChannel channel, boolean sent) {
LOGGER.info("Post Send: {}", message);
}
@Override
public void afterSendCompletion(Message<?> message, MessageChannel channel, boolean sent, Exception ex) {
LOGGER.info("After Send Completion: {}", message);
}
@Override
public boolean preReceive(MessageChannel channel) {
LOGGER.info("Pre Receive");
return true;
}
@Override
public Message<?> postReceive(Message<?> message, MessageChannel channel) {
LOGGER.info("Post Receive: {}", message);
return message;
}
@Override
public void afterReceiveCompletion(Message<?> message, MessageChannel channel, Exception ex) {
LOGGER.info("After Receive Completion: {}", message);
}
}
package raidu.stomp;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.http.ResponseEntity;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.socket.messaging.SessionConnectEvent;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;
@Controller
public class SocketController {
private static final Logger LOGGER = LoggerFactory.getLogger(SocketController.class);
private final SimpMessageSendingOperations simpleMessageSendingOperations;
// 생성자
@Autowired
public SocketController(SimpMessageSendingOperations simpleMessageSendingOperations) {
this.simpleMessageSendingOperations = simpleMessageSendingOperations;
}
// 새로운 사용자가 웹 소켓을 연결할 때 실행됨
@EventListener
public void handleWebSocketConnectListener(SessionConnectEvent event) {
LOGGER.info("Received a new web socket connection");
}
// 사용자가 웹 소켓 연결을 끊으면 실행됨
@EventListener
public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
String sessionId = headerAccessor.getSessionId();
LOGGER.info("sessionId Disconnected: " + sessionId);
}
// /pub/cache 로 메시지를 발행한다.
@MessageMapping("/cache")
public void sendMessage(Map<String, Object> params) {
// /sub/cache 에 구독중인 client에 메세지를 보낸다.
simpleMessageSendingOperations.convertAndSend("/sub/cache/" + params.get("channelId"), params);
}
// @GetMapping(value = "/APItest")
// @ResponseBody
// public ResponseEntity<String> apiTest(){
// printLog("APItest");
// return ResponseEntity.ok("Hello!");
// }
//
// // 콘솔에 요청 시각, 메서드 출력
// public void printLog(String request) {
// System.out.printf("Requested %s: ", request.toUpperCase());
// Date date = new Date();
// SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// System.out.println(dateFormat.format(date));
// }
}
하 .. 분명히 세팅 제대로 했는데 apic에서 connection만 보내면
서버에 로그도 안 찍히고 프론트에서 계속 빠꾸먹길래
왜이래 ... 했는데
Reference
https://eoneunal.tistory.com/17
[Spring WebSocket] Spring WebSocket STOMP 적용
1. STOMP STOMP(Simple Text Oriented Messaging Protocol)는 메시징 전송을 효율적으로 하기 위한 프로토콜로, PUB/SUB 기반으로 동작한다. WebSocket만 사용해서 구현하면 해당 메시지가 어떤 요청인지, 어떤 포맷
eoneunal.tistory.com
SockJS가 crossorigin wildcard를 허용하지 않는다.
ㅋㅋㅋㅋ ㅋㅋㅋ 그래서 걔를 주석 처리하고 해결
하진 못했고 여전히 오류가 난다
2024-07-24T16:50:35.698+09:00[0;39m [31mERROR[0;39m [35m22480[0;39m [2m---[0;39m [2m[socket] [nio-8080-exec-3][0;39m [2m[0;39m[36mo.s.w.s.m.StompSubProtocolHandler [0;39m [2m:[0;39m Failed to parse TextMessage payload=[CONNECT
ho..], byteCount=84, last=true] in session febcb5e2-3b81-450c-ba1f-4a2d3c41d081. Sending STOMP ERROR to client.
org.springframework.messaging.simp.stomp.StompConversionException: Illegal header: ':'. A header must be of the form <name>:[<value>].
at org.springframework.messaging.simp.stomp.StompDecoder.readHeaders(StompDecoder.java:239) ~[spring-messaging-6.1.11.jar:6.1.11]
at org.springframework.messaging.simp.stomp.StompDecoder.decodeMessage(StompDecoder.java:146) ~[spring-messaging-6.1.11.jar:6.1.11]
at org.springframework.messaging.simp.stomp.StompDecoder.decode(StompDecoder.java:114) ~[spring-messaging-6.1.11.jar:6.1.11]
at org.springframework.messaging.simp.stomp.BufferingStompDecoder.decode(BufferingStompDecoder.java:114) ~[spring-messaging-6.1.11.jar:6.1.11]
at org.springframework.web.socket.messaging.StompSubProtocolHandler.handleMessageFromClient(StompSubProtocolHandler.java:279) ~[spring-websocket-6.1.11.jar:6.1.11]
at org.springframework.web.socket.messaging.SubProtocolWebSocketHandler.handleMessage(SubProtocolWebSocketHandler.java:356) ~[spring-websocket-6.1.11.jar:6.1.11]
at org.springframework.web.socket.handler.WebSocketHandlerDecorator.handleMessage(WebSocketHandlerDecorator.java:75) ~[spring-websocket-6.1.11.jar:6.1.11]
at org.springframework.web.socket.handler.LoggingWebSocketHandlerDecorator.handleMessage(LoggingWebSocketHandlerDecorator.java:56) ~[spring-websocket-6.1.11.jar:6.1.11]
at org.springframework.web.socket.handler.ExceptionWebSocketHandlerDecorator.handleMessage(ExceptionWebSocketHandlerDecorator.java:58) ~[spring-websocket-6.1.11.jar:6.1.11]
at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter.handleTextMessage(StandardWebSocketHandlerAdapter.java:113) ~[spring-websocket-6.1.11.jar:6.1.11]
at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter$3.onMessage(StandardWebSocketHandlerAdapter.java:84) ~[spring-websocket-6.1.11.jar:6.1.11]
at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter$3.onMessage(StandardWebSocketHandlerAdapter.java:81) ~[spring-websocket-6.1.11.jar:6.1.11]
at org.apache.tomcat.websocket.WsFrameBase.sendMessageText(WsFrameBase.java:390) ~[tomcat-embed-websocket-10.1.26.jar:10.1.26]
at org.apache.tomcat.websocket.server.WsFrameServer.sendMessageText(WsFrameServer.java:130) ~[tomcat-embed-websocket-10.1.26.jar:10.1.26]
at org.apache.tomcat.websocket.WsFrameBase.processDataText(WsFrameBase.java:484) ~[tomcat-embed-websocket-10.1.26.jar:10.1.26]
at org.apache.tomcat.websocket.WsFrameBase.processData(WsFrameBase.java:284) ~[tomcat-embed-websocket-10.1.26.jar:10.1.26]
at org.apache.tomcat.websocket.WsFrameBase.processInputBuffer(WsFrameBase.java:130) ~[tomcat-embed-websocket-10.1.26.jar:10.1.26]
at org.apache.tomcat.websocket.server.WsFrameServer.onDataAvailable(WsFrameServer.java:85) ~[tomcat-embed-websocket-10.1.26.jar:10.1.26]
at org.apache.tomcat.websocket.server.WsFrameServer.doOnDataAvailable(WsFrameServer.java:184) ~[tomcat-embed-websocket-10.1.26.jar:10.1.26]
at org.apache.tomcat.websocket.server.WsFrameServer.notifyDataAvailable(WsFrameServer.java:164) ~[tomcat-embed-websocket-10.1.26.jar:10.1.26]
at org.apache.tomcat.websocket.server.WsHttpUpgradeHandler.upgradeDispatch(WsHttpUpgradeHandler.java:152) ~[tomcat-embed-websocket-10.1.26.jar:10.1.26]
at org.apache.coyote.http11.upgrade.UpgradeProcessorInternal.dispatch(UpgradeProcessorInternal.java:60) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:57) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:904) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
at java.base/java.lang.Thread.run(Thread.java:1583) ~[na:na]
[2m2024-07-24T16:50:35.699+09:00[0;39m [32m INFO[0;39m [35m22480[0;39m [2m---[0;39m [2m[socket] [nio-8080-exec-3][0;39m [2m[0;39m[36mraidu.stomp.SocketController [0;39m [2m:[0;39m sessionId Disconnected: febcb5e2-3b81-450c-ba1f-4a2d3c41d081
[2m2024-07-24T16:51:06.713+09:00[0;39m [32m INFO[0;39m [35m22480[0;39m [2m---[0;39m [2m[socket] [MessageBroker-1][0;39m [2m[0;39m[36mo.s.w.s.c.WebSocketMessageBrokerStats [0;39m [2m:[0;39m WebSocketSession[0 current WS(0)-HttpStream(0)-HttpPoll(0), 2 total, 0 closed abnormally (0 connect failure, 0 send limit, 0 transport error)], stompSubProtocol[processed CONNECT(0)-CONNECTED(0)-DISCONNECT(0)], stompBrokerRelay[null], inboundChannel[pool size = 6, active threads = 0, queued tasks = 0, completed tasks = 6], outboundChannel[pool size = 2, active threads = 0, queued tasks = 0, completed tasks = 2], sockJsScheduler[pool size = 1, active threads = 1, queued tasks = 0, completed tasks = 0]
apic에서 테스트하면 이렇고
그냥 빈 창에서 테스트하면
이렇다. 콘솔에는 오류 안 뜸
https://velog.io/@ehddnr7355/j96u9o3k
Spring Security가 적용된 프로젝트에서 STOMP 사용하기
프로젝트의 구조를 모놀리식 아키텍처(Monolithic Architecture)로 정했기 때문에, 다른 팀원들이 구현한 프로젝트 위에 채팅 기능을 추가했다. 가장 까다로웠던 부분은 Spring Security가 적용된 프로젝트
velog.io
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>raidu.socket</groupId>
<artifactId>socket</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>socket</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
</dependency> <!-- 이게 버전에 엄청 민감하기 때문에(구글링하면 이미 지원하지 않거나, 지원해도 deprecated거나 하는 게 많음), 뭔가 계속 문제가 난다 싶으면 <version>부터 확인, 없애도 무방 -->
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
apic을 이용한 테스트(데모 코드 기반으로 실행)
① : 웹 소켓 테스트를 위해서는 'ws'라고 써진 버튼을 눌러야 한다.
② : ws:// URL / endpoint 형태로 작성해 준다.
③ : stomp 탭을 눌러서 연결 방식을 stomp로 설정해 준다.
④ :
// /pub/cache 로 메시지를 발행한다.
@MessageMapping("/cache")
public void sendMessage(Map<String, Object> params) {
// /sub/cache 에 구독중인 client에 메세지를 보낸다.
simpleMessageSendingOperations.convertAndSend("/sub/cache/" + params.get("channelId"), params);
}
메시지를 보낼 때 /sub/cache/channelId로 해당 채널을 구독하는 모든 클라이언트에게 송신하는 구조기 때문에
대충 메시지 받을 임의의 채널 이름을 정해서 /sub/cache/CHANNELID 형태로 Subscription URL을 작성해 준다.
따라서 위 스크린샷에서 나는 channelId를 'lala'라고 한 셈이다.
⑤ (중요) : 저건 이유가 왜인지 확실히는 모르겠는데 빈 칸으로 비우고 날리면 포스팅 중반에 언급한 오류가 발생한다.
아마 칸이 있다는 것만으로 json 헤더를 써서 날리는데, 값이 없으니까 ':' 만 간 것 같음.
apic 사이트 구조상 저 헤더를 아예 없앨 수는 없고(?) 적어도 한 칸은 있어야 한다.
한 칸을 작성하고 있으면 동적으로 두 번째 칸이 생기는데, 그 칸을 꼭 지우고 보내야 함. 안 그러면 또 작성되지 않은 두 번째 칸이 ':' 형태로 들어가서 아래의 오류가 나게 된다.
StompConversionException: Illegal header: ':'. A header must be of the form <name>:[<value>].
⑥ : 폼을 작성하고 나서 Connect를 눌러 준다. 스크린샷에서는 이미 연결에 성공해서 Disconnect라고 뜨고 있지만 ...
연결에 성공하면 아래 메시지 창에 "Stomp Connected." 가 나타난다.
---- 메시지를 받기만 할 거라면 여기서 끝내도 되고, 보내기까지 할 거라면 7, 8번 작업까지.
(근데 애초에 메시지를 받는 것도 누군가 보내야 되는 거긴 함 ㅋㅋ)
⑦ : 메시지를 보낼 거라면, Destination Queue 항목에 내가 서버에 지정해 놓은 publish URL을 쓴다. 이때는 채널 이름이나 이런 건 필요 없고 그냥 @MessageMapping으로 지정한 엔드포인트를 퍼블리싱 prefix와 함께 쓰면 된다.
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/sub");
registry.setApplicationDestinationPrefixes("/pub");
}
Destination prefix는 여기(@Configuration, @EnableWebSocketMessageBroker 지정한 파일)에 지정되어 있다.
⑧ : 메시지 형식을 써 준다. json으로 작성하는데 파라메터 이름이 params라고 해서 {params:{ attr1:val, attr2:val, ... }} 이렇게 이름 주고 depth 들어갈 필요는 없고.. 그냥 속성:값, 속성:값 ... 으로 나열하면 된다.
메시지를 publish하기 위해서는 channelId가 필요하기 때문에 나는 channelId를 쓰고 나머지는 임의 작성했다.
// /sub/cache 에 구독중인 client에 메세지를 보낸다.
simpleMessageSendingOperations.convertAndSend("/sub/cache/" + params.get("channelId"), params);
메시지를 이 형식으로 보내기 때문에 params에 channelId가 들어가 있어야 한다.
보낼 때는 params에서 어떤 값만 꺼내서 보내는 게 아니라 일단 들어온 json 데이터 자체를 다시 보내기 때문에
나머지를 임의 작성해도 큰 탈이 나지 않음
연결에 성공하면 위 사진처럼 클라이언트끼리 대화가 가능해진다.
'FE > Quick Start' 카테고리의 다른 글
자주 쓰는 DataFormatting 관련 함수 (0) | 2024.10.21 |
---|---|
[tailwind CSS] quick start (0) | 2024.10.21 |
[Next.js / TypeScript] 웹소켓 채팅 세팅 / 연결하기 (0) | 2024.09.26 |
[Next.js] quick start (1) | 2024.08.30 |
[React] quick start (0) | 2024.07.05 |
댓글