SSE , Server-Sent Event 로 뭔가를 해볼수 있을듯...

2018. 2. 22. 18:00Java/Spring Framework

그렇다 SSE , Server-Sent Event 을 문득 보게 되었다.

SSE 는 아마도 Server-Sent Event 의 약자 인듯 싶다. 



출처 : https://www.packtpub.com/mapt/book/web_development/9781782166320/6/ch06lvl1sec43/listening-for-server-sent-events



아래는 developer.mozilla.org에 있는 내용을 번역한 것이고, 
직접 읽어보시는 편이 좋으실듯 싶습니다. 저는 공부하는 차원에서 정리한거라.. 이제부터 시작합니다. 고고!!!

Server-sent events


전통적으로, 웹 페이지는 새로운 데이터를 받기 위해 서버에 request을 보내야 합니다. 
그건, 페이지가 서버로부터 데이터를 요청하는 것입니다.
server-sent event을 가지고, 서버가 언제든지 웹 페이지에 메세지를 푸쉬함으로 
새로운 데이터를 웹 페이지에 보내는 것이 가능합니다. 
메세지들이 유입되는 것은 웹페이지 안에서 Event 와  데이터로 다뤄질수 있습니다. 

EVENT

계속 깊게 깊게 들어가지는 군요... 그럼 Event에 대해서도 정리 해봅시다.

Event 인터페이스는 DOM 에서  발생하는 모든 event을 나타냅니다. 
다른 것들이 API에 의해 생성(예를 들면 애니메이션이 끝났을때 발생하는 이벤트들, 비디오가 정지하거나 등등)되는데 반해, 어떤건 사용자가 생성(마우스 나 키보드 이벤트들)합니다.
많은 종류의 이벤트들이 있으며, 그 중 일부는 메인 Event 인터페이스를 기반하는 다른 인터페이스를 사용합니다.
Event 자체에 모든 이벤트들에 공통적인 속성과 메소드들을 포함합니다.


Concepts and usage

SSE를 어떻게 사용하는지 배워 봅시다. 

SSE를 사용하는 웹 어플리케이션을 개발하는 것은 매우 쉽습니다.
웹 어플리케이션에 이벤트들을 스트림하기 위해서 서버상에 약간의 코드가 필요로 합니다.
하지만 코드들의 웹 어플리케이션 측면은 다른 유형의 이벤트를 처리하는 것과 거의 동일하게 작동합니다.
(but the web application side of things works almost identically to handling any other type of event)

Receiving events from the server

SSE API는 서버로 부터 이벤트를 받는 것을 시작하기 위해 서버에 연결을 오픈하는 것,
새로운 EventSource 객체를 생성하는 것,
이벤트를 생성하는 스크립트의 URI을 명시하는 것들이 EventSource 인터페이스에 포함되어 있습니다.

var evtSource = new EventSource("ssedemo.php");

이벤트 생성 스크립트가 다른 도메인상에 위치해 있다면,
새로운 eventSource 객체는 URI 와 추가적 딕션어니 둘다 명시하여 생성되어야 합니다.
예를 들어서, 클라이언트 스크립트는 example.com 상에 있다고 가정합니다.

var evtSource  = new EventSource("//api.example.com/ssedemo.php",{ withCredentials: true })

NOTE : EventSource를 사용하는 것은 모든 브라우저에서 지원하지 않습니다. 
브라우저 호환성 체크를 하시길 바랍니다. (https://developer.mozilla.org/en-US/docs/Web/API/EventSource#Browser_compatibility)

음... Edge, IE 는 지원을 하지 않는 군요.... 그래서 사용을 안했던것은 아닐지.. Chrome은 버전 6을 가능합니다.
그렇다고 하더라도 Chrome에서 해봐야죠. 

event source을 인스턴스화 하면, message 이벤트에 대한 리스닝을 하는 것으로 서버로 부터 
메세지를 리스닝하는 것을 시작할수 있습니다. 

evtSource.onmessage = function (e) {
  var newElement = document.createElement('li')
  newElement.innerHTML = 'message: ' + e.data
  eventList.appendChild(newElement)
}

이 코드는 유입되는 메시지들(메세지상에 event 필드가 없는 서버로부터 알게 됩니다.)을 듣고, 
메시지 텍스트를 문서의 HTML안에 목록에 추가합니다.

여러분은 또한 이벤트를 리스닝할수 있습니다. addEventListener()을 통해서 말이죠

evtSource.addEventListener('ping', function (e) {
  var newElement = document.createElement('li')
  var obj = JSON.parse(e.data)
  newElement.innerHTML = 'ping at ' + obj.time
  eventList.appendChild(newElement)
}, false)

이 코드는 event 필드에 "ping"으로 하고, 메시지를 서버가 보낼때 마다 자동으로 호출되는 것을 제외하고 비슷합니다. 
그 다음에 data 필드안에 JSON을 파싱하고 그 정보를 출력합니다.

Sending event from the server

이벤트를  보내는 server-side 스크립트는 MIME 타입 text/event-stream을 사용하여 응답할 필요가 있습니다.
각각의 notification은 newlines의 쌍에 의해 종료된 텍스트 블럭으로 보내집니다.
이벤트 스트림의 포멧을 좀 더 자세히 보려면 , Event stream format을 참고하세요

예제를 위한 PHP 코드가 아래 있답니다.

date_default_timezone_set("America/New_York");
header('Cache-Control: no-cache');
header("Content-Type: text/event-stream\n\n");
$counter = rand(1, 10);
while (1) {
  // Every second, send a "ping" event.
  
  echo "event: ping\n";
  $curDate = date(DATE_ISO8601);
  echo 'data: {"time": "' . $curDate . '"}';
  echo "\n\n";
  
  // Send a simple message at random intervals.
  
  $counter--;
  
  if (!$counter) {
    echo 'data: This is a message at time ' . $curDate . "\n\n";
    $counter = rand(1, 10);
  }
  
  ob_end_flush();
  flush();
  sleep(1);
}

할까? 말까? 아니야 자바로 있을수 있을꺼야.. 봅시다. 

좋네...

Stock Market Example 

주식시장 트랜잭션들을 시뮬레이션 하고 클라이언트에게 이런 것들을 통지하는 것 예제로 만듬.
각 단계들은 아래에 대한 내역을 포함하고 있습니다.
- 그들의 가격들을 포함하는 주식들을 초기화
- 몇초 후에 주식 가격이 업데이트 되고, 주식 처리들이 이뤄집니다.
- 클라이언트는 매초 마다, 주식 처리들의 상세내역을 받을 겁니다.

Stock 클래스
--
 code
--

어플리케이션 시작단계에서 몇개의 랜덤 주식을 초기화 합니다.

StockTransaction 클래스
 --
 code
--

Reactive SSE

StockTransactions의 Flux를 리턴하는 서비스를 생성합니다.
--
 code
--
매초마다 긴 값을 생성하는 Flux을 생성합니다.
그다음, 모든 주식의 가격을 업데이트 합니다.
우리는 StockTransaction을 생성하는 다른 Flux을 생성합니다.
Flux.zip 메소드는 이 Flux들 둘다 갖고 그것을 합칩니다. 
(두개의 Flux들은 n번째 항목을 출력할때) 그것들은 엄격한 순서로에서 결합됩니다. 

Web API
지속적으로 패치될 가장최근의 처리가 발생한 것의 상세 내역인 ,
종단점, GET 방식, /stock/trascation 이라는 것을 만들겁니다.
--
 code
--

MediaType.APPLICATION_STREAM_JSON_VALUE 는 서버가  SSEs을 보낼 것이라는 것을 나타냅니다.
API는 header에 Content-type: appliaction/stream+json 을 가지고 응답을 리턴합니다.

cURL 명령으로 테스트 해보자.


출력은 아래와 같을 것이다. 

Conclusion
간단한 예제를 가지고 설명하려고 애썼다.
어떻게 Spring Boot을 사용하여서 SSE을 보내는지에 대해서 말이다.
좀 더 자세한 정보는 Project Reactor 를 봐라. https://projectreactor.io/
소스는 GitHub에 있다. 



위에 까지 진행하고 난 뒤에 웹브라우저에서 진행하면 전혀 서비스가 되질 않는다.
첫번째 서버에 대한 속성은

package com.example.webfluxexample;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

@RestController
@RequestMapping("/stock/transaction")
public class StockTransactionController {

@Autowired
StockTransactionService stockTransactionService;

@GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE )
public Flux<StockTransaction> stockTransactionEvents(){
return stockTransactionService.getStockTransactions();
}
}

클라이언트 페이지를 아래와 같이 변경하면 뿌려질것이다.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>demo-spring-sse</title>
</head>
<body>
<div>
<ul id="messages"></ul>
</div>
<script type="application/javascript">
if (!!window.EventSource) {
var eventSource = new EventSource("/stock/transaction");
var elements = document.getElementById("messages");
function add(message) {
var element = document.createElement("li");
element.innerHTML = message;
elements.appendChild(element);
}
eventSource.onmessage = function (e) {
var message = JSON.parse(e.data);
console.log(e.data);
add(e.data);
};
eventSource.onopen = function (e) {
add('connection was opened');
};
eventSource.onerror = function (e) {
if (e.readyState == EventSource.CONNECTING) {
add('event: CONNECTING');
} else if (e.readyState == EventSource.OPEN) {
add('event: OPEN');
} else if (e.readyState == EventSource.CLOSING) {
add('event: CLOSING');
} else if (e.readyState == EventSource.CLOSED) {
add('event: CLOSED');
}
};
} else {
alert('The browser does not support Server-Sent Events');
}
</script>
</body>
</html>


위 코드는 ping 이벤트 타입과 함께 매초 이벤트를 생성합니다.
각각의 이벤트의 data는 이벤트가 생성될때 시간에 부합하는 ISO 8601 timestamp을 포함하는 JSON 객체 입니다.
랜덤 인터벌로, 심플 메시지(어떤 이벤트 타입이 없이)가 보내집니다. 

여러분은  모든 예제를 아래 아티클에서 확인할수 있습니다. 


Error handling

문제가 발생했을때 (네트워크 타임아웃 이나 접근 조정에 부합한 문제들 같은), 에러 이벤트가 생성됩니다.
여러분은 계획에따라 EventSource 객체에 , onerror 콜백을 구현함으로써 행동을 취할수 있습니다.

Interfaces

EventsSource 
서버에 연결하는 것, events/data,errors 을 주고받는것, 연결을 끊는것 등등을 다루는 모든 기능들을 정의합니다.


EventSource

EventSource 인터페이스는 Server-sent events에 웹 컨텐츠의 인터페이스 입니다.
EventSource 인스턴스는 이벤트를 text/event-stream 포멧으로 보내는 HTTP 서버에 영속적인 연결을 오픈합니다.
연결은  오픈을 지속합니다. EventSource.close() 호출하는 것으로 닫힐때까지

한번 연결이 오픈되면, 서버로 부터 유입되는 메시지들을 message 이벤트의 형태에서 여러분의 코드에 전달됩니다.  

WebSockets과는 다르지만, server-sent events은 단방향성입니다.
데이터 메시지들은 서버로부터 클라이언트에 한방향으로 전달됩니다. (예를 들면, 사용자의 웹브라우저)
클라이언트에서 서버로 메세지 형태로 데이터를 보낼 필요가 전혀 없을 때, 휼륭한 선택입니다.
예를 들어, EventSource는 소셜 미디어 상태을 업데이트, 뉴스 피드, 또는 클라이언트 측 스토리지 매카니즘, indexDB 나 web storage 같은 것 안에 데이터를 전달하는 것 같은 것들을 다루는데 유용한 접근입니다.

 
여기에 추가적인 확장은 Client-side storage 부분에 대한 숙지도 필요할 듯 싶다.
간략하게 보면, Old fashioned : cookies < New school : Web Storage and IndexedDB < Future : Cache API 







여기까지 입니다.