개발 낙서장

[Spring] WebSocket 통신 본문

Java

[Spring] WebSocket 통신

권승준 2024. 3. 28. 19:56

HTTP

HTTP는 Hypertext Transfer Protocol의 약자로 클라이언트-서버 간 단방향 통신 프로토콜이다.
클라이언트에서 요청(Request)를 보내면 서버에서 해당 요청에 대한 처리 과정을 거쳐 응답(Response)을 주는 방식으로 통신한다.
네이버에서 검색어를 입력하고 검색 버튼을 누르면 해당 검색어에 대한 내용이 좌르륵 뜨는 것이 HTTP 통신이다.

특징

  1. 무상태성(Stateless) : 서버와 클라이언트는 독립적이며 서버에서 클라이언트의 상태를 갖고 있지 않는다.
  2. 클라이언트-서버 단방향 구조 : 클라이언트에서 요청을 보내면 서버에서는 그에 대한 응답을 제공하는 단방향 통신만 가능하다.
  3. 비연결성(Connectionless) : 요청-응답 처리가 완료되면 연결이 끊어진다.
  4. 다양한 메소드 지원 : GET, POST, PUT, PATCH, DELETE 등 HTTP 메소드를 통해 데이터의 CRUD 작업을 수행할 수 있다.

WebSocket

웹 소켓은 HTTP와 다르게 실시간 양방향 통신을 지원한다. 연결 지향적이며 메세지 전송에 대한 규칙을 정의하지 않는다.

특징

  1. HTTP와 호환되며 80/443 포트를 사용해 기존 방화벽 규칙을 재사용할 수 있다.
  2. 초기에는 HTTP 요청으로 시작되지만 이후 단일 TCP 연결로 통신한다.
  3. 클라이언트와 서버 간 전이중 통신 채널을 제공해 실시간으로 자유롭게 메세지 교환이 가능하다.
GET /spring-websocket-portfolio/portfolio HTTP/1.1
Host: localhost:8080
Upgrade: websocket // Upgrade 헤더
Connection: Upgrade // Upgrade 연결
Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg==
Sec-WebSocket-Protocol: v10.stomp, v11.stomp
Sec-WebSocket-Version: 13
Origin: http://localhost:8080

웹소켓 상호작용은 HTTP Upgrade 헤더를 통해 웹소켓 프로토콜로 업그레이드하는 HTTP 요청으로 시작된다.

HTTP/1.1 101 Switching Protocols // 프로토콜 전환
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=
Sec-WebSocket-Protocol: v10.stomp

HTTP에서는 일반적으로 응답에 성공하면 200 상태 코드를 반환하지만 웹소켓에서는 101 상태 코드를 반환한다.

STOMP

STOMP(Simple Text Oriented Messaging Protocol)는 스프링 프레임워크에서 지원하는 웹소켓 기반 메시징 프로토콜이다.
메시지 브로커를 활용하여 pub-sub(발행-구독) 방식으로 클라이언트와 서버가 쉽게 메시지를 주고받을 수 있도록하는 프로토콜이다.
pub-sub 방식은 쉽게 말해 클라이언트가 특정 경로(URL)을 구독하고 서버에서는 해당 경로에 구독하고 있는 클라이언트에게 메시지를 발행하는 방식이다.

STOMP는 텍스트 기반 프로토콜인데 메시지는 다음과 같은 프레임 구조를 가진다.

COMMAND
header1:value1
header2:value2

Body^@
  • COMMAND : 말 그대로 특정 동작이나 유형을 말한다. SEND, SUBSCRIBE 등이 있다.
  • header : 프레임에 대한 메타 데이터들을 포함한다. content-type, id, destination 등이 있다.
  • body : 프레임의 실제 데이터 내용이다. JSON과 같은 형식을 주로 사용하며 생략할 수 있다.
SEND
destination:/queue/trade
content-type:application/json
content-length:44

{"action":"BUY","ticker":"MMM","shares",44}^@

위는 STOMP 메시지의 예시이다.
SEND로 특정 메시지를 보내는 요청이며 "/queue/trade" URL로 해당 요청을 보낸다. content type은 json 형식이며 길이는 44이다.
Body에 보낼 데이터가 담겨있다.

MESSAGE
message-id:nxahklf6-1
subscription:sub-1
destination:/topic/price.stock.MMM

{"ticker":"MMM","price":129.45}^@

퍼블리셔가 구독자들에게 보내는 메세지의 예시이다.
MESSAGE 커맨드를 활용하며 sub-1은 구독 ID이다. 해당 ID에 구독한 클라이언트에게 메시지를 보낸다.
destination은 메시지가 전송된 대상이다.

가이드

https://spring.io/guides/gs/messaging-stomp-websocket

 

Getting Started | Using WebSocket to build an interactive web application

In Spring’s approach to working with STOMP messaging, STOMP messages can be routed to @Controller classes. For example, the GreetingController (from src/main/java/com/example/messagingstompwebsocket/GreetingController.java) is mapped to handle messages t

spring.io

다음은 Spring 공식 문서의 STOMP WebSocket 메시징 가이드를 따른다.

의존성

	// WebSocket
	implementation 'org.springframework.boot:spring-boot-starter-websocket'

Config

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

	// 메세지 브로커 구성
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic"); // 구독 요청
        config.setApplicationDestinationPrefixes("/app"); // 접두사 처리
    }

	// STOMP 엔드포인트 등록
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/gs-guide-websocket"); // 핸드셰이크 엔드포인트 설정
    }
}

모델

@Setter
@Getter
public class HelloMessage {

    private String name;

    public HelloMessage() {
    }

    public HelloMessage(String name) {
        this.name = name;
    }
}

@Getter
public class Greeting {

    private String content;

    public Greeting() {
    }

    public Greeting(String content) {
        this.content = content;
    }
}

클라이언트와 서버가 주고 받을 메시지 모델이다.

컨트롤러

@Controller
public class GreetingController {

    @MessageMapping("/hello")
    @SendTo("/topic/greetings")
    public Greeting greeting(HelloMessage message) throws Exception {
        Thread.sleep(1000); // 지연 시뮬레이션
        return new Greeting("Hello, " + message.getName() + "!");
    }
}

"/hello" 엔드포인트로 들어온 STOMP 메시지를 처리한다.
HelloMessage를 바탕으로 Greeting을 구성해 "/topic/greetings" 엔드포인트로 전송한다.

HTML

<!DOCTYPE html>
<html>
<head>
    <title>Hello WebSocket</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <link href="/main.css" rel="stylesheet">
    <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@stomp/stompjs@7.0.0/bundles/stomp.umd.min.js"></script>
    <script src="/app.js"></script>
</head>
<body>
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being
    enabled. Please enable
    Javascript and reload this page!</h2></noscript>
<div id="main-content" class="container">
    <div class="row">
        <div class="col-md-6">
            <form class="form-inline">
                <div class="form-group">
                    <label for="connect">WebSocket connection:</label>
                    <button id="connect" class="btn btn-default" type="submit">Connect</button>
                    <button id="disconnect" class="btn btn-default" type="submit" disabled="disabled">Disconnect
                    </button>
                </div>
            </form>
        </div>
        <div class="col-md-6">
            <form class="form-inline">
                <div class="form-group">
                    <label for="name">What is your name?</label>
                    <input type="text" id="name" class="form-control" placeholder="Your name here...">
                </div>
                <button id="send" class="btn btn-default" type="submit">Send</button>
            </form>
        </div>
    </div>
    <div class="row">
        <div class="col-md-12">
            <table id="conversation" class="table table-striped">
                <thead>
                <tr>
                    <th>Greetings</th>
                </tr>
                </thead>
                <tbody id="greetings">
                </tbody>
            </table>
        </div>
    </div>
</div>
</body>
</html>

JavaScript

// STOMP 클라이언트 초기화
const stompClient = new StompJs.Client({
    brokerURL: 'ws://localhost:8080/gs-guide-websocket' // 엔드포인트
});

// 연결 성공 시 콜백 함수
stompClient.onConnect = (frame) => {
    setConnected(true);
    console.log('Connected: ' + frame);
    stompClient.subscribe('/topic/greetings', (greeting) => { // `/topic/greetings` 구독
        showGreeting(JSON.parse(greeting.body).content); // 서버로부터 받은 메시지(Body)를 보여줌
    });
};

// 웹소켓 오류 발생 시 콜백 함수
stompClient.onWebSocketError = (error) => {
    console.error('Error with websocket', error);
};

// STOMP 메시지 브로커에서 오류 발생 시 콜백 함수
stompClient.onStompError = (frame) => {
    console.error('Broker reported error: ' + frame.headers['message']);
    console.error('Additional details: ' + frame.body);
};

// 연결에 따른 UI 설정
function setConnected(connected) {
    $("#connect").prop("disabled", connected);
    $("#disconnect").prop("disabled", !connected);
    if (connected) {
        $("#conversation").show();
    }
    else {
        $("#conversation").hide();
    }
    $("#greetings").html("");
}

// STOMP 에 웹소켓 연결 활성화
function connect() {
    stompClient.activate();
}

// 웹소켓 연결 비활성화 및 연결 상태 false 로 설정
function disconnect() {
    stompClient.deactivate();
    setConnected(false);
    console.log("Disconnected");
}

// 입력된 값을 JSON 으로 만들어 `/app/hello` 대상으로 메시지 보냄
function sendName() {
    stompClient.publish({
        destination: "/app/hello",
        body: JSON.stringify({'name': $("#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());
});

구현 화면

여러가지 값을 입력 받게 하고 메시지를 주고 받으면 클라이언트끼리 실시간 통신도 가능해보인다.

'Java' 카테고리의 다른 글

Spring + Redis Cache  (0) 2024.05.13
JDK 17을 쓰는 이유  (1) 2024.05.02
[Spring] AOP로 권한 체크하기  (0) 2024.03.21
[Spring] QueryDSL 페이징  (0) 2024.03.12
Name 필드 네이밍에 대한 고찰  (0) 2024.03.05
Comments