ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [SSE] Server Sent Events
    Language/Java 2024. 3. 31. 20:39

    Sever Sent Events (SSE) 란?

    클라이언트가 HTTP 연결을 통하여 서버로부터 데이터를 수신 할 수 있는 서버 푸쉬 기술이다.

    일반적으로 클라이언트에  실시간으로 메시지 업데이트 또는 지속적인 데이터 스트림을 보내는 데 사용된다.

     

     

    서버의 변경된 데이터를 가져오는 방법에는 다음과 같이 여러가지 방법이 있다.

     

    1) 지속적으로 Request를 보내는 Ajax polling

    2) websocket

    3) server sent events

     

    이 3가지의 방법과 어떤 방법이 달고 장단점을 가지고 있는지 살펴보자 

     

    Ajax Polling 

    그림과 같이 주기를 가지고 Ajax 요청을 보내 변화를 확인 하는 방법이다.

    구현이 단순하나 요청간격이 길면 변화를 빠르게 캐치할 수 없고 

    요청간격이 짧으면 HTTP요청 발생으로 서버에 부담이 된다.

     

    websocket

     

    handshaking할 때 HTTP 통신을 하고 그 후 ws 프로토콜을 사용한다.

    양방향 통신을 지원한다.

     

     

    Server Sent Event 

    Server Sent Event 특징

    • 브라우저는 서버가 생성 한 Stream 을 계속 받음(Server 에서 보내는 Stream 으로 Read Only)
    • Connection 유지를 위해 HTTP protocol 을 사용, HTTP/2를 통한 multiplexing 사용 가능
    • 연결이 끊어지면 EventSource가 오류 이벤트를 발생시키고 자동으로 다시 연결을 시도(error recovery)
    • 표준 기술로 IE 를 제외한 브라우저 대부분을 지원(Pollyfill로 IE 사용 가능)
    • Server에서 클라이언트로만 전달하는 단방향 통신이다.

    Server Sent Event 사용 시점

    • 효율적인 단방향 통신이 필요한 경우
    • 실시간 데이터 스트림밍에 HTTP 를 사용하려는 경우(RestFul 의 GET method 와 유사)
    • 사용 되는 예 (클라이언트는 수신만 받으면 된다)
      • 암호 화폐 또는 주가 피드 구독
      • Twitter 피드 구독
      • 라이브 스포츠 점수 받기
      • 뉴스 업데이트 또는 알림

    왜냐하면 클라이언트에서는 데이터를 받기만 하면 되고 완전히 실시간일 필요는 없기 때문이다.

     

     

    WebSocket vs Server Sent Events

    이 둘의 대표적인 차이점으로는, Socket은 양방향(bidirectional)으로 데이터를 주고 받을 수 있지만, SSE(Server-Sent-Event)를 사용하게 되면 클라이언트는 데이터를 받을 수만(mono-directional) 있다는 점이다.

      Socket Server-Sent-Event
    브라우저 지원 대부분 브라우저에서 지원  대부분 모던 브라우저 지원(polyfills 가능)
    통신 방향 양방향 일방향(서버 -> 클라이언트)
    리얼타임 Yes Yes
    데이터 형태 Binary, UTF-8 UTF-8
    자동 재접속 No Yes(3초마다 재시도)
    최대 동시 접속 수 브라우저 연결 한도는 없지만 서버 셋업에 따라 다름  HTTP를 통해서 할 때는 브라우저당 6개 까지 가능 / HTTP2로는 100개가 기본
    프로토콜 websocket HTTP
    베터리 소모량 작음
    Firewall 친화적 Nope Yes

    Socket은 양방향 통신이기 때문에  SSE역할도 수행해 낼 수 있으나 소켓은 계속 서버랑 계속 handshake를 하고 있습

    ( 소켓은 계속 서버랑 계속 handshake를 하고 있습)

    위에 표에서 보는 것처럼 websocket과 SSE의 스펙 차이점(배터리,재접속, 통신프로토콜 등)때문에 사용처에 따라 선택적으로 사용되는 편이다.

     

    왜냐하면 클라이언트에서는 데이터를 받기만 하면 되고 완전히 실시간일 필요는 없기 때문이다.

     

     

    출처: https://inpa.tistory.com/entry/NODE-📚-Server-Sent-Events-💯-정리-사용법 [Inpa Dev 👨‍💻:티스토리]

     

     

    SSE 구현

    Sever 구현

     

    Sse 관리 클래스

    @Component
    @Slf4j
    public class SseManager {
    
        private final List<SseEmitter> sseEmitters = new CopyOnWriteArrayList<>();
        private final AtomicLong count = new AtomicLong();
    
        public SseEmitter add(SseEmitter emitter) {
            log.info("adding");
            sseEmitters.add(emitter);
    
            emitter.onCompletion(() -> {
                log.info("onCompletion callback");
                //emitter가 완료되면 emitter list에서 제거해줌
                sseEmitters.remove(emitter);
            });
            emitter.onTimeout(() -> {
                log.info("ontimeout callback");
                //timeout되면 emmiter 완료처리를 해준다.
                emitter.complete();
            });
            emitter.onError(error -> {
                log.error("error : {}", error);
                sseEmitters.remove(emitter);
            });
            return emitter;
        }
    
        public void count() {
            Long result = count.incrementAndGet();
            sseEmitters.forEach(emitter -> {
                try {
                    emitter.send(SseEmitter.event()
                            .name("count")
                            .data(result)
                    );
    
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
        }
    }

     

     

    Controller

    @RestController
    @RequestMapping("/sse")
    public class SseController {
    
        private final SseManager sseManager;
    
        private static final Long TIME_OUT_LIMIT = 5 * 60 * 1000L;
    
        public SseController(SseManager sseManager) {
            this.sseManager = sseManager;
        }
    
        // Content type이 text/event-stream 이므로 지정해주어야 한다.
        @GetMapping(value="/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
        public ResponseEntity<SseEmitter> connect() {
            // 생성자 Parameter로 Time Out시간을 받는다.
            // 지정하지 않았을 경우 30초이다.
            // Time Out이 끝나고 Client에서 다시 이 Url로 요청하여 연결한다.
            SseEmitter sseEmitter = new SseEmitter(TIME_OUT_LIMIT);
            sseManager.add(sseEmitter);
    
            /*
                연결 후 만료시간 까지 아무데이터도 보내지 않으면 503 에러를 보내므로
                연결 후 바로 Event 보내기
             */
            try {
                sseEmitter.send(SseEmitter.event()
                        .name("connect")
                        .data("connected!"));
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
    
            return ResponseEntity.ok(sseEmitter);
        }
    
        //외부에서 임 함수를 호출 했을 때 sseManager에 있는 sseEmitter들에게 send event를 날린다.
        @GetMapping("/count")
        public ResponseEntity<Void> count() {
            sseManager.count();
            return ResponseEntity.ok().build();
        }
    }

     

     

    Client 구현

        const sse = new EventSource("http://localhost:8080/sse/connect");
    
        // 연결되었을 때 로직
        sse.addEventListener('connect', (e) => {
            const { data: receivedConnectData } = e;
            console.log('connect event data: ',receivedConnectData);  // "connected!"
        });
    
        // 데이터를 받았을 때 로직
        sse.addEventListener('count', e => {
            const { data: receivedCount } = e;
            console.log("count event data",receivedCount);
            setCount(receivedCount);
        });
    
        // HTML Count 표현
        function setCount(count) {
            document.getElementById("count").innerText = count
    
        }

     

     

    처리 결과

    메서드 한번 호출에 띄워좋은 4개의 브라우저가 동시에 변하는 것을 확인 할 수 있다.

     

     

     
Designed by Tistory.