Hello

[JavaScript] SSE + Spring SseEmitter 본문

기타

[JavaScript] SSE + Spring SseEmitter

nari0_0 2024. 1. 17. 11:56
728x90

어떤 이벤트가 생겼을 때 client ui를 업데이트가 필요했습니다. SSE(Server-Sent-Event)라는 것을 알게 되어 이를 공부한 내용을 간략하게 작성합니다.

SSE는 단방향 통신으로 서버에서 전송한 데이터를 클라이언트는 받기만 할 수 있습니다.

HTTP/2(100개)를 사용하지 않으면 SSE 는 열려 있는 최대 연결 수에 대한 제한이 있습니다. HTTP를 사용할 때 브라우저당 6개로 설정 되어 있습니다.

구독

//동일 도메인 구독
const eventSource = new EventSource('/api/subscribe/sse');
//다른 도메인의 이벤트 구독, URI와 옵션 지정
const eventSource = new EventSource('//example.com/api/subscribe/sse', { withCredentials: true });

이벤트 수신

메시지 발송 옵션 중 event(name)을 지정하지 않은 경우 onmessage를 지정한 경우 addEventListener를 사용해 이벤트를 받음

//name을 지정하지 않은 메시지 수신
eventSource.onmessage = function (e) {
   //some logic...
};

//name 지정한 메시지 수신
eventSource.addEventListener('change-lang', function(e) {
   //some logic...
})

//error handler
eventSource.onerror = function (e) {
  alert("EventSource failed.");
};

이벤트 스트림 형식

스트림 종료를 알리기 위해 '\n\n'이 포함되어야합니다. 

data: message\n\n

retry : 이벤트 송신을 시도할 때에 사용하는 재연결 시간(reconnection time), 밀리초 단위로 지정.

 

서버 코드(SSE in Spring)

@RestController
public class SseController {
    @Autowired
    private SseEmitterService sseEmitterService;

    @GetMapping(value = "/api/subscribe/{id}", produces = "text/event-stream")
    public SseEmitter subscribe(@PathVariable String id,
                                @RequestHeader(name = "Last-Event-ID", required = false, defaultValue = "")String lastEventId) {
        return sseEmitterService.subscribe(id, lastEventId);
    }

}


@Service
public class SseEmitterService {

    private final Map<String, SseEmitter> sseEmitterMap = new ConcurrentHashMap<>();
    private static final Long DEFAULT_TIMEOUT = TimeUnit.MINUTES.toMillis(30);

    public SseEmitter subscribe(String userId, String lastEventId) {
        String key = userId + "_" + System.currentTimeMillis();
        SseEmitter sseEmitter = new SseEmitter(DEFAULT_TIMEOUT);
        sseEmitterMap.put(key, sseEmitter);

        sseEmitter.onCompletion(() -> sseEmitterMap.remove(key));
        sseEmitter.onTimeout(() -> sseEmitterMap.remove(key));

        if (!lastEventId.isEmpty()) {
            sseEmitterMap.keySet().stream().filter(x -> x.startsWith(userId))
                    .filter(x -> lastEventId.compareTo(x) < 0)
                    .forEach(x -> {
                        SseEmitter emitter = sseEmitterMap.get(x);
                        try {
                            emitter.send(SseEmitter.event()
                                    .id(x)
                                    .reconnectTime(1000)
                                    .data("washout"));
                            emitter.complete();
                        } catch (IOException e) {
                            sseEmitterMap.remove(x);
                            emitter.completeWithError(e);
                        }
                    });
        }

        return sseEmitter;
    }

    public void sendUserBy(String userId, Object lang) {
        sseEmitterMap.keySet().stream().filter(x -> x.startsWith(userId))
                .forEach(x -> {
                    SseEmitter emitter = sseEmitterMap.get(x);
                    try {
                        emitter.send(SseEmitter.event()
                                .id(x)
                                .name("change-lang")
                                .data(lang));
                        emitter.complete();
                    } catch (IOException e) {
                        sseEmitterMap.remove(x);
                        emitter.completeWithError(e);
                    }
                });
    }
}

Last-Event-ID

D를 설정하게 되면, 브라우저는 마지막에 발생한 이벤트를 추적할 수 있게 됩니다.

서버 연결이 끊어지면 특수 HTTP 헤더 (Last-Event-ID)가 새 요청으로 설정됩니다.

console.log를 통해 event response 내용 확인하면 lastEventId가 담긴 것을 볼 수 있다.

 

실제 적용하지 못한이유는 탭을 많이 띄어 두고 테스트 했을 때 간헐적으로 한두개의 탭에서 메시지를 받지 못해 기술을 테스트만 하게 되었습니다. 공부를 하며 검색하는 와중에, BroadcastChannel API를 알게되어 사용했습니다.

 

 

참고:

https://jsonobject.tistory.com/558

https://developer.mozilla.org/ko/docs/Web/API/Server-sent_events/Using_server-sent_events

https://developer.mozilla.org/ko/docs/Web/API/Server-sent_events

https://stackoverflow.com/questions/11896160/any-way-to-identify-browser-tab-in-javascript

728x90