실전강의실 4 델파이로풀어보는애플리케이션개발테크닉메신저만들기 1 메신저는인터넷환경에서가장많이쓰이는프로그램중하나이다. 초기메신저는텍스트위주의기능으로가볍고구조도간단했다. 그러나하나둘씩부가기능이추가되고소켓을이용해구현하다보니구조가상당히복잡해졌다. 객체지향프로그래밍 (OOP) 기반의간단한메신저를만들어봄으로써소켓프로그래밍의구조를이해하고, 리치에디트와 ActiveX를이용한다양한메시지표현방법을배워보자. 연재순서 4 회 2006. 8 멋진위젯만들기 5 회 2006. 9 메신저만들기 1 6 회 2006. 10 메신저만들기 2 양병규 delmadang@hanmail.net 빵집개발자이며현재는삼성서울병원 EMR 프로젝트를진행하고있다. 주로델파이를이용해프로젝트를추진하고, 틈틈이델파이와애플리케이션제작에관한강의도맡고있다. 오래전부터짬을낼수있다면새로운운영체제를만들고싶다는꿈을간직해오고있다. 인스턴트메신저가본격적으로발전하게된데에는 ICQ의영향이크다. ICQ 이전의인스턴트메신저는용어자체가생소할정도로거의쓰이지않았다. ICQ를시작으로마이크로소프트 MSN 메신저와네이트온등이차례로발표되면서지금은웹브라우저에버금가는사용률을보이고있다. 인스턴트메신저가확산되면서기능상의변화도많았다. 그중가장큰변화는바로이모티콘기능이다. 텍스트기반의대화에서작은아이콘형태의이미지를감정표현의용도로활용하게되었다. 또파일전송이나간단한그림을그릴수있는잉크기능등다양한부가기능을지원한다. 물론그에따른구현의어려움이동반될수밖에없다. 제작할메신저의방향이번메신저만들기는 2회로나눠다룰예정이다. 이번호에는클라이언트와서버에서의소켓프로그래밍에쓰이는패턴을소개한다. 가장간단한형태의메신저를만들더라도텍스트를주고받는대화자체에대한처리와친구목록등을관리하는시스템적인처리등최소한두가지이상의패킷을처리해야한다. 여기에파일전송이나잉크기능등대화이외의기능이추가된다면패킷과소켓은더욱복잡해진다. 최근들어기능별로한개의포트를할당하는방법이쓰이기도하는데이방식은방화벽등장 애물이많다는단점이있다. 여기서는한개의포트를사용하면서각기능마다한개씩의클라이언트소켓을할당, 패킷설계중심이아닌 OOP적객체설계중심의소켓프로그래밍방법을소개하고자한다. 예를들어친구목록을처리하는시스템패킷과대화텍스트를처리하는메시지패킷을별도의소켓으로처리하면서두개의소켓이동일한포트를사용하는방법이다. 다음시간에는앞서제작한메신저를바탕으로이모티콘기능을추가한다. 이모티콘기능을구현하기위해서는에디터가텍스트가아닌다른형태의서식표현도가능해야한다. 그방법으로리치에디트를사용할것이다. 리치에디트와함께이모티콘의구현에 ActiveX 컨트롤을이용한다. 아울려시스템패킷과메시지패킷이외의또다른패킷을추가하여새로운기능을만들예정이다. 이과정에서패킷변화에따른객체들의변화를살펴볼수있을것이다. 기본기능여기서구현한메신저의기본적인기능을정리하면다음과같다. - 우선회원가입은생략하도록한다. 로그인은패스워드없이임의의닉네임으로로그인이되도록하자. micro software
메신저만들기 1 - 친구추가등친구관리기능은모두시스템패킷으로처리한다. 시스템패킷을최소한으로하기위해친구관리기능을따로만들지않으며, 현재로그인된모든사용자를친구로인식하게한다. - 소켓과크게관계없는기능들은모두생략한다. - 다음호에서이모티콘과추가적인기능을구현한다. 패킷구조소켓프로그래밍을하다보면패킷설계는필수과정이다. 그런데잘못설계할경우자칫대형사고로이어질수있기때문에여간신경쓰이는부분이아닐수없다. 더군다나기능이많아지다보면패킷도상당히복잡해져구현하기도점점어려워진다. 이번시간에강조하고자하는것은기능이많으니패킷도복잡하게만들자는것이아니다. 기능이많을수록 OOP적으로객체를설계해패킷은줄이고안정적이고빠르면서유지보수가손쉬운소켓프로그래밍을만들자는것이다. 따라서패킷은다음과같이아주간단하게만들자. 명령, 데이터 ; 맨앞에는패킷의종류를가리키는명령과실제데이터로나누고명령과데이터는콤마로구분한다. 또한패킷의마지막은세미콜론으로끝내도록한다. 물론대화에서직접세미콜론을사용하는등의예외는각자의방식으로처리한다. < 표 1> 은실제로사용하게될패킷들의전체목록이다. 소켓구분명령데이터설명 Start SYSTEM 사용자이름이소켓은시스템소켓으로사용 Packet MESSAGE 사용자이름이소켓은메시지소켓으로사용 System List 친구목록전체친구목록을받아온다. AddBuddy 추가할친구이름친구가로그인했음을알린다. DeleteBuddy 삭제할친구이름친구가로그오프했음을알린다. Message 상대친구이름대화내용대화내용을전송한다. < 표 1> 패킷목록클라이언트가서버에연결되면곧바로 Start 패킷을서버로전송한다. Start 패킷은이클라이언트소켓이어떤용도로사용되는지를알려주는역할을하는데시스템소켓은명령을 SYS TEM 으로전송하며메시지소켓은 MESSAGE 를전송한다. 그러면서버에서는연결된클라이언트소켓의데이터처리객체를각종류에맞는처리객체를생성하게된다. 클라이언트구조클라이언트는메인폼인친구목록창과대화창, 그리고로그 인할때사용하는대화상자등세개의폼으로구성하자. 우선프로그램이실행되면대화상자를띄워서사용자닉네임을입력받아로그인한다음친구목록창을띄운다. 서버에서친구목록을전송받으면친구목록창에보여주고친구목록을더블클릭하면해당친구와대화할수있는대화창이열리게하자. 클라이언트에서사용할소켓은시스템소켓과메시지소켓으로구분한다. 물론둘다클라이언트소켓을이용한다. 클라이언트소켓은델파이의기본소켓인 TClientSocket을이용하도록한다. 단, TClientSocket을그대로이용하는것이아니라그것을상속하여여기서원하는기능을추가한다음다시그것을상속하여시스템소켓과메시지소켓으로구분한다. < 그림 1> 은클라이언트소켓의상속구조이다. TSystemSocket 1. 친구목록을관리하는소켓 2. ViewStrings : TStrings 를내장하여친구목록을보여주는역할을겸한다. < 그림 1> 클라이언트소켓의상속구조 TClientSocket 델파이의기본클라이언트소켓 TMessengerClientSocket 1. 기본소켓에메신저를구현하기위한기능들을추가한다. 2. 서버에연결되면이소켓의용도를서버에전송하고 3. 서버에서받은데이터를파싱하여처리한다. TMessageSocket 1. 대화를처리하는소켓 2.OnShowMessage : TShowMessage 이벤트를이용하여대화창에대화를출력할수있게한다. 실제로클라이언트에서사용할소켓은친구관리를하는 TSy stemsocket과대화내용을처리하는 TMessageSocket 등두개이다. 이두소켓은모두동일한포트번호를사용한다. 서버에서는같은 IP 로부터연결된두개의클라이언트소켓이어떤용도로사용되는지를알수없으므로이두소켓은연결되자마자곧바로 Start 패킷을서버로전송하여자신의용도를서버에알린다. 이후과정은두소켓이각자정해진용도로구현되도록하면된다. TMessengerClientSocket 이두개의소켓부모클래스인 TMessengerClientSocket 클래스는연결시 Start 패킷을전송하는일과서버에서전송받은패킷을파싱하여하위클래스가편리하게사용될수있도록처리 micro software
실전강의실 4 하는일을한다. 생성자에서이벤트핸들러를할당하고포트번호와서버주소등을설정한다. 생성자의파라미터로사용자이름을전달받아저장해둔다. 하위클래스들이패킷을처리할수있도록 ProcessCommand라는추상메소드를선언한다. 소켓종류를가리키는 Start 패킷의명령역시추상메소드로선언하여하위클래스에서이를오버라이드 (override) 하여자신에맞는명령을리턴할수있게한다. < 리스트 1> 은 TMessenger Client Socket의선언부이다. 할수있도록 ViewStrings: TStrings를내장하여직접처리한다. < 리스트 2> 는 TSystemSocket의구현부이다. < 리스트 2> TSystemSocket 의구현부 // 이소켓의용도가시스템소켓임을알려주는메소드. 이메소드의리턴값은이소켓이서버와연결되면곧바로서버로전송되는 Start 패킷의명령으로사용된다. function TSystemSocket.GetStartData: String; Result := SYSTEM ; < 리스트 1> TMessengerClientSocket 의선언부 TMessengerClientSocket = class(tclientsocket) private FUserName: String; FReceveData: String; // 서버와연결되면발생하는이벤트. 이이벤트에서는 Start 패킷을서버로전송하는일을한다. procedure ConnectEvent(Sender: TObject; Socket: TCustomWinSocket); // 상위클래스에서추상메소드로선언된데이터처리하는메소드로써각명령에맞는메소드를재호출한다. procedure TSystemSocket.ProcessCommand(const Command, Data: if Command = List then CommandList( Data ) else if Command = AddBuddy then CommandAddBuddy( Data ) else if Command = DeleteBuddy then CommandDeleteBuddy( Data ); // 서버에서데이터를전송받을때발생하는이벤트. 전송받은패킷을파싱하여하위클래스에서사용할수있도록처리한다. procedure ReadEvent(Sender: TObject; Socket: TCustomWinSocket); protected //Start 패킷의명령. 이소켓의용도를정하는추상메소드로써하위클래스는반드시이메소드를오버라이드하여자신의용도를정해야한다. function GetStartData: String; virtual; abstract; // 서버로데이터를전송하는메소드 procedure Send(const Command, Data: virtual; // 서버로부터전송받은패킷을파싱하는메소드 procedure Parse; virtual; // 파싱된패킷을처리하는메소드. 하위클래스가자신의용도에맞게오버라이드하여사용한다. procedure ProcessCommand(const Command, Data: virtual; public // 생성자, 반드시사용자이름을전달받는다. constructor Create(const UserName: reintroduce; destructor Destroy; override; // 친구목록전체를전송받은경우이다. FViewStrings 에목록전체를표시한다. procedure TSystemSocket.CommandList(const Data: if FViewStrings <> nil then FViewStrings.Text := Data; // 새친구가로그인했음을알리는경우이다. FViewStrings 에새친구를추가한다. procedure TSystemSocket.CommandAddBuddy(const Data: var Index: Integer; Index := FViewStrings.IndexOf( Data ); if Index < 0 then FViewStrings.Add( Data ); // 친구가로그아웃했음을알리는경우이다. FViewStrings 에서해당친구를삭제한다. procedure TSystemSocket.CommandDeleteBuddy(const Data: var Index: Integer; Index := FViewStrings.IndexOf( Data ); if Index >= 0 then FViewStrings.Delete( Index ); TSystemSocket TSystemSocket은친구목록만다루는클라이언트소켓으로상위클래스의패킷처리메소드인 ProcessCommand을오버라이드하여각명령에맞는메소드를재호출한다. 여기서는편의상친구목록을리스트박스로표현했다. 친구목록을화면에출력 TMessageSocket TMessageSocket은대화를주고받을때사용되는소켓이다. 상위클래스의패킷처리메소드인 ProcessCommand을오버라이드하여대화를전송받았음을이벤트로호출하여처리한다. 또한대화를보낼수있는메소드인 SendMessage 메소드를제공 micro software
메신저만들기 1 하여친구에게메시지를전송할수있도록한다. < 리스트 3> 은 TMessageSocket의구현부이다. < 리스트 3> TMessageSocket 의구현부 // 이소켓의용도가메시지소켓임을알려주는메소드. 이메소드의리턴값은이소켓이서버와연결되면곧바로서버로전송되는 Start 패킷의명령으로사용된다. function TMessageSocket.GetStartData: String; Result := MESSAGE ; // 상위클래스에서추상메소드로선언된데이터처리하는메소드로써대화내용을전송받은경우에호출된다. 이벤트를호출하여화면에표시할수있도록한다. procedure TMessageSocket.ProcessCommand(const Command, Data: if Assigned( OnShowMessage ) then OnShowMessage( Command, Data ); // 친구에게대화를전송하는메소드. 상대친구이름을파라미터로지정한다. procedure TMessageSocket.SendMessage(const ToBuddyName, Msg: Send( ToBuddyName, Msg ); 클라이언트완성하기이제클라이언트를완성할단계이다. 새폼에친구목록으로사용할 TListBox를얹어놓고 OnCreate에서시스템소켓과메시지소켓을생성한다. 그리고시스템소켓의 ViewStrings로 ListBox.Items를할당한후 Open하자. 대화창으로사용할새폼을하나더만들어 TMemo를두개올려놓는다. 하나는메시지입력용, 다른하나는전송되거나받은메시지의출력용이다. 이대화창은목록으로관리할필요가있다. 메시지소켓에서메시지가전송되면열려있는대화창에서해당친구를찾아메시지를보여야하기때문이다. 대화창목록은 TList를이용하여만들자. 주요메소드로친구목록창에서친구를더블클릭하면대화창을생성해주는 ShowBuddy 메소드와메시지를출력해주는 ShowMessage 메소드는반드시만들자. 대화창에서메시지를입력한후엔터키를누르면메시지소켓을이용하여메시지를전송한다. < 리스트 4> 는대화창목록인 TBuddyList의선언부이다. 서버구조앞에서설명했듯이여기서만드는메신저는한개의포트와패킷을최대한단순화하여작성하였다. 서버소켓도하나만사용한다. 클라이언트에서는시스템소켓과메시지소켓등각기능마 < 리스트 4> 시작 TBuddyList = class private FList: TList; // 대화창목록에서메시지소켓을제공함으로써대화창에서는친구목록창을직접참조하지않도록한다. FMessageSocket: TMessageSocket; function GetItems(Index: Integer): TBuddy_Form; procedure SetMessageSocket(const Value: TMessageSocket); protected // 대화창을목록에추가한다. 대화창이생성될때자신을목록에추가한다. procedure Add(Buddy_Form: TBuddy_Form); // 열려있는대화창의수. function Count: Integer; // 열려있는대화창을모두닫는다. procedure Clear; // 대화창을목록에서제거한다. 대화창이닫힐때자신을목록에서제거한다. procedure Remove(Buddy_Form: TBuddy_Form); // 대화창을친구이름으로찾는다. function IndexOf(const BudyName: String): Integer; property Items[Index: Integer]: TBuddy_Form read GetItems; default; public constructor Create; destructor Destroy; override; // 대화창을생성하는메소드. 친구목록에서친구를더블클릭할때호출된다. procedure ShowBuddy(const ABuddyName: // 대화창에메시지를출력하는메소드. 메시지소켓에서메시지가전송될때호출된다. procedure ShowMessage(const FromBuddyName, Msg: property MessageSocket: TMessageSocket read FMessageSocket write SetMessageSocket; 다하나의클라이언트소켓을사용했다. 그것을 Start 패킷으로구분하고 Start 패킷에의해소켓의용도가결정되면서버소켓은그용도에맞는처리기객체를생성한다. 다음부터는각처리기객체가알아서처리하게된다. 또한 Start 패킷을처리하는것역시 Start 전용처리기를이용한다. 그러므로여기서는모두세가지의처리기가사용된다. 패킷처리기외에살펴보아야할부분은서버소켓에서는클라이언트가연결될때마다그클라이언트를처리하는클래스를내부적으로생성하여사용하게된다. 여기서는그클라이언트처리클래스를그냥사용하는것이아니라기본클래스에서상속하여원하는기능을추가해사용한다는것이다. 델파이의기본서버소켓인 TServerSocket은클라이언트의변화를이벤트를통해알려주는데각이벤트에는어떤클라이언트인지를가리키는 Socket: TCustomWinSocket 파라미터가있다. 여기서는클라이언트를가리키는클래스를새로정의한클래스로리턴되도록할것이다. 그러므로이파라미터는우리가새 micro software
실전강의실 4 로정의한클래스형으로타입캐스팅하여사용하게된다. < 그림 2> 는서버소켓의구조이다. TServerSocket 델파이의기본서버소켓 클라이언트를처리하는클래스를내장하고있다. 클라이언트처리객체리스트 TMessengerServerSocket 기본서버소켓의클라이언트처리기를 TMessengerClient 로생성하게한다. 클라이언트에서패킷이전송되거나연결, 해제될때 TMessengerClient 가처리할수있게한다. 클라이언트이벤트발생 TClientProcessor 패킷처리기추상클래스 TServerClientWinSocket 서버소켓에서클라이언트를처리하는기본클래스. 클라이언트가연결, 해제되거나패킷을전송받고전송할때사용된다. TMessengerClient 서버의클라이언트처리클래스에메신저의기능을추가한클래스다. 패킷처리기를내장하고있으며최초에는 Start 패킷처리기를먼저사용하며, Start 패킷을받은후에는 Start 패킷처리기가알아서해당처리기를생성한다. 패킷패서 패킷처리기 OnClientRead 이벤트의핸들러인 ClientRead 메소드에서직접패킷을읽어들일수도있으나클라이언트에서전송된패킷은클라이언트처리클래스가읽어들이는것이 OOP에합리적이므로 TMessengerClient의메소드를호출하여처리하게한다. TMessengerClient 서버소켓에서클라이언트를처리하는클래스로기본소켓기능에원하는기능을추가한클래스이다. 패킷처리기를내장하고있는데 StartProcessor, DataProcessor, CurrentProcessor 등세가지의처리기를가지고있다. StartProcessor는초기에생성되는처리기로연결된클라이언트가어떤종류인지를구분한다. DataProcessor는그후에전송되는패킷을처리한다. 이두객체는실제로생성되는객체이며 CurrentProcessor는이둘중하나를가리키는가상객체이다. TMessengerClient의 Parse 메소드에서는전송받은패킷을세미콜론으로구분하여블록화하고매블록을처리기에게처리하도록요청한다. 이때 Start Proc essor나 DataProcessor가아닌 CurrentProcessor를호출하도록하여나중에새로운처리기가추가되더라도소스코드의수정을최소화하도록한다. TClientProcessor TStartProcessor Start 패킷처리기 Start 패킷에따라다음패킷을처리할처리기를생성한다. TSystemProcessor 시스템패킷처리기 친구목록등을처리한다. TMessageProcessor 메시지패킷처리기 메시지를처리한다. TMessengerClient에서패킷을처리할때사용되는패킷처리기의추상클래스이다. 패킷을처리하는메소드인 Process 추상메소드와하위클래스를위하여서버소켓에연결되어있는다른클라이언트들을간편하게액세스할수있는기능을가지고있다. < 그림 2> 서버소켓의구조 델파이의기본서버소켓인 TServerSocket을상속한클래스이다. 메신저서버의기능을추가하는것이아니라 TServer Socket이내장하고있는클라이언트처리클래스인 TServer ClientWinSocket을여기서새로정의한클라이언트처리클래스인 TMessengerClient로대체하는역할을한다. 기본클라이언트처리기인 TServerClientWinSocket는단순히클라이언트가연결, 해제, 전송등소켓의기본기능에대한처리만담당하므로이기본소켓기능위에새로운기능을추가하면된다. 생성자에서 OnGetSocket 이벤트는 GetSocket 메소드로, OnClientRead 이벤트는 ClientRead 메소드로할당한다. OnGetSocket 이벤트핸들러인 GetSocket 메소드에서는파라미터인 var ClientSocket: TServerClientWinSocket에 TMess engerclient를생성하여할당한다. 그러면서버소켓은기본클라이언트처리기로이것을사용하게된다. TStartProcessor 서버소켓에클라이언트가연결되면처음으로전송되는패킷인 Start 패킷을처리하는클래스이다. Start 패킷의내용에따라클라이언트의처리기를생성하는역할을한다. < 리스트 5> 는 TStartProcessor의구현부이다. < 리스트 5> TStartProcessor 의구현부 procedure TStartProcessor.Process(const Command, Data: //Start 패킷에서 Data 파라미터는사용자의이름을가리키고클라이언트클래스의 UserName 멤버를 Data 로할당한다. FMessengerClient.FUserName := Data; //Start 패킷의명령이 SYSTEM 인경우클라이언트클래스의패킷처리기로 TSystemProcessor 를생성한다. if Command = SYSTEM then FMessengerClient.FDataProcessor := micro software
메신저만들기 1 TSystemProcessor.Create( FMessengerClient ); // 클라이언트클래스의 CurrentProcessor를새로운처리기로변경한다. FMessengerClient.FCurrentProcessor := FMessengerClient.FDataProcessor; // 시스템클라이언트는생성된직후연결된모든클라이언트에새친구가로그인했음을알리고 TSystemProcessor( FMessengerClient.FCurrentProcessor ).SendAddToAll; // 새친구에게는전체목록을전송해준다. TSystemProcessor( FMessengerClient.FCurrentProcessor ).SendListToClient; end else //Start 패킷의명령이 MESSAGE 인경우클라이언트클래스의패킷처리기로 TMessageProcessor를생성한다. if Command = MESSAGE then FMessengerClient.FDataProcessor := TMessageProcessor.Create( FMessengerClient ); // 클라이언트클래스의 CurrentProcessor를새로운처리기로변경한다. FMessengerClient.FCurrentProcessor := FMessengerClient.FDataProcessor; TSystemProcessor TSystemProcessor는친구목록패킷을처리하는처리기클래스이다. Start 패킷처리기인 TSystemProcessor에의해생성된다. 친구목록을클라이언트로전송하거나클라이언트에서친구목록추가, 삭제등요청을받는다. < 리스트 6> 은 TSystem Pro cessor의선언부이다. 된다른클라이언트에서해당친구를찾아서메시지를전달한다. 이때 Command 파라미터가 받는친구 에서 보내는친구 로변경되는것에주의해야한다. < 리스트 7> 은 TMessage Pro cessor의구현부이다. < 리스트 7> TMessageProcessor 의구현부 procedure TMessageProcessor.Process(const Command, Data: var Client: TMessengerClient; i: Integer; // 서버에연결된클라이언트의수만큼반복. for i := 0 to GetClientCount - 1 do Client := GetClient( i ); // 이클라이언트가대상클라이언트이고이클라이언트의처리기가메시지처리기라면 if ( Client.UserName = Command ) and ( Client.CurrentProcessor is TMessageProcessor ) then // 이클라이언트에메시지를전달한다. 이때 받는친구 를 보내는친구 로바꿔전달한다. TMessageProcessor( Client.CurrentProcessor ).SendMessageToClient( FMessengerClient.UserName, Data ); Break; procedure TMessageProcessor.SendMessageToClient(const FromBuddy, Data: FMessengerClient.Send( FromBuddy, Data ); < 리스트 6> TSystemProcessor 의선언부 TSystemProcessor = class(tclientprocessor) protected // 클라이언트에서전송받은패킷을처리한다. procedure Process(const Command, Data: override; // 서버에연결된모든클라이언트로자신이로그인했음을알리는메소드. procedure SendAddToAll; // 클라이언트에다른친구가로그인했을음알리는메소드. procedure SendAddToClient(const Data: // 서버에연결된모든클라이언트로자신이로그오프함을알리는메소드. procedure SendDeleteToAll; // 클라이언트에다른친구가로그오프했음을알리는메소드. procedure SendDeleteToClient(const Data: // 클라이언트에다른친구들의목록을알려주는메소드. procedure SendListToClient; TMessageProcessor TMessageProcessor는메시지를주고받을때사용되는처리기이다. 패킷을처리하는메소드인 Process에서는서버와연결 서버완성하기새폼에서서버소켓인 TMessengerServerSocket을생성하자. 모든기능은서버소켓과클라이언트처리클래스, 그리고패킷처리기가하므로폼에서는별다른작업이없다. 이렇게완성된메신저의사용방법은아주간단하다. 서버를실행한상태에서클라이언트를실행하고대화명을입력한다. 차례로추가클라이언트가등장하면새클라이언트가등장할때마다친구목록이갱신되는것을볼수있을것이다. 친구목록에서한친구를더블클릭하여메시지를전송해보자. 아주간단한메신저이지만얼마든지기능을추가할수있는구조로만들었다. 다음호에서는여기에이모티콘기능을비롯한다양한기능을추가해보도록하겠다. 이달의디스켓 : messenger.zip micro software