채팅 정리
채팅 기능을 구현하면서 그 과정들을 정리했었는데 기존에 정리했던 것 일부분을 넣어 요약 정리를 해봤다.
WebSocket & WebSocket Emulation
HTTP 프로토콜의 특징 중 중요한 부분 중 하나는 비연결성이다.
HTTP는 비연결성이라는 특징을 가지고 있어서 연결을 끊어버린다.
이를 해결하기 위한 방법이 Polling, Long Polling, Streaming이 있다.
관련 글 : SSE
하지만 위 방식 모두 HTTP를 통해 통신하기 때문에 Request/Response 모두 헤더가 불필요하게 크다.
이러한 단점들을 해소하기 위해 나온 것이 WebSocket이다.
WebSocket 프로토콜은 서버-클라이언트 간에 단일 TCP 커넥션을 이용해서 양방향 통신을 제공한다.
request - response가 있는 것이 아니라 커넥션이 open - close 된 것이다.
WebSocket 연결 과정
브라우저에서 socket 통신을 이용하기 위해서는
socket 통신이 가능한지 확인하는 핸드셰이크(Hand Shake) 과정이 필요하다.
핸드쉐이크는 한번의 HTTP 요청과 HTTP 응답으로 이루어진다.
핸드쉐이크가 끝나면 HTTP 프로토콜을 웹소켓 프로토콜로 변환하여 통신을 하는 구조다.
핸드쉐이크는 먼저 클라이언트가 HTTP로 웹소켓 연결 요청을 하면서 시작된다.
Hand Shake 과정
1. 브라우저에서 HTTP 통신을 이용하여 서버에 소켓 통신이 가능한지 요청을 보낸다.
header에 socket을 사용하기 위한 Upgrade, Connection, WebSocket에 관한 정보를 포함한다.
웹소켓 연결 요청에는 Connection: Upgrade
와 Upgrade: websocket
헤더를 통해 웹소켓 요청임을 표시한다.
Sec-WebSocket-Key
헤더를 통해 핸드쉐이크 응답을 검증할 키 값을 보낸다.
2. 서버에서 WebSocket 통신을 지원한다면 101 Switching Protocols 상태 코드를 응답하여 웹소켓 통신을 허용함을 알린다.
서버의 응답역시 HTTP로 오며, 정상적인 응답의 상태코드는 101(Switching Protocols)다.
Sec-WebSocket-Key
헤더를 통해 받은 값에 특정 값을 붙인 후,
SHA-1로 해싱하고 base64로 인코딩한 값을 Sec-WebSocket-Accept
헤더에 보낸다.
4. handshake 과정이 성공적으로 끝나면 TCP 연결은 유지한채 HTTP를 webSocket 프로토콜로 바꾸는 protocol switching 과정이 진행된다.
→ TCP handshake를 통해 HTTP Upgrade Header를 사용하여 WebSocket 프로토콜로 변경
WebSocket을 위한 새로운 소켓이 만들어지고 이 소켓을 이용해 통신한다. → ws / wss
Websocket은 ws는 프로토콜을 사용하고 HTTPS에서는 SSL이 적용된 wss 프로토콜을 사용한다.
ex) URL : wss://www.chat.site
1
2
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.webjars:sockjs-client:1.1.2'
1
2
3
4
5
6
7
8
9
@Configuration
@EnableWebSocket // 👈🏻
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").setAllowedOriginPatterns("*").withSockJS();
}
}
WebSocket 기반의 애플리케이션을 만들었을 경우, 다음과 같은 문제점들이 존재할 수 있다.
- 클라이언트의 브라우저가 WebSocket을 지원하지 않음
- 클라이언트와 서버 사이의 Proxy가 WebSocket Upgrade 헤더를 해석 못해 서버에 전달하지 못함
- 클라이언트와 서버 사이의 Proxy가 Idle 상태에서 Connection을 도중 종료함
위와 같은 상황에서의 해결책은 WebSocket Emulation이다.
우선 WebSocket Connection을 시도하고,
실패할 경우 : HTTP 기반하에서 동작하는 다른 기술로 전환하여 연결을 시도한다.
Node.js를 사용시 Socket.io를 사용하는 것이 일반적이고,
Spring Framework를 사용시 SockJS를 사용하는 것이 일반적이다.
관련 글 : WebSocket
Stomp
WebSocket 프로토콜은 두 가지 유형의 메시지를 정의하고 있지만, 그 메시지의 내용까지는 정의하고 있지 않다.
STOMP은 WebSocket 위에서 동작하는 프로토콜로써, 클라이언트와 서버가 전송할 메시지 유형, 형식, 내용들을 정의한다.
내가 가장 와닿았던 것은
COMMAND를 통해 SEND 또는 SUBSCRIBE, CONNECT 등의 명령을 지정하고, header를 정의할 수 있다는 것이다.
MessageType(COMMAND)에 따라서 Controller를 구분지어 활용할 수 있다는 점이 가장 큰 차이로 와닿았다.
1
implementation 'org.webjars:stomp-websocket:2.3.3-1'
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
27
28
29
30
31
@Configuration
@RequiredArgsConstructor
@EnableWebSocketMessageBroker // 👈🏻
public class StompWebSocketConfig implements WebSocketMessageBrokerConfigurer {
private final StompHandler stompHandler;
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// websocket 에 연결하기 위한 엔드 포인트를 지정
registry.addEndpoint("/ws").setAllowedOriginPatterns("*").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue", "/topic");
registry.setApplicationDestinationPrefixes("/app");
}
/*
enableSimpleBroker를 통해 메시지 브로커가 /topic으로 시작하는 주소를 구독한 Subscriber들에게 메시지를 전달하도록 한다.
setApplicationDestinationPrefixes는 클라이언트가 서버로 메시지를 발송할 수 있는 경로의 prefix를 지정한다.
*/
// StompHandler가 Websocket 앞단에서 token을 체크할 수 있도록 interceptor로 설정
@Override
public void configureClientInboundChannel(ChannelRegistration registration){
// jwt 토큰 검증을 위해 생성한 stompHandler를 인터셉터로 지정해준다.
registration.interceptors(stompHandler);
}
}
1
2
3
public class StompHandler implements ChannelInterceptor {
}
관련 글 : Websocket + stomp + 보안 강화
Message Broker & cache
메시지 브로커는 송신자가 보낸 메시지를 메시지 큐에 적재하고 이를 수신자가 받아서 사용하는 구조이다.
메시지 브로커는 대표적으로 Apache Kafka, Redis, RabbitMQ, Celery 등이 있다.
1
2
3
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'redis.clients:jedis:3.6.3'
관련 글 : Redis Broker , Redis cache
Https
Https 적용 한 코드를 보면 크게 달라진 점은 없다.
SSL을 적용한 후에 실행하면 되기 때문이다.
SSL 적용과정은 여기에 기록해뒀다.
1
2
3
server.ssl.key-store=classpath:localhost.p12
server.ssl.key-store-type=PKCS12
server.ssl.key-store-password=changeit
처음에 https로 적용을 했으니 그에 맞는 프로토콜인 wss를 적용해야한다고 생각이 들어서
아래와 같이 작성했었다.
1
let socket = new SockJS('/wss');
Network 탭에 들어가서 wss 적용이 되었는지 확인을 해보니 내가 설정한 wss는 엔드포인트 였다라는 것을 알게 되었다.
일반적으로 WebSocket의 프로토콜은 HTTP나 HTTPS의 프로토콜에 따라 자동으로 결정되는 것 같다.
1
2
3
if (document.location.protocol === 'https:') {
socket = new SockJS("/coco");
}
여기서 endpoint는 아무거나 해도 된다. (ws나 wss를 적는게 아님!)
그리고 거기에 맞게 java도 수정해주기만 하면 끝!
1
2
3
4
5
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
//소켓에 연결하기 위한 엔드 포인트를 지정
registry.addEndpoint("/coco").setAllowedOriginPatterns("*").withSockJS();
}
REFERENCE