웹에서 실시간 데이터 처리하기(WebSocket, Comet)



HTML5 WebSocket웹에서 양 방향 통신을 지원하며, 그에 따른 실시간 서비스를 구현하기에 적합한 기술입니다.


또한, HTTP 실시간 통신 방식(COMET)인 폴링(Polling), 롱폴링(Long Polling), 스트리밍(Streaming) 방식보다 월등한 성능 차이를 보여줍니다.



기존 HTTP 실시간 통신 방식(COMET):



Ajax API 소스 코드는 이전 포스트를 참조하십시오.




1. 폴링(Polling) 방식:


- 브라우저가 일정한 주기로 HTTP 요청을 보내는 방식입니다.


보통 실시간 데이터의 업데이트 주기는 예측하기 어려우므로, 그에 따른 불필요한 서버 및 네트웍 부하가 늘어납니다.


즉, 불필요한 서버 요청이 다수 생긴다는 말입니다.




- 클라이언트 코드:


  1. function ajax() {
  2.  
  3.     Ajax.request({
  4.         url: 'http://m.fpscamp.com/m.aspx',
  5.         type: 'text',
  6.         method: 'post',
  7.         headers: {
  8.             'content-type': 'application/x-www-form-urlencoded'
  9.         },
  10.         data: {
  11.         },
  12.         onprogress: function (e, total, loaded, per, computable) {
  13.         },
  14.         onerror: function () {
  15.             alert('onerror');
  16.         },
  17.         callback: function (data, status) {
  18.             document.body.innerHTML = data;
  19.         }
  20.     });
  21. };
  22.  
  23. var timer = window.setInterval(function () { ajax(); }, 1000);


위 설명과 같이 window.setInterval() 메서드를 사용하여 브라우저에서 일정한 주기로 HTTP 요청을 서버로 보냅니다.




- 서버 코드(ASP.NET):


  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Web;
  5. using System.Web.UI;
  6. using System.Web.UI.WebControls;
  7. using System.Threading;
  8. using System.Net;
  9. using System.Net.Sockets;
  10.  
  11.  
  12. namespace FPSCamp.Potal.mobile
  13. {
  14.     public partial class m : System.Web.UI.Page
  15.     {
  16.         protected void Page_Load(object sender, EventArgs e)
  17.         {
  18.             Response.Write(DateTime.Now);
                Response.End();
  19.         }
  20.     }
  21. }





2. 롱폴링(Long Polling) 방식:


- HTTP 요청 시 서버는 해당 요청을 일정 시간 동안 대기 시킵니다. 만약, 대기 시간 안에 데이터가 업데이트되었다면, 그 즉시 클라이언트에게 응답을 보내고 전달받은 데이터를 처리 후 서버로 재요청을 시작합니다.


- 데이터 업데이트가 빈번한 경우엔 폴링에 비해 성능상 이점이 크지 않습니다.




- 클라이언트 코드:


  1. function ajax() {
  2.    
  3.     Ajax.request({
  4.         url: 'http://m.fpscamp.com/m.aspx',
  5.         type: 'text',
  6.         method: 'post',
  7.         headers: {
  8.             'content-type': 'application/x-www-form-urlencoded'
  9.         },
  10.         data: {
  11.         },
  12.         onprogress: function (e, total, loaded, per, computable) {
  13.             alert(e); // 이벤트
  14.             alert(total); // 전체 데이터 크기
  15.             alert(loaded); // 전송된  데이터 크기
  16.             alert(per); // 전송된 데이터 크기(percent)
  17.             alert(computable); // 전체 데이터 크기를 알고 있는지 여부
  18.         },
  19.         onerror: function () {
  20.             alert('onerror');
  21.         },
  22.         callback: function (data, status) {
  23.             // 응답 데이터 가공
  24.             document.body.innerHTML = data;
  25.             // 재 요청
  26.             ajax();
  27.         }
  28.     });};


callback() 메서드에서 응답 데이터를 가공 후 재 요청한다.




- 서버 코드(ASP.NET):


  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Web;
  5. using System.Web.UI;
  6. using System.Web.UI.WebControls;
  7. using System.Threading;
  8. using System.Net;
  9. using System.Net.Sockets;
  10.  
  11.  
  12. namespace FPSCamp.Potal.mobile
  13. {
  14.     public partial class m : System.Web.UI.Page
  15.     {
  16.         private DateTime start = DateTime.Now;
  17.         private DateTime? interval = null;
  18.         protected void Page_Load(object sender, EventArgs e)
  19.         {
  20.             this.interval = start.AddMinutes(10);
  21.  
  22.             while (DateTime.Now < this.interval)
  23.             {
  24.                 if (this.isUpdate())
  25.                 {
  26.                     Response.Write(DateTime.Now);
  27.                     Response.OutputStream.Flush();
  28.                     break;
  29.                 }
  30.             }
  31.         }
  32.         private bool isUpdate()
  33.         {
  34.             // 서버 업데이트 주기
  35.             return false;
  36.         }
  37.     }
  38. }


위 코드는 대기시간(this,inerval)을 10분으로 잡았으므로 데이터의 업데이트가 없어도 10분(대기시간)마다 연결을 끊어 버립니다.





3. 스트리밍(Streaming) 방식:


- 서버는 지속적인 업데이트를 위해 무한정(또는 일정 시간 동안) 요청을 대기시키며, "chunked" 기반 메시지를 이용하여 응답 시 연결을 유지 시킵니다.




스트리밍 방식에 대한 예제는 아래 링크로 대체 합니다.


- COMET을 이용한 웹 채팅 만들기:

http://hoons.kr/Lecture/LectureMain.aspx?BoardIdx=38734&kind=11&view=0




정리:


결국, COMET 방식은 데이터 전송 과정(요청/응답)에서 불필요한 많은 양의 네트웍 오버 헤더를 발생 시키며, 연결(HTTP 요청) 대기 시간에 따른 성능 저하도 추가로 발생합니다.


또한, 단방향(반이중 통신)통신인 HTTP 방식을 양방향 통신(이중 통신)으로 구현하기 위한 별도의 복잡한 개발 비용도 발생합니다.







HTML5 WebSocket:


- 이전 HTTP 통신과는 달리 클라이언트와 서버 간 양방향 통신이 가능하며, 이때 네트웍상의 메시지는 0X00바이트로 시작해서 0xFF 바이트로 끝나고 그 사이에는 UTF-8 데이터가 채워지게 됩니다.




- 아래는 폴링(Polling) 방식과 WebSocket 방식의 불필요한 네트워크 오버헤드를 비교한 결과입니다.




1. 폴링 방식:


- HTTP 요청/응답 시마다 아래와 같은 HTTP 헤더 데이터들이 전달되며, 그에 따른 네트웍 오버헤드가 발생한다.





2. WebSocket 방식:


- WebSocket 방식은 HTTP 방식과 달리 불필요한 요청/응답 헤더 데이터가 존재하지 않습니다.






3. 네트워크 오버헤드 비교:



폴링(Polling) 방식:


요청/응답 헤더 데이터 용량: (871 Byte)


1. 1000명 *  헤더 데이터 용량  = 871,000  Byte

2. 10000 *  헤더 데이터 용량  = 8,710,000  Byte

3. 100000 *  헤더 데이터 용량  = 87,100,000 Byte




WebSocket 방식:


메시지 데이터 용량: (2 Byte)


1. 1000 *   메시지 데이터 용량   = 2,000  Byte

2. 10000 *   메시지 데이터 용량   = 20,000  Byte

3. 100000 *   메시지 데이터 용량   = 200,000 Byte



테스트 결과:


위 테스트 결과처럼 폴링 방식 데이터(위의 경우 header 데이터만 있으며, Body가 빠진상태)처리는 WebSocket 처리 방식보다 주고 받는 데이터양에 의해 상당한 네트워크 오버헤드가 발생합니다.







4. 전송 대기시간 비교:



폴링(Polling) 방식:


서버 응답 후 요청에 대한 대기시간이 추가로 발생합니다.


즉, 클라이언트 요청부터 서버 응답까지 50ms 걸렸으며, 서버 응답 후 재요청을 만들기 위한 추가적인 대기시간이 걸립니다.




WebSocket 방식:


최초 서버와 연결 후, 연결이 그대로 유지되므로 클라이언트가 재요청을 보낼 필요가 없습니다. 


즉 추가적인 대기시간이 발생하지 않습니다.



테스트 결과:


위 테스트 결과처럼 연결이 유지되는 WebScoket 방식과는 달리 폴링 방식은 매번 일어나는 요청/응답으로 인해 대기 시간이 추가됩니다.







- 클라이언트 코드:


  1. var WScoket = (function (win, doc) {
  2.  
  3.     var ua = window.navigator.userAgent.toLowerCase();
  4.  
  5.     return function (opt) {
  6.  
  7.         function init() {
  8.  
  9.             this.options = {
  10.  
  11.                 socket: null,
  12.  
  13.                 url: '',
  14.                 onopen: function () { ; },
  15.                 onmessage: function () { ; },
  16.                 onclose: function () { ; },
  17.                 onerror: function () { ; }
  18.             };
  19.  
  20.             extend.call(this.options, opt);
  21.  
  22.             window.WebSocket && start.call(this.options);
  23.  
  24.             return this;
  25.         };
  26.  
  27.         init.prototype = {
  28.             send: send
  29.         }
  30.  
  31.         return new init();
  32.     }
  33.  
  34.     function start() {
  35.  
  36.         this.socket = new WebSocket(this.url);
  37.         appendEvents.call(this, this.socket);
  38.     }
  39.  
  40.     function appendEvents(socket) {
  41.  
  42.         var that = this;
  43.  
  44.         bind(socket, 'open', function (e) { that.onopen.call(socket, e); });
  45.         bind(socket, 'message', function (e) { that.onmessage.call(socket, e); });
  46.         bind(socket, 'close', function (e) { that.onclose.call(socket, e); });
  47.         bind(socket, 'error', function (e) { that.onerror.call(socket, e); });
  48.     }
  49.  
  50.     function send(msg) {
  51.         this.options.socket.send(msg);
  52.     }
  53.  
  54.     // 객체 상속 함수
  55.     function extend() {
  56.  
  57.         var target = this
  58.         , opts = []
  59.         , src = null
  60.         , copy = null;
  61.  
  62.         for (var i = 0, length = arguments.length; i < length; i++) {
  63.  
  64.             opts = arguments[i];
  65.  
  66.             for (var n in opts) {
  67.                 src = target[n];
  68.                 copy = opts[n];
  69.  
  70.                 if (src === copy) continue;
  71.                 if (copy) target[n] = copy;
  72.             }
  73.         }
  74.     };
  75.  
  76. })(window, document);
  77.  
  78. function webScoket()
  79. {
  80.     ws = WScoket({
  81.         url: 'ws://echo.websocket.org/',
  82.         onopen: function (e) {
  83.             alert(this);
  84.             alert(e);
  85.         },
  86.         onmessage: function (e) {
  87.             alert(e.data);
  88.         },
  89.         onclose: function (e) {
  90.             alert('close');
  91.         },
  92.         onerror: function (e) {
  93.             alert('error');
  94.         }
  95.     });
  96. };
  97.  
  98. bind(window, 'load', webScoket);
  99.  
  100. // 이벤트 할당
  101. function bind(elem, type, handler, capture) {
  102.     type = typeof type === 'string' && type || '';
  103.     handler = handler || function () { ; };
  104.  
  105.     if (elem.addEventListener) {
  106.         elem.addEventListener(type, handler, capture);
  107.     }
  108.     else if (elem.attachEvent) {
  109.         elem.attachEvent('on' + type, handler);
  110.     }
  111.  
  112.     return elem;
  113. };




- 서버코드:


서버를 구현하는 코드 예제는 상당히 복잡하므로 아래 링크의 테스트 서버로 대체합니다.



WebSocket Test URL: ws://echo.websocket.org/


WebSocket.org 공식 사이트http://www.websocket.org/echo.html





5. 브라우저 지원현황:



브라우저 지원현황:

http://caniuse.com/#feat=websockets






참고 사이트:


- COMET의 소개:

http://hoons.kr/Lecture/LectureMain.aspx?BoardIdx=37384&kind=11&view=0



- .NET을 활용한 COMET의 구현

http://hoons.kr/Lecture/LectureMain.aspx?BoardIdx=37426&kind=11&view=0



- WebSocket Server 구현 방법:


http://stackoverflow.com/questions/9087168/websocket-successfully-handshaking-but-not-sending-receiving-messages-correctly


https://github.com/Olivine-Labs/Alchemy-Websockets


http://www.codeproject.com/Articles/57060/Web-Socket-Server


http://blakeblackshear.wordpress.com/2011/08/25/implementing-a-websocket-handshake-in-c/