소켓프로그래밍기법의활용 (Using Socket Programming Techniques) 윈도우에서의프로세스간통신기법으로는명명된파이프, DCOM, DDE, 클립보드와각종네트워크프로그래밍기법등을이용할수있다. 이중에서도윈도우 95 와윈도우 NT 3.5 버전부터는내부적인통신프로토콜로기존의 NetBIEU 와함께 TCP/IP 를사실상의표준으로인정하고이를지원하고있다. 또한, DCOM 과윈도우소켓을프로세스간통신의표준으로삼고있으며, 윈도우 NT 4.0 부터는윈도우소켓의 2.0 버전 (WinSock 2.0) 을사용하여보다강화된소켓프로그래밍을지원하게되었다. 이러한소켓프로그래밍을위해서는 Win32 에서지원하는 API 를직접이용하여프로그래밍을할수도있겠으나, 델파이에서지원하는소켓컴포넌트를이용하면쉽게소켓을지원하는어플리케이션을지원할수있다. 이번장에서는소켓컴포넌트를이용하는방법과소켓프로그래밍기법을익혀보도록한다. 소켓프로그래밍의기초 소켓이란네트워크프로토콜을구현할때여기에저장된데이터를포함한커다란집합에대한핸들이다. 쉽게말하면네트워크에대한파일핸들이라고생각하면된다. 네트워크프로그래밍의기본은 TCP/IP 연결을하고, 소켓을생성해서이를전송하거나받는것이다. 소켓에는원래서버용, 클라이언트용소켓이분리되어있는것은아니다. 그렇지만델파이에서는 TServerSocket, TClientSocket 으로분리된소켓을제공하고있다. 이들은모두 TCustomSocket 클래스에서상속받은것으로내부적으로는동일한소켓을사용하고, 사용방법도거의비슷하다. 서버가되는소켓은클라이언트측소켓과는달리클라이언트의연결요구를기다리는 listen 이라는작업을해야하고, 클라이언트는서버에연결 (connect) 해야한다. 여기서양측소켓의 Active 프로퍼티를 True 로설정하면서버와클라이언트가접속된다. 또한, TServerSocket 클래스에는여러개의클라이언트가접속되었을때이를관리할수있는기능을추가로가지고있다. IP 주소와포트 델파이의소켓컴포넌트에는 Address 와 Port 라는프로퍼티가있다. Address 프로퍼티는 IP 주소를나타내며, Port 프로퍼티는서버로들어오는메시지를통과시키는번호이다. 포트가없으면하나의서버컴퓨터에는하나의서버프로그램만설치할수있다. 포트번호
를이용해서동일한서버컴퓨터에여러개의서버프로그램을사용할수있다. 예를들어메일서버, 뉴스그룹서버, 웹서버, FTP 서버, 텔넷서버등의프로그램들은모두다른포트를사용한다. 이런포트번호중일반적으로사용되는서비스에관한것들이있는데 SMTP 는 25, NNTP 는 119, Telnet 은 23, FTP 는 21 을보통사용한다. 소켓과소켓연결 (Socket Connection) 소켓은네트워크어플리케이션이네트워크상의다른시스템사이를통신할수있도록도와주는도구가된다. 각각의소켓은하나의네트워크연결로생각할수있는데, 여기에는어플리케이션이실행되는시스템, 인터페이스종류, 연결에사용된포트에대한주소가있어야한다. 그러므로, 소켓연결에대해서충분히알기위해서는각연결부분에대한소켓의주소를반드시알아야한다. 이렇게소켓연결을하기전에연결부분을담당하게되는소켓에대한정보를제공해야하는데, 일부의정보는어플리케이션이실행되고있는시스템에서알아낼수있다. 예를들어, 각클라이언트소켓의로컬 IP 주소에대한정보는운영체제에서알아낼수있으므로따로제공할필요가없다. 따로제공해야하는정보는현재작업하고있는소켓의종류에따라달라지는데, 클라이언트소켓은연결하고자하는서버에대한정보를제공해야하며서버소켓은제공하는서비스를제공하는포트에대한정보를제공해야한다. 이러한소켓연결에대한정보에는 IP 주소와포트번호가모두포함된다. 호스트 (Host) 란? 호스트는소켓을포함한어플리케이션이동작하는시스템을말한다. 이렇게소켓에대한호스트를지정할때에는다음과같이표준인터넷주소로사용되는 IP 주소표기방식을많이사용한다. 123.123.1.2 하나의시스템은하나이상의 IP 주소를지원하게된다. IP 주소는기억하기도어렵거니와알아보기도쉽지않기때문에, 호스트의이름을지정하는방식을같이사용하여이러한단점을극복한다. 이렇게이름으로된방식의 IP 주소에대한앨리어스를 URLs(Uniform Resource Locators) 라고하며, 다음과같은도메인이름과서비스를포함한형태가된다. http://www.examsite.com
서버소켓은시스템에서로컬 IP 주소를알아낼수있기때문에호스트를지정할필요가없다. 만약로컬시스템이하나이상의 IP 주소를가지고있을경우에는서버소켓은모든 IP 주소에대한클라이언트의요구를이용한다. 서버소켓이연결되면클라이언트소켓은리모트 IP 주소를제공하게된다. 클라이언트소켓은반드시호스트이름이나 IP 주소를입력해서리모트호스트를지정해주어야한다. 연결의종류 소켓연결에는연결의초기화와어떤로컬소켓이연결되는지에따라기본적으로다음과같 은세가지로나누어볼수있다. 1. 클라이언트연결 (Client connections) 클라이언트연결은로컬시스템의클라이언트소켓을리모트시스템의서버소켓에연결하는것을말한다. 클라이언트연결은클라이언트소켓에의해개시되고초기화된다. 먼저클라이언트소켓이연결하고자하는서버소켓에대한정보를제공하면, 클라이언트소켓이서버소켓을찾게되고, 서버의위치를파악하게되면연결을요구한다. 서버소켓은클라이언트요구에대한큐 (queue) 를가지고있어서시간이될때마다연결을시도한다. 일단서버소켓이클라이언트연결을받아들이면클라이언트소켓에연결된서버소켓에대한모든정보를전송하게되며, 클라이언트에의해연결이완료된다 2. 리스닝연결 (Listening connections) 서버소켓이활동적으로클라이언트를찾아서연결을시도하지않고, 수동적으로클라이언트의요구를기다리는하프연결 (half connection) 상태를유지하는형태의연결이다. 서버소켓은큐를리스닝연결과연관지어서관리하며, 큐에는클라이언트의연결요구가계속기록된다. 일단서버소켓이클라이언트의연결요구를받아들이면클라이언트와연결하기위한새로운소켓을생성하게된다. 이렇게하면리스닝연결자체는다른클라이언트요구를받아들일수있도록계속열려있게할수있다. 3. 서버연결 (Server connections) 서버연결은서버소켓에의해서이루어지는것으로, 리스닝소켓이클라이언트요구를받아들이면생성된다. 일단서버가연결을받아들이면서버소켓의정보가클라이언트에게전송되어클라이언트소켓이이정보를받으면연결이완료되는형태이다.
일단연결이되면서버연결과클라이언트연결은차이가없는연결방식이다. 기본적으로클라이언트연결과서버연결은두개의종료점 (endpoint) 를가지며같은능력과같은종류의이벤트를사용한다. 그에비해리스닝연결은단지하나의종료점만을가지고있는본질적으로다른연결방식이다. 서비스프로토콜 네트워크서버와클라이언트를개발하기에앞서, 먼저어플리케이션이제공하거나사용하게될서비스에대한이해가반드시선행되어야한다. 많은서비스들은네트워크어플리케이션이반드시지원해야하는표준프로토콜을가지고있다. 만약 HTTP, FTP 등의표준서비스를지원하는네트워크어플리케이션을제작한다면, 다른시스템과통신하게되는프로토콜에대한이해가필요하다. 만약다른시스템과통신하는데있어서새로운형태의서비스를제공한다면, 제일먼저서비스를사용하게될서버와클라이언트사이의통신프로토콜을디자인해야한다. 이때에는어떤메시지가전달될것이며, 이런메시지들이어떻게조화를이루어야하며, 정보의암호화는어떤형식으로할것인지등을결정해야한다. 가끔네트워크서버와클라이언트어플리케이션에서네트워킹소프트웨어와서비스를사용하는어플리케이션사이에레이어 (layer) 를제공하는경우가있다. 예를들어, HTTP 서버는인터넷과웹서버어플리케이션사이에위치하여컨텐트를제공하고, HTTP 리퀘스트메시지에반응하게된다. 소켓은네트워크서버와클라이언트어플리케이션, 네트워킹소프트웨어사이에인터페이스를제공한다. 이때 ISAPI 등의흔히사용되는표준서버에대한 API 를복사해서사용할수도있고, 자신만의 API 를디자인해서사용할수도있다. 서비스와포트 대부분의서비스는특정포트번호와연관되어있다. 이럴때에는포트번호를서비스에대한번호코드 (numeric code) 로생각할수있다. 만약표준서비스를구현한다면델파이의윈도우소켓객체의메소드를이용하면그서비스에대한포트번호를알아낼수있다. 그에비해새로운서비스를제공하는경우라면윈도우 95 나 NT 의 Services 파일에포트번호를연관시켜지정할수있다. 소켓연결과데이터송수신 다른시스템과소켓연결을하는이유는연결을통해서정보를송수신할수있기때문이다. 이때어떤정보를주고받을지와언제어떤방식을사용할지등은소켓연결에사용된서
비스에좌우된다. 이렇게데이터를읽고쓰는데에는두가지방식이있다. 소켓에데이터를읽고쓸때비동기적인방식을사용해서네트워크어플리케이션에서다른코드의실행을방해하지않는것을논-블로킹연결 (Non-Blocking connections) 이라고하며, 데이터를읽고쓰는작업을쓰레드를이용하여독립적으로실행하는형태의연결을블로킹연결 (Blocking connections) 이라고한다. 블로킹연결 (Blocking connections) 클라이언트소켓에서는 ClientType 프로퍼티를 ctblocking 으로설정하면블로킹연결이생성된다. 클라이언트어플리케이션에따라서는읽고쓰는데에새로운쓰레드를생성하기를원할수도있는데, 이렇게하면어플리케이션은연결이완료되어데이터를읽고, 쓸때까지다른쓰레드를실행할수있다. 서버소켓에서는 ServerType 프로퍼티를 stthreadblocking 으로설정하면블로킹연결이생성된다. 블로킹연결은연결에의한데이터교환이될때까지실행이되지않으므로, 다른클라이언트연결에대해서항상새로운쓰레드가생성된다. 블로킹연결과쓰레드의이용 클라이언트소켓은블로킹연결이사용될때새로운쓰레드가자동으로생성되지않는다. 만약, 클라이언트어플리케이션이데이터를읽고, 쓰는것이외에다른작업이없다면이러한방식도큰상관이없겠지만, 연결이이루어지지않은경우에도사용자인터페이스에서다른작업을할수있게하려면새로운쓰레드를생성해야한다. 그에비해서버소켓은블로킹연결이생성될때마다각클라이언트연결에대해새로운쓰레드가생성된다. 그렇게때문에연결을통해클라이언트가데이터를읽고쓰는작업을할때다른클라이언트가이를기다리지않아도된다. 서버소켓은 TServerClientThread 객체를사용해서각연결에대한쓰레드를구현한다. 일단서버클라이언트쓰레드가실행되면, 연결되어있는클라이언트측에서연결을통해데이터를쓰고있는지검사하여그렇다면 OnClientRead 이벤트를발생시키며, 그렇지않으면 OnClientWrite 이벤트를발생시키고서버가데이터를쓰기시작한다. TWinSocketStream 클래스의활용 논 - 블로킹연결과블로킹서버연결모두연결에의해데이터를읽고, 쓸수있게되면특 정이벤트를받게된다. 이때이벤트핸들러에서는실제로데이터를읽고, 쓰는작업을윈 도우소켓객체의메소드를이용해서실행한다.
블로킹클라이언트연결에서는반드시자기자신이반대편의연결이데이터를읽고, 쓸준비가되었는지를파악해야한다. 이때 TWinSocketStream 객체를이용해서연결을통한데이터읽기, 쓰기를실행하면, 적절한시간간격등을조율할수있는메소드를제공받을수있다. 예를들어, WaitForData 메소드를호출하면서버소켓이준비될때까지기다리게할수있다. 클라이언트쓰레드의작성 클라이언트접속을위한쓰레드를작성할때에는 New Thread Object 대화상자를이용하여새로운쓰레드객체를정의할수있다. 새로운쓰레드객체의 Execute 메소드는실제쓰레드연결을통한데이터읽기와쓰레드를관리한다. 다음의코드가전형적인클라이언트쓰레드의예이다. procedure TMyClientThread.Execute; var TheStream: TWinSocketStream; buffer: string; TheStream := TWinSocketStream.Create(ClientSocket1.Socket, 60000); //TWinSocketStream 을생성한다. try while (not Terminated) and (ClientSocket1.Active) do try GetNextRequest(buffer); TheStream.Write(buffer, Length(buffer) + 1); // 버퍼의내용을서버에기록한다.... except if not(exceptobject is EAbort) then Synchronize(HandleThreadException); finally TheStream.Free;
쓰레드를이용하려면 OnConnect 이벤트핸들러를작성하면된다. 서버쓰레드의작성 서버접속에대한쓰레드는 TServerClientThread 에서상속받는다. 서버쓰레드를구현하기위해서는 Execute 메소드가아닌, ClientExecute 메소드를오버라이드해야한다. 클라이언트쓰레드와비슷하게구현하지만, 클라이언트소켓컴포넌트대신에 TServerClientWinSocket 객체를사용한다는점이가장큰차이점이다. 이객체는 ClientSocket 프로퍼티를통해접근이가능하다. 그리고, 예외처리는 HandleException 메소드를호출하면된다. procedure TMyServerThread.ClientExecute; var Stream: TWinSocketStream; Buffer: array[0.. 9] of Char; while (not Terminated) and ClientSocket.Connected do try Stream := TWinSocketStream.Create(ClientSocket, 60000); try FillChar(Buffer, 10, 0); // 버퍼를초기화한다. if Stream.WaitForData(60000) then if Stream.Read(Buffer, 10) = 0 then ClientSocket.Close; //1 분까지기다린다.... end else ClientSocket.Close; finally Stream.Free; except HandleException;
이쓰레드를사용하려면 OnGetThread 이벤트핸들러에서쓰레드를생성하면된다. 논 - 블로킹연결 (Non-Blocking connections) 클라이언트소켓에서 ClientType 프로퍼티를 ctnonblocking 으로설정하면논-블로킹연결이이루어진다. 이경우반대편의서버가데이터를읽거나쓰게되면클라이언트소켓이이를알수있으며, 이때 OnRead 또는 OnWrite 이벤트핸들러에의해서반응하게된다. 서버소켓에서는 ServerType 프로퍼티를 stnonblocking 으로설정하면논-블로킹연결이생성되는데, 논-블로킹클라이언트연결처럼클라이언트에서데이터를읽고쓰게되면 OnClientRead 또는 OnClientWrite 이벤트핸들러에의해서반응하게된다. 이러한이벤트에서소켓연결과관련한윈도우소켓객체가파라미터로전달되며, 이들객체를이용하면여러가지메소드를활용할수있다. 소켓연결에서데이터를읽을때에는 ReceiveBuf, ReceiveText 메소드를사용할수있다. ReceiveBuf 메소드는사용하기전에 ReceiveLength 메소드를사용해서반대편연결에서전송하려는데이터의바이트수를결정한후에사용한다. SendBuf, SendStream, SendText 메소드등을이용하면데이터를쓸수있다. 또한, 데이터를쓰고나서더이상소켓연결을유지할필요가없을때에는 SendStreamThenDrop 메소드를사용해서스트림에서읽은데이터를모두전송하고연결을닫도록할수있다. 참고로 SendStream, SendStreamThenDrop 메소드를사용하면소켓이연결이종료된후스트림을자동으로메모리에서해제하므로, 스트림객체를직접해제할필요가없다. 클라이언트소켓의이용 어플리케이션을 TCP/IP 클라이언트로사용할때에는클라이언트소켓컴포넌트 (TClientSocket) 를폼이나데이터모듈에추가한다. 클라이언트소켓에는연결하고자하는서버소켓과서버에서제공받을서비스를지정하게된다. 각각의클라이언트소켓컴포넌트는연결에서의클라이언트측종료점을나타내게되는클라이언트윈도우소켓객체 (TClientWinSocket) 을사용한다. 서버의지정 클라이언트소켓컴포넌트는서버시스템과연결하고자하는포트를지정할때사용할수
있는프로퍼티가있다. 서버시스템은 Host 프로퍼티에서호스트의이름을이용해서지정하거나, Address 프로퍼티에직접 IP 주소를적어넣을수있다. 만약둘다지정한경우에는호스트이름을사용한다. 포트역시 Port 와 Service 프로퍼티를이용해서지정할수있는데, Port 프로퍼티에는포트의번호를직접지정하는것이고 Service 프로퍼티에는포트번호와연관된표준서비스의이름을지정할경우간접적으로포트가지정되는것이다. 둘다지정된경우에는서비스이름을사용한다. 연결의생성 연결하고자하는서버에대한정보를클라이언트소켓컴포넌트에설정하고나면, 런타임에 서 Open 메소드를호출하여연결을시도하게된다. 디자인시에 Active 프로퍼티를 True 로설정하면어플리케이션이시작할때연결을시도한다. 연결에대한정보얻기 서버소켓과의연결이완료되면클라이언트윈도우소켓객체를사용해서연결에대한여러가지정보를얻어올수있게된다. 이때이객체를얻어오기위해 Socket 프로퍼티를사용한다. 윈도우소켓객체에는클라이언트와서버소켓이사용하는포트번호와주소를결정할때사용하는프로퍼티가있으며, SocketHandle 프로퍼티를이용해서윈도우소켓 API 를호출할때사용할소켓연결에대한핸들을얻을수도있다. 또한, Handle 프로퍼티를이용해서소켓연결에서의메시지를받는윈도우에접근할수있으며 ASyncStyles 프로퍼티를이용해서윈도우핸들이받게되는메시지의종류를결정할수도있다. 연결종료 서버어플리케이션과의소켓연결을통한통신이끝나면, Close 메소드를이용해서연결을 종료할수있다. 이때서버측에서연결을종료하면 OnDisconnect 이벤트가발생하므로, 적절한처리를해줄수있다. 서버소켓의이용 어플리케이션을 TCP/IP 서버로둔갑시키려면먼저서버소켓컴포넌트인 TServerSocket 을폼이나데이터모듈에올려놓는다. 서버소켓에서제공하려는서비스나클라이언트의요구를기다릴때사용할포트를지정할수있다. 각서버소켓컴포넌트는서버윈도우소켓객체 (TServerWinSocket) 를사용하여리스닝연결에서의서버측종료점을이루게한
다. 또한, 서버가받아들인클라이언트소켓과의연결에서의서버종료점에대한클라이언 트윈도우소켓객체 (TServerClientWinSocket) 도활용한다. 포트의지정 서버소켓이클라이언트의요구를기다리기전에 ( 이런기다림을 listening 이라고한다.) 서버가사용할포트를지정해주어야한다. 이때 Port 프로퍼티를사용해서포트를지정할수있다. 서버어플리케이션이특정포트번호와연관된표준서비스를제공하는경우라면 Service 프로퍼티를지정함으로써간접적으로포트를지정할수도있다. 만약에 Port 와 Service 프로퍼티를모두지정한경우라면서버소켓은서비스이름을사용하게된다. 클라이언트요구대기 (Listening for client request) 일단서버소켓컴포넌트의포트번호를설정하면, 런타임에서 Open 메소드를사용하여리스닝연결 (listening connection) 을생성할수있게된다. 만약에어플리케이션이시작할때리스닝연결을자동으로시작하게하고싶으면, 디자인시에 Active 프로퍼티를 True 로설정하면된다. 클라이언트에대한연결생성 리스닝서버소켓컴포넌트는클라이언트연결요구를받는족족이를허용한다. 이렇게 되면 OnClientConnect 이벤트가발생하며, 적절한처리를이벤트핸들러에서해주면된다. 연결에대한정보얻기 일단서버소켓에서리스닝연결을시작하면, 서버윈도우소켓객체를사용해서연결에대한정보를얻을수있게된다. 서버윈도우소켓객체는 Socket 프로퍼티를이용해서접근이가능하다. 이윈도우소켓객체를이용하면서버소켓컴포넌트가받아들인클라이언트소켓객체들과의모든활성화된연결을찾아낼수있으며, SocketHandle 프로퍼티를이용해서소켓연결에대한핸들을얻을수도있다. 이핸들을사용해서윈도우소켓 API 를직접호출할수도있다. 또한, Handle 프로퍼티를이용하면소켓연결에서날아온메시지를받은윈도우에접근할수있다. 각각의클라이언트어플리케이션에대한활성화된연결은서버클라이언트윈도우소켓객체 (TServerClientWinSocket) 에캡슐화되어있다. 이들모두에게접근할때에는서버윈도우소켓객체의 Connections 프로퍼티를이용하면된다. 서버클라이언트윈도우소켓객체에는연결의양쪽종료점을형성하는클라이언트와소켓객체에서사용하는포트번호
와주소를결정할수있는프로퍼티가있으며, SocketHandle 이라는프로퍼티를사용하면윈도우소켓 API 호출을할때사용할수있는소켓연결에대한핸들을얻을수도있다. 또한, Handle 프로퍼티를사용하면소켓연결에서의메시지를받은윈도우에접근할수있으며, ASyncStyles 프로퍼티에서메시지의종류를결정할수도있다. 연결의종료 리스닝연결을종료할때에는 Close 메소드를사용한다. 이메소드를통해클라이언트어플리케이션과의모든연결을단절시킬수있으며, 리스닝연결이종료되므로서버소켓은더이상새로운연결을허용하지않게된다. 클라이언트가서버소켓과의연결을종료하면 OnClientDisconnect 이벤트가발생하며, 적절한이벤트핸들러를이용해서정리를해준다. 소켓이벤트 앞에서몇가지이벤트에대하여이미언급한바가있다. 이를간단히먼저정리하면논- 블로킹연결이나블로킹연결에서소켓연결에대해언제데이터를읽고, 쓸것인지에대한이벤트가있으며, 서버측에서연결을종료할때클라이언트소켓에서받게되는 OnDisconnect 이벤트, 그리고클라이언트에서연결을종료할때서버소켓에서받게되는 OnClientDisconnect 이벤트가있다. 또한, 클라이언트와서버소켓모두연결에서에러메시지를받게되면 OnError 이벤트가발생한다. 그밖에연결을열고, 완료할때까지여러가지이벤트가발생하게된다. 클라이언트이벤트 클라이언트소켓이연결을열게되면, 다음과같은이벤트들이순차적으로발생한다. 1. 서버소켓을찾기전에 OnLookup 이벤트가발생한다. 이시점에서는찾는서버소켓을바꾸기위해 Host, Address, Port, Service 프로퍼티를변경할수없다. Socket 프로퍼티를이용해서클라이언트윈도우소켓객체에접근할수있으며, 그객체의 SocketHandle 프로퍼티를이용해서윈도우 API 를호출할수있다. 2. 윈도우소켓이설정되고, 초기화된다. 3. 일단서버소켓을찾으면 OnConnecting 이벤트가발생한다. 이시점에서는윈도우소 4. 켓객체를사용해서서버소켓에대한정보를얻을수있게되며, 연결에사용되는실제포트와 IP 주소를알수있다. 5. 연결요구가서버에의해받아들여지면클라이언트소켓에서연결이완료된다.
6. 연결이완료되면 OnConnect 이벤트가발생한다. 연결이완료되는즉시데이터를읽고, 쓰는작업을해야할때에는 OnConnect 이벤트핸들러에적절한코드를작성하면된 다. 서버이벤트 서버소켓컴포넌트는리스닝연결과클라이언트연결의두가지형태의연결을형성할수 있다. 이들각각에따라발생하는이벤트가다르기때문에, 따로나누어설명하겠다. - 리스닝연결이벤트 리스닝연결이생성되기직전에 OnListen 이벤트가먼저발생한다. 이시점에서 Socket 프로퍼티를이용해서서버윈도우소켓객체에접근할수있게된다. 이객체의 SocketHandle 프로퍼티를이용해서연결이생성되기전에소켓에대한여러가지사항을바꾸어볼수가있다. 예를들어, 서버가리스닝에사용하는 IP 주소를제한하는등의처리를할수가있다. - 클라이언트연결이벤트 1. 연결의서버측종료점을형성하는소켓에대한윈도우소켓핸들을넘겨주는 OnGetSocket 이벤트가클라이언트측에소켓객체를넘겨줄때발생한다. 이때개 발자가 TServerClientWinSocket 클래스대신에자신만의윈도우소켓객체를대신 사용하도록할수있다. 즉, OnGetSocket 이벤트핸들러에서자신의윈도우소켓객 체를생성할수있다. 2. 이어서 OnAccept 이벤트가발생하는데, 여기에는새로운 TServerClientWinSocket 객체를이벤트핸들러에넘겨준다. 여기에서처음으로 TServerClientWinSocket 객 체를이용해서클라이언트에연결된서버측종료점에대한정보를이용할수있게된 다. 3. ServerType 이 stthreadblocking 이면 OnGetThread 이벤트가발생한다. 여기에서 자신만의 TServerClientThread 객체를생성할수있으며, 이를대신사용하게된다. 이어서쓰레드가실행되기시작하면 OnThreadStart 이벤트가발생한다. 쓰레드의 초기화나쓰레드가연결을통해데이터를읽고, 쓰는작업을하려고윈도우소켓 API 를호출해야한다면, 이이벤트핸들러에서작업을한다. 4. 클라이언트가연결을완료하면 OnClientConnect 이벤트가발생한다. 논-블로킹서 버의경우에는이시점에서데이터를읽고, 쓰기를시작하면된다.
1:1 채팅예제의제작 그러면, 실제로예제를만들어나가면서지금까지설명한것들을익혀보도록하자. 이번에만들예제는 1:1 채팅을가능하게하는프로그램으로하나의어플리케이션에 TClientSocket 과 TServerSocket 을모두올려놓고, 이프로그램이경우에따라서채팅서버가되기도하고, 클라이언트가되기도하는프로그램이다. 본래채팅프로그램을제대로만들려면서버프로그램에여러개의클라이언트가접속하는형태로제작해야하지만, 이예제는네트워크프로그래밍의기본을이해시키려는목적으로제작하는것이므로 1:1 채팅만을지원하도록하였다. 이런식으로클라이언트와서버의기능을모두갖춘프로그램은프로그램을테스트하기에편리하고실제메시지를처리하면서클라이언트와서버에서메시지를처리하는방법을동시에익힐수있는장점이있다. 먼저폼에메모컴포넌트를 2 개와 TPanel, TStatusBar 컴포넌트를하나씩얹는다. 패널위에는버튼컴포넌트 3 개와 IP 주소를입력할에디트박스를하나추가하여다음과같이디자인한다. 물론 1:1 채팅프로그램을만들기위해서 TClientSocket 과 TServerSocket 컴포넌트도추가해야할것이다. 그리고포트를결정해야하는데, 필자는 1001 번으로결정하였다. ClientSocket1 과 ServerSocket1 컴포넌트의 Port 프로퍼티를 1001 로설정하였다. 그리고, StatusBar1 컴포넌트를선택하고오른쪽버튼을클릭한후 Panels Editor 메뉴를선택하여패널에디터를띄운뒤에 Add New(Ins) 버튼을클릭하여 StatusPanel 을하나추가한다. 이제이 1:1 채팅어플리케이션은클라이언트이면서동시에서버로동작할수있도록준비가끝난셈이다. 이를위해서전역변수를 2 개추가해야하는데, 하나는채팅어플리케이
션이클라이언트가접속하기를기다리는지여부를결정하는 Listening 변수와현재서버로 동작하고있는지를나타낼 IsServer 변수가그것이다. var Form1: TForm1; Listening: Boolean; IsServer: Boolean; 폼의 OnCreate, OnClose 이벤트핸들러를다음과같이작성하여처음폼이생성될때에는 Listening 변수를초기화하고, 폼을닫을때에는소켓을닫도록한다. procedure TForm1.FormCreate(Sender: TObject); Listening := False; procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction); ServerSocket1.Close; ClientSocket1.Close; Listen 버튼의 OnClick 이벤트에서는현재의폼을서버로대기하도록하는기능을한다. 버튼의 OnClick 이벤트핸들러를다음과같이작성한다. procedure TForm1.Button1Click(Sender: TObject); Listening := not Listening; if Listening then ClientSocket1.Active := False; ServerSocket1.Active := True; Statusbar1.Panels[0].Text := 'Listening...'; end else
if ServerSocket1.Active then ServerSocket1.Active := False; Statusbar1.Panels[0].Text := ''; 이버튼을클릭하면현재의 Listening 변수의값을변경한다. 그리고, 이변수의값이 True 이면 ServerSocket1 의 Active 프로퍼티를 True 로설정하고 StatusPanel 의 Text 프로퍼티를 Listening 으로설정한다. 이값이 False 이면서버소켓의 Active 프로퍼티를 False 로설정한다. Connect 버튼을클릭하면 Edit1 의내용을 ClientSocket1 컴포넌트의 Address 프로퍼티로설정한다. 참고로앞에서도설명했듯이소켓의 Address 와 Host 프로퍼티중에서하나를이용하는데, Address 프로퍼티는 IP 주소를이용한다. 그리고, 이주소를이용하여서버소켓에접속한다. 그리고, Disconnect 버튼을클릭하면클라이언트소켓을닫고, Listen 버튼의 OnClick 이벤트핸들러를호출한다. procedure TForm1.Button2Click(Sender: TObject); if ClientSocket1.Active then ClientSocket1.Active := False; if Length(Edit1.Text) > 0 then ClientSocket1.Address := Edit1.Text; ClientSocket1.Active := True; procedure TForm1.Button3Click(Sender: TObject); ClientSocket1.Close; Button1Click(nil); 이제각버튼의 OnClick 이벤트핸들러는모두작성하였다. 이제부터서버소켓과클라이언트소켓의이벤트핸들러를하나씩작성하면서이들의역할을알아보도록하자. 먼저서버소켓의이벤트핸들러를작성하도록하자. 앞에서도설명했듯이서버소켓에클
라이언트소켓이접속을시도하면 OnGetSocket 이벤트에이어서 OnAccept 이벤트가발생한다. 이이벤트에서는서버가클라이언트의접속을받아들이기로할때발생하는이벤트이므로 IsServer 변수를 True 로설정하여이제어플리케이션이서버로동작하고있음을나타내게하고, StatusPanel 에연결된클라이언트의 IP 주소를나타내도록한다. OnAccept 이벤트의 Socket 파라미터는클라이언트소켓을나타낸다. procedure TForm1.ServerSocket1Accept(Sender: TObject; Socket: TCustomWinSocket); IsServer := True; Statusbar1.Panels[0].Text := 'Connected to: ' + Socket.RemoteAddress; 이어서클라이언트가연결을완료하면 OnClientConnect 이벤트가발생한다. 지금작성하고있는채팅어플리케이션과같은논-블로킹서버의경우에는이시점에서데이터를읽고, 쓰기를시작할수있다. Memo2 컴포넌트에는클라이언트소켓에서발생한메시지를표시할것이므로, 이시점에서부터내용을보여주도록메모컴포넌트의내용을지우도록한다. 그리고, OnRead 이벤트는서버소켓이클라이언트소켓으로부터데이터를전달받을때발생하는데 Socket 파라미터의 ReceiveText 프로퍼티에클라이언트소켓에서전송한텍스트값이들어가있다. 그러므로이를 Memo2 컴포넌트에보여주도록이벤트핸들러를작성한다. procedure TForm1.ServerSocket1ClientConnect(Sender: TObject; Socket: TCustomWinSocket); Memo2.Lines.Clear; procedure TForm1.ServerSocket1ClientRead(Sender: TObject; Socket: TCustomWinSocket); Memo2.Lines.Add(Socket.ReceiveText); 마지막으로연결이종료될때에발생하는 OnClientDisconnect 이벤트핸들러에서는서버 소켓의 Active 프로퍼티를 False 로설정하고, 처음의 Listening 상태로들어가기위해서
Button1Click 이벤트핸들러를다시호출한다. 이때이를호출하면 Listening 변수의값이 변경되므로먼저 Listening 변수값을변경한뒤에호출해야원래의값이보존될것이다. procedure TForm1.ServerSocket1ClientDisconnect(Sender: TObject; Socket: TCustomWinSocket); ServerSocket1.Active := False; Listening := not Listening; Button1Click(nil); 이번에는클라이언트소켓의이벤트핸들러를작성할차례이다. 클라이언트소켓도서버소켓과접속이되었을때 OnConnect 이벤트가발생한다. 여기에서도마찬가지로접속된서버소켓이위치한컴퓨터의이름을나타내도록하는데, Socket 파라미터의 RemoteHost 프로퍼티를사용하면마이크로소프트의 UNC 이름에해당되는컴퓨터이름이나타난다. procedure TForm1.ClientSocket1Connect(Sender: TObject; Socket: TCustomWinSocket); Statusbar1.Panels[0].Text := 'Connected to: ' + Socket.RemoteHost; 이어서나타날수있는 OnRead 이벤트핸들러에서는서버소켓을 Socket 파라미터에서얻 을수있다. 서버의텍스트내용을클라이언트로동작하고있는어플리케이션의메모컴포 넌트에보여주도록다음과같이이벤트핸들러를작성한다. procedure TForm1.ClientSocket1Read(Sender: TObject; Socket: TCustomWinSocket); Memo2.Lines.Add(Socket.ReceiveText); 클라이언트소켓의 OnDisconnect 이벤트핸들러에서는 Button1Click 이벤트핸들러를호 출하여서버소켓으로동작할수있도록한다. procedure TForm1.ClientSocket1Disconnect(Sender: TObject;
Socket: TCustomWinSocket); Button1Click(nil); 그리고, 이런접속과정에서에러가발생할경우에는 OnError 이벤트가발생하는데, 이벤트 핸들러를다음과같이작성하여발생된에러상황을나타내도록한다. procedure TForm1.ClientSocket1Error(Sender: TObject; Socket: TCustomWinSocket; ErrorEvent: TErrorEvent; var ErrorCode: Integer); Memo2.Lines.Add('Error connecting to : ' + Edit1.Text); ErrorCode := 0; 마지막으로 Memo1 컴포넌트의 OnKeyDown 이벤트핸들러에서눌려진키가리턴키일경우에소켓으로전송하도록한다. 이때 IsServer 변수를검사하여서버일경우와클라이언트일경우에서로다른소켓에다가 SendText 메소드를이용하여텍스트를전송한다. procedure TForm1.Memo1KeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); if Key = VK_Return then if IsServer then ServerSocket1.Socket.Connections[0].SendText(Memo1.Lines[Memo1.Lines.Count - 1]) else ClientSocket1.Socket.SendText(Memo1.Lines[Memo1.Lines.Count - 1]); 여기서클라이언트소켓의경우에는간단하지만, 서버소켓의경우여러개의클라이언트소켓과물릴수있기때문에 Connections 라는컬렉션객체가포함되어있다. 그렇지만, 우리가작성한어플리케이션은단지 1:1 통신만지원하므로 Connections[0] 으로접속된클라이언트소켓을지칭할수있다. 이것으로간단한 1:1 통신을지원하는채팅어플리케이션이완성되었다. 컴파일하고실행한뒤에서버와클라이언트컴퓨터에각각띄우도록한다.
그리고, 서버측에서는 Listen 버튼을클릭하여클라이언트프로그램의접속을대기하도록하고, 클라이언트측에서는에디트박스에 IP 주소를적어넣은뒤에 Connect 버튼을클릭하여서버에접속하도록하자. 성공적으로접속이되면, 상태바에연결된컴퓨터의 IP 주소 ( 서버컴퓨터의경우 ) 또는연결된컴퓨터의컴퓨터이름 ( 클라이언트컴퓨터의경우 ) 이나타날것이다. 이제메모컴포넌트에서통신을시도하면다음과같이채팅을할수있을것이다. 블로킹연결을이용한파일전송예제 소켓을이용한프로그래밍을할때앞서설명한채팅어플리케이션과같이연결을유지하면서통신을할필요가있을때에는중단되지않는논-블로킹연결을지원하도록해야하지만, 경우에따라서는전송하는측과전송받는측의데이터전송에있어서서로비동기적으로처리하는것이효율적일때가많다. 이럴때에는블로킹연결을이용하게되는데, 블로킹연결을이용하여소켓프로그래밍을하는것은해당되는연결의쓰레드를생성하여이를실행하도록하는형태로제작해야하기때문에논-블로킹연결을지원하는어플리케이션에비해다소까다로운점이많다. 그러면, 블로킹연결을통해클라이언트에서서버로지정된파일을전송하는예제를작성해보도록하자. 이예제는 Stig Johansen 이공개한예제를바탕으로작성하였다. 예제프로그램의구조는클라이언트어플리케이션에서파일열기대화상자에서지정한파일을에디트박스에지정한 IP 주소로전송한다. 그리고, 서버어플리케이션에서는클라이언트와연결되면서버에저장할파일이름을지정하면, 여기에파일을저장하도록한다. 먼저, 서버어플리케이션을작성하도록하자. 폼위에다음과같이메모컴포넌트와서버소켓, 파일저장대화상자컴포넌트를하나씩추가하고, 메모컴포넌트의 Align 프로퍼티는
alclient, ScrollBars 프로퍼티는 ssvertical, ReadOnly 프로퍼티는 True 로설정한다. 그 리고 ServerSocket1 컴포넌트의 ServerType 프로퍼티는 stthreadblocking, Port 프로퍼 티는 2001 로설정하도록하자. 블로킹연결을지원하는서버를작성하기위해서는 TServerClientThread 에서상속받은쓰레드클래스를이용하는것이핵심이다. 여기서는파일과소켓의스트림을내부적으로사용하여파일의전송을구현하기때문에, private 섹션에소켓스트림과파일스트림필드를추가하고외부에서접근할수있도록 public 섹션에프로퍼티로선언하도록한다. TServerThread = class(tserverclientthread) private FSocketStream: TWinSocketStream ; FFileStream: TFileStream; protected procedure ClientExecute; override; public property SocketStream: TWinSocketStream read FSocketStream write FSocketStream ; property FileStream: TFileStream read FFileStream write FFileStream ; 쓰레드클래스에서꼭오버라이드해서구현해야하는메소드가 ClientExecute 로, 다중쓰레드를지원하는경우에 Execute 메소드를오버라이드하는것과같은역할을한다. 다중쓰레드프로그래밍에대해서는 41 장에서자세히다루므로이를참고하기바란다. 폼의 OnCreate 이벤트핸들러에서서버소켓의 Active 프로퍼티를 True 로설정하여서버소켓이클라이언트소켓과의연결을받아들일수있도록이를열어놓도록한다.
procedure TForm1.FormCreate(Sender: TObject); ServerSocket1.Active := True; 그리고서버소켓의 OnAccept, OnListen, OnThreadStart, OnThreadStop 이벤트핸들러를 다음과같이작성하여클라이언트소켓과의연결상황을메모컴포넌트에나타내도록한다. procedure TForm1.ServerSocket1Accept(Sender: TObject; Socket: TCustomWinSocket); Memo1.Lines.Add('Accept ' + Socket.RemoteAddress); procedure TForm1.ServerSocket1Listen(Sender: TObject; Socket: TCustomWinSocket); Memo1.Lines.Add('Listening... '); procedure TForm1.ServerSocket1ThreadStart(Sender: TObject; Thread: TServerClientThread); Memo1.Lines.Add('Start Thread of ' + Thread.ClientSocket.LocalAddress); procedure TForm1.ServerSocket1ThreadEnd(Sender: TObject; Thread: TServerClientThread); Memo1.Lines.Add('End Thread'); 블로킹연결에있어서가장중요한것은 OnGetThread 이벤트핸들러에서서버쓰레드를생성하는작업이다. 서버쓰레드를생성할때두번째파라미터를 False 로선언하면생성과동시에실행되는것이지만, 다음과같이 True 로설정하면쓰레드객체의프로퍼티를변경한뒤에 Resume 메소드로쓰레드가실행된다.
procedure TForm1.ServerSocket1GetThread(Sender: TObject; ClientSocket: TServerClientWinSocket; var SocketThread: TServerClientThread); var SocketStream: TWinSocketStream ; FileStream: TFileStream ; FileName: string; FileName := 'Default.file'; SocketStream := TWinSocketStream.Create(ClientSocket, 20); if SaveDialog1.Execute then FileName := SaveDialog1.FileName; FileStream := TFileStream.Create(FileName, fmcreate or fmshareexclusive); SocketThread := TServerThread.Create(True, ClientSocket); (SocketThread as TServerThread).SocketStream := SocketStream; (SocketThread as TServerThread).FileStream := FileStream; SocketThread.FreeOnTerminate := True ; SocketThread.Resume ; 여기에서전송되어온파일의이름을대화상자에서선택할수있도록하고, 파일스트림객체를생성하여쓰레드객체의프로퍼티에대입하고, 마찬가지로 TWinSocketStream 객체를생성하여이를소켓스트림프로퍼티에대입한다. 이제쓰레드가실제로실행되는 ClientExecute 메소드를구현하면서버프로그램이완성된다. 이를구현하기에앞서소켓스트림의전체내용을읽어오는역할을하는 ReadStream 함수를다음과같이구현한다. function ReadStream (Stream: TWinSocketStream; Buffer: Pointer; Count: Integer): Boolean; var P: PChar; Total, Delta, TimeOut: Integer; if Count = 0 then Result := True;
Exit; TimeOut := 0; Result := True; Total := 0; P := Buffer; while Total < Count do try Delta := Stream.Read(P^, Count - Total); except Exit; if Delta = 0 then Inc(Timeout); while not Stream.WaitForData(1000) and (TimeOut < 20) do Inc(TimeOut); if Timeout >= 20 then Result := False; Exit; end else TimeOut := 0; Inc(P, Delta); Inc(Total, Delta); 이루틴은꽤유용하게사용되므로잘익혀두기바란다. 구현된내용을간단히설명하면소켓스트림에서데이터를읽어올때버퍼와시간제한을이용하여적절한버퍼링을해주는것이주된내용이다. 이런작업이필요한이유는이런버퍼링작업이없이송신측에서는무조건데이터를밀어넣고, 수신측에서는무조건데이터를가져올경우에는간혹손실되는패킷이생기기때문이다. 실제로뉴스그룹에서도이런문제로어려움을겪는많은개발자들이있었는데, 이런문제를이루틴으로해결할수있다.
소스를보면쉽게이해할수있을것이나간단한설명을덧붙이자면, 스트림의 Read 메소드에의해실제읽어온데이터의바이트수를 Delta 라는변수에대입하게되는데이때이값이 0 이면 WaitForData 메소드를이용하여버퍼에데이터가들어오는지기다려보고, 이를여기서는최대 20 번까지기다려본후에그래도데이터가전송되어오지않으면연결이끊어진것으로간주하고실행을중지하게된다. 이루틴을이용하여 TServerThread 객체의 ClientExecute 메소드는다음과같이구현한다. procedure TServerThread.ClientExecute; var FileLength: Integer; MemoryStream: TMemoryStream; if ReadStream(SocketStream, Addr(FileLength), SizeOf(FileLength)) then MemoryStream := TMemoryStream.Create; MemoryStream.SetSize(FileLength); ReadStream(SocketStream, MemoryStream.Memory, FileLength); FFileStream.CopyFrom(MemoryStream, MemoryStream.Size); MemoryStream.Free; if Assigned(FSocketStream) then FSocketStream.Free; if Assigned(FFileStream) then FFileStream.Free; Terminate; 여기서눈여겨보아야할것은파일스트림과소켓스트림의원활한전달을위해중간에 TMemoryStream 형의변수를이용한다는점이다. 그리고, 처음에일단 ReadStream 메소드를이용하여전달된파일의크기를먼저받아본다는점이중요하다. 이는클라이언트에서파일을전송할때먼저파일의크기를전송하고나서, 실제파일을전송한다는것을의미한다. 그리고, 이값을이용하여서버에서파일을생성하고스트림을복사한다. 이것으로서버프로그램이완성되었다. 이번에는이서버프로그램과연결해서사용할클라이언트프로그램을작성해보자. 서버와는달리클라이언트프로그램에는연결한서버의 IP 주소를적어넣을에디트박스와버튼을하나의패널에올려놓고, 메모컴포넌트를다음과같이추가하여디자인하도록하자.
클라이언트소켓의 ClientType 프로퍼티는 ctblocking 으로설정하고, Port 프로퍼티는서버와같은 2001 로설정한다. 그리고먼저클라이언트소켓의 OnConnect, OnDisconnect, OnError 이벤트핸들러를다음과같이작성하여통신상황을나타내도록한다. procedure TForm1.ClientSocket1Connect(Sender: TObject; Socket: TCustomWinSocket); Memo1.Lines.Add('Connected to ' + Socket.RemoteAddress); procedure TForm1.ClientSocket1Disconnect(Sender: TObject; Socket: TCustomWinSocket); Memo1.Lines.Add('Disconnected to ' + Socket.RemoteAddress); procedure TForm1.ClientSocket1Error(Sender: TObject; Socket: TCustomWinSocket; ErrorEvent: TErrorEvent; var ErrorCode: Integer); Memo1.Lines.Add('Error Code is ' + IntToStr(ErrorCode)); 그리고, 서버프로그램에서의 ReadStream 과마찬가지로클라이언트에서파일의내용을파
일스트림에읽어들인후, 이를소켓스트림에기록하는역할을하는 WriteStream 함수를 다음과같이구현한다. procedure WriteStream(Stream: TWinSocketStream; const Buffer: Pointer; Count: Integer); var P: PChar; Total, Delta, TimeOut: Integer; if Count = 0 then Exit; Total := 0; TimeOut := 0; Delta := 0; P := Buffer; while Total < Count do try Delta := Count - Total; if Delta > 16384 then Delta := 16384; // 최대값 Delta := Stream.Write(P^, Delta); except Exit; Inc(P, Delta); Inc(Total, Delta); 비교적간단한구현내용이므로쉽게이해할수있을것이다. 참고로여기서는한번에최대 16384 바이트를하나의패킷으로스트림에기록하도록하였다. 소켓연결상태등에따라크기를변경할수도있겠다. 마지막으로 Button1 의 OnClick 이벤트핸들러를다음과같이작성한다. procedure TForm1.Button1Click(Sender: TObject); var FileStream: TFileStream;
FileLength: Integer; if OpenDialog1.Execute then FileStream := TFileStream.Create(OpenDialog1.FileName, fmopenread or fmsharedenynone); FileLength := FileStream.Size; if FileLength > 0 then ClientSocket1.Address := Edit1.Text; ClientSocket1.Active := True; if ClientSocket1.Active then ClientSocket1.Socket.SendBuf(FileLength, SizeOf(FileLength)); ClientSocket1.Socket.SendStream(FileStream); ClientSocket1.Active := False; 일단파일열기대화상자를이용하여전송할파일을선택하게하고, 이파일에대한파일스트림객체를생성한다. 그리고, 파일스트림의크기를먼저 SendBuf 메소드를이용하여전송하고, 파일스트림의내용을 SendStream 메소드를이용하여전송한다. 이와같이소켓을이용하여스트림을전송할때에는패킷을나누어전송하고, 스트림의크기를먼저전송하게하는것이에러를줄일수있는요령이다. 물론, 독자적인프로토콜을정의하여이를이용하는것이가장이상적일것이다. 이것으로클라이언트어플리케이션이완성되었다. 이제클라이언트와서버어플리케이션을띄우고파일을선택하여전송하도록해보자. 다음그림은서버와클라이언트어플리케이션의실행화면이다.
정리 (Summary) 이번장에서는델파이에서기본적으로제공되는소켓컴포넌트를이용하여프로그래밍을하는방법에대해서알아보았다. 소켓컴포넌트는과거뉴스그룹에서델파이판매전략에서 C/S 버전에만포함된것이격렬한논란거리가된컴포넌트이다. 그렇기때문에, 윈속을지원하는수많은프리웨어컴포넌트들이나오게되었고, 델파이 4 에서는프로페셔널버전에소켓컴포넌트만추가한새로운제품군을등장시켰다. 그만큼사용빈도도높고, 또한쓸모도많다. 그러므로, 여기에서소개한소켓컴포넌트뿐만아니라, Francois Piette 가공개한프리웨어컴포넌트들도매우뛰어나고더잘된것들도많으므로이런프리웨어에도관심을두고찾아보기바란다.