2010년 1월 10일 일요일

플래시 플랫폼으로 만들어 보는 P2P 채팅 애플리케...

방장은 누규? 채팅사이트 조이팅스토리를 운영하며 마케팅, 사업 등에 관심이 많은 철이


플래시 플랫폼으로 만들어 보는 P2P 채팅 애플리케이션

플래시 플레이어 10이 출시되면서 가장 주목 받는 점을 꼽자면, RTMFP(Real Time Media Flow Protocol)라는 UDP 프로토콜 기반 위에서 통신하는 새로운 프로토콜이 제시되면서 플래시 플레이어간의 Peer to Peer(P2P)가 가능해진 것이다.
P2P 기능 구현이 가능해진 것은 플래시 플레이어에 고유 아이디를 부여해, IP 기반이 아닌 플래시 플레이어의 고유 아이디를 기반으로 서로 통신할 수 있도록 됐기 때문이다. 플래시 플레이어간의 연결은 NetConnection 객체를 이용해 할 수 있으며, 플래시 플레이어간의 데이터 전송은 NetStream 객체를 이용하면 된다. 예제는 텍스트 기반의 통신을 하는 간단한 채팅 애플리케이션을 구현하겠지만, NetConnection이란 객체가 오디오나, 비디오 데이터로 전송 가능하므로, 화상 채팅 같은 애플리케이션으로 충분히 발전시킬 수 있을 것이다. 그럼 Peer to Peer의 기본 원리부터 알아보자.

간단한 채팅 서버 기능 구현하기
NetConnetion 객체를 이용해 플래시 플레이어간 통신을 하려면, 일단 고유 아이디가 필요하다. 플래시 플레이어의 고유 아이디 생성 방법은 FMS(Flash Media Server 3.x) 이상을 활용하거나, 어도비에서 제공하는 Stratus(http://labs.adobe.com/technologies/stratus) 서비스를 이용하면 고유 아이디를 생성할 수 있다. FMS는 비용적인 측면이 있으니, Stratus 서비스를 이용해 고유 아이디를 생성해 보자. <그림 1-1>에서 보이는 것처럼 Developer Key Registration 메뉴를 클릭하고 개발자 키를 발급받으면, Stratus 서비스를 이용할 수 있다.

<그림 1-1> Stratus 서비스의 Developer Key를 발급받는 화면
 

개발자 키를 발급받았다면, 채팅 서버를 구현해 보자. 구현할 사항은 다음과 같다.

l        Stratus 서비스에서 고유 키 발행 받기
l        클라이언트의 접속을 감시할 채널 스트림 객체 생성하기
l        클라이언트의 데이터를 청취할 스트림 객체 생성하기
l        클라이언트의 텍스트 데이터를 전송 받을 메쏘드 등록하기

가장 처음으로 해야 할 것은 Stratus 서비스에서 고유 키를 발행 받는 일이다. Stratus 서비스에서 고유 키를 발행 받아 보자.

[Stratus 서비스에서 고유 키 발행 받기]

//Stratus 개발자 키
private const DEVELOPER_KEY:String = "Stratus Developer Key";
//Stratus의 기본 경로
private const STRATUS_URL:String = "rtmfp://stratus.adobe.com";
//Stratus 서비스에 연결할 NetConnection 객체
private var netConnection:NetConnection;

private function onConnect():void
{                                         
    //NetConnection 객체 생성
netConnection = new NetConnection();

//NetConnection 객체의 연결 상태를 확인할 이벤트 등록
netConnection.addEventListener(
NetStatusEvent.NET_STATUS, netConnectionHandler);
  
    //Stratus 서비스에 NetConnection 객체 연결
netConnection.connect(STRATUS_URL + "/" + DEVELOPER_KEY);
}

private function netConnectionHandler(event:NetStatusEvent):void             
{
    //Stratus 서비스에 연결이 되면 neared 속성에 고유 아이디가 발급된다.
trace(netConnection.nearID);
//output
//9951da938d8db2204b401e780252fbcb157281cf0048a09e9881030afbc0d5ec
}

onConnect();


여기서 발행된 고유키는 클라이언트가 접속할 주소라고 생각하면 된다. 고유 키를 발급 받았으니, 이번엔 고유 키로 접속하는 클라이언트를 감시하는 기능을 구현해 보자. 클라이언트를 감시하는 기능은 Stratus 서비스에 연결된 netConnection 객체를 그대로 사용하며, NetStream 객체를 이용해 클라이언트가 접속할 수 있는 채널을 생성하고, 해당 채널에 접속하는 클라이언트를 감시하는 방식이다. LocalConnection 객체 사용법과 상당히 비슷한 부분이다. 구현방식을 살펴보자.


[클라이언트 감시 채널 구현하기]

//플래시 플레이어의 NetStream이 직접 접근 가능한 NetStream 객체 생성

listenerStream = new NetStream(netConnection
,NetStream.DIRECT_CONNECTIONS);

//NetStream 객체의 상태를 볼 수 있는 이벤트 등록
listenerStream.addEventListener(NetStatusEvent.NET_STATUS
, listenerHandler);

//listenTest라는 수신 채널 생성
listenerStream.publish("listenTest");

//채널에 접속한 클라이언트의 기능 정의                                                                   
listenerStream.client = {
        //플래시 플레이어에서 직접적으로 연결됐을 때, 메쏘드 정의
           onPeerConnect : function(client:NetStream):Boolean
           {
           //현재 접속한 클라이언트의 아이디 출력
              trace(client.farID + "가 접속했습니다. n");         
              return true;                                                                                        }          
}

                                                    

클라이언트가 해당 채널로 접속하게 되면 onPeerConnect() 메소드가 실행되며, 연결된 클라이언트가 보낸 NetStream 객체의 farID 속성을 이용해 접속한 클라이언트의 고유 키를 알 수 있게 된다.
이제 간단한 채팅 서버 구현의 마지막 단계다. 클라이언트의 고유 키를 알게 됐으므로, 클라이언트가 데이터를 전달할 수 있도록 데이터를 전달할 메쏘드를 만들어 줘야 한다. 클라이언트가 데이터를 전달할 수 있도록 하기 위해 클라이언트의 데이터를 청취할 스트림 객체를 생성하고, 스트림 객체에 클라이언트가 호출할 수 있는 메쏘드를 등록해보자. 이번엔 간단한 채팅 서버가 구현된 전체 소스를 보도록 하겠다.

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml"
           layout="absolute" applicationComplete="init()">
           <mx:Script>
           <![CDATA[                             
             private const DEVELOPER_KEY:String = "";
             private const STRATUS_URL:String = "rtmfp://stratus.adobe.com";
       private var netConnection:NetConnection;
             private var listenerStream:NetStream;
             private var receiveStream:NetStream;
            
private function init():void{
               onConnect();
             }
                               
             private function onConnect():void
             {                                      
                netConnection = new NetConnection();
                netConnection.addEventListener(
NetStatusEvent.NET_STATUS, netConnectionHandler);
                netConnection.connect(STRATUS_URL + "/" + DEVELOPER_KEY);
             }
                               
             private function netConnectionHandler(event:NetStatusEvent):void
             {
                trace("NetConnection event: " + event.info.code );
                trace(netConnection.nearID);
                result.text += netConnection.nearID+  "n";
          
             //접속이 완료 되었다면
                if(event.info.code == 'NetConnection.Connect.Success'){
                 
               //플래시 플레이어의 NetStream이 직접 접근 가능한 NetStream 객체 생성
 listenerStream = new NetStream(
netConnection, NetStream.DIRECT_CONNECTIONS);

                listenerStream.addEventListener(
NetStatusEvent.NET_STATUS, listenerHandler);

                listenerStream.publish("listenTest");
                                                                                                                               
               listenerStream.client = {
                   onPeerConnect : function(caller:NetStream):Boolean
                   {
                      trace("저는 " + caller.farID + "입니다n");         

                   //클라이언트의 데이터를 청취할 NetStream
                      receiveStream = new NetStream(netConnection, caller.farID);

                   //클라이언트의 스트림 상태를 확인할 이벤트 등록
               receiveStream.addEventListener(
NetStatusEvent.NET_STATUS, receiveHandler);
  
   //클라이언트가 데이터를 전달할 메쏘드 등록
 //(서버가 클라이언트가 되는 격)
                      receiveStream.client = {
                      
                              test : function(msg:String):void{
                                 result.text += msg +"n";
                              }
                      }

                   //클라이언트가 데이터를 전달하기 위해 발행한 채널 감시
   receiveStream.play("testChannel");
                      return true;                                                                                          }        
}                                          
                }//END IF
          }
                  
          private function receiveHandler(evt:NetStatusEvent):void{
            trace("클라이언트가 보낸 NetStream상태: " + evt.info.code);
          }
                  
           private function listenerHandler(evt:NetStatusEvent):void{
             result.text +="클라이언트의 접속 상태 " + evt.info.code+"n";                        
           }
           ]]>
</mx:Script>
<mx:TextArea x="10" y="10" width="659" height="429" id="result"/>
</mx:WindowedApplication>


이렇게 구현된 채팅 서버는 [그림1-2] [그림1-3]처럼 간단한 로그를 찍는 수준으로 구현했다.

<그림 1-2> 간단한 채팅 서버가 구현된 화면
 

<
그림 1-3> 채팅 서버가 실행되면서 찍힌 로그
 

간단한 채팅 클라이언트 구현하기
서버는 구현됐고, 클라이언트에서 데이터가 전달되기만을 기다리고 있다. 간단하게 클라이언트도 구현해서 서버로 전달해 보자. 클라이언트의 구현은 서버처럼 Stratus 서비스를 이용해야 고유 키를 발급받을 수 있으므로, 처음 Stratus 서비스에 접속하는 부분은 동일하지만, 그 이후부터는 반대 되는 개념으로 구현된 것을 알 수 있을 것이다. 참고로 클라이언트는 서버를 구동하면 발행되어 나오는 고유 키를 활용하게 되므로, 클라이언트의 기능을 구현할 때는 서버가 실행되어 고유 키를 발급받은 상황이어야 한다.

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml"
           layout="absolute" applicationComplete="init()">
           <mx:Script>
           <![CDATA[
          
import flash.utils.setInterval;                            
           private const DEVELOPER_KEY:String = "Developer Key";
           private const STRATUS_URL:String = "rtmfp://stratus.adobe.com";
           private var netConnection:NetConnection;
           private var listenerStream:NetStream;
           private var sendStream:NetStream;
                               
           private function init():void{
                     onConnect();
           }
                               
           private function onConnect():void
           {                                         
          netConnection = new NetConnection();
             netConnection.addEventListener(
NetStatusEvent.NET_STATUS, netConnectionHandler);
             netConnection.connect(STRATUS_URL + "/" + DEVELOPER_KEY);
}
                               
           private function netConnectionHandler(event:NetStatusEvent):void
           {
                     trace("NetConnection event: " + event.info.code );
                     trace(netConnection.nearID);
                     result.text += netConnection.nearID+  "n";
                    
                  if(event.info.code == 'NetConnection.Connect.Success'){

                   //서버가 발행 받은 서버의 고유 키를 이용해 서버에 접근
                        listenerStream = new NetStream(
netConnection, "서버의 고유키");

                        listenerStream.addEventListener(
NetStatusEvent.NET_STATUS, listenerHandler);

                   //서버 고유키의 listenTest채널로 접속
                        listenerStream.play("listenTest");                                        
                       

                   //서버로 데이터를 보낼 NetStream객체 생성
                        sendStream = new NetStream(
netConnection, NetStream.DIRECT_CONNECTIONS);

                        sendStream.addEventListener(
NetStatusEvent.NET_STATUS, listenerHandler2);

                   //서버가 청취하고 있는 채널 발행
                        sendStream.publish("testChannel");
                 
                   //1초마다 서버에 등록된 test메소드를 호출한다.
                        setInterval(function():void{
                                sendStream.send("test","테스트");
                        },1000);                                                  
                     }
         }
                  
         private function listenerHandler(evt:NetStatusEvent):void{
              result.text +="서버 상태: " + evt.info.code+"n";                             
         }
                  
        private function listenerHandler2(evt:NetStatusEvent):void{
               result.text +="서버에 데이터를 보내는 상태: " + evt.info.code+"n";                          
         }
           ]]>
           </mx:Script>
<mx:TextArea x="10" y="10" width="659" height="429" id="result"/>      
</mx:WindowedApplication>


<그림 1-4>는 예제로 구현한 간단한 채팅 서버와 클라이언트이며, 서버에 테스트라는 문자열이 1초마다 한번씩 찍힌걸 확인할 수 있다.

 
우리가 구현한 예제는 이렇다. 클라이언트에서 서버의 고유 키를 갖고 서버로 접근하게 되면, 서버는 클라이언트의 고유 키를 알아내고 클라이언트가 수신할 수 있는 test() 메쏘드를 등록해 주고, 클라이언트의 고유 키에 등록된 채널을 청취한다. 클라이언트는 1초마다 지정된 채널에 등록된 서버의 메쏘드를 호출해 인자 값으로 데이터를 전달하면, 서버는 화면에 출력하는 매우 간단한 예제다. 하지만 이 간단한 예제가 기본 원리다. 예제를 구현하면서 다음과 같은 규칙을 발견 했을 것이다. NetStream 객체의 publish() 메쏘드는 채널을 발행하고, play() 메쏘드는 채널에 접근하는 것이며, 발행된 채널에 플래시 플레이어가 직접 통신할 수 있도록 하기 위해서는 NetStream객체를 생성할 때 NetStream.DIRECT_CONNECTIONS 상수를 이용하는 점이다. 그리고 LocalConnection 객체처럼 채널을 발행한 쪽에서 채널을 듣고 있는 쪽에 등록된 메쏘드를 호출할 수 있다는 점을 알았을 것이다.

일단 이렇게 해서 서버와 클라이언트의 동작원리를 알 수 있는 애플리케이션을 구현했다. 그런데 우리가 원하는 P2P는 서버와 클라이언트의 분리적인 개념을 넘어서 서버의 역할과 클라이언트의 역할을 동시에 할 수 있어야 한다. 따라서 예제로 만든 서버와 클라이언트의 기능을 합쳐야 진정한 P2P라 할 수 있다. 두 기능을 합쳐 <그림 1-5>처럼 양방향으로 채팅할 수 있는 애플리케이션을 만들어 보자.

<그림 1-5> Strarus 서비스를 이용해 간단히 구현한 양방향 채팅 애플리케이션
 
  
일단 <그림 1-5>에서 보는 StratusP2P testBed는 같은 내용의 애플리케이션이며, 빌더에서 두 개의 애플리케이션을 실행하기 위해 별도의 프로젝트로 만든 것이다. 일단 두 애플리케이션을 실행한 후 발행된 Net ID가 화면에 출력되면, 고유 키를 복사해 연결할 애플리케이션에 붙여 넣고 Connect peer 버튼을 클릭한다. 그러면 <그림 1-5>처럼 연결되었습니다라는 메시지가 출력된 것을 볼 수 있다. 그 후부터 하단의 입력 창에 텍스트를 입력하고 send 버튼을 클릭하면 연결된 애플리케이션에 텍스트 데이터가 전달된다. <그림 1-5>에서 구현한 소스의 내용은 다음과 같으며, 최대한 간단하게 구현했다. 기본 원리에서 이미 설명된 부분이므로 내용을 이해하는데 크게 무리는 없을 것이다. 


<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml"
           layout="absolute" applicationComplete="init()" height="340"
width="400"
           verticalScrollPolicy="off"
           horizontalScrollPolicy="off">

           <mx:Script>
           <![CDATA[                             
           private const DEVELOPER_KEY:String = "Stratus develop Key";
           private const STRATUS_URL:String = "rtmfp://stratus.adobe.com";
           private var netConnection:NetConnection;
           private var listenerStream:NetStream;
           private var sendStream:NetStream;
           private var isPeerConnected:Boolean = false;
                               
           private function init():void
{
          //Stratus 서비스에 연결
             onConnect();

connectPeerBtn
.addEventListener(MouseEvent.CLICK, connectPeerHandler);
}
                               
           private function connectPeerHandler(e:MouseEvent):void
{
          //고유키를 이용해 플레이어와 연결
             connectPeer(peerId.text);                                         

           }
                               
           private function connectPeer(targetId:String):void
{
          //이미 다른 플레이어와 연결되어 있다면 더 이상 연결하지 않는다.
             if(isPeerConnected) return;
             isPeerConnected = true;

             //연결된 아이디를 입력 창에 기입하고 연결 액션을 할 수 없게 한다.
             peerId.text = targetId;
             peerId.editable = false;
             peerId.enabled = false;
             connectPeerBtn.enabled = false;
            
          //고유 키를 가진 플레이어에 연결한다.
             sendStream = new NetStream( netConnection, targetId );           
             sendStream.addEventListener(
NetStatusEvent.NET_STATUS, peerListenerHandler );

          //연결된 플레이어에서 호출할 수 있는 메쏘드를 등록
             sendStream.client =
             {
                 sendText : function(msg:String):void{
                   result.text += msg +"n";
               }
              }

              //연결된 플레이어의 채널을 청취.
              sendStream.play("listenChannel");
           }
                               
           private function peerListenerHandler(event:NetStatusEvent):void
{
  //플레이어와 연결하고 플에이어의  NetStream을 청취할 수 있는 상태라면
             if(event.info.code == 'NetStream.Play.Start')
{
               isPeerConnected = true;
             }
                                         
           }
                                         
           private function onConnect():void
  {
               //Stratus 서비스에 연결
               netConnection = new NetConnection();                                          
netConnection.addEventListener(
NetStatusEvent.NET_STATUS, netConnectionHandler);
netConnection.connect(STRATUS_URL + "/" + DEVELOPER_KEY);

           }
                               
           private function netConnectionHandler(event:NetStatusEvent):void
           {
               //Stratus 서비스와 연결이 성공했다면
               if(event.info.code == 'NetConnection.Connect.Success')
{
   //발급된 고유 키를 출력한다.
   result.text += "Net ID -> " + netConnection.nearID+  "nn";
  
   //발급된 고유 키에 해당되는 채널을 생성한다.
                  listenerStream = new NetStream(
netConnection, NetStream.DIRECT_CONNECTIONS);
listenerStream.addEventListener(
NetStatusEvent.NET_STATUS, listenerHandler);

               //채널 생성
                  listenerStream.publish("listenChannel");                                      

                  listenerStream.client = {

                   //채널에 다른 플레이어의 NetStream이 연결되면 실행될 메소드
                      onPeerConnect : function(caller:NetStream):Boolean
                        {
                       //연결된 채널을 청취한다.
                            connectPeer(caller.farID);                                                                           return true;                                        
                        }     
                  }
                }

        }
                  
         private function listenerHandler(evt:NetStatusEvent):void
{
    //발행한 채널에서 상태 변화가 발생하면 출력한다.       
            result.text += evt.info.code+"n";                            

         }
                  
         private function sendMsg():void
{
    //발행한 채널에 연결된 클라이언트에 등록된 sendText 메쏘드를 호출한다.
            listenerStream.send("sendText",msgText.text);
            msgText.text = "";

         }

           ]]>
           </mx:Script>

           <mx:TextArea x="10" y="40"
width="378" height="207" id="result"
wordWrap="true" fontSize="11" fontFamily="돋움"/>

           <mx:TextInput x="10" y="10" width="246" id="peerId"/>

           <mx:TextArea x="10" y="255" width="315" height="53" id="msgText"/>

           <mx:Button x="333" y="254" label="send"
width="55" height="54" id="msgSendBtn" click="sendMsg()"/>

           <mx:Button x="264" y="9" label="Connect peer" id="connectPeerBtn"/>

</mx:WindowedApplication>


NetConnection 객체와 NetStream 객체의 API 문서를 보면, 더 많은 기능을 활용할 수 있는 부분을 찾아 볼 수 있을 것이며, 간단한 예제지만 구현해보고 나면 이런 막강한 기능을 어디에 써야 할지 고민에 빠지게 될 것이다.
사실 스킨도 입혀서 좀 더 멋지고, 화려하게 할 수 있는 기술 문서를 작성하고 싶었지만, 여러분에게 이런 좋은 기능을 하루 빨리 소개하고 싶었다. 기본 원리를 이해하고 P2P 통신을 하는 애플리케이션까지 구현해봤다면, 파일 전송 기능도 구현해 보고, 비디오, 오디오를 활용한 화상 채팅 구현에 도전해 보길 바란다. 플래시로 구현하는 P2P에 대한 반응이 좋다면, 다음 기술 문서는 매쉬업과 P2P의 활용 예제를 다뤄볼까 한다.



http://www.dude.co.kr

P 이경철님의 파란블로그에서 발행된 글입니다.

댓글 없음:

댓글 쓰기