Ws 프로토콜 기반으로 클라이언트와 서버 사이에 소켓 커넥션을 유지하며 지속적인 완전 양방향 연결 스트림을 만들어주는 기술

Request Header
GET /spring-websocket-portfolio/portfolio HTTP/1.1
Host: localhost:8080
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg==
Sec-WebSocket-Protocol: v10.stomp, v11.stomp
Sec-WebSocket-Version: 13
Origin: http://localhost:8080
Upgrade: websocket : websocket upgrade 헤더를 선언
Connection: Upgrade : 업그레이드된 연결을 사용한다는 의미
sec-web-key : 클라이언트가 랜덤으로 생성한 값을 Base64로 인코딩한 문자열
Response Header
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=
Sec-WebSocket-Protocol: v10.stomp
HTTP/1.1 101 Switching Protocols : 기존의 HTTP 프로토콜이 아닌 WebSocket 프로토콜로 변경된다는 의미
Sec-WebSocket-Accept : 받은 key에 GUID를 붙이고 SHA-1 해시로 계산해 base64로 인코딩한 것
| 차이점 | TCP Socket | Web Socket |
|---|---|---|
| 추상화 정도 | 저수준 | 추상화되어 있음 |
| protocol | 4계층(전송계층)에서 동작 | 7계층(애플리케이션 계층)에서 동작 |
| Data 전송방법 | 바이트스트림을 통한 데이터 전송 | 구조화된 메시지 형식의 데이터 전송 |
| 방화벽 | 방화벽에 의해 차단될 수 있는 새 TCP 포트를 열어야 함 | 방화벽을 사용하여 웹이 아닌 인터넷 연결을 차단하는 환경에 유용 |
HTTP와 REST에서 어플리케이션은 많은 URL들로 설계된다.
클라이언트는 이러한 URL들에 요청을하고 서버는 HTTP URL, method, 헤더를 기반으로 적절한 핸들러를 이용해 요청을 처리한다.
WebSocket은 일반적으로 첫 연결을 위한 하나의 URL이 존재한다.
이후, 모든 어플리케이션 메시지는 같은 TCP 연결을 통해 전송된다.

위 그림과 같이 HTTP는 모든 통신을 요청과 응답을 한번씩 주고 받지만, WebSocket은 단 한번의 WebSocket 요청과 수락 이후에 연결을 끊기 전까지 자유롭게 양방향으로 데이터를 주고받을 수 있다.
WebSocket은 low-level 프로토콜이지만 WebSocket 클라이언트와 서버는 Sec-WebSocket-Protocol을 통해 STOMP와 같은 high-level 프로토콜을 사용할 수 있다.
Spring에서 WebSocket의 기능을 직접 구현해본다.
WebSocketHandler를 이용해 WebSocket을 활성화하기 위한 설정
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(webSocketHandler(), "/websocket-endpoint")
.setAllowedOriginPatterns("*")
.withSockJS();
}
public WebSocketHandlerImpl webSocketHandler(){
return new WebSocketHandlerImpl();
}
}
WebSocketHandlerRegistry : WebSocketHandler를 등록하기 위한 인터페이스addHandler(WebSocketHandler webSocketHandler, String... paths) : WebSocketHandler와 endPoint 경로를 등록하는 메서드setAllowedOriginPatterns : 도메인이 다른 서버에서 접속할 수 있는 범위를 설정withSockJS() : SockJS 라이브러리를 사용하는 것을 의미SockJS란?
WebSocket이 지원되지 않는 브라우저에서도 양방향 통신을 가능하게 하는 라이브러리
WebSocket의 메시지와 생명주기 이벤트를 관리하는 Handler
여기서는 커스텀 핸들러를 구현하기 위해 WebSocketHandler 인터페이스를 구현체를 작성하였다.
public class WebSocketHandlerImpl implements WebSocketHandler {
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
log.info("WebSocket 연결 성공");
}
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
if(message instanceof TextMessage) {
String payload = ((TextMessage) message).getPayload();
log.info("Received message: {}", payload);
}
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
log.info("WebSocket 에러 발생");
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
log.info("WebSocket 연결 종료");
}
@Override
public boolean supportsPartialMessages() {
return false;
}
}
afterConnectionClosed : WebSocket 연결이 성공적으로 닫히고 호출되는 메서드afterConnectionEstablished : WebSocket 연결이 성공하고 열리고 나서 호출되는 메서드handleMessage : 새로운 WebSocket 메시지가 왔을 때 호출되는 메서드handleTransportError : WebSocket 메시지 전송에서 발생하는 에러를 처리supportsPartialMessages() : 핸들러가 partial message를 처리하는지의 여부Simple Text Oriented Messaging Protocol
메시지 전송을 호율적으로 하기 위한 프로토콜
클라이언트는 send 나 subscribe 명령어를 통해 destination 헤더와 함께 메시지에 대한 전송이나 구독을 한다.
publish 송신 subscribe 수신
SUBSCRIBE
id:sub-1
destination:/topic/price.stock.*
^@
SEND
destination:/queue/trade
content-type:application/json
content-length:44
{"action":"BUY","ticker":"MMM","shares",44}^@
destination:/topic/messages
content-type:application/json
subscription:sub-0
message-id:mi1yksot-244
content-length:51
{"from":"a","text":"aa","time":"13:37:38"}

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketStompConfig implements WebSocketMessageBrokerConfigurer {
/*
/chat 엔드포인트를 정의합니다.
클라이언트는 이 엔드포인트에 연결해 WebSocket을 통해 STOMP 통신을 시작합니다.
withSockJS(): SockJS를 사용하여 WebSocket을 지원하지 않는 환경에서도 동작하도록 합니다.
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/chat").withSockJS();
}
/*
메시지 브로커를 설정합니다.
enableSimpleBroker("/topic", "/queue"):
/topic과 /queue로 시작하는 메시지를 브로커가 처리합니다.
setApplicationDestinationPrefixes("/app"):
클라이언트가 서버에 메시지를 보낼 때 /app으로 시작하는 경로를 사용합니다.
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic", "/queue");
config.setApplicationDestinationPrefixes("/app");
}
}
@Controller
public class WebSocketController {
@MessageMapping("/chat") // 클라이언트가 /app/chat 경로로 보낸 메시지를 처리
@SendTo("/topic/messages") // 처리된 메시지를 /topic/messages 구독자들에게 전달
public OutputMessageModel send(final MessageModel message) throws Exception { // MessageModel 객체로 message 수신
final String time = new SimpleDateFormat("HH:mm:ss").format(new Date());
return new OutputMessageModel(message.getFrom(), message.getText(), time); // OutputMessageModel 객체로 변환하여 응답
}
MessageModel model;
}