caffeine-library / pro-spring-5

🌱 전문가를 위한 스프링5를 읽는 스터디
5 stars 0 forks source link

[keyword] 17장 웹소켓 키워드 정리 #104

Closed wooyounggggg closed 2 years ago

wooyounggggg commented 2 years ago

주제

17장 웹소켓을 읽고 중요✨ 하다고 생각하는 키워드와 선택한 이유에 대해서 코멘트로 달아주세요.

103

wooyounggggg commented 2 years ago

웹소켓

클라이언트 - 서버간 전이중 단일 소켓 연결 방식으로 연결하여 메시지를 주고받는다. 웹소켓 초기 handshake는 HTTP나 HTTPS를 사용하여 연결할 수 있음

웹소켓 사양에서는 ws:// 및 wss:// 스키마를 정의하고 있음

기본적인 TCP/IP 연결을 기반으로 하고, 클라이언트 - 서버간 초기 핸드셰이크 시 HTTP 요청을 웹소켓 프로토콜로 업그레이드 하여 연결 확립

서로의 데이터 전송이 수행되는 동안, 클라이언트 및 서버는 서로에게 동시에 메시지를 보낼 수 있음

스프링 웹소켓

스프링 4.0 이상에서, STOMP웹소켓 스타일 메시징을 지원하며, spring-websocket 모듈은 웹소켓 및 JSR-356(자바 웹소켓)과도 호환됨.

브라우저에 따라 웹소켓을 지원하지 않는 경우가 있는데, 스프링에서는 이러한 상황 대처를 위해 SockJS 프로토콜 옵션을 제공함.

웹소켓은 단일 URL을 사용해 초기 핸드셰이크 시 연결을 확립하며, 이 연결을 통해 데이터를 전달함.

binchoo commented 2 years ago

웹소켓

스프링에서 웹 소켓 설정

자바 구성

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(echoHandler(), "/echoHandler").setAllowedOrigins("http://127.0.0.1:8080");
    }

    @Bean
    public EchoHandler echoHandler() {
        return new EchoHandler();
    }
}

컨트롤러

public class EchoHandler extends TextWebSocketHandler {
    private ApplicationContext context;
    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage textMessage) throws IOException {
        session.sendMessage(new TextMessage(textMessage.getPayload()));
    }
}

웹 브라우저의 웹 소켓 API

브라우저가 WebSocket 라이브러리를 제공하는 경우

패킷 형태

GET http://localhost:8080/echoHandler HTTP/1.1
Host: localhost:8080
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36
Upgrade: websocket
Origin: http://127.0.0.1:8080
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate, br
Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7,ja;q=0.6
Sec-WebSocket-Key: TJ6la7hLR/sGZlrGGYADtQ==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
HTTP/1.1 101
Upgrade: websocket
Connection: upgrade
Sec-WebSocket-Accept: pk4GHTePNYL39XP8eRw41GZsBvI=
Sec-WebSocket-Extensions: permessage-deflate;client_max_window_bits=15
Date: Sat, 18 Dec 2021 06:29:59 GMT

이후 웹 소켓 프로토콜로 바이너리 메시지를 주고 받는다.

binchoo commented 2 years ago

SockJS

스프링에서 SockJS 설정

registry.addHandler(echoHandler(), "/echoHandler").setAllowedOrigins("http://127.0.0.1:8080").withSockJS();

.withSockJS를 붙이면 설정 완료.

자바스크립트 SockJS 클라이언트

<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.2/stomp.min.js"></script>
<script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>

패킷 형태 (크롬)

1. 전송 정보 요청

GET http://localhost:8080/echoHandler/info?t=1639810276303 HTTP/1.1
Host: localhost:8080
Connection: keep-alive
sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="96", "Google Chrome";v="96"
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36
sec-ch-ua-platform: "Windows"
Accept: */*
Origin: http://127.0.0.1:8080
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1:8080/
Accept-Encoding: gzip, deflate, br
Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7,ja;q=0.6

응답

HTTP/1.1 200
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Access-Control-Allow-Origin: http://127.0.0.1:8080
Access-Control-Allow-Credentials: true
Cache-Control: no-store, no-cache, must-revalidate, max-age=0
Content-Type: application/json;charset=UTF-8
Content-Length: 77
Date: Sat, 18 Dec 2021 06:51:16 GMT
Keep-Alive: timeout=20
Connection: keep-alive

{"entropy":-60414885,"origins":["*:*"],"cookie_needed":true,"websocket":true}

패킷 형태 (IE)

1. 전송 정보 요청

GET http://localhost:8080/echoHandler/info?t=1639810940694 HTTP/1.1
Accept: */*
Referer: http://127.0.0.1:8080/
Accept-Language: ko,ja;q=0.5
Origin: http://127.0.0.1:8080
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko
Host: localhost:8080
Connection: Keep-Alive

응답

HTTP/1.1 200
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Access-Control-Allow-Origin: http://127.0.0.1:8080
Access-Control-Allow-Credentials: true
Cache-Control: no-store, no-cache, must-revalidate, max-age=0
Content-Type: application/json;charset=UTF-8
Content-Length: 79
Date: Sat, 18 Dec 2021 07:02:20 GMT
Keep-Alive: timeout=20
Connection: keep-alive

{"entropy":-1669636006,"origins":["*:*"],"cookie_needed":true,"websocket":true}

2. 스트리밍 요청

POST http://localhost:8080/echoHandler/843/mua04e0i/xhr_streaming?t=1639810941370 HTTP/1.1
Accept: */*
Referer: http://127.0.0.1:8080/
Accept-Language: ko,ja;q=0.5
Origin: http://127.0.0.1:8080
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko
Content-Length: 0
Host: localhost:8080
Connection: Keep-Alive
Pragma: no-cache

응답

HTTP/1.1 200
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Access-Control-Allow-Origin: http://127.0.0.1:8080
Access-Control-Allow-Credentials: true
Cache-Control: no-store, no-cache, must-revalidate, max-age=0
Content-Type: application/javascript;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 18 Dec 2021 07:02:20 GMT
Keep-Alive: timeout=20
Connection: keep-alive

3. iframe 요청

GET http://localhost:8080/echoHandler/iframe.html HTTP/1.1
Accept: text/html, application/xhtml+xml, image/jxr, */*
Referer: http://127.0.0.1:8080/
Accept-Language: ko,ja;q=0.5
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko
Accept-Encoding: gzip, deflate
Host: localhost:8080
Connection: Keep-Alive

응답

<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <script>
    document.domain = document.domain;
    _sockjs_onload = function(){SockJS.bootstrap_iframe();};
  </script>
  <script src="https://cdn.jsdelivr.net/sockjs/1.0.0/sockjs.min.js"></script>
</head>
<body>
  <h2>Don't panic!</h2>
  <p>This is a SockJS hidden iframe. It's used for cross domain magic.</p>
</body>
</html>

4. 메시지 전송

POST http://localhost:8080/echoHandler/711/xu4yl200/xhr_send?t=1639811328399 HTTP/1.1
Accept: */*
Content-type: text/plain
Referer: http://127.0.0.1:8080/
Accept-Language: ko,ja;q=0.5
Origin: http://127.0.0.1:8080
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko
Content-Length: 8
Host: localhost:8080
Connection: Keep-Alive
Pragma: no-cache

["1234"]

메시지 수신

HTTP/1.1 200
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Access-Control-Allow-Origin: http://127.0.0.1:8080
Access-Control-Allow-Credentials: true
Cache-Control: no-store, no-cache, must-revalidate, max-age=0
Content-Type: application/javascript;charset=UTF-8
Date: Sat, 18 Dec 2021 07:08:48 GMT
Keep-Alive: timeout=20
Connection: keep-alive
Content-Length: 10

a["1234"]
binchoo commented 2 years ago

3. Stomp

공식 사이트: https://stomp.github.io/

Stomp 1.2 스펙: https://stomp.github.io/stomp-specification-1.2.html

스프링 공식문서: https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#websocket-stomp

HTTP 위에 올라가는 서브 프로토콜이며, 응용 단의 발행 구독 패턴의 메시징을 제공. 스프링은 STOMP를 지원하는 단순한 인메모리 브로커를 지원한다. 래빗MQ나 액티브MQ와도 연동 가능.

스프링에서 Stomp 구성

자바 구성

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
            registry.addEndpoint("/ws").withSockJS(); // HTTP 수준 웹소켓 엔드포인트 등록
    }
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.setApplicationDestinationPrefixes("/app"); // STOMP 수준
        config.enableSimpleBroker("/topic"); // STOMP 수준
}

@EnableWebSocketMessageBroker

웹소켓 위의 서브 프로토콜을 사용하는 브로커 기반 메시징을 활성화

컨트롤러

@Controller
public class StockController {
    private TaskScheduler taskScheduler;
    private SimpMessagingTemplate simpMessagingTemplate;

    private List<Stock> stocks = new ArrayList<Stock>();
    private Random random = new Random(System.currentTimeMillis());

    ...

    @PostConstruct
    private void broadcastTimePeriodically() { //지속적인 퍼블리시 정의
        taskScheduler.scheduleAtFixedRate(() -> broadcastUpdatedPrices(), 1000);
    }

    @MessageMapping("/addStock") //Stomp로부터 프레임을 수신하는 어플리케이션 핸들러 정의
    public void addStock(Stock stock) throws Exception { //object mapper가 JSON을 Stock 객체로 바꿔줬음
        stocks.add(stock);
        broadcastUpdatedPrices();
    }

    private void broadcastUpdatedPrices() {
        for(Stock stock : stocks) {
            stock.setPrice(stock.getPrice() + (getUpdatedStockPrice() * stock.getPrice()));
            stock.setDate(new Date());
        }

        simpMessagingTemplate.convertAndSend("/topic/price", stocks); //메시지 브로커에 퍼블리시, object mapper 동작
    }
}

@MessageMapping

메시지 핸들러를 지정하는 어노테이션. 해당 메서드의 시그니쳐는 아래의 어노테이션이나 인자를 사용해 자유롭게 구성 가능.

@SubscribeMapping

STOMP 프로토콜만 지원. 구독 메시지를 전달 받을 메서드를 지정.

SimpMessagingTemplate

특정 목적지로 메시지를 전송하는 기능. 세션 인증된 유저에게 메시지를 전송하는 기능 등을 제공.

자바스크립트 Stomp 클라이언트

<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.2/stomp.min.js"></script>
<script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>

예제 어플리케이션 구조

image

binchoo commented 2 years ago

STOMP: 핸들러 매핑은 누가 하는지

세션 ID가 획득

헤더에 세션 ID를 넣어주기 때문에, Annotated Application Handlers에서 활용 가능.

  @Controller
  public class InboxController {

      @Autowired
      SimpMessagingTemplate simpMessagingTemplate;

      @MessageMapping("/{chatroom}")
      public void broadcastUserMessage(@Header("simpSessionId") String sessionId,
                  @DestinationVariable("chatroom") String chatRoomName, @Payload String message) {
          String chatRoomMessageBrokerDestination = "/chatroom/" + chatRoomName;
          simpMessagingTemplate.convertAndSend(chatRoomMessageBrokerDestination, sessionId + ": " + message);
      }
  }

GIF 2021-12-20 오후 10-15-54

웹소켓 보안 설정

매핑은 그렇다 쳐도, HTTP 레이어에서 유저 인증 (스프링 시큐리티) 부분은 STOMP 단에서도 살아있지 않을까 싶다. 추가 조사가 필요하다.

문서에서 아주 쉽게 설명되어있다. https://docs.spring.io/spring-security/site/docs/5.0.7.RELEASE/reference/html/websocket.html