WebSocket
레퍼런스에 따르면, websocket은 웹 브라우저(클라이언트)와 서버간의 full-duplex(양방향), bi-directional(전이중적), persistent connection(지속적인 연결)의 특징을 갖는 프로토콜이라고 규정한다.
여기서 핵심은 클라이언트와 서버간의 지속적인 연결을 한다는 점이다. Websocket이 탄생하기 이전에는 HTTP을 통해 양방향 통신을 하기 위해 polling, long polling, http streaming과 같은 방식을 사용했다.
Http Polling : 클라이언트가 지속적으로 서버로 request를 하여 이벤트를 수신하는 방식이다. 가장 간단한 방법이지만, 지속적으로 서버에 요청을 던지기 때문에 서버의 오버헤드를 고려할 수 밖에 없는 상황이다.
Http Long Polling: polling에 비해 클라이언트는 이벤트를 받기 전까지 다음 요청을 날리지 않는다. 하지만, 원하는 이벤트를 얻기 위해 지속적으로 요청해야한다는 점에서 서버의 부담은 여전히 증가된다.
Http Streaming: 서버는 클라이언트로부터 request를 받으면, response을 주고 연결을 끊지 않고. 이벤트가 발생함에 따라 클라이언트로 전송하는 방식인데 역시나 근본적인 원인인 해결하지 못한다.
물론 위의 방식으로 원하는 데이터를 클라이언트와 서버간의 주고 받는데 문제는 없다. 하지만 HTTP 프로토콜 특성의 request-response의 지속적인 수행 그리고 그에 따른 중복적인 패킷전달(http-header) 문제로 인해 속도 저하 및 오버헤드는 근본적으로 문제가 있게 된다.
쉽게 말하면, 내가 원하는 데이터에 비해 동반되는 데이터들이 너무 많고, 지속적으로 이 데이터를 포함해야 하며 맺고 끊는 연결을 계속하는 등 리소스의 낭비가 크다는 점이다.
그래서 웹 브라우저 환경에서 tcp 통신처럼 연결 지향 프로토콜이 필요했는데 이를 해결하기 위해, 2011년 Websocket 프로토콜이 탄생하게 되었다.
그렇다면, Websocket과 TCP는 어떤 차이가 있는 걸까?
1. 웹소켓은 연결 요청에 대해 http를 통해 switching 및 Handshaking이 이루어진다. (웹소켓 프로토콜 분석하기)
2. TCP는 Binary 데이터만 주고 받을 수 있지만, Websocket은 Binary 데이터 뿐만 아니라 Text 데이터를 주고 받을 수 있다
탄생 배경과 정의 그리고 특성으로 미루어 보아, WebSocket은 HTTP와 TCP의 특성을 섞어 놓은 프로토콜이며
결국 핵심은 웹 브라우저 환경에서 연결지향 통신하기 위한 기술이라는 점이다.
Spring WebSocket
Spring Websocket은 Spring 4.0 부터 지원하며, Maven 3.2+, gradle 4+, jdk8 이상 필요하다.
Spring에서는 2가지 방식으로 Websocket 을 구현 할 수 있다.
- WebSocket 데이터를 직접 처리
- Stomp 프로토콜을 사용하여 메세징 처리
1) Websocket Data 직접 처리
Config를 통해 Websocket 옵션을 설정할 수 있다. 그리고 웹소켓 핸들러를 상속받은 클래스는 low level 수준에서 원시적으로 데이터를 처리할 수 있으며, 다음과 같은 4가지 이벤트를 처리 할 수 있다.
| @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { // WebSocket을 사용할 수없는 경우 대체 전송을 사용할 수 있도록 SockJS 폴백 옵션을 활성화합니다. // SockJS 클라이언트는 "/ws"에 연결하여 사용 가능한 최상의 전송 (websocket, xhr-streaming, xhr-polling 등)을 시도. registry.addHandler(new WsTranportHandler(), "/ws").setAllowedOrigins("*").withSockJS(); } } | cs |
눈여겨 봐야 할 설정은 registerStompEndpoints 메소드이다.
Websocket은 안타깝게도 모든 브라우저를 지원하지 않는데, 만일 브라우저 버전이 Websocket을 호환하지 않는 경우 설정을 통해 폴백 옵션을 활성화 할 수 있다.
또한 CORS(Cross Origin Resource Sharing) 문제 또한 옵션을 통해 해결할 수 있다.
@Component
public class WsTranportHandler extends TextWebSocketHandler {
// connection이 맺어진 후 실행된다
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
System.err.println("session connected +=" + session);
}
// 메세지 수신
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
System.err.println("handle message +=" + session) + ", message=" + message);
//echo Message
session.sendMessage(message);
}
// transport 중 error
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
System.err.println("transport error =" + session +", exception =" + exception);
}
// connection close
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
System.err.println("session close -=" + session);
}
}
그리고 WebSocketHandler를 상속받은 클래스는 직접 세션레벨에서 데이터를 Handle 해야한다.
2) Stomp 프로토콜을 사용하여 메세징 처리
우선 STOMP 프로토콜은 (simple text oriented messaging protocol)의 약자이며, 텍스트 기반의 프로토콜이다.
스프링 문서에 Websocket과 함께 STOMP 프로토콜을 사용하는 방법은 위와 같은데, Spring 내부의 In Memory Broker를 통해 메세지를 처리한다.
도식화 된 그림의 키포인트는 3가지이다.
1) Receive Client
2) Send Client
3) Brkoer
Receive Client
- 메세지를 받기 위해 특정 토픽이 사전에 서버에 subscribe 되어야 한다.
Send Client
- 서버와 연결된 클라이언트는 특정 path로 ex) /app/message 전달한다.
Broker
- 메세지 브로커는 Kafka, RabbitMQ, ActiveMQ 등의 오픈소스들 처럼 MQ 이며, pub/sub 모델을 따른다. 토픽에 따라 메세지를 전달해야 하는 사용자를 구분한다.
- 연결된 클라이언트의 세션을 관리한다.
- 특정 토픽과 메세지를 Mapping 하여, 토픽을 구독하는 세션에 존재하는 클라이언트에게 메세지를 전달한다.
Configuration
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | @Configuration @EnableWebSocketMessageBroker public class WebSocketStompConfig implements WebSocketMessageBrokerConfigurer { //messageBroker config @Override public void configureMessageBroker(MessageBrokerRegistry config) { //in-memory message-broker, topic에 대한 prefix 설정 config.enableSimpleBroker("/topic"); //메세지를 수신하는 handler의 메세지 prefix 설정 config.setApplicationDestinationPrefixes("/api"); }; @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/ws").setAllowedOrigins("*").withSockJS(); } }
| cs |
Controller
| @RestController public class MessageHandleController { @MessageMapping("/echo") @SendTo("/topic/messages") public EchoMessage echo(String message) throws Exception { System.err.println(message); return new EchoMessage(message,LocalDateTime.now()); } } | cs |
그림의 SimpleAnnotationMethod로 된 부분을 @MessageMapping으로 처리할 수 있다. 설정의 setApplicationDestinationPrefix의 /api로 설정했기 때문에, 최종적으로 메세지를 보낼려고 할 경우 /api/echo 로 메세지를 보낸다면, MessageHandler는 메세지를 수신한다.
@MessageMapping을 통해 메세지를 수신했다면, @SendTo를 통해 특정 토픽을 구독하는 클라이언트에게 메세지를 보낼 수 있다.
Reactive & Spring Webflux
Spirng5에서는 비동기 측면의 통신에 대한 지원을 확대했으며, Webflux의 Reactor를 통해 reactive-stream에 대한 사양을 정의 했다.
기존의 수 많은 Http 요청의 처리 성능(throughput)을 증가 시키기 위해, 톰캣 쓰레드를 더 많이 할당하는 방식으로 구현되었으나 Spring5 에서는 적은 수의 쓰레드로 Reactive-stream에서 비동기 처리를 지향한다.
Reactive Websocket
Spring5 에서는 websocket 채널에 Reactive 능력을 추가함에 따라 조금 더 유연하게 사용할 수 있도록 지원하였다.
spring5 에서 web flow는 다음과 같다.
https://blog.monkey.codes/how-to-build-a-chat-app-using-webflux-websockets-react/
1. Spring4에서 웹소켓을 직접 처리하는 방식과 같이 기본적으로 WebsocketHandler를 구현하여 처리해야한다.
2. handler는 연결이 설정될 때마다, 웹소켓 세션이 제공되며 세션에는 receive 와 send 를 포함하는 flux stream을 갖게 된다.
3. GMS(UnicastProcessor)를 통해 모든 웹소켓 세션은 연결된다.
4. GMS를 통해 메세지를 보내야 하며, publisher는 websocket session을 통해 receive한 flux 메세지를 수신한다.
5. GMS는 모든 웹소켓 세션을 연결하고 있지 않고 가장 최근 생성된 25개의 subscrber만 갖고 있으며, 메세지를 전달한다.
reactive websocket echo server 구현하기
websocket을 spring5에서 구현하면, 기본적으로 톰캣이 아닌, netty conatiner 에서 동작한다.
https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-websocket
https://www.baeldung.com/spring-5-reactive-websockets
https://blog.monkey.codes/how-to-build-a-chat-app-using-webflux-websockets-react/
https://docs.spring.io/spring/docs/5.0.0.BUILD-SNAPSHOT/spring-framework-reference/html/websocket.html
http://www.egovframe.go.kr/wiki/doku.php?id=egovframework:rte3.5:ptl:sockjs
https://adrenal.tistory.com/20