본문 바로가기

.Net Technology/Comet

(2) 닷넷을 이용한 코멧의 구현

월간 마소에 2010년 7월호에 기고한 원고입니다. 

연재순서
1회 | 2010. 06 | 코멧의 소개와 활용전략
2회 | 2010. 07 | 닷넷을 이용한 코멧의 구현
3회 | 2010. 08 | 코멧을 이용한 웹 채팅 만들기


지난 시간에는 코멧이 무엇이며 코멧을 어떻게 활용할 수 있을지에 대해서 살펴보았다. 다시금 느끼는 것이지만 기술의 변화는 정말 빠르다. 짧은 몇 개월 사이에 코멧을 지원하는 프레임워크는 계속해서 새 버전을 출시했으니 말이다. 이번 강좌는 닷넷 개발자들을 위한 강의로서 ASP.NET을 통해서 코멧을 어떻게 구현하고 활용할 수 있을지에 대해서 살펴보고자 한다. 

먼저 닷넷에서 코멧을 구현하기 위한 방법들을 살펴보기 전에 짧게 복습을 하고 진행해 보도록 하겠다. 서버의 이벤트를 감지해서 바로 클라이언트로 전달하는 방법은 세가지가 있다고 언급했었다. 바로 폴링, 롱폴링, 스트리밍 방식 이렇게 3개가 그것이다. 그리고 코멧의 핵심은 바로 롱폴링에 있다고 전했었다. 왜냐하면 웹 접근성 때문이다. 스트리밍을 순수 HTTP 프로토콜로 지원하기란 한계가 있기 때문이다. 그래서 자바애플릿이나 플래쉬 혹은 실버라이트와 같은 브릿지 애플리케이션을 이용해서 스트리밍 서비스를 구현해야만 하지만 이것을 이용하게 되면 웹 접근성에 대한 제약이 상당하다. 예를 들어, 최근에 가파르게 성장하고 있는 모바일 웹으로 접근하게 될 경우 순수 HTML만 지원하게 될 가능성이 높기 때문이다. 


[그림1] 폴링, 롱폴링, 스트리밍의 비교


롱폴링과 아이프레임

그렇다면 ASP.NET과 롱폴링을 구현한 예제를 살펴보도록 하겠다. 솔직히 ASP.NET이 아닌 어떤 플랫 폼으로도 쉽게 따라서 구현이 가능할 것이다. 여기서 중요한 것은 ASP.NET의 서버 사이드 코드가 아니라 클라이언트의 코드와 동작방식을 이해하는 것이기 때문이다. 필자는 평범함 ASP.NET 웹 사이트를 만들었고, 다음과 같은 세가지 파일을 추가하였다. 

FrameBasic.aspx  - 최초 로딩되며 서버의 메시지를 보여줄 페이지
FrameBasic.js  - 서버와의 통신을 담당할 스크립트
FrameBasicData.aspx, FrameBasicData.aspx.cs  - 요청을 받으면 특정 시간 동안 커넥션을 물고 있다가 데이터를 반환한다.




이렇게 세 개의 파일이 준비 되었다면 다음과 같은 코드를 추가해 보도록 하자.
 

 <html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
    <title></title>
    <script src="FrameBasic.js" type="text/javascript"></script>
</head>
<body onload="foreverFrame('FrameBasicData.aspx')">
    <form id="form1" runat="server">
    <div id="Head">
    </div>
    <div id="Content">
    </div>
    </form>
</body>
</html>


[코드1] FrameBasic.aspx의 코드

[코드1]을 살펴보면 페이지가 로드될 때 FrameBasic.js 파일을 임포트 시키고 HTML파일이 처음 로드 될 때 foreverFrame이라는 자바스크립트 함수를 호출하고 있다. 이 함수는 FrameBasic.js파일에서 정의할 예정이다. 그리고 두 개의 div를 가지고 있고 Head 라는 아이디를 가지고 있는 div에는 프레임을 삽입할 예정이고 Content 라는 아이디를 가지고 있는 div에는 서버에서 받아온 메시지를 뿌려줄 예정이다. 그렇다면 FrameBasic.js 파일을 살펴보도록 하자.

 

var FrameUrl;
function foreverFrame(url) {
    var body = document.getElementById("Head");
    var iframe = body.appendChild(document.createElement("iframe"));
    iframe.style.display = "none";
    iframe.src = url;
    iframe.id = "forever";
    FrameUrl = url;
}
function callback(text) {
    var body = document.getElementById("Content");
    body.innerHTML += text + "<br/>";
    
    var forever = document.getElementById("forever");
    forever.src = FrameUrl;
}

[코드2] FrameBasic.js의 코드
 
이 코드는 지난 기사에서 살펴본 코드와 크게 다르지 않다. 먼저 페이지가 로드되면서 foreverFrame이라는 함수를 호출하게 된다. 이 함수에서 하는 역할은 간단하다. Head div를 찾아서 거기에 새로운 아이프레임을 동적으로 생성하여 붙이는 것이다. 그럼 서버에서는 callback 함수를 호출하는 스크립트를 담고 있는 HTML문서를 리턴하게 되고, callback 함수에서는 다시 한번 서버를 호출하게 되면서 서버와의 연결을 유지하려고 하는 것을 볼 수 있다. 그렇다면 서버 코드를 살펴보도록 하자. 

 

 protected void Page_Load(object sender, EventArgs e)
{
    //3의 배수가 나올 때까지 휴식
    do
    {
        System.Threading.Thread.Sleep(1000);
    } while (DateTime.Now.Second % 3 != 0);     //시간 내보내기
    Response.Write("<script>parent.callback(\"" + DateTime.Now.ToLongTimeString() + "\");</script>");
        
    Response.End();   
}

[코드3] FrameBasicData.aspx.cs의 코드
 
서버 코드는 특별한 설명 없이도 쉽게 이해할 수 있을 것이다. 일초에 한번씩 현재 시간의 초를 확인하여 만약 3의 배수라면 현재 시간을 반환하는 코드이다. Response.End()를 호출한 것은 현재 서버에서만 생성한 코드만 출력하고 나머지 HTML 출력들은 생략하기 위해서이다. 이렇게 해서 실행을 해보면 다음 [화면1]처럼 3초에 한번씩 원하는 정보가 출력되는 것을 볼 수 있을 것이다.
 

[화면1] 프레임을 활용한 롱폴링


이렇게 프레임을 이용해서 커넥션을 유지할 수 있지만 UX적으로 상당히 안 좋은 경험을 사용자들에게 전해주게 된다. 왜냐하면 아래 상태바를 보면 서버에서 커넥션을 연결하고 있는 동안은 프로그레스 바가 계속 진행중으로 표시 되기 때문이다. 그래서 구글토크는 이 방법을 해결하기 위해서 htmlfile이라는 ActiveX객체를 사용했다고 지난 강좌에서 소개했었다. 그렇다면 위의 프로젝트에서 자바스크립트 코드만 htmlfile을 이용하도록 변경해 보도록 하자.
 

var FrameUrl;
function foreverFrame(url) {
    //URL기록
    FrameUrl = url;
    //htmlfile 만들기
    var transferDoc = new ActiveXObject("htmlfile");
    transferDoc.open();
    transferDoc.write(
        "<html><script>" +
        "</script></html>");
    transferDoc.close();
    var ifrDiv = transferDoc.createElement("div");
    transferDoc.body.appendChild(ifrDiv);
    
    transferDoc.parentWindow.callback = function (msg) {
            var body = document.getElementById("Content");
            body.innerHTML += msg + "<br/>";
    }
    ifrDiv.innerHTML = "<iframe id='ifr' src='" + url + "'></iframe>";
   
}

[코드4] htmlfile을 이용하도록 설정

코드를 보면 transferDoc이라는 변수를 선언하고 새로운 htmlfile이라는 객체를 생성하여 할당하였다. 그리고 이 문서 안에서 html 파일 구조를 생성한 뒤에 div태그를 추가하였다. 그 뒤에 이 div 레이어 안에 프레임을 삽입하여 넣게 된다. 구조적으로 보면 새로운 html이 기존 html 안에 생성된 것이며 그 안에 frame을 삽입했기 때문에 브라우저는 이것의 로드 상태를 감지하지 못하게 된다. 그럼 이렇게 변경한 파일을 실행해 보면 다음 [화면2]처럼 상태 줄의 프로그레스 진행률이 보여지지 않은 것을 볼 수 있을 것이다.


[화면2] htmlfile을 활용하여 로드 된 화면


롱폴링과 청크 인코딩

HTTP 프로토콜이 버전1.1로 확장되면서 청크 인코딩을 지원하게 되었다. 청크의 뜻은 덩어리라는 뜻으로 특정 요청을 서버에서 처리할 때까지 기다리는 것이 아니라 중간, 중간 메시지를 덩어리로 잘라서 먼저 클라이언트에게 보내주고 클라이언트는 받은 양만큼 먼저 렌더링을 시작하게 된다. 지금 이 글을 읽고 있는 독자가 웹 개발자이거나 웹 프로그램을 학습해 본 경험이 있다면 충분히 이해할 수 있을 것이다. 바로 Flush 메서드가 바로 이 작업을 처리하기 때문이다. 

서버를 직접 구현해야 한다면 청크 인코딩의 프로토콜에 대해서 완벽히 이해할 필요가 있다. 하지만 ASP.NET이나 PHP와 같은 웹 서버와 통신을 하게 된다면 서버 단에서 자동으로 프로토콜에 맞추어 통신을 대행하기 때문에 우리가 특별히 이해할 필요는 없다. 그럼 이번에는 청크 인코딩을 이용해서 롱폴링을 구현해 보도록 하겠다. 위에서 작성한 프로젝트에서 서버 단의 파일만 다음과 같이 추가해보도록 하자.
 

protected void Page_Load(object sender, EventArgs e)
    {
        //3분동안 반복
        DateTime EndTime = DateTime.Now.AddMinutes(3);         //3의 배수가 나올때까지 휴식
        do
        {
            System.Threading.Thread.Sleep(3000);
            Response.Write("<script>parent.callback(\"" + DateTime.Now.ToLongTimeString() + "\");</script>");
            Response.Flush();
        } while (DateTime.Now < EndTime);
        Response.End();
    }

[코드5] 서버 단의 코드

이 코드는 3분 동안 커넥션을 맺고 있으면서 현재 날짜를 반환하는 코드이다. 하지만 이 코드를 실행하면 한참동안은 코드가 보이지 않다가 몇 분이 지나야 지금까지 받은 데이터들이 한꺼번에 브라우저에 보이는 것을 볼 수 있을 것이다. 이것을 간혹 혹자는 버그로 간주하는 경우도 있지만 이것은 엄연한 브라우저의 동작이 그렇게 정의가 되어 있기 때문이다. 왜냐하면 인터넷 익스플로러의 경우 청크 인코딩으로 브라우저에 데이터를 전달할 경우 최소 1KB 이상이 도착했을 때 렌더링을 시작하게 되기 때문이다. 왜냐하면 너무 작은 데이터가 왔을 때 렌더링을 시작하는 것을 방지하기 위해서라고 보면 된다. 참고로 파이어폭스는 2KB부터 인코딩을 시작하게 되기 때문에 만약 즉시 렌더링을 강제하기 위해서는 무조건 2KB정도의 쓰레기 데이터를 보내주어야 한다.


C#으로 구현하는 IIS

지금까지는 ASP.NET 웹 서버와 통신하는 롱폴링에 대해서 살펴보았다. 하지만 채팅과 같이 사용자의 상태를 감시하거나 보다 자연스러운 양방향 통신을 최적화 되어 구현하기 위해서는 웹 서버를 직접 구축해야 하는 것이다. 즉, IIS나 아파치와 같은 웹서버를 직접 구현하여 구동해야 된다는 것이다. 

이러한 웹 서버를 구현하기 위해서는 네트워크 프로그래밍의 기초와 함께 HTTP프로토콜에 대해서 완벽히 이해하고 있어야 한다. HTTP 프로토콜은 어차피 TCP통신 기반으로 데이터가 옮겨지기 때문에 TCP통신의 개념이 있다면 충분히 이해할 수 있을 것이다. 하지만 여기서 네트워크 프로그래밍이나 TCP통신의 개념을 설명하기에는 지면상 여유가 없기 때문에 HTTP 프로토콜에 대해서만 먼저 정리해 보도록 하겠다. 

특정 웹 사이트를 처음 방문할 경우 HTML뿐만 아니라 이미지, 스타일시트, 스크립트 등 모든 구성요소들을 자신의 PC로 다운 받게 된다. 다음 그림은 어떤 게임 포털 사이트를 처음 방문했을 때 순차적으로 다운되는 모습을 보여주고 있다.


[화면3] 사이트를 처음 방문했을 때의 브라우저 동작

이 사이트는 총 157번의 HTTP 요청을 만들고 있고 페이지 전체 크기는 약 1.1메가 정도 되는 것을 볼 수 있다. 그리고 가운데 상태코드(Status)를 보면 처음 302를 빼고 모두 200을 반환하고 있는 것을 볼 수 있을 것이다. 동작 순서를 요약하자면, 브라우저는 처음에 HTML을 다운 받는다. 그 뒤에 브라우저는 HTML을 분석하여 문서 위에서부터 차례대로 구성요소(플래쉬, 스크립트, 이미지등)들을 다운로드 하게 되고 다운로드 된 구성요소들은 인터넷 임시파일 폴더에 저장한다. 

브라우저는 요청?응답(Request-Response) 방식으로 각각 서버에 요청하고 그 응답들을 받아 처리하게 된다. 브라우저는 다음과 같은 요청을 보내게 되고, 이 사이트에서는 아래와 같은 요청들이 약 150번 정도 일어났다고 보면 된다. 다음 요청은 이 사이트의 로고가 수정되었는지 물어보는 요청이다.

 
 

요청(Request) 브라우저->서버 

GET /logo_nx.gif HTTP/1.1
Accept: */*
Accept-Language: ko
UA-CPU: x86
Accept-Encoding: gzip, deflate
If-Modified-Since: Fri, 25 Jan 2008 08:39:11 GMT
If-None-Match: "5f655c22d5fc81:6f"
User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; ..
Host: s.nx.com
Connection: Keep-Alive

응답(Reponse) 서버->브라우저 

HTTP/1.1 304 Not Modified
Date: Mon, 03 Mar 2008 12:12:44 GMT
Etag: "5f655c22d5fc81:6f"



즉, TCP 통신 개념으로 살펴 보자면 메시지가 위와 같은 순서대로 서버에 전달되는 것이고 서버는 이 메시지를 받아서 주소를 파싱한 뒤에 해당 파일을 읽어서 스트림으로 브라우저로 전달하게 되는 것이다. 이렇게 전달하고 나면 브라우저는 커넥션 연결을 끊게 된다. 그럼 한번 C#콘솔 프로그램을 생성하여 이러한 웹 서버를 직접 만들어 보도록 하겠다. 먼저 html파일 2개가 필요하다. 필자는 index.html이라는 파일과 index2.html이라는 파일을 먼저 만들었다. 안의 내용은 중요하지 않기 때문에 대충 적도록 하자. 필자는 다음과 같은 html파일을 생성하였다. 

 

 <html>
<head></head>
<body>
<h2>HOONS - 23th seminar</h2> 
<br/>
Hello! WebServer
<br/><br/><br/>
<a href="index2.html">click here</a>
</body>
</html> <html>
<head></head>
<body>
<h2>This is Second page</h2> 
<br/>
Hello WebServer
</body>
</html>

[코드5] index.html과 index2.html

이 파일을 생성했다면 콘솔 프로젝트를 하나 생성해서 네트워크 서버를 만들어 보도록 하겠다. 필자는 이번 기사에서는 지면상 핵심 코드만 설명하도록 하겠다. 나머지 코드는 HOONS닷넷 공지사항에 올라와있는 “23회 정기세미나 발표자료”를 통해서 다운받아 보도록 하자.
 

 private void LoginThread()
{
    TcpListener listener = new TcpListener(IPAddress.Any, ServerPort);
    listener.Start();
    Console.WriteLine("사용자 접속 대기.");
    while (true)
    {
        Socket socket = listener.AcceptSocket();         try
        {
            if (socket.Connected)
            {
                Console.WriteLine("사용자 접속");
                //클라이언트 생성
                Client ct = new Client(socket, this);
                // GET HTTP/1.1 인지 확인한다.
                string url = ct.ReadLine();
                string[] tempValue = url.Split(' ');
                //URL확인
                Console.WriteLine(url);
                url = tempValue[1];
                //헤더 확인
                Console.WriteLine(ct.ReadLine());
                Console.WriteLine(ct.ReadLine());
                Console.WriteLine(ct.ReadLine());
                Console.WriteLine(ct.ReadLine());
                Console.WriteLine(ct.ReadLine());
                Console.WriteLine(ct.ReadLine());
                //모두 읽어왔다면
                StreamReader sr = new StreamReader(url.Replace("/", AppDomain.CurrentDomain.BaseDirectory));
                string body = sr.ReadToEnd();
                sr.Close();
                ct.SendMessage("HTTP/1.1 200 OK");
                ct.SendMessage("Cache-Control: private");
                ct.SendMessage("Content-Type: text/html; charset=utf-8");
                        
                ct.SendMessage("");
                ct.SendMessage(body);
                        
                ct.Dispose();
            }
        }
        catch (Exception e)
        {
            Console.WriteLine(e.ToString());
        }
        System.Threading.Thread.Sleep(200);
    }
}

[코드5] 웹 서버의 핵심 코드

이 코드는 사용자가 처음 접속했을 때 헤더값을 읽어오게 된다. 가장 처음에 전달하는 것이 바로 HTTP 프로토콜과 요청하고자 하는 페이지이다. 그래서 그 첫 줄을 파싱해서 파일명을 가져와야 한다. 그 외의 속성은 실제로 이 예제에서 크게 사용하지 않기 때문에 그냥 읽어서 콘솔에 뿌려주었다. 그리고 이러한 헤더 속성을 다 읽어오게 되면 200 상태코드 값 헤더값을 반환해주고 연결을 끊게 된다. 

그럼 이 코드를 실제로 구동해 보도록 하겠다. 


[화면4] 콘솔 프로그램으로 만든 C#웹 서버


[화면5] 웹 브라우저에서 접속한 화면


정리

이번 기사에서는 닷넷을 이용하여 코멧 서비스를 활용하는 방법을 살펴보았다. 채팅처럼 사용자 간의 메시지가 자연스럽게 주고 받는 서비스라면 당연히 뒤에서 만든 것처럼 웹 서버를 직접 구현해야겠지만 간단히 특정 정보를 모니터링 하는 정도라면 여기서 살펴본 롱 폴링 기법으로도 충분히 성능을 높일 수 있다. 다음 기사에서는 롱폴링과 C# 프로그램을 활용하여 채팅 프로그램을 구현하는 방법들에 대해서 살펴보도록 하겠다.