Closed wooyounggggg closed 2 years ago
클라이언트 - 서버간 전이중 단일 소켓 연결 방식으로 연결하여 메시지를 주고받는다. 웹소켓 초기 handshake는 HTTP나 HTTPS를 사용하여 연결할 수 있음
웹소켓 사양에서는 ws:// 및 wss:// 스키마를 정의하고 있음
기본적인 TCP/IP 연결을 기반으로 하고, 클라이언트 - 서버간 초기 핸드셰이크 시 HTTP 요청을 웹소켓 프로토콜로 업그레이드 하여 연결 확립
서로의 데이터 전송이 수행되는 동안, 클라이언트 및 서버는 서로에게 동시에 메시지를 보낼 수 있음
스프링 4.0 이상에서, STOMP
및 웹소켓 스타일 메시징
을 지원하며, spring-websocket 모듈은 웹소켓 및 JSR-356(자바 웹소켓)과도 호환됨.
브라우저에 따라 웹소켓을 지원하지 않는 경우가 있는데, 스프링에서는 이러한 상황 대처를 위해 SockJS 프로토콜 옵션을 제공함.
웹소켓은 단일 URL을 사용해 초기 핸드셰이크 시 연결을 확립하며, 이 연결을 통해 데이터를 전달함.
@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()));
}
}
브라우저가 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
이후 웹 소켓 프로토콜로 바이너리 메시지를 주고 받는다.
registry.addHandler(echoHandler(), "/echoHandler").setAllowedOrigins("http://127.0.0.1:8080").withSockJS();
.withSockJS를 붙이면 설정 완료.
<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>
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}
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}
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
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>
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"]
공식 사이트: 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와도 연동 가능.
@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
메시지 핸들러를 지정하는 어노테이션. 해당 메서드의 시그니쳐는 아래의 어노테이션이나 인자를 사용해 자유롭게 구성 가능.
@Message
@Payload
@Header
@Headers
MessageHeaders
MessageHeaderAccessor
DestinationVariable
Principal
@SubscribeMapping
STOMP 프로토콜만 지원. 구독 메시지를 전달 받을 메서드를 지정.
SimpMessagingTemplate
특정 목적지로 메시지를 전송하는 기능. 세션 인증된 유저에게 메시지를 전송하는 기능 등을 제공.
<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>
생성자
new Stomp.over(new SockJS("/ws"))
SockJS로 래핑된 웹소켓에 올릴 Stomp 클라이언트 생성.
stomp.connect(login, passcode, connectCallback, errorCallback, closeEventCallback, host)
stomp.subscribe(dest, callback)
stomp.send(dest, header, body)
stomp.disconnect(callback)
헤더에 세션 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);
}
}
매핑은 그렇다 쳐도, HTTP 레이어에서 유저 인증 (스프링 시큐리티) 부분은 STOMP 단에서도 살아있지 않을까 싶다. 추가 조사가 필요하다.
문서에서 아주 쉽게 설명되어있다. https://docs.spring.io/spring-security/site/docs/5.0.7.RELEASE/reference/html/websocket.html
주제
17장 웹소켓을 읽고 중요✨ 하다고 생각하는 키워드와 선택한 이유에 대해서 코멘트로 달아주세요.
103