Real time Apps #5 - WebSocket


참조 URL
  1. http://dev.w3.org/html5/websockets/
  2. http://tools.ietf.org/html/rfc6455
  3. http://www.infoq.com/articles/Web-Sockets-Proxy-Servers
  4. http://en.wikipedia.org/wiki/Protocol_overhead
  5. http://html5please.com/
  6. https://github.com/gimite/web-socket-js - WebSocket polyfill library
  7. http://weblogs.asp.net/dwahlin/archive/2013/04/13/building-an-html5-web-sockets-server-with-asp-net-4-5.aspx
  8. http://jsperf.com/choosing-websocket-vs-mozwebsocket
  9. https://developer.mozilla.org/en-US/docs/WebSockets
  10. https://developer.mozilla.org/ko/docs/WebSockets

 

목차
  1. Polling - 있어?
    [ASP.NET MVC] - Real time Apps - Polling
  2. LongPolling - 있으면 보내줘!
    [ASP.NET MVC] - Real time Apps - LongPolling
  3. SSE ( Server Send Events ) - 있으면 보내줘 기다릴께~ 
    [ASP.NET MVC] - Real time Apps - SSE ( Serve Sent Events )
  4. WebSocket - 어! 왔네~
    [ASP.NET MVC] - Real time Apps - WebSocket
  5. SignalR - over WebSocket ( MS Platform )
    [ASP.NET MVC] - Real time Apps - SignalR over WebSocket


 이 포스트에 있는 내용이 언제나 확실한 정답은 아닙니다. 진실이라고 생각해 왔던 전제가 시간의 지남에 따라 들어나지 않았던 다른 이면 때문에 좋은 방향으로 이끌어 낼 수 있는 역할로 변환 되는게 역사적으로도 많은 증명 있었습니다. 그렇지만 저는 현재 상황에서 최선의 답을 찾고자 노력하였으며 이 글을 읽는 다른 분들에게 다음 길을 갈 수 있도록 도와주는 디딤돌이 되고자 노력하고자 포스팅을 통해 공유하고자 하는 것입니다. 그리고 프로그래머라는 타이틀을 달고 살아야 한다면 "왜"라는 의문을 항상 가지고 다니면서 자신의 위치에 안주하지 않고 항상 노력하는 모습으로 살아 가고자 합니다. 언제든 지적이나 오류가 있으면 피드백 부탁 드리겠습니다.

ing™       


 이제까지 단 방향 통신에 대해서 살펴 봤다면 지금 부터는 양 방향 통신을 지원하는 WebSocket에 대해서 알아 보고자 한다. 표준 WebSocket의 API는 W3C에서, 프로토콜은 IETF(Internet Engineering Task Force)에서 HTML5의 하위로 재정을 하고 있으며 지금도 현재 진행형이다. 그리고 WebSocket은 HTTP와 마찬가지로 80번 포트를 통해 웹 서버에 연결한다. HTTP 프로토콜의 버전은 1.1이지만 아래 헤더에서 보듯이 Upgrade 헤더를 이용하여 웹 서버에 접속한다. 


[HTTP 헤더 1] Client to Server WebSocket 헤더 정보



[HTTP 헤더 2] Web Server to Client 헤더 정보


[HTTP 헤더 3] HTTP Connect 메소드 헤더 정보


 브라우저는 "Upgrade: WebSocket" 헤더와 함께 랜덤하게 생성한 키를 서버에 보내고 웹 서버는 이 키로 토큰을 생성하여 브라우저에 돌려준다. 위 헤더 정보는 WebSocket지원 버전에 따라 헤더 정보는 달라 질 수 있다. 이런 방식으로 handshaking을 하여 WebSocket을 연결하면 Protocol Overhead 방식으로 웹 서버와 브라우저가 WebSocket 통신을 하게 된다. Protocol Overhead 방식은 메타 데이터와 네트워크 라우팅 기법으로 여러 TCP 커넥션을 생성하지 않고 하나의 80번 포트의 TCP 커넥션을 사용하여 연결하고, 별도의 헤더로 논리적인 데이터 흐름 단위를 사용하여 여러 개의 가상 커넥션을 사용하는 효과를 가져오는 방식이다. ( Protocal Overhead는 일종의 공유기라 생각하면 쉬울것이다. 하나의 공인 IP를 가지고 내부적으로 여러대의 내부 IP에서 나눠 사용하듯이 말이다.) 이런 방식으로 HTTP와 같은 포트를 사용하여 방화벽이 있는 환경에서도 별도의 설정 없이도 WebSocket을 사용 할 수 있도록 지원하고 있다.


 이제까지의 내용을 간단한 도식으로 WebSocket이 웹 서버와 통신하는 흐름을 '그림6'를 통해 간략적으로 살펴 보자.

[그림6] WebSocket 통신 방법


1. Client에서 서버에 연결 시도 한다. 

2. 연결 시 헤더에 Upgrade정보를 보내며 서버와 Handshaking을 하여 연결 한다.

3. 서버에서 발생된 이벤트가 있으면 WebSocket을 통해 브라우저에 보낸다.

4. 연결 종료시까지 3단계를 수행 한다.


위 순서의 2번에 대해서  '그림7'과 같이 도해식으로 좀더 자세히 살펴 보자.

[그림7] WebSocket 도해식


1. Client가 서버에게 요청을 하면서 헤더에 Upgrade, WebSocket.Version, Key... 등을 같이 보내준다.

2. 서버는 자신이 응답할 수 있는 웹소켓이고 버전이 맞으면 Token을 내려 보낸다.

3. 클라이언트는 이 토큰을 가지고 웹 서버와 Protocal Overhead 방식으로 통신을 하게 된다.


 이런 방식으로 브라우저와 웹 서버가 뒷단에서 수행하여 웹 소켓 연결이 되도록 한다. 이제 준비가 되었으니 가볍고 빠른 방식으로 웹 서버에서 필요한 데이터를 주고 받으면 될 것이다.


 이제 클라이언트를 담당하는 브라우저에 대해서 점검해 봐야 할 것이다. 그렇지만 아직까지 모든 브라우저에서 지원하는 방식은 아니다. 아래 '그림8'와 같이 WebSocket를 지원하는 브라우저를 확인 해 보자.


[그림8] WebSocket 지원 브라우저

http://caniuse.com/#feat=websockets 참조 )


 IE는 10 이상부터 지원이 되고 있으며 파이어 폭스(19+)나, 크롬(25+)도 지원하고 있다. IE는 비교적 가장 최신 버전에서만 지원하고 있으므로 개발할 때 염두에 두고 진행해야 할 것이다. 한국의 제한된 시장에서는 아직도 IE 10 이하 버전을 사용하고 있는 사용자들이 많기 때문에 개발시 검토를 잘 하고 진행해야 할 것이다. 하지만 후에 검토할 signalR( 또는 Socket.IO)을 통해서 브라우저 호환성을 극복하여 브라우저마다 다른 호환 범위를 하나의 프로그래밍 개발 접근 방식으로도 해결할 수 있는 방법이 있으니 너무 일찍 좌절감을 맞보지 않기를 소망한다. 희망을 가지고 코드 사이드에서 WebSocket를 진행하도록 하겠다.


 아래 '코드7'는 브라우저에서 WebSocket의 지원하는 메소드 및 간단한 코드 조각이다. (이 코드를 확장하여 개발 범위에 맞게 가감하여 사용하면 될 것이다.)


[코드7] 브라우저에서 WebSocket을 사용하는 메소드 예제


 클라이언트에서의 웹 소켓과 관련된 코드는 '코드7'이 전부라고 알고 있으면 되겠다. 이 작은 코드가 웹 소켓에서 발생하는 모든 이벤트를 보여주고 있다. 이제 이 작은 코드 조각으로 웹 서버를 통해 어떻게 응용이 되는지 살펴 보자. 아래 '코드8'은 .Net 진영에서 제공되는 ASP.NET MVC와 Razor 뷰 문법으로 구성되었으며 서버 사이드의 자체 문법 보다는 전체적인 흐름을 파악하는데 중점을 두고 코드를 살펴 보기를 바란다.


<!-- WebSocket으로 받은 데이터를 반영할 수 있는 DOM --> <ol id="messages"></ol> @using (Ajax.BeginForm("PostMessage"new AjaxOptions())) {      <p>         Say : @Html.TextBox("messageText")         <input type="submit" />      </p> } @section scripts{     <script type="text/javascript">         $(function () {             $("form").submit(function () {                 var form = $(this);                 var txtMessageText = $(form.find('#messageText'));                 $.post('@Url.Action("PostMessage")', { 'messageText': txtMessageText.val() }, function () {                     txtMessageText.val('');                     txtMessageText.focus();                 });                 return false;             });             start();         });         var start = function () {             var wsImpl = window.WebSocket || window.MozWebSocket;             // 웹 소켓 초기화             window.ws = new wsImpl('ws://localhost:8181/', 'my-protocol');             ws.onmessage = function (evt) {                 $('<li>').text(evt.data).appendTo('#messages');             };             ws.onopen = function () {                 $('<li>').text('웹 소켓 연결 되었습니다.').appendTo("#messages");             };             ws.onclose = function () {                 $('<li>').text('웹 소켓이 닫혔습니다.').appendTo("#messages");             }         }     </script> }

[코드8] WebSocket의 브라우저에서 실행될 클라이언트 사이드 코드


1. @using (Ajax.BeginForm("PostMessage", new AjaxOptions())) : form 테그를 완성하는 Razor 문법이다. 여기에서 '@' 구문은 Razor 문법의 시작을 알리는 예약어다.

2. $(function () { $("form").submit(function () { : 는 1번에서 선언된 테그에서 발생하는 submit 이벤트를 받아서 페이지 이동을 하지 못하도록 하고 사용자가 입력한 값을 ajax로 통신하여 서버에 submit하도록 한다.

3. var wsImpl = window.WebSocket || window.MozWebSocket : 웹 소켓 객체를 생성한다. ( https://developer.mozilla.org/en-US/docs/WebSockets 사이트에서 보면 Gecko 6.0에서 개명되어 WebSocket와 MozWebSocket이 일부 브라우저와 버전에 따라서 공전할 수 있기 때문에 사용한 코드다 ) 

4. window.ws = new wsImpl('ws://localhost:8181/', 'my-protocol') : 서버에서는 8181 포트로 초기화를 할 것이기에 이포트를 설정하였고 뒤에 인자는 임의의 값으로 설정하면 된다. 이 코드를 통해 웹 서버와 통신할 수 있도록 초기화를 한것이다. 이 부분에서 서버와의 통신은 80번으로 하지만 내부적으로는 8181 포트를 사용하는 것처럼 동작하게 되는 것이다.

5. onmessage, onopen, onclose : 이름으로 유추할 수 있듯이 서버에서 보내주는 메세지가 있거나 상태가 변경이 되면 해당 function이 실행이 되도록 이벤트를 연결한 것이다. 이벤트가 발생되어 실행이 되면 messages의 id를 가진 '<ol id="messages"></ol>' 테그의 하위 노드에 li가 추가되면서 메시지가 보여질 것이다.




var wsImpl = window.WebSocket || window.MozWebSocket;


여담이지만 위의 작은 코드에도 성능 향상을 기법이 들어가 있다. 전역 객체로 할당 받는 것과 지역 객체로 할당받는 것에 대해서 속도 측정을 해 놓은 사이트가 있다. 'http://jsperf.com/choosing-websocket-vs-mozwebsocket'에서 Performance를 직접 측정할 수 있다.


아래 코드에 대해서 측정할 수 있으며 'Run Test' 버튼을 통해 측정 한다.


1. window.WebSocket = window.WebSocket || window.MozWebSocket;

2. var WebSocket = window.WebSocket || window.MozWebSocket;


Chrome에서 1번 코드와 2번 코드의 성능에 대해서 측정을 하면 아래와 같은 결과가 나오고 있다.
(측정값은 브라우저와 컴퓨터 성능에 따라 다른 결과가 도출 될 수도 있다.)






 이제 클라이언트인 브라우저 코드를 살펴 보았다면 서버측 코드를 살펴 보자. 살펴 보기에 앞서 아래 코드는 MVC 패턴의 Controller 역할을 맡은 소스 코드라는 것을 염두에 두고 살펴보기 바란다.



코드를 작성하기 전에 프로젝트 솔루션에 추가해야 하는 외부 참조 Dll이 있다. 

'https://github.com/statianzo/Fleck'에서 진행되는 웹 소켓 프로젝트이며 관련 소스를 다운 받아 분석할 수 있다. 그렇지만 간단하게 사용하거나 아래 소스코드를 실행해 보기를 원한다면 NuGet를 통해서 'Fleck'로 검색 후 추가하면 된다.


#region Fields
/// <summary>
/// 웹 소켓 초기화시 중복 초기화가 되지 않도록 잠금 역할하는 객체
/// </summary>
private static object webSocketLock = new object();
 
/// <summary>
/// 연결된 클라이언트들의 커넥션 리스트 객체
/// </summary>
private static List<IWebSocketConnection> allSockets = null;
 
/// <summary>
/// 웹 소켓 호스트 역할 객체
/// </summary>
private static WebSocketServer server = null;
#endregion
 
/// <summary>
/// 생성자
/// </summary>
public WebSocketController()
{
    #region WebSocket 초기화
    if (allSockets == null)
    {
        lock (webSocketLock)
        {
            if (allSockets == null)
            {
                allSockets = new List<IWebSocketConnection>();
                server = new WebSocketServer("ws://localhost:8181");
 
                InitWebsocket();
            }
        }
    }
    #endregion
}
 
public ActionResult Index()
{
    @ViewBag.Message = "WebSocket Test";
    return View();
}
 
/// <summary>
/// 메세지 받아서 waiting 풀어 주기
/// </summary>
/// <param name="messageText">입력 받은 값</param>
public void PostMessage(string messageText)
{
    // 웹 소켓에 받은 메세지를 보내주기
    foreach (var socket in allSockets)
    {
        socket.Send(messageText);
    }
}
 
/// <summary>
/// 웹 소켓 초기화
/// </summary>
[NonAction]
public void InitWebsocket()
{
    server.Start(socket =>
    {
        // 웹 소켓 연결 할 때
        socket.OnOpen = () =>
        {
            allSockets.Add(socket);
        };
 
        // 웹 소켓이 닫을 때
        socket.OnClose = () =>
        {
            Console.WriteLine("Close!");
            allSockets.Remove(socket);
        };
 
        // 웹 소켓에서 메세지 전송 할 때
        socket.OnMessage = message =>
        {
            allSockets.ToList().ForEach(s => s.Send("Echo: " + message));
        };
    });
}

[코드9] WebSocket의 서버 사이드 코드


1. private static object webSocketLock = new object() : 쓰레드에서 동기화를 구현하기 위한 객체이며 여러개의 쓰레드에서 하나의 자원을 동시 접근시 순차적으로 접근이 가능하도록 Lock를 걸어주는데 사용한다.

2. private static List<IWebSocketConnection> allSockets = null : 하나의 WebSocket 연결을 관리 하는 리스트 객체다. 이 객체에는 클라이언트를 구분하고 그룹을 짓고 메세지를 보내는 역할을 하는 리스트를 나타낸다.

3. private static WebSocketServer server = null : 실질적으로 WebSocket의 핵심 로직이 들어간 서버 객체다. 이 객체가 클라이언트와 WebSocket 방식으로 통신을 한다.

4. new WebSocketServer("ws://localhost:8181") : 웹 소켓을 localhost의 8181 포트로 동작하도록 한다. - 프로토콜 오버헤드 방식으로 동작하므로 외부에서는 80으로 접속하지만 내부적으로는 8181포트로 동작한다.

5. PostMessage(string messageText) : 브라우저에서 submit시 받아 처리하는 코드로 메시지를 받으면 접속된 모든 클라이언트에 동일한 메시지를 전달하도록 되어 있다.

6. [NonAction] InitWebsocket() : NonAction은 브라우저에서 직접적으로 InitWebsocket를 접근 하지 못하도록 하는 속성이다. 

7. socket.OnOpen = () => { allSockets.Add(socket); } : WebSocket이 브라우저와 handshaking이 이뤄지고 나서 연결이 될 때 발생되는 이벤트다. 접속한 브라우저의 연결된 정보를 관리 하도록 리스트에 저장한다.

8. socket.OnClose = () => { allSockets.Remove(socket); } : 연결된 WebSocket이 닫히면 관리되는 리스트에서도 제거한다.

9. socket.OnMessage = message => {  } : 관리되는 리스트에게 같은 메세지를 일괄적으로 클라이언트에게 웹소켓으로 보내준다.



 이제 전체 소스를 살펴 보았으니 '그림9'을 통해 결과를 확인 해 보도록 하자.



[그림9] WebSocket 결과 화면 ( 위쪽이 Chrome, 아래쪽이 IE10 )



1. 처음 Chrome 브라우저를 통해 접속을 하고 '첫번째 웹 소켓 통신'을 입력하고 버튼을 눌러 서버에 전송 하였다.

2. IE10으로 같은 주소를 접속하고 'IE에서 보내는 웹 소켓 메시지'를 입력하고 버튼을 눌러 서버에 전송 하였다.

    - Chrome에서도 동일한 메시지가 실시간으로 확인 할 수 있다.

3. Chrome에서도 'Chrome에서 보내는 웹 소켓 메시지'를 입력하고 버튼을 눌러 서버에 전송 하였다.

    - IE에서도 동일한 메시지가 실시간으로 확인 할 수 있다.



 이번에는 WebSocket의 통신 흐름 잡아 내 보도록 하자.  아래 '그림10'처럼 F12를 눌러 개발자 도구를 활성화 시키고 'WebSocket'를 선택하고 브라우저에서 값을 입력하여 서버에 보내면 웹 소켓 통신으로 받은 패킷을 아래처럼 잡을 수 있다. 


[그림10] Chrome에서 WebSocket의 패킷을 잡아낸 그림


 이 방법으로 웹 소켓과의 통신을 추적할 수 있게 되었다.



이제 웹 소켓에 대해서 대략적으로나마 살펴 보았다. 생각보다 코드의 양은 많지 않았지만 생소할 수도 있어서 코드 주석과는 별개로 밑에 세부 설명을 추가 하였으니 좀더 쉽게 이해가 되었으면 하는 바램입니다. 눈으로만 지나치시지 마시고 한번씩이라도 실습을 통해 나의 경험으로 체득하심을 극히 권해 드리며 프로그램 만으로도 행복할 수 있는 세상이 오기를 바라며 이번 포스트를 마칩니다.



 다음에 소개해 드릴 signalR은 지금보다 더 쉽고 편한 방법으로 개발 할 수 있는 라이브러리에 대해서 알아 보도록 하겠습니다. 흥미로운 signalR을 기대해 주세요~



 Guillermo Rauch가 만든 Socket.IO( http://socket.io/ )는 WebSocket, FlashSocket, Ajax Long Polling, Ajax Multipart streaming, Forever Iframe, JSONP Polling을 하나의 API로 추상화 해서 제공하고 있다. 즉 브라우저와 웹 서버의 종류를 파악하여 가장 적합한 기술을 자동 선택하여 사용하는 방식을 제공한다. 간다히 케이스를 정하자면 WebSocket를 지원하지 않는 브라우저에서도 상관 없이 같은 개발 방식으로 개발할 수 있도록 지원한다는 것이다. 그렇지만 아직까지는 Node.js 플랫폼 위에서만 동작하는 제약이 있다.


[코드10] Socket.IO in Browser


[코드11] Socket.IO on Node.js


 이로써 Socket.io를 사용할 준비는 모두 끝났다. 브라우저에서 페이지를 열면 콘솔에는 "{ server : 'hello world!' }"가서버 콘솔에는 "{ browser : 'data' }"가 출력되는 것을 확인할 수 있을 것이다. Socket.io에 대해서 깊게 알고 싶다면 "http://socket.io/"에서 가셔서 확인할 수 있을 것이다. 물론 Windows에서도 IISNodehttps://github.com/tjanczuk/iisnode ) 모듈을 통해서 node에 대한 개발 및 테스트를 할 수 있다.






소스 코드 자체에 주석과 직관적인 코딩으로 충분히 파악이 가능할 것으로 예상하므로 별도의 설명을 생략하도록 하겠습니다. 포스트의 내용이 장황한 설명 보다는 주석과 소스코드 자체 만으로도 이해할 수 있도록 하기 위해 노력하였습니다. 실 개발에서도 적용할 수 있도록 간단하면서도 현실적인 예제 프로그램을 통해 각 소스를 만들고 이해 시키고자 하였으며 실무에 필요한 개발요구 사항들을 해결 하는데 도움이 되고자 노력하였습니다. 그리고 소스와 같이 있는 주석을 이용해 nDoc이나 별도의 자동 Document 제작 유틸로 API 문서를 만드는 데에도 도움이 되었으면 한다. 
※ DOC에 대한 프로그램 정보 Util link

ing™       



+ Recent posts