ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring ServerSentEvent (작성중...)
    SPRING/Spring Framework 2022. 4. 12. 01:06

    작성중입니다. → 완전히 다적힐때까지 조금만 기다려주세요...

    <피드백 언제나 환영합니다! 댓글 감사합니다>

    알림을 보내줘야할때 또는 실시간으로 데이터를 보내줘야 할 경우가 있습니다. 그때 사용하는 기술중 하나인 SSE(Server Sent Event)를 알아볼 예정입니다.

    실시간 웹앱 개발시 사용되는 방법

    실제 실시간 통신에서 사용되는 기술들이 있습니다. 그중 3가지를 뽑아 간단하게 알아 보겠습니다.

    • Polling(client pull)
      클라이언트가 일정한 주기로 서버에 업데이트 요청을 보내는 방법, 지속적인 HTTP 요청발생 → 리소스 낭비 발생
    • WebSocket(server push)
      실시간 양방향 통신을 위한 스펙, 서버와 브라우저가 지속적으로 연결된 TCP 라인을 통해 실시간으로 데이터를 주고받는 HTML5 사양 → 연결지향 양방향 전이중 통신이 가능 (채팅, 게임 주식, 차트등에 사용된다) → 서버 클라이언트간 양방향 통신
    • Server Sent Event(SSE)
      이벤트가 (서버 → 클라이언트) 방향으로만 흐르는 단방향 통신 채널 http 요청을 보낼 필요 없이 http 연결을 통해 서버에서 클라이언트로 데이터를 보낼 수 있다.

    Server Sent Event(SSE)

    • HTTP 스트리밍을 통해 서버에서 클라이언트로 단방향의 Push Notification을 전송할 수 있는 HTML5 표준 기술
    • EventStream 최대 개수는 HTTP/1.1 6개, HTTP/2 사용시 최대 100개까지 유지 가능 합니다.
    • Javascript의 EventSource를 이용하여 사용 가능 → 접속에 문제가 있으면 자동으로 연결을 재시도 하는 특징이 있습니다.
    • IE 에서 polyfill을 통해 사용가능 → 이제는 보내줍시다...
    • Server To Client 단반향 연결 지원 → 서버에서 데이터를 프론트로 일방적으로 내려주는 케이스에서 적절

    Spring 에서 SSE를 적용 해보자

    적용전 해줘야 할는게 있는데 바로 Postman 과 VsCode 를 설치 해야 합니다 → Data 전송 과 화면 표시를 위해

    • VsCode Extension LiveServer 설치 → html 파일을 바로 로드하여 확인하기 위해
    • PostMan → 서버 Event 발생 도구
    • Spring → web, lombock, devtool (dependecy 설정)

    요구 사항

    • 화면
      • 데이터를 화면에서 실시간으로 표시
      • [ Title ] --> [ Text ]
    • 서버 컨트롤러
      • Cros / GetMapping → SseEmitter 를 이용 (서버 → 클라이언트) 단방향 통신 대기
      • Cros / PostMapping(ToALL) → 연결 되어 있는

    먼저 코드를 적어놓고 하나씩 분석해 가면서 설명 하겠습니다.

    • VsCode - sseIndex.html

      VsCode 에 임의로 html 파일을 만들어 표시되는걸 보기위한것 → 간단히 작성할 예정

      <!DOCTYPE html>
      <html lang="en">
      
      <head>
          <meta charset="UTF-8">
          <meta http-equiv="X-UA-Compatible" content="IE=edge">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
          <title>Document</title>
      </head>
      
      <body>
          <input id="userId" disabled="false">
          <ol id="list">
      
          </ol>
          <script>
              $(document).ready(function () {
      
                  let userId = Math.floor((Math.random() * 1000) + 1);
                  document.getElementById("userId").value = userId; // LiveServer 에서 사용하는 userId
                  let urlENdPoint = 'http://localhost:8080/subscribe?userId=' + userId;
                  let eventSource = new EventSource(urlENdPoint); // javascript SSE 받아서 사용하는 부분
      
                  eventSource.addEventListener("latestNews", function (event) {
                      console.log("urlEndPoint : " + urlENdPoint)
                      console.log("EVENT : " + event);
                      console.log("EVENT : " + event.data);
                      let artilceSource = JSON.parse(event.data);
                      console.log("artilceSource : "+ artilceSource)
      
                      $("#list").append("<li>" + artilceSource.Title +" --> "+ artilceSource.Text + "</li>");
                  });
      
              })
          </script>
      
      </body>
      
      </html>
    • NewsController.java
      package com.example.RealTimeNews.controller; // package 위치 -> Controller 형태로 받을 예정
      
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.http.MediaType;
      import org.springframework.web.bind.annotation.*;
      import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
      
      import java.io.IOException;
      import java.util.List;
      import java.util.Map;
      import java.util.concurrent.ConcurrentHashMap;
      import java.util.concurrent.CopyOnWriteArrayList;
      
      @RestController
      @Slf4j
      public class NewsController {
      
          public List<SseEmitter> emitters = new CopyOnWriteArrayList<>();
          public Map<String, SseEmitter> mapEmitters = new ConcurrentHashMap<>();
          // method for client subscription
          @CrossOrigin
          @GetMapping(value = "/subscribe", consumes = MediaType.ALL_VALUE)
          public SseEmitter subscribe(@RequestParam String userId) {
              SseEmitter sseEmitter = new SseEmitter(Long.MAX_VALUE);
              sendInitEvent(sseEmitter);
      
              logFormListEmitters(sseEmitter);
              logFormMapEmitters(userId, sseEmitter);
      
              return sseEmitter;
          }
      
          // method for dispatching events to all clients
          @CrossOrigin
          @PostMapping(value = "/dispatchEventToAll")
          public void dispatchEventToClients(@RequestParam String title, @RequestParam String text){
              Map<String, String> mapForJson = Map.of("Title", title, "Text", text);
              for (SseEmitter emitter : emitters) {
                  try {
                      emitter.send(SseEmitter.event().name("latestNews").data(mapForJson));
                      log.info("PostMapping for all clients SSE Emitters : {}", emitters);
                  } catch (IOException e) {
                      emitters.remove(emitter);
                      log.info("Exception SSE Emitters : {}", emitters);
                  }
              }
          }
      
          // method for dispatching events to specific clients
          @CrossOrigin
          @PostMapping(value = "/dispatchEventToSpecific")
          public void dispatchEventToSpecificClients(@RequestParam String title, @RequestParam String text, @RequestParam String userId){
              Map<String, String> mapForJson = Map.of("Title", title, "Text", text);
              SseEmitter sseEmitter = mapEmitters.get(userId);
              if (sseEmitter != null) {
                  try {
                      sseEmitter.send(SseEmitter.event().name("latestNews").data(mapForJson));
                      log.info("PostMapping for specific clients SSE Emitters : {}", mapEmitters);
                  } catch (IOException e) {
      //                emitters.remove(sseEmitter);
                      mapEmitters.remove(userId);
                      log.info("Exception SSE Emitters : {}", mapEmitters);
                  }
              }
          }
      
          private void sendInitEvent(SseEmitter sseEmitter) {
              try {
                  sseEmitter.send(SseEmitter.event().name("INIT"));
              } catch (IOException e) {
                  e.printStackTrace();
              }
          }
      
          private void logFormListEmitters(SseEmitter sseEmitter) {
              log.info("------------------All Clients-----------------");
              log.info("first SSE Emitters : {}", emitters);
      
              sseEmitter.onCompletion(() -> emitters.remove(sseEmitter));
              log.info("onCompletion remove SSE Emitters : {}", emitters);
      
              emitters.add(sseEmitter);
              log.info("after add SSE Emitters : {}", emitters);
          }
      
          private void logFormMapEmitters(String userId, SseEmitter sseEmitter) {
              log.info("------------------SpecificClients-----------------");
              log.info("first SSE Emitters : {}", mapEmitters);
              mapEmitters.put(userId, sseEmitter);
              log.info("map SSE Emitters : {}", mapEmitters);
      
      
              sseEmitter.onCompletion(() -> mapEmitters.remove(userId));
              log.info("onCompletion remove SSE Emitters : {}", mapEmitters);
      
              sseEmitter.onTimeout(() -> mapEmitters.remove(userId));
              log.info("onTimeout remove SSE Emitters : {}", mapEmitters);
      
              sseEmitter.onError((e) -> mapEmitters.remove(userId));
              log.info("onError remove SSE Emitters : {}", mapEmitters);
          }
      }
    • PostMan 데이터

      http://localhost:8080/dispatchEventToAll?title=Corona is ending&text=I am Happy

      • Params / key : title | value : Corona is ending
      • Params / key : text | value : I am Happy

      http://localhost:8080/dispatchEventToSpecific?title=only this id sent&text=hello!!!&userId=853

      • Params / key : title | value : only this id sent
      • Params / key : text | value : hello!!!
      • Params / key : userId | value : 853 (VsCode 에 있는 userId)

    참조

    댓글

Designed by Tistory.