SSE - Server Sent Event란?
SSE는 Server Sent Event의 약어로 서버의 데이터를 실시간으로, 지속적으로 Client단으로 Streaming하는 기술입니다.
기존에는 서버의 변경된 데이터를 Client에서 가져오기 위해서 페이지 새로고침, 지속적으로 request를 보내는 ajax 폴링, 외부 플러그인 이용 등을 사용해야만 했습니다. 하지만 이럴경우 갱신되지 않은 의미없는 응답이 리턴될 수 도 있어서 의미없는 HTTP 프로토콜 오버헤드를 발생시킬 수도 있습니다.
이 외에도 websocket 기반으로 양방향 통신 서비스를 사용하여 server의 정보를 clienth로 전달할 수 있지만, HTTP 통신을 이용하는것이 아닌 웹소켓만을 위한 별도의 서버와 프로토콜을 통신하기 때문에 비용이 많이 든다는 단점이 있다.
하지만 SSE는 기존 HTTP 웹 서버에서 HTTP 프로토콜만으로 동작되며 구현도 간단하기 때문에 서버와 Clienth 양측 모두 쉽게 개발이 가능합니다.
Client와 Server와 데이터를 주고받는 방식들
Client와 서버간 비동기적으로 업데이트를 통해 메세지를 받을 수 있는 기술은 몇가지가 존재하는데 크게 Client pull(클라이언트에서 요청)과 Server push(서버에서 밀어넣기) 두 가지 범주로 나눌 수 있습니다.
- Clinet pull
클라이언트가 주기적으로 서버에 업데이트를 요청합니다. 서버는 업데이트된 정보로 응답하거나 변경되지 않은 응답을 보낼 수 있습니다. Client pull에도 Short polling과 Long polling 두 가지로 나뉘어 집니다.
- Short polling
기본적으로 클라이언트가 서버에 주기적으로 데이터를 요청을 요청하는 것입니다. 만일 서버에서 업데이트가 있을경우, 응답을 받고 연결을 끊습니다. 만일 서버에서 특별한 업데이트가 없을경우, 서버에서 클라이언트로 특별한 응답을 보내고 연결을 끊습니다. - Long polling
이것은 Short polling의 살짝 변형된 방식으로 만일 서버에서 업데이트가 없는경우, 업데이트가 있을때까지 연결을 유지합니다. 서버에서 업데이트가 있는경우, 서버에서 응답을 받고 연결을 종료합니다. 만일 일정기간동안 업데이트가 없는경우, 서버에서 특별한 응답을 클라이언트로 보낸 후 연결을 종료합니다.
- Short polling
- Server push
Server에서 메세지가 사용 가능한 데이터를 즉시 클라이언트로 보내는 기술입니다. Server push에는 Server-Sent Events와 Web Socket의 두 가지 유형이 있습니다.
- Server-Sent Event
Server-Sent Event는 오직 서버에서 클라이언트로 Text 메세지를 보내는 브라우저 기반의 웹 어플리케이션입니다. Server-Sent Event는 HTTP 프로토콜 기반의 영구적인 연결을 기반으로 합니다. Server-Sent Events는 HTML5의 일부인 W3C에서 표준화한 network protocol과 EventSource Client 인터페이스를 가지고 있습니다. - WebSocket
WebSocket은 웹 어플리케이션에서 양방향 실시간 동시 통신을 구현하는 기술입니다. WebSocket은 HTTP 이외의 프로토콜을 기반으로 하므로 네트워크 인프라(proxy servers, NATs, firewalls 등)의 추가 설정이 필요할 수 있습니다. 하지반 WebSocket은 HTTP 기반으로는 달성하기 어려운 성능을 제공할 수 있습니다.
주로 채팅과 같은 Client와 서버간의 양방향 데이터 송신을 위해서 사용하고 있습니다.
- Server-Sent Event
SSE network protocol
Server events를 구독하기 위해서 클라이언트는 GET 요청을 Header정보와 같이 보내야 합니다.
- Accept : text/event-stream은 표준에서 요구하는 이벤트의 Media Type 유형을 나타냅니다.
- Cache-Control : no-cache는 모든 이벤트 캐싱을 비활성화합니다.
- Connection : keep-alive는 지속적인 연결이 사용중임을 나타냅니다.
GET /sse HTTP/1.1 Accept: text/event-stream Cache-Control: no-cache Connection: keep-alive
서버는 Header가 포함된 응답으로 구독을 확인해야 합니다.
- Conteot-Type : text/event-stream; charset=UTF8은 표준에서 요구하는 media type 및 이벤트 인코딩을 나타냅니다.
- Transfer-Encoding : chunked는 서버가 동적으로 생성된 콘텐츠를 스트리밍하므로 콘텐츠 크기를 미리 알 수 없음을 나타냅니다.
HTTP/1.1 200 Content-Type: text/event-stream;charset=UTF-8 Transfer-Encoding: chunked
구독 후 서버는 메세지를 사용할 수 있을때 즉시 클라이언트로 보냅니다. 이벤트는 UTF-8 인코딩의 텍스트 메세지 입니다. 이벤트는 두개의 줄 바꿈 문자 \n\n으로 서로 구분됩니다. 각 이벤트는 단일 개행문자 \n으로 구분된 하나 이상의 이름 값 필드로 구성됩니다.
data: The first event.
data: The second event.
서버는 단일 개행문자로 데이터를 구분할 수 있습니다.
data: The third
data: event.
id 필드에서 서버는 고유한 이벤트 식별자를 보낼 수 있습니다. 연결이 끊어지면 클라이언트는 자동으로 다시 연결하여 마지막으로 수신한 이벤트 ID를 Last-Event-ID 헤더와 함께 보내야 합니다.
id: 1
data: The first event.
id: 2
data: The second event.
event 필드에서 서버는 이벤트 유형을 보낼 수 있습니다. 서버는 동일한 구독 유형이 없을뿐만 아니라 드른 유형의 이벤트를 보낼 수 있습니다.
event: type1
data: An event of type1.
event: type2
data: An event of type2.
data: An event without any type.
retry 필드에서 서버는 시간초과(밀리초 단위)를 보낼 수 있으며, 그 후에 연결이 끊어지면 클아이언트가 자동으로 다시 연결해야 합니다. 이 필드가 지정되지 않을경우 표준에 따라 3000밀리초여야 합니다.
retry: 1000
라인이 콜론 문자(:)로 시작하면 클라이언트에서는 이를 무시해야합니다. 이것은 서버에서 주석을 보내거나 일부 프로시 서버가 시간 초과에 의해 연결을 닫는것을 방지하는데 사용할 수 있습니다.
: ping
SSE client : Event Source 인터페이스
아래의 설명은 SSE를 사용하기 위해 Client에서 사용하는 Event Source에 관한 내용입니다.
서버와 연결하기 위해서 클라이언트에서는 EventSource object를 생성해야만 합니다.
여기서 괄호()안에는 호출할 path를 입력합니다.
var eventSource = new EventSource('/sse);
Server-Sent Events는 서버에서 클라이언트로 이벤트를 보내도록 설계되었음에도 불구하고 GET 쿼리 매개변수를 사용하여 클라이언트에서 서버로 데이터를 전달할 수 있습니다.
var eventSource = new EventSource('/sse?event=type1);
...
eventSource.close();
eventSource = new EventSource('/sse?event=type1&event=type2);
...
서버와의 연결 종료를 위해서 close() method를 사용해야합니다.
eventSource.close();
서버와의 연결 상태를 나타내는 readyState라는 속성이 있습니다.
- EventSource.readyState = 0 : Connecting(연결이 아직 설정되지 않았거나 닫혀 있고 클라이언트가 다시 연결중임을 나타냅니다.)
- EventSource.readyState = 1 : Open(클라이언트는 연결이 열려 있고 이벤트를 수신할 때 이벤트를 처리하고 있습니다.)
- EventSource.readyState = 2 : Closed(연결이 열려있지 않고 클라이언트가 다시 연결을 시도하지 않는 경우 치명적인 오류가 발생했거나 close() 메서드가 호출됩니다.
서버와 연결 설정을 하려면 open 이벤트 핸들러로 구독을 해야합니다.
eventSource.onopen = function () {
console.log('connection is established');
};
연결 상태의 일부 변경이나 치명적인 오류를 처리하려면 oneerror 이벤트 핸들러를 구독해야 합니다.
eventSource.onerror = function (event) {
console.log('connection state: ' + eventSource.readyState + ', error: ' + event);
};
이벤트 필드 없이 수신 이벤트를 처리하려면 onmessage 이벤트 핸들러를 구독해야합니다.
eventSource.onmessage = function (event) {
console.log('id: ' + event.lastEventId + ', data: ' + event.data);
};
이벤트 필드로 수신 이벤트를 처리하려면 해당 이벤트에 대한 이벤트 핸들러에 등록해야 합니다.
eventSource.addEventListener('type1', function (event) {
console.log('id: ' + event.lastEventId + ', data: ' + event.data);
}, false);
EventSource client interface는 대부분의 브라우저에서 구현이 됩니다.
SSE Java server : Spring Web MVC
Spring Web MVC 프레임워크 5.2.0은 Servlet 3.1 API를 기반으로 하며 thread pools를 사용하여 비동기 Java 웹 애플리케이션을 구현합니다. 이러한 응용 프로그램은 tomcat 8.5 및 jetty 9.3과 같은 Servlet 3.1+ 컨테이너에서 실행할 수 있습니다.
Spring Web MVC 프레임워크로 이벤트를 구현하려면 아래의 작업이 필요합니다.
- Controller class를 만들고 Client로 데이터를 전달해야하기 때문에 @RestController Annotation을 작성합니다.
- SseEmitter를 반환하고 GET 요청을 처리하고 text/event 스트림을 생성하는 클라이언트 연결을 생성하는 메서드를 만듭니다.
- 새 SseEmitter를 만들어 저장하고 메서드에서 반환합니다.
- 다른 thread에서 이벤트를 비동기식으로 보내고 저장된 SseEmitter를 가져와 필요한 만큼 SseEmitter.send 메서드를 호출하여 Client로 데이터를 전송합니다.
- 이벤트 전송을 완료하려면 SseEmitter.complete() 메서드를 호출합니다.
- 예외적으로 이벤트 전송을 완료하려면 SseEmitter.completeWithError() 메서드를 호출합니다.
아래는 간단한 Controller 소스입니다.
@RestController
public class SseWebMvcController
private SseEmitter emitter;
@GetMapping(path="/sse", produces=MediaType.TEXT_EVENT_STREAM_VALUE)
SseEmitter createConnection() {
emitter = new SseEmitter();
return emitter;
}
// in another thread
void sendEvents() {
try {
emitter.send("Alpha");
emitter.send("Omega");
emitter.complete();
} catch(Exception e) {
emitter.completeWithError(e);
}
}
}
데이터 필드만 있는 이벤트를 보내려면 SseEmitter.send(Object object) 메서드를 사용해야합니다. 필드 데이터, Id, 이벤트, 재시도 및 주석과 함께 이벤트를 보내려면 SseEmitter.send(SseEmitter.SseEventBuilder builder) 메서드를 사용해야 합니다.
아래 예제에서는 동일한 이벤트를 많은 클라이언트에 보내기 위해 SseEmitter class를 구현했습니다. 클라이언트 연결을 생성하기 위해 스레드로부터 안전한 컨테이너에 SseEmitter를 저장하는 add(SseEmitter emitter) 메소드가 있습니다. 이벤트를 비동기적으로 보내기 위해 연결된 모든 클라이언트에 동일한 이벤트를 보내는 send(Object obj)메서드가 있습니다.
아래는 SseEmitters 간단한 소스 내용입니다.
class SseEmitters {
private final List<SseEmitter> emitters = new CopyOnWriteArrayList<>();
SseEmitter add(SseEmitter emitter) {
this.emitters.add(emitter);
emitter.onCompletion(() -> {
this.emitters.remove(emitter);
});
emitter.onTimeout(() -> {
emitter.complete();
this.emitters.remove(emitter);
});
return emitter;
}
void send(Object obj) {
List<SseEmitter> failedEmitters = new ArrayList<>();
this.emitters.forEach(emitter -> {
try {
emitter.send(obj);
} catch (Exception e) {
emitter.completeWithError(e);
failedEmitters.add(emitter);
}
});
this.emitters.removeAll(failedEmitters);
}
}
SSE 활용해보기
단기적인 주기적 이벤트 스트림 처리
아래의 예에서는 서버는 문장이 끝날때 까지 매초 짧은 지속 기간의 주기적 이벤트 스트림을 보내는 화면을 구현하겠습니다.
먼저 Gradle 설정은 아래와 같이 합니다.
클라이언트단에서 sse 설정 하는것을 확인해야하므로 thymeleaf와 spring-web을 설정합니다.
이 예에서 서버는 단어가 끝날 때까지 1초마다 짧은 기간의 주기적 이벤트 스트림을 보냅니다. 이를 구현하기 위해 언급한 SseEmitters 클래스가 사용되었습니다. 이벤트를 비동기적으로 주기적으로 보내기 위해 캐시된 스레드 풀이 생성되었습니다. 이벤트 스트림은 지속 시간이 짧기 때문에 각 클라이언트 연결은 컨트롤러 메서드 내에서 별도의 작업을 스레드 플에 제출합니다.
package core.controller;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
@RestController
public class SseWebMvcController {
private static final String[] WORDS = "The quick brown fox jumps over the lazy dog.".split(" ");
private final ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
@GetMapping(path = "/words", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
SseEmitter getWords() {
SseEmitter emitter = new SseEmitter();
cachedThreadPool.execute(() -> {
try {
for (int i = 0; i < WORDS.length; i++) {
emitter.send(WORDS[i]);
TimeUnit.SECONDS.sleep(1);
}
emitter.complete();
} catch (Exception e) {
emitter.completeWithError(e);
}
});
return emitter;
}
}
localhost:8080/words를 호출하면 아래와 같이 데이터가 전달됨을 알 수 있습니다.
Client단에서 Sse 이벤트를 발생시키고자 한다면 Controller와 HTML소스를 아래와 같이 작성합니다.
- Controller
@RestController
public class SseWebMvcController {
private static final String[] WORDS = "The quick brown fox jumps over the lazy dog.".split(" ");
private final ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
@GetMapping(path = "/words", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
SseEmitter getWords() {
SseEmitter emitter = new SseEmitter();
cachedThreadPool.execute(() -> {
try {
for (int i = 0; i < WORDS.length; i++) {
emitter.send(WORDS[i]);
TimeUnit.SECONDS.sleep(1);
}
emitter.complete();
} catch (Exception e) {
emitter.completeWithError(e);
}
});
return emitter;
}
@GetMapping("/wordsPage")
public ModelAndView getSearchLocation() {
ModelAndView mv = new ModelAndView("/wordsPage");
return mv;
}
}
- wordPage.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Server-Sent Events client example with EventSource</title>
</head>
<body>
<h1>Client SSE function</h1>
<div id="words">
</div>
<script>
if (window.EventSource == null) {
alert('The browser does not support Server-Sent Events');
} else {
var eventSource = new EventSource('/words');
// Sse 연결시 실행
eventSource.onopen = function () {
console.log('connection is established');
};
// Sse 에러 발생시 실행
eventSource.onerror = function (error) {
console.log('connection state: ' + eventSource.readyState + ', error: ' + event);
};
// 서버에서 데이터 전송시 실행
eventSource.onmessage = function (event) {
console.log('id: ' + event.lastEventId + ', data: ' + event.data);
// li를 생성해서 li에 data를 입력
var data = document.createElement('li');
data.innerHTML = event.data;
// div 밑에 계속적으로 추가
document.getElementById("words").append(data);
if (event.data.endsWith('.')) {
eventSource.close();
console.log('connection is closed');
}
};
}
</script>
</body>
</html>
아래처럼 서버에서 데이터를 전송할때 마다 데이터가 화면에 생성됨을 확인할 수 있습니다.
[참조]
'개발 > Spring' 카테고리의 다른 글
[Spring] 의존성 주입의 정의 및 의존성 주입 3가지 방식 (생성자 주입, 수정자 주입, 필드 주입) (0) | 2022.04.30 |
---|---|
[Spring Boot] Thymeleaf 사용하기 (0) | 2022.03.06 |
[Spring] @RequestParam과 @PathVariable의 차이는? (0) | 2022.02.20 |
[Spring] @Controller와 @RestController 차이 (0) | 2022.02.08 |
[Spring Boot] Spring Security 로그인기능 구현하기 (0) | 2022.01.29 |