관리 메뉴

HAMA 블로그

[웹개발] SSE ( Server-Sent Events) 란 무엇인가 본문

소프트웨어 사색

[웹개발] SSE ( Server-Sent Events) 란 무엇인가

[하마] 이승현 (wowlsh93@gmail.com) 2017. 3. 19. 20:01


Published: November 30th, 2010

https://www.html5rocks.com/en/tutorials/eventsource/basics/번역 


저는 웹 개발 경험이 부족하여 최근에 알게된 기술들이 많습니다. 이것도 꽤 오래된 내용인 듯 한데요. SSE (Server-Sent Events : HTML5 표준안 권고사항) 에 대해서 소개하는 글을 번역해 보았습니다. 간략하게 요약하면 이 SSE 는 어느정도 웹소켓의 역할을 하면서 더 가볍습니다. 주로 서버에서 받는 (푸쉬) 위주의 작업에 유용하게 사용 될 수 있습니다. 웹소켓과 같은 양방향은 아니기 때문에 보낼때는 Ajax 를 활용합니다. 


"Play2 와 Iterratee 로 채팅구현 10분완성"에서Concurrent.broadcast와 iteratee,EventSource를 이용하여 업데이트된 내용을 클라이언트들에 전송하고, Ajax 를 이용하여 메세지를 받는 채팅구현을 소개하고 있습니다. 추후에 저 내용에 Angular2 를 이용해 클라이언트측을 추가한 버전을 이용하여 Play2와 Angular2 를 이용한 Reactive 채팅 구현이라는 글을 게시할 예정입니다. 관심있으시면 참고하세요. 

* 참고로 Play2 에서 의 사용되는 모습은 대략 아래와 같습니다. ( Play2 와 Server-Send Events 이용하여 데이터 헨들링하기)



val
source = scala.io.Source.fromFile(Play.getExistingFile("conf/coosbyid.txt").get)
val jsonStream = lineEnumerator(source) &> lineParser &> validCoordinate &> asJson val eventDataStream = jsonStream &> EventSource() Ok.chunked(eventDataStream).as("text/event-stream")

자 시작합니다~!



소개 


 "Server-Sent Events (SSE) 란 무엇입니까?" 라고 궁금해 하는 사람들에 대해 저는 별로 놀라지 않을 거 같네요. 왜냐면 많은 사람들이 그것에 대해 들어 본 적이 별로 없었을 것이란 걸 잘 알기 때문이죠. 지난 몇 년 동안 스펙은 상당한 변화를 겪었으며 API는 WebSocket API와 같이 더 새롭고 더 섹시한 통신 프로토콜에 의해 뒷전으로 밀려갔습니다. SSE의 개념은 아마 익숙 할 수도 있을 것입니다. 웹 응용프로그램은 서버에 의해 생성된 업데이트 스트림를 "구독" 하고 새 이벤트가 발생할 때마다 클라이언트에 알림을 보냅니다. 그렇지만 이런 Server-Sent Events를 제대로 이해하려면 Ajax 전임자들의 한계를 어느 정도는 이해해야 할 겁니다. 


폴링은 대다수의 AJAX 응용 프로그램에서 사용되는 전통적인 기술입니다. 기본 개념은 응용 프로그램이 서버에서 데이터를 반복적으로 요청하는 것입니다. HTTP 프로토콜에 익숙하다면 데이터를 가져 오는 것이 요청 / 응답 형식을 중심으로 진행된다는 것을 알 텐데요. 클라이언트는 요청을 하고 서버가 데이터로 응답 할 때까지 기다립니다. 갱신되지 않은 의미없는 응답이 리턴될 수 도 있습니다. 즉 단점으로는 더 많은 HTTP 오버 헤드를 만들 것 입니다. 하지만 일정하게 갱신이 되는 서버 데이터의 경우 매우 유용하며 기존 단순한 모델을 유지 할 수 있습니다.

긴 폴링 (Hanging GET / COMET)은 폴링에 약간의 변형입니다. 긴 폴링에서 서버에 사용 가능한 데이터가 없으면 서버는 새 데이터를 사용할 수 있을 때까지 요청을 보류합니다. 따라서이 기술은 종종 "Hang GET"이라고합니다. 정보를 사용할 수있게되면 서버가 응답하고 연결을 닫은 후 프로세스가 반복됩니다. 결과적으로 서버는 새로운 데이터가 사용 가능할 때마다 지속적으로 응답합니다. 단점은 이러한 절차의 구현은 일반적으로 '무한' iframe에 스크립트 태그를 추가하는 것과 같은 술수가 필요하다는 것입니다. 

반면에 Server-Sent 이벤트는 처음부터 효율적으로 설계되었습니다. SSE를 사용하여 통신 할 때 서버는 초기 요청을 하지 않고도 필요할 때마다 데이터를 앱으로 푸시 할 수 있습니다. 즉, 서버에서 클라이언트로 업데이트를 스트리밍 할 수 있습니다. SSE는 서버와 클라이언트 사이에 단일 단방향 채널을 엽니다.

Server-Sent Events와 long-polling의 가장 큰 차이점은 SSE는 브라우저에서 직접 처리되므로 사용자는 메시지를 청취(구독) 해야 한다는 것입니다. (즉 그런 코딩이 추가됨)

Server-Sent Events vs. WebSockets

웹 소켓을 통해 Server-Sent 이벤트를 선택하는 이유는 무엇입니까? 좋은 질문입니다.  ^^

SSE가 그림자에 머물러있는 이유 중 하나는 WebSocket과 같은 API가 양방향 전이중 통신을 수행하기 위한 더 풍부한 프로토콜을 제공하기 때문입니다. 양방향 채널을 보유하면 게임, 메시징 앱 및 양방향으로 거의 실시간으로 업데이트해야하는 경우에 더 매력적입니다. 하지만 일부 시나리오에서는 클라이언트에서 데이터를 전송하지 않아도 되며 서버 작업에서만 업데이트 해주면 됩니다. 몇 가지 예는 친구의 상태 업데이트, 주식 시세 표시기, 뉴스피드 또는 기타 자동화 된 데이터 푸시 메커니즘 (예 : 클라이언트 측 웹 SQL 데이터베이스 또는 IndexedDB 객체 저장소 업데이트)입니다. 이때 서버에게 데이터를 보내야 하는 경우 XMLHttpRequest는 항상 친구입니다. (역주: 채팅 기능을 예로 들면  SSE 로 브로드캐스팅된 내용을 듣고, Ajax 를 통해 서버로 말함) 

SSE는 전통적인 HTTP를 통해 전송됩니다. 즉, 작동하려면 특별한 프로토콜이나 서버 구현이 필요하지 않습니다. 반면 WebSockets는 프로토콜을 처리하기 위해 전이중 연결과 새로운 웹 소켓 서버가 필요합니다. 또한 서버 보낸 이벤트에는 자동 재 연결, 이벤트 ID 및 임의 이벤트를 보내는 기능과 같이 WebSockets은 디자인 측면에서 부족한 다양한 기능이 있습니다.

JavaScript API

이벤트 스트림을 구독하려면 EventSource 객체를 만들고 스트림의 URL을 전달합니다.

if (!!window.EventSource) {
  var source = new EventSource('stream.php');
} else {
  // Result to xhr polling :(
}

참고 : EventSource 생성자에 전달 된 URL이 절대 URL 인 경우 해당 출처 (scheme, domain, port)가 호출 페이지의 출처와 일치해야합니다.그런 다음 메시지 이벤트에 대한 핸들러를 설정하십시오. 선택적으로 open 과 error 를 수신 대기 할 수 있습니다.

source.addEventListener('message', function(e) {
  console.log(e.data);
}, false);

source.addEventListener('open', function(e) {
  // Connection was opened.
}, false);

source.addEventListener('error', function(e) {
  if (e.readyState == EventSource.CLOSED) {
    // Connection was closed.
  }
}, false);

서버에서 업데이트된 내용이 푸시되면 onmessage 함수가 실행되고 e.data 속성에서 새 데이터를 사용할 수 있습니다. 마법 같은 부분은 연결이 닫힐 때마다 브라우저가 ~ 3 초 후 자동으로 소스에 다시 연결된다는 것이며 서버 구현은 이 재 연결 시간 초과를 제어 할 수도 있습니다.

그게 전부에요. 이제 클라이언트는 stream.php에서 이벤트를 처리 할 준비가 되었습니다.~

Event Stream Format

소스에서 이벤트 스트림을 보내는 것은 SSE 형식인 text/event-stream Content-Type을 사용하여 일반 텍스트 응답을 작성하면서 수행되며 기본 형식에서 응답에는 "data :" 행 다음에 메시지가 오고 스트림 뒤에는 두 개의 "\n"문자가 있어야 스트림을 끝낼 수 있습니다.

data: My message\n\n

Multiline Data

메시지가 길면 여러 개의 "data :"행을 사용하여 메시지를 분할 할 수 있습니다. "data :"로 시작하는 두 줄 이상의 연속 된 줄은 하나의 데이터 조각으로 간주되어 하나의 메시지 이벤트 만 발생합니다. 각 행은 단일 "\ n"으로 끝나야합니다 (마지막 행은 2로 끝나야 함). 메시지 처리기에 전달 된 결과는 개행 문자로 연결된 단일 문자열입니다. 예 :

data: first line\n
data: second line\n\n

e.data에 "first line \ second line"을 생성합니다. 그런 다음 e.data.split ( '\ n') .join ( '')을 사용하여 "\ n"문자를 다시 생성 할 수 있습니다.

Send JSON Data

여러 줄을 사용하면 구문을 깨지 않고 JSON을 쉽게 보낼 수 있습니다.

data: {\n
data: "msg": "hello world",\n
data: "id": 12345\n
data: }\n\n

해당 스트림을 처리 할 수있는 클라이언트 측 코드

source.addEventListener('message', function(e) {
  var data = JSON.parse(e.data);
  console.log(data.id, data.msg);
}, false);

Associating an ID with an Event

"id :"로 시작하는 줄을 포함시켜 스트림 이벤트와 함께 고유 한 ID를 보낼 수 있습니다.

id: 12345\n
data: GOOG\n
data: 556\n\n

ID를 설정하면 브라우저가 마지막 이벤트를 추적하여 서버 연결이 끊어지면 특수한 HTTP 헤더 (Last-Event-ID)가 새 요청으로 설정됩니다. 이렇게 하면 브라우저가 어떤 이벤트를 실행하기에 적합한 지 스스로 결정할 수 있게 됩니다. message 이벤트는 e.lastEventId 속성을 포함합니다.

Controlling the Reconnection-timeout

브라우저는 각 연결이 닫힌 후 대략 3 초 후에 원본에 다시 연결하려고 시도하며 "retry :"로 시작하는 줄과 재 연결을 시도하기 전에 대기 할 시간 (밀리 초)을 포함하여 시간 제한을 변경할 수 있습니다.

다음 예제에서는 10 초 후에 다시 연결을 시도합니다.

retry: 10000\n
data: hello world\n\n

Specifying an event name

단일 이벤트 소스는 이벤트 이름을 포함시켜 여러 유형의 이벤트를 생성 할 수 있습니다. "event :"로 시작하는 행 다음에 이벤트의 고유 한 이름이 오는 경우 이벤트는 해당 이름과 연관됩니다. 클라이언트에서 이벤트 리스너를 설정하여 해당 특정 이벤트를 청취 할 수 있습니다.

예를 들어 다음 서버 출력은 일반적인 'message'이벤트, 'userlogon'및 'update'이벤트의 세 가지 유형의 이벤트를 보냅니다.

data: {"msg": "First message"}\n\n
event: userlogon\n
data: {"username": "John123"}\n\n
event: update\n
data: {"username": "John123", "emotion": "happy"}\n\n

클라이언트의 이벤트 리스너 설정 :

source.addEventListener('message', function(e) {
  var data = JSON.parse(e.data);
  console.log(data.msg);
}, false);

source.addEventListener('userlogon', function(e) {
  var data = JSON.parse(e.data);
  console.log('User login:' + data.username);
}, false);

source.addEventListener('update', function(e) {
  var data = JSON.parse(e.data);
  console.log(data.username + ' is now ' + data.emotion);
}, false);

Server Examples

PHP의 간단한 서버 구현 :

<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache'); // recommended to prevent caching of event data.

/**
 * Constructs the SSE data format and flushes that data to the client.
 *
 * @param string $id Timestamp/id of this connection.
 * @param string $msg Line of text that should be transmitted.
 */
function sendMsg($id, $msg) {
  echo "id: $id" . PHP_EOL;
  echo "data: $msg" . PHP_EOL;
  echo PHP_EOL;
  ob_flush();
  flush();
}

$serverTime = time();

sendMsg($serverTime, 'server time: ' . date("h:i:s", time()));

Download the code

Node JS: 의 유사한 구현

var http = require('http');
var sys = require('sys');
var fs = require('fs');

http.createServer(function(req, res) {
  //debugHeaders(req);

  if (req.headers.accept && req.headers.accept == 'text/event-stream') {
    if (req.url == '/events') {
      sendSSE(req, res);
    } else {
      res.writeHead(404);
      res.end();
    }
  } else {
    res.writeHead(200, {'Content-Type': 'text/html'});
    res.write(fs.readFileSync(__dirname + '/sse-node.html'));
    res.end();
  }
}).listen(8000);

function sendSSE(req, res) {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive'
  });

  var id = (new Date()).toLocaleTimeString();

  // Sends a SSE every 5 seconds on a single connection.
  setInterval(function() {
    constructSSE(res, id, (new Date()).toLocaleTimeString());
  }, 5000);

  constructSSE(res, id, (new Date()).toLocaleTimeString());
}

function constructSSE(res, id, data) {
  res.write('id: ' + id + '\n');
  res.write("data: " + data + '\n\n');
}

function debugHeaders(req) {
  sys.puts('URL: ' + req.url);
  for (var key in req.headers) {
    sys.puts(key + ': ' + req.headers[key]);
  }
  sys.puts('\n\n');
}

Download the code

sse-node.html:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
</head>
<body>
  <script>
    var source = new EventSource('/events');
    source.onmessage = function(e) {
      document.body.innerHTML += e.data + '<br>';
    };
  </script>
</body>
</html>

Cancel an Event Stream

일반적으로 브라우저는 커넥션이 끊어졌을때 이벤트 소스에 자동으로 다시 연결되지만 클라이언트 또는 서버로 부터 동작을 취소 할 수 있습니다.

클라이언트에서 스트림을 취소하려면 다음을 호출하시구요.

source.close();

서버에서 스트림을 취소하려면 "text/event-stream" Content-Type 이 아닌 200 OK 이외의 HTTP 상태 (예 : 404 찾을 수 없음)를 반환하면 됩니다. 두 가지 방법 모두 브라우저가 연결을 다시 설정하지 못하게 합니다.





SSE의 장점 정리 -  spoqa 기술블로그 (https://spoqa.github.io/2014/01/20/sse.html) 참고 

1.전통적인 HTTP를 통해 통신하므로 다른 프로토콜이 필요가 없습니다.

2.재접속 처리 같은 대부분의 저수준 처리가 자동으로 됩니다.

3.표준 기술답게 IE를 제외한 브라우저 대부분을 지원합니다.

4. HTML과 JavaScript만으로 구현할 수 있으므로 현재 지원되지 않는 브라우저(IE 포함)도 polyfill을 이용해 크로스 브라우징이 가능합니다. (여기서 polyfill이란 브라우저가 지원하지 않는 API를 플러그인이나 JavaScript 등으로 흉내 내 구현한 것을 뜻합니다. polyfill에 대한 자세한 설명은 이 블로그 포스트를 참조하시기 바랍니다.)


SSE 단점 및 Websocket 과의 비교 


- 인터넷 익스플로러 미지원 ( ㅋㅋ )  등 



http://streamdata.io/blog/push-sse-vs-websockets/   참고하시길

Comments