[HTML5] Server-Sent Events

Posted in 모바일/HTML5 // Posted at 2010. 8. 31. 16:54
728x90
웹에서 서버 푸시(Sever Push) 구현
그 동안 웹에서 서버 푸시(Server Push) 구현을 위해 얼마나 많은(?) 노력이 있어 왔던가?

서버 푸시란 서버에서 클라이언트로 데이터를 (능동적으로) 전송해 주는 통신 방식 즉 통신의 방향과 관계되는 용어이다. 즉 서버 푸시란 데이터 전송 방향이 '서버 -> 클라이언트' 가 되는 것이다

이와 반대되는 개념을 클라이언트 폴링(Polling) 방식이라 한다
클라이언트 폴링은 클라이언트에서 서버로 질의를 하고 서버는 이에 응답만 하는 구조이다
폴링의 데이터 전송 방향은 '클라이언트 -> 서버' 가 된다

TCPIP 한 계층위에 존재하는 HTTP 프로토콜에 기반한 웹 통신은 비연결 지향적 성격을 가지고 있다.
클라이언트는 서버로 요청(Request)을 하고 서버는 이에 응답(Response)을 하고 이후 연결은 닫힌다

물론 HTTP 1.1 부터는 KeepAlive 라는 개념이 추가되어 HTTP 연결을 일정 시간 이상 유지하도록 하는 매커니즘이 추가되었다. 그러나 이것은 매번 새로운 연결(Connection)에 따른 오버헤드를 줄이기 위한 목적이지 소켓 통신과 같은 완전한 연결 유지, 상태유지, 양방향 통신을 위한 것은 아니다
HTTP 연결유지와 관련해서는 다음의 글을 참고하자
[HTTP 프로토콜] Stateless
[HTTP 프로토콜] 연결 유지가 능사인가?

응용 프로그램에서 서버푸시 방식이 필요한 경우가 매우 많다.
가장 이해하기 쉬운 예가 채팅 서비스 이다.
채팅은 서버와 클라이언트가 실시간으로 그리고 지속적으로 데이터를 주고 받아야 한다. 자신의 메시지를 즉시 서버로 전송해야 하며 서버는 이 메시지를 다른 사람에게 바로 전달해야 한다. 즉 서버가 채팅 메시지들를 중계하여 각각의 사용자(클라이언트)들에게 실시간으로 데이터를 뿌려 주는 구조 즉 서버푸시 구조의 대표적인 사례라 하겠다

웹에서도 이러한 서버 푸시의 요구사항은 늘 있어 왔다.
그러나 통신 구조의 한계로 인해 직접적인 서버푸시 구현은 힘들며 대안으로써의 여러 기법이 이용되어 왔다

웹이 서버푸시를 위해 선택한 여러 대안들
순수 웹 기술로는 서버푸시를 구현한다는 것은 현재까지도 불가능 한 것이 사실이다

이전 세이클럽과 같은 웹 사이트에서 제공하는 채팅 서비스는 순수 웹 기술로 구현된 것이 아니라
자바 애플릿과 같은 플로그인 기술로 구현된 것이다. 플러그 인 기술은 추가 실행 엔진을
설치해야 하며 구동되는 환경은 웹이 아니라 별도의 런타임이기에 소켓통신이 가능하다
결국 세이클럽의 채팅은 HTTP가 아니라 소켓 통신을 이용한 서비스 인 것이다


이처럼 지금까지 가장 많이 사용된 대안은 웹 플러그 인 기술을 사용하는 것이었다

플러그 인을 사용하지 않고 서버푸시를 흉내내기 위해서는 앞서 설명한 폴링(Polling) 기법이 이용되었다.숨긴 frame, iframe 을 이용하거나 Ajax 등을 이용하여 주기적으로 매번 서버로 질의를 하는 형태이다

그러나 이와같은 방식은 가장 큰 문제점은 구현이 깔끔하지 못하다는 것과 대역폭의 낭비가 심하다는 것이다. 서버에서 전달할 데이터가 있건 없건 클라이언트는 계속 질의해 보는 수 밖에 없다

서버 푸시는 전달할 데이터가 있을 때에만 클라이언트로 전송하면 되지만,폴링은 서버의 상태를 알지 못하니 계속 호출해 볼 수 밖에 없는 것이다.
그러나 순수 웹 기술로는 서버푸시를 구현할 수 없기에 폴링 방식을 이용해서 푸시 효과를 흉내 낼 수 밖에 없는 것이다

마지막으로 순수 웹 기술로 서버 푸시 효과를 구현하는 비교적 최적의 기술인 Comet 이 있다
Comet는 요청에 대한 연결을 응답시 까지 유지시켜 웹에서의 서버푸시를 효과적으로 구현하기 위한 기술을 일컫는데 사실 나도 Comet 를 실제로 다루어 보지는 않아서 딱히 구체적으로 언급할 것은 없다
다만 Comet이 서버 푸시를 구현하는 통신 구조와 Server-Sent Events가 이것을 표준화 한 하나의 형태라는 것은 알고 넘어가자. 참고로
위키사전과 HTML& API 입문 서적에 정의한 Comet를 옮겨 본다
위키사전>
In web development, Comet is a neologism to describe a web application model in which a long-held HTTP request allows a web server to push data to a browser, without the browser explicitly requesting it

HTML&API 입문>
Comet은 HTTP에서 의사푸시를 구현하는 기술입니다. 클라이언트로부터의 요청에 대해 서버는 응답이 완료되었음을 알려주지 않고 연결을 유지하므로 계속 클라이언트에게 응답을 보낼 수 있습니다
그러나 서버 자원과 대역폭을 효율적으로 사용하려면 서버는 일정 간격마다 응답을 완료하고, 클라이언트는 재접속할 필요가 있습니다.(이 때문에 Comet를 Long-polling이라 부리기도 합니다)


Long-polling 과 기존 polling 의 통신 구조 차이를 다음 그림으로 이해하자



웹에서 서버 푸시를 위한 HTML5의 표준안: Sever-Sent Events
HTML5 의 Server-Sent Events(SSE) 스펙은 웹 환경에서 서버푸시를 구현하기 위해 제안된 
표준 기술이다. 앞서 설명한 의사서버푸시기술인 Comet 을 표준화한 기술이라고도 한다

Java.net 에서 소개하는 Server-Sent Events 설명을 보면 더욱 명확하다

HTML5 also applies the Comet communication pattern by defining Server-Sent Events (SSE), in effect standardizing Comet for all standards-compliant web browsers - java.net

Server-Sent Events 는 서버 푸시를 구현을 위한 심플한 자료구조와 인터페이스, 통신 매커니즘을 정의하고 있으며 일반적인 DOM 이벤트 형태로 수신 데이터를 처리 할 수 있는 등 서버푸시 구현이 매우 간단해 진다

Server-Sent Events 가 완전한 서버푸시?
SSE를 더 알아보기 전에 한가지 집고 넘어가고자 한다
SSE가 완전한 서버푸시인가? 즉 SSE가 소켓통신과 같이 서버->클라이언트로의 능동적인 통신 방식인가? 하는 문제이다. 답은 그렇지 않다

SSE 역시 (이전 다른 대안들처럼) 서버푸시 효과를 내기 위한 기술이다
SSE는 (폴링과 유사하게) 클라이언트에서 서버로 반복적으로 질의를 하는 방식 즉 '클라이언트 -> 서버' 로의 통신 방향이며 소켓통신과 같은 완전히 능동적인 서버푸시 기술은 아니라는 점이다

그렇다고 SSE를 무시할 수는 없다
일단 SSE는 웹 서버푸시를 구현을 위해 귀결되는 표준 기술로 자리매김 할 것이며 간단하고 명료한
프로그래밍 모델과 API 그리고 효과적인 통신을 위한 데이터 포맷 지원 등 여러 장점을 제공 해 준다


브라우저 지원 현황

HTML5 Server-Sent Events 를 지원하는 브라우저 현황을 살펴 보자


그림1. 브라우저별 Server-Sent Events 지원 현황 (출처: http://caniuse.com/)

위 표를 보면 사파리, 크롬 그리고 부분적으로 오페라가 지원한다고 나와있다
그러나 실제 테스트를 해 보면 사파리 에서만 정상 동작한다(아이폰 사파리 포함)

오페라 브라우저의 경우 위 표 하단의 설명처럼 과거 스펙을 지원한다고 나와있다
현재의 SSE 스펙은 정상 동작하지 않는 것을 확인하였다. 오페라에서 과거 스펙의 SSE를 구현하는 예는 다음의 글에서 확인할 수 있다 -> Event Streaming to Web Browsers

Server-Sent Events 다루기

이제부터 SSE를 실제로 다루기 위한 기본적인 개념들을 알아 보도록 하자

EventSource
EevnetSource 객체는 SSE 구현을 위한 핵심 객체이다
이 객체를 통해 자동으로 서버 요청이 주기적으로 일어나며 데이터 수신 이벤트를 정의할 수 있다
즉 EventSource는 서버 요청을 위한 출발점이며 또한 데이터 수신을 위해 정의되는 일종의 EndPoint 라고도 할 수 있다. EventSource 는 요청 대상이 되는 서버 URL을 매개변수로 취하며 객체가 생성되는 순간 이벤트 스트림이 열리게 된다


다음 코드처럼 EventSource 객체를 생성하는 것만으로 주기적인 서버 호출이 일어난다 
var eventSource = new EventSource("server.asp");


데이터 수신 이벤트
서버로 부터 전달받는 데이터를 처리하기 위해 수신 이벤트를 정의한다
이 이벤트 역시 EventSource 객체를 통해 정의하면 된다

다음에 알아볼 서버 데이터 포맷에 특별한 이벤트 이름이 지정되지 않은 경우 기본 값인 message 이벤트로 처리하며 된다. 다음 코드는 EventSource 객체에 message 이벤트를 정의한 것이다
이벤트로 전달되는 data 속성으로 수신 데이터를 액세스 할 수 있다
eventSource.addEventListener("message",
    function(e){          
      alert(e.data);
    }
,false);


서버 데이터 포맷과 규칙
서버측에서 클라이언트로 전달하는 데이터는 일반적인 텍스트 형태이지만 그 포맷이 정해져 있으며 몇 가지 규칙을 따라야 한다

- MIME 타입: 서버 데이터는 text/event-stream 라는 MIME 타입으로 제공되어야 한다
- 문자 인코딩: 서버 데이터의 문자 인코딩은 UTF-8 형식이어야 한다

- 빈줄은 이벤트를 구분하는 역할을 한다
- 주석은 :(콜론)으로 시작한다

- 데이터는 '필드 명: 필드 값' 형식이어야 한다(콜론과 필드 값 사이에 공백 하나를 포함할 수 있다)
  필드 설명> 필드 명에 해당하는 필드 설명을 보자
   data : 서버가 전달할 실제 데이터를 정의한다
   retry: 반복 주기를 설정한다(단위: millisecond)
   event: 이벤트 이름을 지정한다(지정하지 않으면 기본값인 message 가 된다)
   id: 이벤트 id를 지정한다. 클라이언트에서 마지막 이벤트 번호를 저장하기 위해 사용된다

다음은 서버 데이터를 규칙에 맞게 구성한 하나의 예이다. 2초의 반복주기를 가지도록 하며 두 개의 이벤트를 정의하고 있다. 그리고 하나의 이벤트에는 각각 두개의 데이터를 정의하고 있다
retry: 2000

event: firstEvent
data: 이벤트1,데이터1
data: 이벤트1,데이터2

event: secondEvent
data: 이벤트2,데이터1
data: 이벤트2,데이터2
 
실제 환경에서는 데이터가 동적으로 변화하기 마련인데 asp.net 이나 asp,php 등으로 데이터를 적절히 구성하면 된다. 다만 서버데이터 포맷과 규칙을 지키기만 하면 된다

반복 주기
요청을 반복하는 주기는 서버 데이터 포맷에 정의된 retry 시간을 기준으로 한다
retry 2000 이면 2초마다 반복 호출하게 된다
단 서버 데이터 포맷에 retry 정의가 없다면 브라우저 기본 값을 따른다
데스크탑 사파리의 경우 기본 값이 3초이다

개인적으로 가장 특이하고 효과적으로 보이는 것이 바로 이 retry 부분이다
주기적인 호출을 위한 반복 시간을 클라이언트가 아닌 서버에서 지정한다는 것이 매우 고무적이다
이는 서버입장에서 반복 시간을 컨트롤 할 수 있다는 의미이며 기존 폴링 방식에 비해 통신 구조를 보다 효과적으로 설계할 수 있다는 장점을 가져다 준다


Server-Sent Events 데모 만들어 보기
이제 실제 Server-Sent Events 를 이용한 서버푸시 데모를 제작해 보자

서버 데이터 준비
먼저 클라이언트가 수신하게 될 서버 데이터를 규칙에 맞도록 정의한다
서버 데이터를 반환하는 것은 asp,php,asp.net, cgi 등 어떤 것이든 규칙에만 맞으면 된다
이 데모에서는 ASP 기술을 이용하여 server.asp 를 다음과 같이 정의한다
2초 반복 주기와 두개의 이벤트, 한 이벤트에 각각 두개의 데이터를 정의했다
<% Response.ContentType = "text/event-stream" %>
retry: 2000

data: 이벤트1,데이터1
data: 이벤트1,데이터2

data: 이벤트2,데이터1
data: 이벤트2,데이터2

클라이언트 구현
서버 데이터를 수신할 클라이언트를 정의한다
EventSource 객체를 통해 송,수신을 처리하며 기본이름은 message 이벤트로 수신데이터를 처리한다
<!DOCTYPE html>
<html>
<head></head>
<body>
  <button onclick="start();">Start</button>
  <div id="message"></div>   
</body>
</html>
<script type="text/javascript">  
function start(){
  var eventSource = new EventSource("http://mkexdev.net/test/server.asp");      

  eventSource.addEventListener("message",
    function(e){
      var data = e.data.split("\n");
      var one = data[0];
      var two = data[1];
     
      document.getElementById("message").innerHTML += one + two + "<br>";
    }
  ,false);
}
</script>


실행화면
아래 그림은 데스크탑 사파리에서 데모를 실행한 화면이다
한번 호출에 총 두개의 이벤트 데이터가 표시되며 2초 간격으로 반복 호출 된다
아래 그림은 6초 동안 총 3번 server.asp를 호출한 것이며 총 6개의 데이터가 출력된 모습이다
(사파리)


참고> 이벤트 명을 명시하여 클라이언트가 지정 이벤트만을 수신하도록 할 수 있다
앞의 데모에서는 이벤트명을 지정하지 않아 기본값인 message 이벤트를 처리하여 전체 데이터를 수신하도록 하였지만 이벤트 명을 지정하게 되면 클라이언트에서는 특정 이벤트 데이터만 수신할 수 있다

- server.asp
<% Response.ContentType = "text/event-stream" %>
retry: 2000

event
: firstEvent

data: 이벤트1,데이터1
data: 이벤트1,데이터2

event: secondEvent
data: 이벤트2,데이터1
data: 이벤트2,데이터2


- 클라이언트 코드(명시적으로 특정 이벤트명일 지정하여 이벤트 수신합)
function start(){
  var eventSource = new EventSource("http://mkexdev.net/test/server.asp");     
  eventSource.addEventListener("secondEvent",
    function(e){
      var data = e.data.split("\n");
      var one = data[0];
      var two = data[1];
     
      document.getElementById("message").innerHTML += one + two + "<br>";
    }
  ,false);
}

- 실행화면
실행을 해 보면 secondEvent로 정의된 이벤트 데이터만 표시되는 것을 확인할 수 있다


다만 이렇게 특정 이벤트만을 명시적으로 지정하여 수신하더라도 서버데이터는 일부가 아닌 전체가 다운로드 된다. 즉 서버데이터는 모두 가져온 후 클라이언트에서 특정 이벤트만 필터하는 것이다
아래 그림은 이 시나리오에서 Fiddler 로 응답 데이터를 캡쳐한 화면이다




참고 자료>
http://dev.w3.org/html5/eventsource/
http://today.java.net/article/2010/03/31/html5-server-push-technologies-part-1