반응형

WebSocket 서버 만들기

의존성 추가

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

WebSocketInterceptor

@Slf4j
@Component
public class WebSocketInterceptor implements ChannelInterceptor {
    @SneakyThrows
    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);

        if (accessor.getCommand() == StompCommand.CONNECT) {
            String authToken = accessor.getFirstNativeHeader("auth-token");

            if (!"spring-chat-auth-token".equals(authToken)) {
                throw new AuthException("fail");
            }
        }

        return message;
    }
}

WebSocketConfig

@RequiredArgsConstructor
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    private final WebSocketInterceptor webSocketInterceptor;

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/sub");
        config.setApplicationDestinationPrefixes("/pub");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 연결 URL : ws://localhost:8080/ws-stomp/websocket
        registry.addEndpoint("/ws-stomp")
            // .setAllowedOrigins("http://localhost:3000") // "http://localhost:3000" 페이지로부터의 요청만 허용 
            .setAllowedOriginPatterns("**") // 전체 페이지로부터의 요청 허용
            .withSockJS();
    }

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(webSocketInterceptor);
    }
}

ChatMessage

@AllArgsConstructor
@NoArgsConstructor
@Data
public class ChatMessage {
    private Integer roomSeq;
    private String message;
}

ChatController

@Slf4j
@RequiredArgsConstructor
@RestController
public class ChatController {
    private static final Set<String> SESSION_IDS = new HashSet<>();
    private final SimpMessagingTemplate messagingTemplate;

    @MessageMapping("/chat") // "/pub/chat"
    public void publishChat(ChatMessage chatMessage) {
        log.info("publishChat : {}", chatMessage);

        messagingTemplate.convertAndSend("/sub/chat/" + chatMessage.getRoomSeq(), chatMessage);
    }

    @EventListener(SessionConnectEvent.class)
    public void onConnect(SessionConnectEvent event) {
        String sessionId = event.getMessage().getHeaders().get("simpSessionId").toString();
        SESSION_IDS.add(sessionId);
        log.info("[connect] connections : {}", SESSION_IDS.size());
    }

    @EventListener(SessionDisconnectEvent.class)
    public void onDisconnect(SessionDisconnectEvent event) {
        String sessionId = event.getSessionId();
        SESSION_IDS.remove(sessionId);
        log.info("[disconnect] connections : {}", SESSION_IDS.size());
    }
}

JavaScript로 접속하기

index.html

  • 경로 : ~/src/main/resources/static/index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<input id="input"/>
<button id="send">send</button>
<pre id="messages"></pre>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.4.0/sockjs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<script>
    const client = Stomp.over(new SockJS("/ws-stomp"));

    client.connect({},
        () => {
            client.subscribe("/sub/chat/1", (payload) => {
                document.querySelector("#messages").appendChild(document.createTextNode(payload.body + "\n"));
            });
        },
        (error) => {
            console.error(error);
        }
    );

    document.querySelector("#send").addEventListener("click", () => {
        client.send("/pub/chat", {}, JSON.stringify({
            roomSeq: 1,
            message: document.querySelector("#input").value,
        }));
    });
</script>
</body>
</html>

WebSocketClient로 접속하기

StompClientTest

@Slf4j
public class StompClientTest {
    public static void main(String[] args) {
        StompChatClient client = new StompChatClient("ws://localhost:8080/ws-stomp/websocket", "/sub/chat/1");
        client.connect();

        while (true) {
            String line = new Scanner(System.in).nextLine();

            try {
                String[] commands = line.split(":");
                Integer roomSeq = Integer.parseInt(commands[0]);
                String message = commands[1];

                client.send("/pub/chat", new ChatMessage(roomSeq, message));
            } catch (Exception e) {
                log.error("error", e);
            }
        }
    }

    @RequiredArgsConstructor
    private static class StompChatClient extends StompSessionHandlerAdapter {
        private final String url;
        private final String subscribe;
        private StompSession session;

        @Override
        public Type getPayloadType(StompHeaders headers) {
            return ChatMessage.class;
        }

        @Override
        public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
            this.session = session;
            this.session.subscribe(subscribe, this);
        }

        @Override
        public void handleFrame(StompHeaders headers, Object payload) {
            ChatMessage chatMessage = (ChatMessage) payload;
            log.info("receive : {}", chatMessage);
        }

        public void connect() {
            WebSocketHttpHeaders httpHeaders = new WebSocketHttpHeaders();

            StompHeaders stompHeaders = new StompHeaders();
            stompHeaders.add("auth-token", "spring-chat-auth-token");

            WebSocketStompClient stompClient = new WebSocketStompClient(new StandardWebSocketClient());
            stompClient.setMessageConverter(new MappingJackson2MessageConverter());
            stompClient.connect(url, httpHeaders, stompHeaders, this);
        }

        public void send(String destination, ChatMessage chatMessage) {
            session.send(destination, chatMessage);
        }
    }
}
반응형

'Development > Spring' 카테고리의 다른 글

[Spring] SQL Mapper (with MyBatis)  (0) 2020.12.27
[Spring] ORM (with JPA, Hibernate)  (0) 2020.12.27
[Spring] Dependency Injection  (0) 2020.12.27
[Spring] Distributed Lock (with MySQL, Redis)  (4) 2020.12.27
[Spring] Transactional  (0) 2020.12.27

+ Recent posts