그렇지 않다. 컴퓨터 프로그램 개발 경력이 어느정도 되는 사람도 해보지 못한 분야가 네트웍쪽이다. 그만큼 네트웍 프로그래밍은 일반 개발자에게 친숙하지 않다. 이에 대한 첫번째 이유는 그래픽, 주변하드웨어, 통신 관련 프로그래밍책은 많지만 네트웍에 관련된 책은 이에 비해 훨씬 적기 때문이다. 더구나 시중에 나와있는 책중에서도 윈도우용으로 나온것은 극히 드물다. 두번째 이유는 네트웍프로그래밍을 하기 위해서는 랜환경이 갖추어져야 하는데 컴퓨터한대에서만 개발을 해온 일반 개발자에게는 역시 극복하기 어렵기 때문이다. 그래서 네트웍프로그래밍은 어렵게 느껴진다. 그래서 필자는 네트웍 프로그래밍을 시작하려고 하는 독자들을 위해서 좀 쉽게 이글을 진행시켜보고 싶다. 그리고 네트웍 프로그래밍을 하기 위해 선택한 언어는 델파이 3.0이다. 참고로 앞으로의 진행을 위해서 갖추어야 할 환경은 아래와같다.
델파이 3.0
랜 환경(랜 환경이 아니면 최소한 랜카드라도 설치되 있어야 한다.)
파라독스 DB(오라클이나 사이베이스도 상관없다.)
그리고 연재를 진행하면서 TCP/IP나 소켓에 대한 여러가지 이론에 대한 설명은 하지 않겠다. 지금까지 소켓프로그래밍에 대한 설명은 잡지, 책등에서 많이 되었다. 소켓에 대한 개념은 다른 책이나 잡지를 통해서 살펴보고 여기서는 델파이를 가지고 네트웍프로그래밍을 하는 방법에 대해서만 설명하겠다. 하지만 네트웍에 사용되는 여러 가지 개념및 용어를 모른다고 해도 걱정할 필요는없다. 네트웍프로그램에 대한 개념이 없더라도 네트웍용어 사용을 최소화해서 새로운 델파이 컴포넌트를 배운다고 생각이 들도록 용어를 가능한 쉽게 쓰도록 노력하였다. 여기에 나온 개념만 익히면 다른 네트웍 관련 책을 읽기가 한결 쉬울것이다. 그리고 보통 네트웍 프로그래밍에 관련된 책을 보면 TCP/IP구현외에 TCP를 이용해 구현한 FTP, Telnet, SMTP, Web서버 구현에 대한 내용을 많이 다루고 있다. 이런 TCP/IP를 용한 프로토콜의 구현및 응용에 대한 내용은 기회가 돠면 알아보기로 하고, 이번 연재의 목적을 TCP/IP에 대한 개념잡기로 잡았다. 그러기 위해서 우리는 첫회에 덜파이 소켓 컴포넌트에 대해서 알아보고, 2, 3, 4회에 걸쳐TCP/IP를 이용한 두개의 프로그램을 제작해보자.
I.델파이 TCP/IP 컴포넌트
TCP/IP를 가지고 프로그램을 하는데 있어 가장 중요한 개념은 소켓 개념이다. 소켓은 네트웍 프로토콜 구현에 저장된 데이터의 보다 큰 집합과 관련된 핸들이다. 소켓과 관련된 데이터는 TCP연결과 현재 연결 상태에 대한 IP어드레스와 포트같은 것들을 포함한다. 쉽게 생각하면 소켓은 일반적인 언어에서 파일핸들과 같은 개념으로 보면 된다. TCP연결을 하고 소켓을 생성해서 소켓을 전송하거나 소켓을 전송받는것이 네트웍 프로그래명의 기본이다. 나머지는 이 소켓을 해석하는 작업이다.
델파이에서 TCP/IP 소켓 프로그래밍을 지원하기 위해서 제공하는 컴포넌트는 2개이다.
TServerSocket : 서버 소켓 컴포넌트
TClientSocket : 클라이언트 소켓 컴포넌트
Socket은 원래 서버용 소켓이니 클라이언트용 소켓이니 하는 개념이없다. 이 두 컴포넌트 모두 내부적으로 같은 소켓을 사용한다. 그리고 이 두 컴포넌트는 사용방법(이벤트나 프라퍼티)도 거의 비슷하다.
서버 소켓이 클라이언트 소켓과 다른 점은 서버는 클라이언트의 요청을 기다리는 listen이라는 작업을 해야하고 클라이언트는 서버에 connect해야 한다. 이 두 컴포넌트에서는 각각 listen과 connect를 위해 모두 active라는 프라퍼티를 가지고 제어 한다. 그래서 이 두 컴포넌트를 사용하려면 active를 true로 해주어야 한다. 이렇게 하면 서버와 클라이언트가 접속된다. 그리고 TServerSocket은 여러개의 클라이언트가 접속되어 있을 경우 이를 관리해주는 기능이 포함되어 있다.
이 두 컴포넌트에서 active 말고 가장 중요한 프라퍼티는 address와 port이다. Address는 ip조소를 뜻한다. 그리고 port는 서버로 들어오는 메시지를 통과시키는 통로 번호쯤으로 생각해도 된다. 이 포트가 없다면 하나의 서버 컴퓨터에는 하나의 서버 프로그램만이 설치 될수 있다. 이 포트 번호를 통해 같은 서버컴퓨터에 여러개의 서버 프로그램이 사용될수 있다. 가령 웹서버, FTP서버, Telnet서버등 이들은 모두 다른 port를 사용한다. 즉 TCP/IP로 들어오는 데이타는 바로 들어오는 것이 아니고 port를 일단 거치다고 생각하면 된다. 참고로 잘알려진 서버가 사용하는 포트는 SMTP는 25번, NNTP는 119번, TELNET 23번 , FTP는 21번으로 정의되어 있다.
II.서버만들기
1.서버의 기능분석
이번 예제에서 무엇을 알수 있나
서버가 클라이언트로 부터 데이타를 어떻게 받는가?
서버가 클라이언트에게 데이타를 어떻게 보내는가?
첫번째 예제로 만들 프로그램은 서버의 역할을 수행하는 프로그램이다. 실제 프로그램에서 서버의 기능을 지원하려면 여러가지 상황을고려해야 한다. 하지만 여기서 만들서버는 여러 클라이언트들을 연결하는 기능과 클라이언트에서 데이타가 전달됐을때 이 데이타를 읽고 메시지를 전달한 클라이언트에게 받은 메시지를 다시 전달하는 echo기능을 지원하는 서버이다. 앞에서도 말했지만 서버는 하나이고 클라이언트는 여러개가 가능하다. 그래서 한대의 컴퓨터에서 서버를 띄워놓고 여러개의 클라이언트를 띄워놓고 여러개의 클라이언트접속을 테스트 해볼수 있다.
여러개의 클라이언트가 동시접속할수 있기 때문에 이를 관리해야 한다. 델파이 3.0에서 지원해주는 서버 소켓은 한 서버에 여러개의 클라이언트가 접속할수 있는 기능을 자체적으로 지원한다. 그래서 첫번째 예제에서는 클라이언트가 하나혹은 여러개인지 고려할필요가 없다. 어차피 서버의 리스닝역할을 통해 클라이언트의 접속을 받아들이는것은 서버 소켓이 다 알아서 해주는 기능이다.
이번 프로그램에서 서버가 해야하는일은 메시지를 받고 다시 메시지를 전송하는 기능만 하면 된다. 기본적인 소켓은 IP주소와 포트번호가 모두 필요하지만 델파이에서 제공하는 소버소켓에는 IP주소를 명시하는 부분이 없다. 이것은 당연하다. 하지만 클라이언트 프로그램은 접속해야 하는 서버의 IP주소와 포트를 모두 명시해주어야 한다. 그리고 클라이언트는 서버에서 받은 메시지를 화면에 출력하면 된다. 클라이언트가 자기가 보낸 메시지를 다시 받기 위해서 서버가 echo기능을 지원해줄 필요는 없다. 하지만 클라이언트와 서버가 서로 데이타를 실제로 주고 받는지 확인할수 있는 가장 기본적인 방법이다
echo기능을 지원하는 서버를 만들기 위해서 프로그램에서 해야 할것을 정리하면 아래와 같다
.
포트 번호를 설정한다. 예제에서는 6000을 사용함.
리스닝을 시작하기 위해서 서버 소케을 open한다. (active를 true로 해준다.)
클라이언트에서 데이타가 전달됐을때 발생하는메시지를 처리한다.
ReceiveText로 전달된 메시지를 받고 이를 메모장에 출력한다.
다시 이 메시지를 클라이언트에게 전달하기 위해서 SendText를 사용한다.
참고로 클라이언트가 서버에 연결되었거나 연결이 해제될때 클라이언트가 특정 메시지를 서버에 보내지 않는다. 즉 서버가 클라이언트로 받을수 있는 메시지는 연결이 확립되고 난후이고 연결이 끊어지면 메시지를 받을수 없다. 그러므로 서버가 받는 메시지는 클라이언트 프로그램이 실제로 서버에 보낸 데이타 뿐이다.
2.서버폼디자인
서버프로그램은 TServerSocket과 메모컴포넌트로 구성되어 있다.(그림1참조)
Tmemo :
서버 프로그램에 있는 메모장은 클라이언트에서 들어오는 메시지를 출력한다.
<그림1> 서버폼 PIC1.BMP
3.프라퍼티
TServerSocket서버의 포트를 6000으로 하자. 포트는 잘알려진 포트를 제외하고 어떠한 포트를 써도 상관없다. 1024이상의 포트를 사용하면 일반적인 프로그램과 충돌을 일으키지 않을 것이다. 채팅서버, 메일서버, FTP서버, Web서버등 많은 서버가 한번에 실행될수 있지만 이들 서버는 모두 다른 포트를 사용한다. 같은 포트를 사용하면 리스트닝 도중에 에러가 발생된다.
4.이벤트
echo서버에서 사용하는 이벤트는 서버 소켓의 OnClientRead뿐이다. 서버 프로그램의 전체 소스는 아래와 같다.
클라이언트로 부터 데이타가 들어왔을때 발생되는 OnClientRead이벤트에서 해야할일은 앞에서 정리했었다. 여기서는 코딩상의 몇가지 특성을 살펴보자. echo서버 프로그램에서 server컴포넌트의 프라퍼티나 메쏘드를 사용하는 곳은 한군데도 없다. 서버 프로그램에서 유일하게 코딩이 필요한 부분은 이 이벤트뿐이다. 여기서 주위깊게 봐야할것은 파라미터로 전달된 Socket 뿐이다. 그리고 이 파라미터는 클라이언트 소켓이라고 생각하면 된다. 즉 OnClientRead의 두번째 파라미터는 메시지를 전달해준 클라이언트소켓에 대한 정보를 가지고 있다. 그래서 서버 소켓의 특정 함수를 이용해서 서버로 전달된 데이타를 읽는다든가 역시 서버 소켓의 특정 함수를 이용해서 클라이언트에게 데이타를 전달하는 것이 아니다. 이 파라미터는 정말로 클라이언트 소켓이다. 그리고 이렇게 생각하는것이 프로그램을 이해하기 편하다. 만약 서버에 전자와같은 함수방식으로 제어된다면 여러개 클라이언트에서 온 메시지를 어떻게 구분할것이고 어떤 클라이언트에게 메시지를 보낼지도 항상 구분해주어야 하는 어려움이 생긴다.
이렇게 해서 서버는 다만들었다. 실행을 잠시 보류하고 클라이언트 프로그램을 만들어보자.
III.클라이언트만들기
1.클라이언트 기능 분석
이번 예제에서 무엇을 알수 있나
클라이언트가 서버에게 데이타를 어떻게 보내는가?
클라이언트가 서버로부터 데이타를 어떻게 받는가?
클라이언트프로그램은 서버에게 메시지를 전달하고 서버에서 전달된 메시지를 화면에 출력하는 기능을 하게 된다. 앞에서 만든 서버가 echo기능을 하기 때문에, 클라이언트가 hello라는 메시지를 서버에게 보내면 서버는 클라이언트에게 다시 hello를 보낸다. 현재 여기서 구현된 클라이언트는 매우 간단하다. 대부분의 서버 클라이언트 프로그램에서 클라이언트는 메시지를 보내는 일만 하고 서버가 이 메시지를 받아서 이에 대한 처리를 한다. 그래서 서버는 많은 일을 처리하고, 클라이언트는 최소한의 일만 처리한다. 이번 예제나 채팅프로그램에서는 서버가 해야 할일이 클라이언트에 비해서 많지 않기 때문에, 확실한 클라이언트서버 환경을 맞보기에는 적합하지 않다. 하지만 3회와 4회를 거쳐 실질적으로 구현해야할 TCP/IP SQL서버는 메시지를받아서 메시지를 해석하고, 그리고 이 메시지에 따라서 적합한 SQL 문을 만들어서 이 SQL문을 처리하는 모든 일을 처리한다. 이에 비해 클라이언트는 상대적으로 간단하다.
클라이언트폼디자인
TClientSocket : 클라이언트를 서버에 연결
TMemo : 서버에서 전송되는 메시지를 표시
TEdit : 서버에 보낼 메시지를 입력
<그림2> 클라이언트 프로그램 폼 pic2.bmp
3.프라퍼티
클라이언트 소켓 : address를 127.0.0.1으로 한다. 아니면 서버 컴퓨터의 IP주소를 써도 된다. 그리고 이 소켓을 open하기 위해 active를 true로 바꾼다.
에디트 컴포넌트 : OnKeyDown이벤트를 작성한다.
4.이벤트
클라이언트 프로그램에서 작성해야 하는 이벤트는 두개이다. 첫번째 이벤트는 서버에서 날아오는 데이타를 읽어서 화면에 출력하는 이벤트이고 두번째는 에디트 컴포넌트에서 Enter키를 치면 입력한 내용을 서버에 보내는 키보드 이벤트이다. 아래에 전체 프로그램에 대한 소스가 있다.
procedure TForm1.Edit1KeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
if key = vk_return then
ClientSocket1.socket.sendText(edit1.text);
end;
end.
첫번째 이벤트 OnRead에서 특별히 어려운 것은 없다. 서버에서 발생하는 이벤트를 작성할때와 비교해서 크게 달라진점은 없다. 두번째 파라미터는 쉽게 생각하면 서버소켓이라고 생각해도 된다. 소켓이 무엇인지 다시 한번 생각해보자. 소켓은 IP 주소, 포트, 데이타에 관한 내용을 담고 있는 구조체다. 위의 예에서 socket은 서버주소, 사용한 포트, 그리고 서버에서 클라이언트로 보낸 데이타를 가지고 있는 클래스이다. 그리고 ReceiveText는 소켓이 포함하고 있는 실제 데이타를 리턴하는 함수이다.
에디트 컴포넌트의 OnKeyDown 이벤트에서 ClientSocket1.socket이 의미하는 것은 클라이언트 소켓이다. 에디트 컴포넌트에서 리턴키가 입력되면 SendText라는 함수를 통해서 클라이언트 소켓을 구성하고 서버에게 소켓을 전송하는 기능을 가진것이 SendText함수의 기능이다. 이 클라이언트 소켓을 전달한 서버에 대한 정보는 ClientSocket1에 저장되어 있다. 이 함수를 통해서 서버에게 클라이언트의 정보, 서버의 정보그리고 실제 데이타에 대한 소켓이 클라이언트에서 서버로 전송된다. SendText함수는 클라이언트 소켓을 만들어서 서버로 전송하는 기능을 가진 함수이다. 결국 서버에는 이 소켓이 그대로 전달된다.
5.두개의 프로그램 실행시키기
이렇게 해서 서버 프로그램과 클라이언트 프로그램을 만들어 보았다. 이를 어떻게 테스트 할수 있는가? 이 프로그램을 테스트하려면 랜환경이 갖추어져야 한다. 아니면 컴퓨터 한대에 랜카드가 설치되 있어야 한다. 이럴경우 127.0.0.1이라는 특별한 IP주소를 사용해서 프로그램을 테스트 할수 있다. 이 주소는 피드백 IP주소라고 생각하면 된다.위의 예제에서 클라이언트 소켓은 IP주소와 포트 번호 모두를 명시했다. 그때 사용하는 IP주소는 서버로 쓰일 곳의 IP주소이거나 127.0.0.1중의 하나를 사용하면 된다. 서버로 사용될 컴퓨터가 꼭 NT일 필요는 없다. 서버로 사용될 컴퓨터는 고정 IP주소만 가졌다면 윈도우95라도 전혀 상관없다.
우선 서버 프로그램을 먼저 실행시켜야 한다. 그리고 서버 프로그램은 서버 컴퓨터에 계속실행되고 있어야 한다. 이런 상태에서 클라이언트 프로그램을 실행시켜서 에디트 컴포넌트에 메시지를 적고 엔터키를 누르면 똑같은 메시지가 화면에 출력된다. 출력된 메시지는 서버가 보낸 데이타이다. 같은 컴퓨터에 서버프로그램을 띄우고, 여러개의 클라이언트 프로그램을 띄워도 문제없이 잘돌아간다.(이때 각 클라이언트 프로그램은 같은 주소 같은 포트를 사용한다. 그렇지만 소켓을 최종적으로 구분하는 소켓ID가 있으므로 실질적으로 이를 가지고 각 클라이언트를 구분하게 된다. 이 소켓 ID는 파일 핸들처럼 유일한 ID이다. ) 여러 클라이언트가 동시에 메시지를 보내도 각 메시지가 반송되어서 어느 클라이언트로 전송되야 할지는 자동으로 해결된다. 여러 클라이언트가 연결되어 있을때의 교통혼잡은 소켓의 구조 때문에 자동으로 해결된다. 이를 걱정하지 말자.
두개의 프로그램을 띄워서 테스트한 예가 그림3에 있다. 위에 있는 윈도우가 서버이고 아래에 있는 것이 클라이언트이다. 클라이언트에서 hello라고 치면 서버 메모컴포넌트에 hello가 출력된다.
<그림3> 클라이언트와 서버 프로그램 실행화면
지금까지 살펴본 예에서 전송한 데이타는 string 형태였다. 하지만 소켓이 꼭 string타입만 전송할수 있는 것은 아니다. 이진 데이타 형태의 데이타도 보낼수 있다. 이런 형태의 데이타를 보낼수 있다는 의미는 레코드전송이나 파일등의 전송도 가능하다는 의미이다.
6.클라이언트와 클라이언트간 통신을 C/S에서 어떻게 구현할것인가?
소켓 프로그래밍을 처음하는 입장에서는 서버의 유용성을 크게 인식하지 못한다. 그리고 서버의 개념이 필요하지도 않다. 소켓을 가지고 클라이언트 서버환경에 익숙하지 않는 사람이 네트웍에서 두사람이 게임을 하는 프로그램을 작성한다고 보자. 그러면 A 클라이언트와 B클라이언트를 생각할것이고, A는 B에게 메시지를 보내고, B는 A에게 메시지를 보내면 된다. 즉 이런 모델은 클라이언트 서버모델이 아니라 단지 클라이언트 모델이다. 그리고 이렇게 생각하는 것이 당연하다. 하지만 똑같은 상황에서 이를 클라이언트서버개념으로 생각하면 이렇다. A는 서버에게 B 에게 메시지를 보내라는 메시지를 보낸다. B도 마찬가지로 A에게 메시지를 보내라는 메시지를 보낸다. 그러면 서버는 A로부터 메시지를 받는다. 물론 B에서 들어온 메시지가 있다면 이 메시지도 받는다. 서버는 A에서 날아온 메시지를 해석해서 메시지를 B에게 전달한다. 즉 A, B사이에 메시지를 전달해주는 역할을 하는것이 서버의 역할이다. 양자간 통신에서는 구지 서버가 필요없지만 클라이언트가 많은 경우에는 서버를 두고 이 서버가 각 클라이언트의 중재 역할을 하도록 하면 된다. 이에 대한 실질적인 예는채팅프로그램에서 구현해 보도록 하자.
IV.클라이언트/서버만들기
세번째 예제 프로그램은 클라이언트와 서버의 기능을 모두 갖춘 프로그램이다. 위에서 살펴본 두개의 프로그램을 하나로 합쳐놓은 프로그램이다. 이렇게 서버와 클라이언트의 기능을 합쳐서놓으면 프로그램을 테스트 하기가 한결 쉽다. 그리고 실질적인 메시지를 처리하면서 클라이언트와 서버에서 메시지를 처리하는 방법을 좀더 확실히 알수 있다. 그리고 이번 프로그램에는 클라이언트, 서버 소켓 컴포넌트에서 제공해주는 몇개의 함수, 이벤트, 프라퍼티를 배워보자,
1.프로그램의 기능 분석
이번 예제에서 무엇을 알수 있나
서버가 접속된 클라이언트 각각에 메시지를 어떻게 보내는가?
클라이언트의 접속상황(연결, 해제)을 서버가 어떻게 알수 있는가?
서버의 상태를 어떻게 보여줄수 있는가?
이번 프로그램은 서버의 기능과 클라이언트의 기능을 겸하는 프로그램을 작성하는 의미도 있지만 실질적으로 TCP/IP를 다루기 위해서 보내는 함수와 받는 함수말고도 많은 함수들과 여러 테크닉들이 필요하다. 이를 좀더 익히기 위해서 두개의 컴포넌트에서 제공해주는 여러기능중에서 보내기받기 함수말고 가장 필요한 몇가지 사항에 대해서 좀더 알아보자.
이 프로그램을 실행시킨뒤 서버로 작동하게 하면 서버가 제대로 작동중인지 메시지가 메모컴포넌트에 뜬다. 이를 리슨이라 함은 앞에서 설명했었다. 그리고 서버로 들어오는 메시지가 있으면 이를 메모장에 출력한다. 그리고 메시지를 받았다는 의미로 클라이언트에 ok메시지를 보낸다. 그리고 클라이언트가 접속 요청을 해오면 클라이언트 소켓의 ID 를 리스트 박스 컴포넌트에 보관하다. 소켓ID는 클라이언트가 접속을 요청해서 접속요청이 끝날때까지 같은 번호를 계속유지한다. 즉 소켓이 만들어질때마다 서로 다른 소켓 ID 가 생성되는 것은 아니다. 소켓이 전달될때마다 소켓 ID 가 다르다면 각 소켓이 어느 클라이언트에서 전송된 것인지 프로그램에서 구분해내기가 까다로워진다. 그리고 메모장 옆에는 현재 접속된 클라이언트의 소켓ID 가 있다. 이 소켓중 하나를 선택해서 에디트 컨트롤에 메시지를 적고 Enter키를 치면 이 메시지가 특정 클라이언트로만 전송된다. 또 클라이언트가 접속을 해제하면 이 목록에서 소켓ID가 제거된다. 그리고 클라이언트에서 혹시 에러가 발생하면 에러 메시지가 출력된다.
반대로 클라이언트로 작동하게 되면 클라이언트가 서버와연결되었는지를 표시한다.그리고 서버와 연결이 성공적으로 되면 server found 메시지를 출력한다. 그리고 클라이언트를 닫으면 close메시지를 출력한다. 그리고 메모컴포넌트에는 서버가 보낸 메시지를 출력한다.
2.폼디자인
화면을 보면 알겠지만 이 프로그램은 크게 세 부분으로 나뉘어져 있다.
Server Information panel 부분
listening 버튼: 서버 소켓을 오픈한다.. ( btSOpen) 괄호안은 컴포넌트의 name속성이다.
Cloe 버튼 서버 소켓을 클로우즈한다. (btSClose)
Receive Message 메모 : 서버로 들어오는 메시지나 서버에서 발생한 메시지, 서버의 접속상황들을 출력한다. ( moServer)
Client List 리스트 박스 : 현재 접속되어 있는 클라이언트 소켓의 ID 를 보관하다. (lbClient)
Client Information panel부분
connect 버튼 서버에 연결한다. (btCOpen)
Close 버튼 : 서버와 연결을 끊는다. (btCClose)
Receive Message 메모 : 클라이언트로 들어오는 메시지나 서버에서 발생한 메시지, 서버의 접속상황들을 출력한다. (moClient)
그리고 서버 소켓과 클라이언트 소켓모두를 사용한다. ( client, server)
SendMessage 부분
에디트 컨트롤 : 현재 프로그램이 서버로 진행중이면 리스트 박스에서 선택된 클라이언트에게 메시지를 보내고 클라이언트면 서버에게 메시지를 전송한다. (edit1)
< 그림4 > 클라이언트/서버 폼화면
3.이벤트
여기서 사용하는 이벤트는 10개이다. 이벤트를 정리하면 아래와 같다.
에디트 컴포넌트
OnKeyDown : 현재 실행중인 프로그램이 서버용인지 클라이언트 용인지 구분해서 클라이언트가 서버에게 메시지를 보낼지 서버가 특정 클라이언트에게 메시지를 보낼지를 판단해서 소켓을 전송한다.
서버 컴포넌트
OnClientConnect : 클라이언트가 접속 요청을 해오면 리스트 박스에 접속된 클라이언트의 소켓ID 를 기록한다.
OnClientDisConnect : 클라이언트가 접속을 끊을때 리스트 박스에서 끊을려고 하는 클라이언트 소켓 ID 를 삭제한다.
OnClientError : 클라이언트에서 에러가 발생하면 이벤트가 발생한다.
OnClientRead : 클라이언트에서 날아온 데이타가 있으면 이벤트가 발생한다.
OnListen : 서버가 리슨에 성공하면 이벤트가 발생한다.
클라이언트 컴포넌트
OnDisConnect 접속을 끊을경우(명시적으로 client.close를 호출해도 이 이벤트가 호출되고 프로그램을 종료시켜도 이 이벤트가 발생된다.)
OnConnect : 클라이언트가 서버 접속에 성공하면 이 메시지가 발생한다.
OnError : 에러가 발생하면 이벤트가 발생한다.
OnRead : 서버로부터온 데이타가 있으면 이벤트가 발생한다.
참고로 서버프로그램을 종료하면 서버는 접속되어 있는 모든 클라이언트를 자동으로 close한다.
10개의 이벤트 중에서 중요한 5개의 이벤트를 보자 먼저 에디트 컴포넌트에서 발생한 키보드 이벤트이다.
procedure TForm1.Edit1KeyDown(Sender: TObject; var Key: Word;
현재 프로그램이 서버모드로 작동중인지 클라이언트 모드로 작동중인지를 알수 있는 방법을 제시하고 있다. 변수 하나를 만들어서 현재 서버로 실행되고있는지 클라이언트로 실행되고 있는지를 나타내도 되지만 client의 active프라퍼티와 server의 active프라퍼티는 둘중하나만 open될수 있다. 즉 둘다 open될수는 없다. 이 프라퍼티가 배타적으로 사용되기 때문에 이 것으로 서버모드인지 클라이언트 모드인지를 구분할수 있다. 클라이언트 모드면 단순히 입력된 문자열을 서버로 보내면 된다. 하지만 서버 모드일경우에는 좀 복잡해보일지도 모르겠다. 이 부분의 전체적인 구조를 보기전에 처음보는 프라퍼티먼저 살펴보자.
지금까지 두개의 소켓 컴포넌트를 사용하면서 느낄수 있을지도 모르겠지만 TServerSocket , TClientSocket컴포넌트간에 차이가 없다. 이것을 구지 두개로 구분할필요가 없지 않느냐하고 생각할수도 있고 이것은 맞는 생각이다. 원래 소켓은 서버용과 클라이언트용이 구분되어있지 않다. 하지만 델파이에서 이 두 소켓 클래스가 구분되어 있는 것은 여러가지 이유가 있다. 그중 여기서 살펴볼것은 다중 클라이언트를 관리할수 있는 기능이 TServerSocket에는 들어가 있다는점이다. 현재 접속되어 있는 클라이언트 소켓은 Socket.Connections[]에 있다. Connections[0](접속된 첫번째 클라이언트)은 지금까지 계속 보아왔던 TCustomWinSocket타입이다. 그래서 지금까지메시지를 전달하기 위해서 사용했던 SendText함수를 사용할수 있다. 이 부분의 코딩은 현재 클라이언트에 접속되어 있는 소켓의 핸들을 하나씩 비교해서 선택된 소켓의 핸들을 알아낸다, 그다음 이 소켓의 SendText함수를 호출한것이다. 현재 접속된 클라이언트 소켓의 핸들값은 리스트 박스에 모두 저장되어있다. 사용자가 서버모드에서 리턴키를 치면 리스트 박스에서 선택된 핸들값을 찾아야 하기 때문에 for문과 listbox.items.indexof함수,listobx1.itemindex를 사용했다. 이를 좀더 확장해서 클라이어트에서 클라이언트로 메시지를 전달할수도 있다. 이에 대한 좀더 확장된 예는 채팅프로그램을 구현하면서 알아보자. 그리고 채팅 프로그램에서 다시 애기하겠지만 한사람이 전달한 메시지는 모든 클라이언트에게 전송되어야 한다. 이것을 구현하려면 역시 Socket.Connections[]을 사용하면 된다.
여기서 하는일은 클라이언트가 접속되었을때 접속되었다는 메시지를 출력하고 접속된 소켓의 핸들을 리스트 박스에 추가한다. 이렇게 추가된 소켓 핸들을 가지고 서버는 특정 클라이언트 소켓을 구분해낼수 있다. 그래서 특정 클라이언트에게만 메시지를 전달해줄수 있다. 여태까지는 메시지를 받는 소켓에게만 메시지를 전달할수 있었다.
i := lbClient.items.IndexOf(inttostr(socket.handle));
lbClient.items.Delete(i);
moServer.lines.add('client close');
end;
클라이언트 접속이 해제되었을 경우 여기서 하는일은 리스트 박스에서 해제된 소켓의 핸들을 지우는 작업이다. 접속 해제된 소켓의 핸들값을 기억하고 있는 리스트 박스의 항목을 알아내기 위해서 items.indexof를 사용했고 아이템을 제거하기위해서 items.Delete함수를 사용했다.
아래는 특별한 역할은 하지 않지만 클라이언트가 연결이 끊어졌을때 이런 이벤트가 발생되고 아래와 같이 처리해주면 되는구나 하는것을 보여주기위한 예이다. 참고하기 바란다.
그림5에 실행화면이 있다. 위에서 부터 첫번째 윈도우가 서버이고, 밑에 있는 두개의 프로그램은 클라이언트이다. 서버 프로그램은 listening버튼을 누르면 된다. 네트웍이 제대로 동작중이면 메모컴포넌트에 listening이라 표시된다. 그리고 서버 프로그램의 리스트 박스를 보면 두개의 클라이언트가 접속되어 있고, 각 클라이언트의 핸들값이 있는 것을 볼수 있다.
V.여러가지 소켓 컴포넌트
DWinSock컴포넌트
얻을수 있는 곳 : http://www,aait.com/dwinsock
기능및 특징
델파이 2.0과 3.0에서 사용할수 있는 쉐어웨어 컴포넌트이다. 2개의 기본적인 서버 소켓과 클라이언트 소켓뿐만 아니라 8개(텍스트 서버/클라이언트, 바이너리 서버/클라이언트)의 전문화된 컴포넌트를 제공한다.
WinSock 컴포넌트
얻을수 있는곳 : 하이텔 vtool 동우회
기능및 특정 :
윈속을 컴포넌트로 구현한 기본적인 컴포넌트이다. 하나의 소켓 컴포넌트만 지원한다. 그래서 서버와 클라이언트용이 따로구분되어 있지 않다. 하지만 다른 컴포넌트와는 달리 소스가 포함되어 있기 때문에 TCP/IP내부 과정을 모두 제어해볼수 있기 때문에 TCP/IP를 공부하는데 많은 도움이 된다.
VI.마치기전에
프로그램을 테스트 할때와 실제로 서버와 클라이언트를 만들어서 운용할때 IP주소와 port주소가 다를수 있다. 테스트 할때는 자기 컴퓨터에서 서버와 클라이언트를 모두 띄워놓고 하는 것이 빠르기 때문이다. 이럴경우 IP주소를 자꾸 바꿔주어야 하는데 이를 쉽게 해결해줄수 있는 방법은 INI를 이용하는 방법이다.
TCPINFO.ini
[value]
address=127.0.0.1
port=10000
TCPINFO.ini는 위와 같이 구성하고 클라이언트와 서버의 IP주소및 port를 지정하기위해서 아래과 같은 함수를 호출하면 된다.
여기서 getexecpath함수는 프로그램이 실행된 디렉토리 위치를 반환하는 함수이다. 이 두개의 함수와 getexecpath함수는 tcplib.pas에 정의되어 있다. 참고로 Getexecpath는 아래처럼 정의되어 있다.
result := ExtractFilePath(paramstr(0));
사용된 함수모두 델파이 기본 함수이므로 도움말을 찾아보면 쉽게 알수 있을것이다.
소스 구성
첫번째 예제
ex1.dpr
uex1.pas
uex1.dfm
두번째 예제
ex02.dpr
ex02.pas
ex02.dfm
세번째 예제
clisvr.dpr
clisvr.pas
clisvr.dfm
기타 파일
tcpinfo.ini
tcplib.pas
소켓프로그래밍
특정 클라이언트에게만 데이타 전송하려면 어떠한 방법을 사용할수 있을까? 이에 대한 분석은 다음호의 채팅프로그램을 제작하면서 귓속말하기를 구현하면서 알수 있을것이다.
또하나 소켓프로그램밍을 하면서 생기는 문제점은 많은 데이타를 클라이언트에 보낼때이다. 이때 발생하는 문제점은 없는지, 그리고 패킷을 전달할때 특정한 포맷을 가져야 하는지 이런 문제들을 생각해야 한다. 실제로 TCP/IP를 가지고 프로그래밍을 하고자 할때 미쳐생각하지 못했던 많은 문제점이 생겨난다. 소켓프로그램을 하고자 할때 발생할수 있는 문제점과 그 해결방을 채팅 프로그램과 TCP/IP SQL서버를 제작하면서 계속 알아보도록 하자.
참고 자료:
Internet WinSock - 에프원출판사
WinSock - 마소 95년 6월~9월
2회원고 : 채팅 클라이언트/서버
첫번째 예제와 두번째 예제는 풀소스를 실어야 하고 나머지는 디스켓으로 해도 상관없습니다. 가장 간다한 예제이지만 개념을 잡는데 가장 중요한 예제입니다. 실프로그램에 대한소스는 중간중간 중요한 부분만 발췌합니다.
연재를 5회로 구성했으면 합니다. 추가되는 부분은 3회부분으로 TCP/IP프로토콜을 이용한 애플리케이션 프로토콜에 대해서 알아본다면 델파이 네트웍프로그래밍이란 주제가 더 확실해 질것 같습니다. 1회 TCP/IP개념, 2회 TCP/IP 기본 예제 3회 TCP/IP잘알려진 프로토콜 4회, 5회가 TCP/IP응용으로 나뉘어지는 거죠.(3회 제목 : TCP/IP 애플리케이션 포로토콜)
2회 목차
I.채팅프로그램 설계
II.서버
III.클라이언트
IV.더 나은 기능들
마치기전에
박스기사 : Del2Java
참고기사 : 델파이에서 링크트 리스트 구현
2회 : 채팅 클라이언트 서버
지난호에서 소켓프로그래밍을 개관함으로써 네트웍 프로그래밍의 기본은 배웠다. 이번에는 소켓프로그램의 기본 예제라 할수 있는 채팅 프로그램을 만들어보자. 채팅 프로그램은 소켓을 가지고 네트웍 프로그램을 하기 위한 가장 좋은 예제이다. 실용성면에서나 응용성면 에서나 TCP/IP 에서 클라이언트 서버모델에 대한 이해를 이 예제를 통해서 습득할수 있다. 그리고 다른 애플리케이션 프로토콜을 만들때에도 상당히 도움을 준다. 지난호의 목적이 델파이에서 소켓프로그램작성이라면 이번달은 네트웍에서 클라이언트 서버모델에 대한 이해를 목적으로 한다.
그리고 채팅프로그램을 응용하면 화상통신이나, 머드게임같은 것도 만들수 있다. 이 모든것에 대한 기초가 바로 채팅 프로그램이다. 채팅은 접속이라는 영화에서 채팅 공간을 통해서 서로의 아픔을 넘어선 두 사람의 인연을 만들어 낸다. 그리고 TV 드라마중 채팅을 소재로한 내용이 있다. 형사와 벙어리 여인의 대화 수단으로써 채팅이 이용된다. 이렇듯 채팅은 우리 생활에(미래에는 지금과 같은 모습은 아닐지도 모르지만) 확고한 생활양식으로 잡아가고 있는 것 같다. 채팅은 구현이 비교적 쉽고 사용면에서도 일반인에게 가장 많이 알려져 있는 클라이언트 서버 모델이다. 우선 몇사람이 채팅하는 상황을 체크해보고 채팅프로그램을 어떻게 설계해야 하는지를 알아보자.
채팅프로그램 설계
#철수님이 들어오셨습니다.
철수 : 안녕하세요.
영희 : 어서오세요, 철수님
영수 : 어서오세요. 철수님
철수 : 요새 채팅에 대한 일반인들의 관심이 매우 높아졌습니다.
철수 : 저도 접속이라는 영화를 보고 한번 호기심에 이끌려서 발을 디뎠습니다.
영희 : 그럼 채팅이 처음이신가요?
철수 : 예.
...
철수 : 오늘만나서 반가왔 습니다.
철수 : 다음에 또 볼수 있었으면 좋겠네요.
영희 : 안녕히가세요
영수 : 안녕히가세요
#철수님이 퇴장 하셨습니다.
채팅프로그램을 만드는데 있어서 가장 먼저 이해해야 할것은 채팅서버와 채팅 클라이언트를 왜 만드는가 하는점이다. 대화만을 본다면 철수, 영희, 영수가 사용하는 프로그램이 채팅 클라이언트가된다. 여기서보면 서버의 기능은 없는 것처럼 보인다. 그리고 여러명에서 대화를 할때도 서버의 기능을 필요로 하지 않는다. 하지만 지난번에도 언급했지만 대화하는 사람이 두 사람이라면 서버가 그 기능을 필요로 하지 않지만 세사람 이상이라면 서버의 기능이 필요하다. 서로간에 전화를 하는 상황을 생각해보자. 집에 있는 전화기가 클라이언트가 되고 , 전화국이 서버가 된다. 여기서 전화국이 하는일은 전화와 전화사이의 중계역할만을 한다.
아래 그림1은 3사람이 서버없이 통신을 하는 그림이고, 그림2는 서버(전화국)가 있을 경우의 상황이다. 서버가 없다면 이해하긴 편할지 모르지만 한 클라이언트는 모든 클라이언트와 접속이 되있어야 한다. 접속되어 있지 않으면 그사람과 통신을 할수가없다. 하지만 서버가 있다면 각각의 클라이언트는 서버만을 알면 된다. 비용면에서도 훨씬 적게든다. 이 상황을 잘 분석해야한다. 그러면 채팅 서버를 왜만들어야 하는지 그리고 채팅 클라이언트가 해야 하는일이 무엇인지를 알수 있다. 다시 한번 강조하지만 중요한것은 채팅 서버를 이해하는 일이다.
서버가 할일부터 살펴보자. 서버는 항상 대기중에 있어야 한다. 활성화 시간만 본다면 서버는 항상 동작중이어야 하고 클라이언트는 필요할 경우만 동작하면 된다. 전화국은 항상 돌아가야 하고 우리는 필요할 경우에만 전화를 사용하면 된다. 이렇게 서버가 계속 작동중인상태로 들어가는것을 리스닝이라 했다. 서버가 첫번째로 할일은 바로 리스닝이다. 그러면 어느 순간에 클라이언트가 접속을 해온다.
두번째는 할일은 클라이언트가 접속을 요청해오면(프로그램에서는 서버 소켓에 ClientConnect이벤트가 호출된다.) 요청을 받아들이고, 모든 클라이언트에게 한 사람이 들어왔음을 알려주는 메시지를 보내준다. 그러면 위의 대화에서는 누구누구님이 들어오셨습니다를 현재 접속되어있는 모든 클라이언트에게 메시지를 보낸다. 그다음 철수가 메시지를 서버에게 전달한다. 서버는 이 메시지를 받아서 (OnClientRead) 모든 클라이언트(철수를 포함한 클라이언트)에게 전달한다. 그리고 이 과정이 계속적으로 반복되고 이것이 채팅 프로그램의 전체적인 운영의 흐름입니다. 채팅을 계속하다가 누눈가 한명이 빠져나가면 누구가 퇴장하셨습니다. 라는 메시지가 모든 클라이언트에게 전달된다.
그럼 클라이언트 입장에서 생각해보자. 클라이언트는 서버에 접속을 요청한다. 만약 서버가 작동중이라면 클라이언트는 서버에 접속되었다는 메시지를 받아야 한다. 그래야 접속이 가능한지 알수 있다. 접속에 성공하면 누구누구님이 입장하셨습니다. 라는 메시지를 받는다. 이제 부터는 할말을 서버에게 보내기만 하면 된다. 클라이언트는 몇명의 클라이언트가 있는지 신경쓸 필요가 없다. 단지 서버에 전하고 싶은 메시지만 전하면 된다. 그리고 클라이언트는 서버에서 전달되는 모든 메시지를 받는다. 서버에 비하면 클라이언트가 해야 할일은 지극히 간단하다. 그럴수 밖에 없는 이유는 클라이언트는 자기자신과 서버만 고려하면 되기 때문이다. 이것을 표현한 그림이 그림 3이다. 이 그림을 머릿속에 잘 보관하자.
단순히 여기까지만 해서 채팅 프로그램을 작성한다면 아래 서버 소스와 클라이언트 소스만 가지면 충분하다. 이런 골격의 채팅 프로그램의 구조를 가지고 실질적으로 사용될 수 있는 프로그램을 만들면서 TCP/IP를 다양한 상황에서 어떻게 결합시킬수 있는지를 살펴보아야 한다. 첫번째로 작성할 예제 프로그램과 두번째로 작성할 예제 프로그램은 이런 점에서 분명한 차이가 있다. 첫번째 예제프로그램에서는 TCP/IP를 가지고 채팅(서버, 클라이언트)을 어떻게 구현할수 있는지 기본 구조를 보여주고 두번째 예제 프로그램은 이를 실제로 어떻게 적용하고 응용할수 있는가를 보여준다. 두개의 프로그램을 모두 정확히 이해할 필요가 있다. 우선 첫번째 예제 프로그램의 서버 프로그램부터 보자.
서버 프로그램은 기본폼화면과 TServerSocket(Server) 컴포넌트만 사용한다. Server 컴포넌트의 포트는 6000으로 하고, 디자인 타임시에 active를 true로 해주었다.
서버 프로그램에서 하는일은 클라이언트로 부터 데이타가 전달되면 (Socket.ReceiveText) 현재 서버에 접속되어 있는 모든 클라이언트에게 이 메시지를 다시 전달하는 역할만 한다.. 이름 그림으로 표현하면 그림 4와 같다.
현재 서버에 접속되어 있는 모든 클라이언트에 대한 정보는 Connections에 있다. Connections프라터티는 TCustomWinSocket타입이다. 하지만 현재 몇개의 클라이언트가 접속되어 있는지는 알수 없기 때문에 try except 코드를 사용했다. 할당되지 않은 소켓에 대해서 위와같은 코드는 예외상황을 발생시키기 때문에 이를 이용하면 클라이언트의 접속, 해제를 체크하지 않고도 몇개의 클라이언트가 접속상태에 있는지 체크해볼수 있다.
procedure TForm1.Edit1KeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
if key = vk_return then
begin
Client.Socket.SendText(edit1.text);
edit1.text := '';
end;
end;
end.
클라이언트 프로그램은 기본폼, 메모컴포넌트, 에디트 컴포넌트, 클라이언트 소켓 컴포넌트를 사용한다. 서버에서와 마찬가지로 클라이언트 소켓 포트 6000, address 는 127.0.0.1로 되어 있다. 역시 active는 true로 디자인 타임시 설정되어 있다. 첫번째 이벤트는 서버에서 보낸 데이타가 있을 경우 이를 메모장에 출력하는 이벤트이다. 두번째 이벤트는 에디트 컨트롤에서 엔터키가 입력되면 메시지를 서버로 전송하는 코드이다. 위의 코드들은 지난달에사용되었기 때문에 특별히 어려운점은 없다,.
그리고 이 두개의 프로그램을 실행시킨 화면이 그림5에 있다. 채팅 서버와 채팅 클라이언트를 만드는데 작성한 코드는 두 프로그램 합쳐서 10여줄 밖에 안된다. 그리고 이 10여줄코드에서도 어려운 코드는 하나도 없다. 간단하지 않은가? 네트웍 프로그래밍이 그리 어려운것은 아니다. 하지만 어떻게 돌아가는지 알기가 힘들기 때문에 어려울 뿐이다. 다음달에 알아볼 애플리케이션 프로토콜 TELNET, FTP, POP3, SMTP, NMTP등도 바로 이것을 기본으로 하고 있다. 네트웍 프로그래밍에 대해서 막역한 두려움이 채팅프로그램으로 깨지길바란다.
이번 연제에서 가장 중요한 프로그램소스가 바로 위에서 소개한 두개의 프로그램이다. 실제로 우리가 작성하게될 채팅 프로그램은 이보다 복잡하지만 여기서 부터는 응용이기 때문에 개발자에 따라서 상당히 달라질수 있는 부분이다.
위의 프로그램을 사용해보면 느낄수 있겠지만 이 프로그램에서 가장 부족한 것은 메시지가 누구로부터 전달됐는지 메시지 앞에 표시되지 않는점이다. 앞에서 보았던 실제 채팅 상황에서는 철수가 메시지를 보냈으면 자기이름이 출력되었다. 이를 해결하려면 서버에 메시지를 전달할때 Client.Socket.SendText( ꇤ떼
그럼 이러한 기초적인 지식을 바탕으로 해서 실제로 사용할수 있는 서버 프로그램을 만들어 보도록 하자.
II.서버
1.서버의 기능분석
이번 예제에서 무엇을 알수 있나
클라이언트/서버모델에서 서버가 담당할 일은 무엇인가?
여러개의 클라이언트 관리를 어떻게 할것인가?
귓속말을 어떻게 구현할수 있나
채팅규약을 왜 만들어야 하는가
서버의 기능에 대해서는 앞에서부터 계속 설명했다. 가장 중요한 것은 서버가 왜 필요한가를 이해하는것이다. 위의 몇가지 질문에 대해서 답하면 아래와 같다. 클라이언트/서버 모델에서 서버가 담당해야 할일은 클라이언트간의 중간 역할, 클라이언트의 서비스 요청 처리이다. 채팅 프로그램에서는 첫번째 역할이 주를 차지한다. 이번 채팅 프로그램 가지고는 클라이언트의 서비스 요청이 구체적으로 어떠한 것인지를 탐지하기가 힘들다. 개념적으로 이해했다고 해도 그 기능을 서버에서 지원해야 할지 클라이언트에서 지원해야 할지 확실하지 않을수도 있다. 두번째 서버의 기능은 SQL TCP/IP를 통해서 확인하고 여기서는 첫번째 역할에만 촛점을 맞추자. 두번째 의문에 대한 대답은 위에서본 첫번째 예제에서처 럼 Connections[i]를 통해서 관리하고 있다. 클라이언트의 관리는 서버소켓에 의해서 관鱁된다. 클라이언트 소켓과 서버 소켓의 가장 큰 차임점이 바로 이것이다. 첫번째원고에서도 강조했었지만 원래 소켓이라고 하는 것에는 클라이언트용 소켓이라든가 서버 소켓이라든가 하는 개념은 존재하지 않는다. 그렇지만 현실에서 서버의 기능을 생각한다면 서버는 여러개의 클라이언트를 관리해야 한다. 나머지 두개에 대한 대답은 먼저 구현하고 그 대답을 살펴보도록 하자.
서버폼디자인
서버폼을 디자인해보자. 서버는 사용자에게는 별로 필요가 없다. 실제로 제작되는 여러 서버 프로그램들은 화면이 없을수도 있고 간단한 대화상자로 구성될수도 있다. 이렇게 화면이 간단하거나 없는 경우에는 로그 파일(TXT파일)을 남긴다. 그래서 누가 언제 들어와서 언제 나갔는지를 체크해볼수 있게 할수도 있다.
그림 6 서버의 메인폼 화면
그림6에서 볼수 있듯이 메인 화면은 세 부분으로 구성되어 있다. 화면상단에 보이는 것은 서버의 작동을 제어하는 버튼(TBitBtn)이다. 서버 소켓을 열고 닫는 역할을 한다.
그아래에 있는 스트링 그리드는 현재 접속되어 있는 클라이언트들의 상태를 보여준다. 접속번호, 사용자아이디, 접속주소, 접속시간등을 그리드에 표현한다. 그리드에는 현재 접속되어 있는 클라이언트를 보여준다.
프라퍼티
서버 메인 프로그램에서 특별히 신경써야 할 프라퍼티는 없다. 서버 소켓의 프라퍼티도 디펄트값을 사용한다. 왜나하면 서버가 사용할 포트번호등은 전달에 살펴보았듯이 SetServerSocket함수가 그 역할을 해주기 때문이다. 나머지는 소스를 참조하기 바란다.
이벤트
이 프로그램에서 사용하는 이벤트 중에서 살펴볼 주요 이벤트는 아래와 같다.
# 폼의 OnCreate 이벤트
if not SetServerSocket(Server, GetExecPath + 'tcpinfo.ini') then
ShowMessage('초기화를 할수 없습니다.');
Personal := TPersonal.Create;
INI := TINIFile.create(GetExecpath + 'chatsvr.ini');
if Ini.ReadString('value', 'AutoOn', 'false') = 'true' then
btSOpenClick(Sender);
INI.free;
chatsvr.ini파일
[value]
AutoOn=true
첫번째 if 문은 서버소켓을 오픈한다. 그리고 Personal 클래스는 현재 클라이언트의 여러가지정보(접속시간, 클라이언트주소, 사용자아이디)를 관리한다. 클라이언트 관리에 대해서는 아래단락을 참조하면 된다. 그 아래있는 라인들은 서버 프로그램이 시작되면서 자동으로 서버를 리스닝 모드에 둘지를 결정한다. 서버를 상황에 따라서 제어하고 싶을때 어떤한 방법을 사용할수 있는지를 보여주기 위해서 작성해보았다. 여기서 btSOpen함수는 서버를 오픈시키는 비트버튼을 클릭하면 발생하는 이벤트이다. 많은 부분에서 INI를 충분히 이용할수 있으므로 적용범위를 확대해보자.
# 서버 오픈 버튼 OnClick 이벤트
서버 소켓을 오픈하다. (Server.open) 포트번호는 오픈전에 SetServerSocket함수에 의해 세팅된다.
#서버 소켓의 OnClientConnect 이벤트(클라이언트가 접속요청을 해왔을 경우)
클라이언트가 접속을 요청해왔을 경우 발생하는 이벤트이다. 클라이언트의 접속요청시 클라이언트정보를 Personal에 저장하지는 않는다. 그러므로 그리드에 반영되지 않는다. 그리고 실질적인 클라이언트 접속에 대한처리도 여기서 일어나지 않는다. 다음단락 스크립트처리 부분에서 좀더 자세히 살펴보자.
#서버 소켓의 OnDisConnect(클라이언트가 접속해제할 경우)
클라이언트가 접속을 해제해 왔을경우 발생한다. 하지만 그렇게 중요하지는 않다. 왜냐하면 이 메시지가 발생하는 것은 실질적으로 소켓연결이 끊어지는 시점이기 때문이다. 이 메시지가 발생되기 전에 클라이언트는 서버에게 이젠 접속을 마칠것임을 알린다. 하이텔 채팅이면 /quit가 그 역할을 한다. 물리적인 연결이 끊어지는 시점과 논리적인 연결이 끊어지는 시점에 대한 이해는 다음 단락 스크립트 처리 부분에서 보자.
#서버 소켓의 OnClientRead(클라이언트에서 보내진 데이타가 있을경우)
서버에서 가장 중요한 부분이다. 서버에서 어떤 요청이 들어왔을경우(메시지를 전달하는 것도 하나의 요청이다. 최소한 그렇게 해석할수 있다.)이에 대한 모든 처리를 이 함수가 담담한다. 이 부분이 짜여지는 정도에 따라서 서버의 여러 가지 기능이 추가되고 여러 가지 프로그램이 만들어진다. 좀더 자세한 내용은 다음단락의 스크립트 처리를 보면된다.
5.클라이언트 정보관리
각각의 클라이언트 관리는 TPersonal클래스가 한다. 이 클래스에 대한 선언은 아래와 같다.
TPersonal = class
public
address, userid, time : array[1..MAX_COUNTS] of string;
counts : integer;
constructor Create;
function Add(add, user : string) : boolean;
function Delete(user : string) : boolean;
function GetByUser(user : string) : integer;
function GetByAddress(add : string) : integer;
end;
이들 함수의 내부적인 구현은 소스 코드를 참조하면 된다. 코드가 어렵지는 않기 때문에 쉽게 이해할수 있다. 참고로 Add와 Delete할때 여기서는 배열을 사용한다. 가장 기본적인 구조라할수 있다. 여기서의 문제점은 Delete할때이다. 이를 해결하기 위해서 링크트리스트를 이용하면 된다. 델파이에서 링크트리스트를 만드는 방법은 박스기사에서 참고하기 바란다. 필자가 여기서 배열을 사용한 이유는 클라이언트가 30여개정도라면 한사람이 빠져나감으로써 걸리는 Delete시간이 짧기 때문이다. 에디터같은 것을 만들때 링크트리스트를 이용해야만 하는데 그것은 중간에 끼워넣기, 삭제등이 자주일어나고 옮겨야할 내용도 수만수십만바이트가 되기 때문이다. 함수들을 몇개 살펴보자.
Add(add, user : string) : boolean;
새로운 유저를 추가한다. Add 파라미터는 클라이언트의 주소이고, user는 채팅에 사용할 사용자 이름이다. 클라이언트가 접속요청을 해오면 이 함수를 불러 접속한 클라이언트를 관리한다.
Delete(user : string) : boolean;
유저를 지운다. 클라이언트가 접속을 해제하면 이 함수를 호출해서 더이상 메시지를 보내지 않도록 한다.
GetByUser(user : string) : integer;
사용자이름을 가지고 특정 클라이언트를 찾는다 반환하는 값은 인덱스이다. (첫번째 클라이언트의 인덱스는 0이다) 귓속말 구현에 사용된다.
GetByAddress(add : string) : integer;
클라이언트 주소를 가지고 특정 클라이언트를 찾는다 반환하는 값은 인덱스이다. (첫번째 클라이언트의 인덱스는 0이다)
6.스크립트 처리
이번 강좌의 응용에서 가장 중요한 부분이 바로 스크립트라는 개념을 도입하고, 이를 지원하는 부분이다. 왜 스크립트라는 개념을 도입하는가? 그건 향후 효율성 때문이다. 단지 채팅 프로그램만을 만들거라면 스크립트를 구현할 필요가 없다. 하지만 좀더 효과적인 통신을 한다던가 이를 응용해서 전자칠판, 게임등을 좀더 쉽게 만들기 위해서는 스크립트를 만드는 것이 효과적이다. 그럼 어떠한 방법으로 스크립트를 전달하고 전달받을까?
일반적인 메시지는 아래와같이 구성된다.
사용자 이름 + 전달메시지
하지만 이것을 명령어체계안으로 집어넣어서 아래와 같이 만든다.
Msg() : 사용자 이름 + 전달메시지
둘의 차이점은 앞에 Msg가 붙어있다는 점이다. 메시지만 전달한다면 이렇게 하는 의미가 없겠지만 Msg말고 다른 기능을 하나 추가해보면 장점을 쉽게 확인할수 있다.. 귓속말 함수를 생각해보자. Msg()함수는 그 다음에 나오는 메시지를 모든 클라이언트에게 전송한다. 하지만 귓속말 함수
One(철수): 사용자이름 + 전달메시지
이명령어를 서버로 보내며 Msg함수와는 달리 철수에게만 괄호 다음에 나오는 메시지를 보낸다. 이런 식으로 클라이언트에서 필요한 기능을 생각해서 적적할 함수이름과 파라미터를 생각하면 여러가지 기능을 생각할수 있다. 아래에 기본적으로 지원할수 있는 함수들이 나열되어 있다.
사용자를 등록하고자 할때
예)connect(철수)
클라이언트가 서버에 접속하고자 할때 이명령어를 사용한다. 위에서 클라이언트가 서버 접속요청을 하면 사용자 등록을 하지 않고 이 명령어가 전달될때 사용자 등록을 하게 된다. 서버와 클라이언트가 단지 물리적인 연결이 성립되면 주소는 알수 있지만 사용자 이름을 알수없기 때문에 connect에서 사용자 관리를 하는 것이다.
일반메시지를 전하고자 할때
예)msg()철수 : 안녕하세요
나가고자 할때
예) disconnect(철수)
클라이언트가 접속해제하고자 할때 이 명령어를 사용한다. connect함수와 만찬가지이다.
귓속말을 하고자할때
예) one(철수)영희 : 안녕
현재 접속중인 사용자 명단을 알고싶을때
예) getinfo()
이함수를 서버에 보내면 서버는 클라이언트에게 현재 접속되어 있는 사용자 이름에 대한 정보를 연속적으로 보내게 된다.
이들을 지원한다고 해도 클라이언트 프로그램은 거의 바뀌지 않는다. 클라이언트는 단지 명령어를 보내는 역할만 한다. 메시지를 해석해서 각 명령에 맞는 역할을 하는것은 서버의 몫이다. 필자가 사용하는 명령어 형식은 아래와 같다
명령어(파라미터)메시지
명령어는 우리가 원하는 기능에 대한 이름이다. 파라미터는 귓속말처럼 기능에 추가적인 정보를 주고자할때 사용한다. 메시지는 서버로 전달될 실제 내용이다. 클라이언트에서 이런 형태로 메시지가 들어오면 서버는 아래의 함수를 통해서 메시지를 분석한다.
function GetCommand(str : string) : string;
function GetParam(str : string) : string;
function GetMsg(str : string) : string;
이들 함수의 내부 구현은 소스코드를 참조하면 된다. 이들 함수들은 TCPLIB.PAS에 선언되어 있다.
그림 위에서 설멸할때 지나간 OnClientRead의 소스 코드를 보자.
var
command, param, msg, text, t : string;
i : integer;
begin
text := Socket.ReceiveText;
command := GetCommand(text);
param := GetParam(text);
msg := GetMsg(text);
if command = 'connect' then
begin
Personal.add(Socket.RemoteAddress, param);
end
else if command = 'disconnect' then
begin
Personal.Delete(param);
end
else if command = 'msg' then
begin
i := 0;
while true do
begin
try
Server.Socket.Connections[i].SendText(msg);
inc(i);
except
break;
end;
end
end
else if command = 'one' then
begin
i := Personal.GetByUser(param);
Server.Socket.Connections[i].SendText(msg);
Socket.SendText(msg);
end
else if command = 'getinfo' then
begin
for i := 0 to personal.counts - 1 do
socket.SendText(personal.userid[i]+ ' ');
end;
ReDrawGrid;
end;
여기서는 다섯개의 명령어를 지원하다. 다 살펴볼수는 없고 이중 귓속말하기 부분만 살펴보자. (if command = one부분)
클라이언트가 서버에게 보낸 메시지는 ꒝ne(철수)영희 : 안녕철수야 ꕋ甄
command := one
param := 철수
msg := 영희 : 안녕철수야
소스를 보면 GetByUser(철수)함수를 통해서 클라이언트 소켓에 대한 인덱스를 찾는다. 그리고 나서 찾은 소켓에게만 msg값을 보낸다. 그다음 문장은 메시지를 보낸 영희에게 에코를 시켜주기 위한 라인이다. 그리 어렵지는 않다. 나머지 4개의 명령어로 이런식으로 분색해보면 쉽게 이해가 갈것이다.
그림 7은 접속부터 접속해제까지 스크립트가 오고가고 처리되는 과정이다. 이들 함수를 얼마나 많이 지원하는가, 그리고 어떠한 형태로 지원하는가에 따라 여러가지 프로그램이 만들어 질수 있다. 네트웍 게임 프로그램을 만든다면 이 부분을 확실하게 만들어 두는 것이 프로그램을 편하고 효율적으로 할수 있게 할것이다.
III.클라이언트
1.클라이언트 기능 분석
이번 예제에서 무엇을 알수 있나
클라이언트에서 하는 일이 서버에서 하는일과 가장큰차이점은 무엇인가?
범용성 있는 클라이언트를 어떻게 만들수 있는가?
채팅 클라이언트로 실제로 사용되기 위해서 어떠한 기능들이 필요한가?
클라이언트 기능은 서버가 처리해야할 기능에 비해 구현해야할 기능이 별로 없다. 클라이언트에서 제공되는 것첨 보이는 많은 기능들은 서버에서 구현되어지고 클라이언트는 단지 서버에 명령만 보내면 된다. 서버가 스크립트 처리가 주 작업이라면 클라이언트는 서버에서 들어오는 메시지 출력이 주 작업이다. 어떠한 메시지가 어떻게 해석되어 누구에게 가야할지는 서버가 그 역할을 맏는다. 클라이언트는 들어오는 메시지를 해석할 필요없이 단지 뿌려주기만 하면 된다. 그리고 전달하고 싶은 내용만 서버에 전달하면 된다. 으로써 첫번째 질문에 답을 했다. 채팅 클라이언트가 아닌 범용 클라이언트를 만들기 위해서는 클라이언트에서 서버로 전달할수 있는 스크립트의 사용을 좀더 체계화해야 한다. 여기서는 몇가지 명령을 살펴봄으로써 전자칠판이나 게임에 사용될수 있는 좀더 나은 스크립트를 위한 기초지식을 갖을수 있을것이다. 위의 간단한 채팅서버와 채팅 클라이언트에서 가장 부족했던 기능은 누가 메시지를 전달했는지 표시되지 않는 점이다. 그리고 채팅 서버에 들어왔는데 현재 누가 접속되어있는지 알수없다면 채팅으로서의 기능을 할수 없다. 이 두가지 기능이 채팅 클라이언트로써 갖추어야 할 최소한의 기능이 된다. 이런한 것들 구현할수 있는 방법을 알아보도록 하자.
클라이언트폼디자인
채팅 클라이언트 프로그램은 5개의 폼으로 구성되어 있다.
메인 폼 : 서버에서 들어오는 메시지를 표시하고, 서버에 메시지를 보낸다
클라이언트 기능을 사용하기 위해 윈도우 상단에는 버튼들이 있다.
그 아래는 서버에서 들어오는 메시지를 출력하는 메모장이 있고, 그 아래에는
서버에 전달할 메시지를 입력하는 메모장이 있다.(그림8)
서버명령어 폼 : 서버에 스크립트를 전달한다.(그림9)
사용자이름 입력폼 : 서버에 접속할때 사용할 사용자ID입력대화상자(그림10)
말머리를 입력폼 : 전달할 메시지의 앞에 들어갈 말머리입력 대화상자(그림11)
귓속말 설정 폼 : 귓속말을 할 사용자 ID를 입력한다.(그림12)
그림 8 - 12
프로그램이 시작되면 메인폼이 뜬다. 여기서 서버접속 버튼을 선택하면 사용자이름 입력폼이 뜬다. 여기서 사용할 사용자 이름을 입력하고 확인버튼을 누르면 서버에 접속이 된다. 그리고 메인화면에는 서버에서 발생시킨 메시지가 출력된다. 여기서 귓속말을 하고 싶으면 서버명령어 폼을 띄우고 사용자 리스트 스크립트 명령어를 작생해서 확인 보튼을 누른다. 그러면 현재 사용자를알수 있고 귓속말 설정폼을 띄워서 그 이름을 입력해준다. 이제부터는 메인화면에서 입력하는 내용은 다른 클라이언트에게는 전달되지 않고, 특정인에게만 전달된다. 이러면 둘만의 대화를 즐길수가 있다.(채팅하는 사람들 사이에서는 잠수한다고 한다.그리고 이렇게 유지되는 방을 잠수방이라고 한다.)
서버 명령어 폼을 보면 콤보 박스를 통해서 서버에 보낼수 있는 서비스를 선택하고 해당서비스에 전달할 파라미터가 있으면 파라미터란에 파라미터를 써주고(여기에는 주로 유저 아이디가 들어간다.)그리고 전달할 메시지가 있으면 메시지를 써주면 된다. 어떤 서비스는 파라미터와 메시지 모두를 사용할수 도 있고, 파라미터만 사용할수도 있고, 메시지만 사용할수도 있다. 다른 것들은 간단하므로 화면만 보면 금방알수 있다.
그림 13 귓속말을 설정하는 폼
프라퍼티
특별히 신경써야 하는 컴포넌트는 없다. 클라이언트 소켓에 대한 호스트 주소값과 포트 번호는 서버프로그램에서 작성할때 처럼 라이브러리 함수에 의해서 자동으로 세팅된다.
버튼들은 해당 버튼을 눌렀을 경우 각각의 기능에 맞는 윈도우 폼을 띄우는 역할만 한다.
컴포넌트의 이름은 지금까지 사용했던 방식을 사용한다. 그래서 그림 버튼들은 bb를 사용한다 예를들어 말머리 윈도우를 띄우는 버튼의 이름은 bbHead가 된다. 서버에서 들어오는 메시지를 출력하는 메모컴포넌트는 moMsg이다. 나머지것들은 소스를 확인하기 바란다.
이벤트
#폼의 OnCreate이벤트에서 하는일
if not SetClientSocket(Client, GetExecPath + 'tcpinfo.ini') then
자이제 프로그램을 실행시켜보자. 그러기 위해서 tcpip.ini파일을 설정하자. 포트로 6000, ADDRESS로 127.0.0.1을 사용하자. 서버가 있을경우 서버IP를 사용하면 서버 프로그램을 서버 컴퓨터에서 실행시켜야 하는 불편함이 있기 때문에 개발할때는 127.0.0.1을 사용하는 것이 좋다. 우선 서버 프로그램을 실행하자. 특별한 에러사항이 발생하지 않으면 그림 과같이 화면이 나오고, 서버는 리스닝 모드로 자동 실행된다. 서버에서 할일을 끝났다. 클라이언트 프로그램을 같은 컴퓨터에서 실행하자. 실행화면이 뜨면 서버 연결 버튼을 누르자. 그럼 사용자 이름을 묻는다. 사용자 이름을 입력하자. 입력을 마치면 서버프로그램의 그리드에 지금 막 접속된 클라이언트정보가 추가된다.
클라이언트가 서버에 접속을 성공하면 그림처럼 서버에 등록되어 있는 공지상항이 전달된다. 여기서 또다른 클라이언트 프로그램을 같은 컴퓨터에서 하나 더 띄우고 위에서 했던 과정을 그대로 해보자. 이렇게 하면 두사람을 채팅할수 있다.
더 나은 기능들
들어가는 말, 나가는말
클라이언트가 서버에 접속하면 누구누구님이 들어오셨습니다라는 메시지가 모든 클라이언트에게 전달된다. 여기서 들어가는 말은 이런 것을 뜻한다. 특별한 인사말을 넣는다거나 할때 사용할수 있는 기능이다. 이를 고정적으로 사용할수도 있지만 아래 chatsvr.ini파일에 EnterMessage, ExitMessage를 정의해놓고 클라이언트 접속시 이 메시지를 뿌려주는것이 항상 똑같은 말이 나오는 것보다는 좋을 것이다. 채팅 프로그램을 꼭 채팅하는데만 사용할 필요는 없지 않은가?
[value]
EnterMessage=어서오세요일심히 채팅하세요
ExitMessage=즐거우셨습니까?안녕히가세요
공지사항
들어가는 말을 이용해서 공지사항을 전달할수 있다. 즉 chatsvr.ini의 EnterMessage에 공지사항을 써주면 된다. 이를 조금 응용하면 공지사항을 DB로 부터 읽어서 INI에 출력 하게 할수도 있다. 그러면 채팅프로그램을 고치지 않고도 상항에 따라서 동적으로 변하는 공지사항을 만들수 있다 하지만 여기서 고려해야 할점이 있다. 공지사항이 한 라인을 넘어갈경우에 대한 처리이다. 위에서와같이 하면 INI저장할때 라인구분을 할수가없다. 현재 등록되어 있는 들어가는 말을 INI에 저장할때 그냥 저장하지 말고 아래와 같이 라인끝에 엔터값대신 특수한 문자(~!)를 대신 써서 저장하면 된다.
for i := 0 to fmconf.moEnter.lines.count - 1 do
str := str + fmconf.moEnter.lines[i] +
INI.WriteString('Value', 'EnterMessage', str);
이값을 읽어서 화면에 뿌려줄때 translate(str,
박스기사 == DEL2JAVA
필자가 웹싸이트를 항해하고 있다가 우연히 이 회사를 알게되었다. 여기서는 델파이로 만들어진 소스를 자바로 바꾸어주는 유틸리티를 판매하고 있었다. (1.6버전)
아래소스는 이 프로그램이 번역한 델파이 프로그램소스중 가장 중요한 부분만 발췌한것이다. 번역해 놓은 것을 보면 프로그램이 어떤식으로 해서 델파이 코드를 자바로 컨버트 하는지 추측할수 있다.(지면상의 이유로 많이 단축시켰고, 라인수를 줄이기 위해 간단한 라인은 합쳤다.) 그리고 델파이로 짠 프로그
> tcp/ip로 서버와 클라이언트 통신( 채팅) 프로그램을 만들고자 하는데...
> 이 놈들의 콤포넌트 사용법을 모르겠군요.
> 좀 갈켜 주세요. 제발
원고 제목 : 델파이 TCP/IP프로그래밍(제목을 이렇게 바꾸었으면 합니다.)
1회원고 : TCP/IP 컴포넌트
2회원고 : 채팅 클라이언트/서버
3회원고 : SQL TCP/IP 서버
4회원고 : SQL TCP/IP 클라이언트
1회 목차
I.델파이 TCP/IP 컴포넌트
II.서버만들기
III.클라이언트 만들기
IV.서버/ 클라이언트
V.여러가지 윈속 컴포넌트
VI.마치기전에
1회 : TCP/IP 컴포넌트
네트웍프로그래밍, 과연 어려운가?
그렇지 않다. 컴퓨터 프로그램 개발 경력이 어느정도 되는 사람도 해보지 못한 분야가 네트웍쪽이다. 그만큼 네트웍 프로그래밍은 일반 개발자에게 친숙하지 않다. 이에 대한 첫번째 이유는 그래픽, 주변하드웨어, 통신 관련 프로그래밍책은 많지만 네트웍에 관련된 책은 이에 비해 훨씬 적기 때문이다. 더구나 시중에 나와있는 책중에서도 윈도우용으로 나온것은 극히 드물다. 두번째 이유는 네트웍프로그래밍을 하기 위해서는 랜환경이 갖추어져야 하는데 컴퓨터한대에서만 개발을 해온 일반 개발자에게는 역시 극복하기 어렵기 때문이다. 그래서 네트웍프로그래밍은 어렵게 느껴진다. 그래서 필자는 네트웍 프로그래밍을 시작하려고 하는 독자들을 위해서 좀 쉽게 이글을 진행시켜보고 싶다. 그리고 네트웍 프로그래밍을 하기 위해 선택한 언어는 델파이 3.0이다. 참고로 앞으로의 진행을 위해서 갖추어야 할 환경은 아래와같다.
델파이 3.0
랜 환경(랜 환경이 아니면 최소한 랜카드라도 설치되 있어야 한다.)
파라독스 DB(오라클이나 사이베이스도 상관없다.)
그리고 연재를 진행하면서 TCP/IP나 소켓에 대한 여러가지 이론에 대한 설명은 하지 않겠다. 지금까지 소켓프로그래밍에 대한 설명은 잡지, 책등에서 많이 되었다. 소켓에 대한 개념은 다른 책이나 잡지를 통해서 살펴보고 여기서는 델파이를 가지고 네트웍프로그래밍을 하는 방법에 대해서만 설명하겠다. 하지만 네트웍에 사용되는 여러 가지 개념및 용어를 모른다고 해도 걱정할 필요는없다. 네트웍프로그램에 대한 개념이 없더라도 네트웍용어 사용을 최소화해서 새로운 델파이 컴포넌트를 배운다고 생각이 들도록 용어를 가능한 쉽게 쓰도록 노력하였다. 여기에 나온 개념만 익히면 다른 네트웍 관련 책을 읽기가 한결 쉬울것이다. 그리고 보통 네트웍 프로그래밍에 관련된 책을 보면 TCP/IP구현외에 TCP를 이용해 구현한 FTP, Telnet, SMTP, Web서버 구현에 대한 내용을 많이 다루고 있다. 이런 TCP/IP를 용한 프로토콜의 구현및 응용에 대한 내용은 기회가 돠면 알아보기로 하고, 이번 연재의 목적을 TCP/IP에 대한 개념잡기로 잡았다. 그러기 위해서 우리는 첫회에 덜파이 소켓 컴포넌트에 대해서 알아보고, 2, 3, 4회에 걸쳐TCP/IP를 이용한 두개의 프로그램을 제작해보자.
I.델파이 TCP/IP 컴포넌트
TCP/IP를 가지고 프로그램을 하는데 있어 가장 중요한 개념은 소켓 개념이다. 소켓은 네트웍 프로토콜 구현에 저장된 데이터의 보다 큰 집합과 관련된 핸들이다. 소켓과 관련된 데이터는 TCP연결과 현재 연결 상태에 대한 IP어드레스와 포트같은 것들을 포함한다. 쉽게 생각하면 소켓은 일반적인 언어에서 파일핸들과 같은 개념으로 보면 된다. TCP연결을 하고 소켓을 생성해서 소켓을 전송하거나 소켓을 전송받는것이 네트웍 프로그래명의 기본이다. 나머지는 이 소켓을 해석하는 작업이다.
델파이에서 TCP/IP 소켓 프로그래밍을 지원하기 위해서 제공하는 컴포넌트는 2개이다.
TServerSocket : 서버 소켓 컴포넌트
TClientSocket : 클라이언트 소켓 컴포넌트
Socket은 원래 서버용 소켓이니 클라이언트용 소켓이니 하는 개념이없다. 이 두 컴포넌트 모두 내부적으로 같은 소켓을 사용한다. 그리고 이 두 컴포넌트는 사용방법(이벤트나 프라퍼티)도 거의 비슷하다.
서버 소켓이 클라이언트 소켓과 다른 점은 서버는 클라이언트의 요청을 기다리는 listen이라는 작업을 해야하고 클라이언트는 서버에 connect해야 한다. 이 두 컴포넌트에서는 각각 listen과 connect를 위해 모두 active라는 프라퍼티를 가지고 제어 한다. 그래서 이 두 컴포넌트를 사용하려면 active를 true로 해주어야 한다. 이렇게 하면 서버와 클라이언트가 접속된다. 그리고 TServerSocket은 여러개의 클라이언트가 접속되어 있을 경우 이를 관리해주는 기능이 포함되어 있다.
이 두 컴포넌트에서 active 말고 가장 중요한 프라퍼티는 address와 port이다. Address는 ip조소를 뜻한다. 그리고 port는 서버로 들어오는 메시지를 통과시키는 통로 번호쯤으로 생각해도 된다. 이 포트가 없다면 하나의 서버 컴퓨터에는 하나의 서버 프로그램만이 설치 될수 있다. 이 포트 번호를 통해 같은 서버컴퓨터에 여러개의 서버 프로그램이 사용될수 있다. 가령 웹서버, FTP서버, Telnet서버등 이들은 모두 다른 port를 사용한다. 즉 TCP/IP로 들어오는 데이타는 바로 들어오는 것이 아니고 port를 일단 거치다고 생각하면 된다. 참고로 잘알려진 서버가 사용하는 포트는 SMTP는 25번, NNTP는 119번, TELNET 23번 , FTP는 21번으로 정의되어 있다.
II.서버만들기
1.서버의 기능분석
이번 예제에서 무엇을 알수 있나
서버가 클라이언트로 부터 데이타를 어떻게 받는가?
서버가 클라이언트에게 데이타를 어떻게 보내는가?
첫번째 예제로 만들 프로그램은 서버의 역할을 수행하는 프로그램이다. 실제 프로그램에서 서버의 기능을 지원하려면 여러가지 상황을고려해야 한다. 하지만 여기서 만들서버는 여러 클라이언트들을 연결하는 기능과 클라이언트에서 데이타가 전달됐을때 이 데이타를 읽고 메시지를 전달한 클라이언트에게 받은 메시지를 다시 전달하는 echo기능을 지원하는 서버이다. 앞에서도 말했지만 서버는 하나이고 클라이언트는 여러개가 가능하다. 그래서 한대의 컴퓨터에서 서버를 띄워놓고 여러개의 클라이언트를 띄워놓고 여러개의 클라이언트접속을 테스트 해볼수 있다.
여러개의 클라이언트가 동시접속할수 있기 때문에 이를 관리해야 한다. 델파이 3.0에서 지원해주는 서버 소켓은 한 서버에 여러개의 클라이언트가 접속할수 있는 기능을 자체적으로 지원한다. 그래서 첫번째 예제에서는 클라이언트가 하나혹은 여러개인지 고려할필요가 없다. 어차피 서버의 리스닝역할을 통해 클라이언트의 접속을 받아들이는것은 서버 소켓이 다 알아서 해주는 기능이다.
이번 프로그램에서 서버가 해야하는일은 메시지를 받고 다시 메시지를 전송하는 기능만 하면 된다. 기본적인 소켓은 IP주소와 포트번호가 모두 필요하지만 델파이에서 제공하는 소버소켓에는 IP주소를 명시하는 부분이 없다. 이것은 당연하다. 하지만 클라이언트 프로그램은 접속해야 하는 서버의 IP주소와 포트를 모두 명시해주어야 한다. 그리고 클라이언트는 서버에서 받은 메시지를 화면에 출력하면 된다. 클라이언트가 자기가 보낸 메시지를 다시 받기 위해서 서버가 echo기능을 지원해줄 필요는 없다. 하지만 클라이언트와 서버가 서로 데이타를 실제로 주고 받는지 확인할수 있는 가장 기본적인 방법이다
echo기능을 지원하는 서버를 만들기 위해서 프로그램에서 해야 할것을 정리하면 아래와 같다
.
포트 번호를 설정한다. 예제에서는 6000을 사용함.
리스닝을 시작하기 위해서 서버 소케을 open한다. (active를 true로 해준다.)
클라이언트에서 데이타가 전달됐을때 발생하는메시지를 처리한다.
ReceiveText로 전달된 메시지를 받고 이를 메모장에 출력한다.
다시 이 메시지를 클라이언트에게 전달하기 위해서 SendText를 사용한다.
참고로 클라이언트가 서버에 연결되었거나 연결이 해제될때 클라이언트가 특정 메시지를 서버에 보내지 않는다. 즉 서버가 클라이언트로 받을수 있는 메시지는 연결이 확립되고 난후이고 연결이 끊어지면 메시지를 받을수 없다. 그러므로 서버가 받는 메시지는 클라이언트 프로그램이 실제로 서버에 보낸 데이타 뿐이다.
2.서버폼디자인
서버프로그램은 TServerSocket과 메모컴포넌트로 구성되어 있다.(그림1참조)
Tmemo :
서버 프로그램에 있는 메모장은 클라이언트에서 들어오는 메시지를 출력한다.
<그림1> 서버폼 PIC1.BMP
3.프라퍼티
TServerSocket서버의 포트를 6000으로 하자. 포트는 잘알려진 포트를 제외하고 어떠한 포트를 써도 상관없다. 1024이상의 포트를 사용하면 일반적인 프로그램과 충돌을 일으키지 않을 것이다. 채팅서버, 메일서버, FTP서버, Web서버등 많은 서버가 한번에 실행될수 있지만 이들 서버는 모두 다른 포트를 사용한다. 같은 포트를 사용하면 리스트닝 도중에 에러가 발생된다.
4.이벤트
echo서버에서 사용하는 이벤트는 서버 소켓의 OnClientRead뿐이다. 서버 프로그램의 전체 소스는 아래와 같다.
unit uex1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
ScktComp, StdCtrls;
type
TForm1 = class(TForm)
ServerSocket1: TServerSocket;
Memo1: TMemo;
procedure ServerSocket1ClientRead(Sender: TObject;
Socket: TCustomWinSocket);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
procedure TForm1.ServerSocket1ClientRead(Sender: TObject;
Socket: TCustomWinSocket);
var
str : string;
begin
str := socket.ReceiveText;
memo1.lines.add(str);
socket.sendtext(str);
end;
end.
클라이언트로 부터 데이타가 들어왔을때 발생되는 OnClientRead이벤트에서 해야할일은 앞에서 정리했었다. 여기서는 코딩상의 몇가지 특성을 살펴보자. echo서버 프로그램에서 server컴포넌트의 프라퍼티나 메쏘드를 사용하는 곳은 한군데도 없다. 서버 프로그램에서 유일하게 코딩이 필요한 부분은 이 이벤트뿐이다. 여기서 주위깊게 봐야할것은 파라미터로 전달된 Socket 뿐이다. 그리고 이 파라미터는 클라이언트 소켓이라고 생각하면 된다. 즉 OnClientRead의 두번째 파라미터는 메시지를 전달해준 클라이언트소켓에 대한 정보를 가지고 있다. 그래서 서버 소켓의 특정 함수를 이용해서 서버로 전달된 데이타를 읽는다든가 역시 서버 소켓의 특정 함수를 이용해서 클라이언트에게 데이타를 전달하는 것이 아니다. 이 파라미터는 정말로 클라이언트 소켓이다. 그리고 이렇게 생각하는것이 프로그램을 이해하기 편하다. 만약 서버에 전자와같은 함수방식으로 제어된다면 여러개 클라이언트에서 온 메시지를 어떻게 구분할것이고 어떤 클라이언트에게 메시지를 보낼지도 항상 구분해주어야 하는 어려움이 생긴다.
이렇게 해서 서버는 다만들었다. 실행을 잠시 보류하고 클라이언트 프로그램을 만들어보자.
III.클라이언트만들기
1.클라이언트 기능 분석
이번 예제에서 무엇을 알수 있나
클라이언트가 서버에게 데이타를 어떻게 보내는가?
클라이언트가 서버로부터 데이타를 어떻게 받는가?
클라이언트프로그램은 서버에게 메시지를 전달하고 서버에서 전달된 메시지를 화면에 출력하는 기능을 하게 된다. 앞에서 만든 서버가 echo기능을 하기 때문에, 클라이언트가 hello라는 메시지를 서버에게 보내면 서버는 클라이언트에게 다시 hello를 보낸다. 현재 여기서 구현된 클라이언트는 매우 간단하다. 대부분의 서버 클라이언트 프로그램에서 클라이언트는 메시지를 보내는 일만 하고 서버가 이 메시지를 받아서 이에 대한 처리를 한다. 그래서 서버는 많은 일을 처리하고, 클라이언트는 최소한의 일만 처리한다. 이번 예제나 채팅프로그램에서는 서버가 해야 할일이 클라이언트에 비해서 많지 않기 때문에, 확실한 클라이언트서버 환경을 맞보기에는 적합하지 않다. 하지만 3회와 4회를 거쳐 실질적으로 구현해야할 TCP/IP SQL서버는 메시지를받아서 메시지를 해석하고, 그리고 이 메시지에 따라서 적합한 SQL 문을 만들어서 이 SQL문을 처리하는 모든 일을 처리한다. 이에 비해 클라이언트는 상대적으로 간단하다.
클라이언트폼디자인
TClientSocket : 클라이언트를 서버에 연결
TMemo : 서버에서 전송되는 메시지를 표시
TEdit : 서버에 보낼 메시지를 입력
<그림2> 클라이언트 프로그램 폼 pic2.bmp
3.프라퍼티
클라이언트 소켓 : address를 127.0.0.1으로 한다. 아니면 서버 컴퓨터의 IP주소를 써도 된다. 그리고 이 소켓을 open하기 위해 active를 true로 바꾼다.
에디트 컴포넌트 : OnKeyDown이벤트를 작성한다.
4.이벤트
클라이언트 프로그램에서 작성해야 하는 이벤트는 두개이다. 첫번째 이벤트는 서버에서 날아오는 데이타를 읽어서 화면에 출력하는 이벤트이고 두번째는 에디트 컴포넌트에서 Enter키를 치면 입력한 내용을 서버에 보내는 키보드 이벤트이다. 아래에 전체 프로그램에 대한 소스가 있다.
unit uex02;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, ScktComp;
type
TForm1 = class(TForm)
ClientSocket1: TClientSocket;
Edit1: TEdit;
Memo1: TMemo;
procedure ClientSocket1Read(Sender: TObject; Socket: TCustomWinSocket);
procedure Edit1KeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
procedure TForm1.ClientSocket1Read(Sender: TObject;
Socket: TCustomWinSocket);
var
str : string;
begin
str := socket.ReceiveText;
memo1.lines.add(str);
end;
procedure TForm1.Edit1KeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
if key = vk_return then
ClientSocket1.socket.sendText(edit1.text);
end;
end.
첫번째 이벤트 OnRead에서 특별히 어려운 것은 없다. 서버에서 발생하는 이벤트를 작성할때와 비교해서 크게 달라진점은 없다. 두번째 파라미터는 쉽게 생각하면 서버소켓이라고 생각해도 된다. 소켓이 무엇인지 다시 한번 생각해보자. 소켓은 IP 주소, 포트, 데이타에 관한 내용을 담고 있는 구조체다. 위의 예에서 socket은 서버주소, 사용한 포트, 그리고 서버에서 클라이언트로 보낸 데이타를 가지고 있는 클래스이다. 그리고 ReceiveText는 소켓이 포함하고 있는 실제 데이타를 리턴하는 함수이다.
에디트 컴포넌트의 OnKeyDown 이벤트에서 ClientSocket1.socket이 의미하는 것은 클라이언트 소켓이다. 에디트 컴포넌트에서 리턴키가 입력되면 SendText라는 함수를 통해서 클라이언트 소켓을 구성하고 서버에게 소켓을 전송하는 기능을 가진것이 SendText함수의 기능이다. 이 클라이언트 소켓을 전달한 서버에 대한 정보는 ClientSocket1에 저장되어 있다. 이 함수를 통해서 서버에게 클라이언트의 정보, 서버의 정보그리고 실제 데이타에 대한 소켓이 클라이언트에서 서버로 전송된다. SendText함수는 클라이언트 소켓을 만들어서 서버로 전송하는 기능을 가진 함수이다. 결국 서버에는 이 소켓이 그대로 전달된다.
5.두개의 프로그램 실행시키기
이렇게 해서 서버 프로그램과 클라이언트 프로그램을 만들어 보았다. 이를 어떻게 테스트 할수 있는가? 이 프로그램을 테스트하려면 랜환경이 갖추어져야 한다. 아니면 컴퓨터 한대에 랜카드가 설치되 있어야 한다. 이럴경우 127.0.0.1이라는 특별한 IP주소를 사용해서 프로그램을 테스트 할수 있다. 이 주소는 피드백 IP주소라고 생각하면 된다.위의 예제에서 클라이언트 소켓은 IP주소와 포트 번호 모두를 명시했다. 그때 사용하는 IP주소는 서버로 쓰일 곳의 IP주소이거나 127.0.0.1중의 하나를 사용하면 된다. 서버로 사용될 컴퓨터가 꼭 NT일 필요는 없다. 서버로 사용될 컴퓨터는 고정 IP주소만 가졌다면 윈도우95라도 전혀 상관없다.
우선 서버 프로그램을 먼저 실행시켜야 한다. 그리고 서버 프로그램은 서버 컴퓨터에 계속실행되고 있어야 한다. 이런 상태에서 클라이언트 프로그램을 실행시켜서 에디트 컴포넌트에 메시지를 적고 엔터키를 누르면 똑같은 메시지가 화면에 출력된다. 출력된 메시지는 서버가 보낸 데이타이다. 같은 컴퓨터에 서버프로그램을 띄우고, 여러개의 클라이언트 프로그램을 띄워도 문제없이 잘돌아간다.(이때 각 클라이언트 프로그램은 같은 주소 같은 포트를 사용한다. 그렇지만 소켓을 최종적으로 구분하는 소켓ID가 있으므로 실질적으로 이를 가지고 각 클라이언트를 구분하게 된다. 이 소켓 ID는 파일 핸들처럼 유일한 ID이다. ) 여러 클라이언트가 동시에 메시지를 보내도 각 메시지가 반송되어서 어느 클라이언트로 전송되야 할지는 자동으로 해결된다. 여러 클라이언트가 연결되어 있을때의 교통혼잡은 소켓의 구조 때문에 자동으로 해결된다. 이를 걱정하지 말자.
두개의 프로그램을 띄워서 테스트한 예가 그림3에 있다. 위에 있는 윈도우가 서버이고 아래에 있는 것이 클라이언트이다. 클라이언트에서 hello라고 치면 서버 메모컴포넌트에 hello가 출력된다.
<그림3> 클라이언트와 서버 프로그램 실행화면
지금까지 살펴본 예에서 전송한 데이타는 string 형태였다. 하지만 소켓이 꼭 string타입만 전송할수 있는 것은 아니다. 이진 데이타 형태의 데이타도 보낼수 있다. 이런 형태의 데이타를 보낼수 있다는 의미는 레코드전송이나 파일등의 전송도 가능하다는 의미이다.
6.클라이언트와 클라이언트간 통신을 C/S에서 어떻게 구현할것인가?
소켓 프로그래밍을 처음하는 입장에서는 서버의 유용성을 크게 인식하지 못한다. 그리고 서버의 개념이 필요하지도 않다. 소켓을 가지고 클라이언트 서버환경에 익숙하지 않는 사람이 네트웍에서 두사람이 게임을 하는 프로그램을 작성한다고 보자. 그러면 A 클라이언트와 B클라이언트를 생각할것이고, A는 B에게 메시지를 보내고, B는 A에게 메시지를 보내면 된다. 즉 이런 모델은 클라이언트 서버모델이 아니라 단지 클라이언트 모델이다. 그리고 이렇게 생각하는 것이 당연하다. 하지만 똑같은 상황에서 이를 클라이언트서버개념으로 생각하면 이렇다. A는 서버에게 B 에게 메시지를 보내라는 메시지를 보낸다. B도 마찬가지로 A에게 메시지를 보내라는 메시지를 보낸다. 그러면 서버는 A로부터 메시지를 받는다. 물론 B에서 들어온 메시지가 있다면 이 메시지도 받는다. 서버는 A에서 날아온 메시지를 해석해서 메시지를 B에게 전달한다. 즉 A, B사이에 메시지를 전달해주는 역할을 하는것이 서버의 역할이다. 양자간 통신에서는 구지 서버가 필요없지만 클라이언트가 많은 경우에는 서버를 두고 이 서버가 각 클라이언트의 중재 역할을 하도록 하면 된다. 이에 대한 실질적인 예는채팅프로그램에서 구현해 보도록 하자.
IV.클라이언트/서버만들기
세번째 예제 프로그램은 클라이언트와 서버의 기능을 모두 갖춘 프로그램이다. 위에서 살펴본 두개의 프로그램을 하나로 합쳐놓은 프로그램이다. 이렇게 서버와 클라이언트의 기능을 합쳐서놓으면 프로그램을 테스트 하기가 한결 쉽다. 그리고 실질적인 메시지를 처리하면서 클라이언트와 서버에서 메시지를 처리하는 방법을 좀더 확실히 알수 있다. 그리고 이번 프로그램에는 클라이언트, 서버 소켓 컴포넌트에서 제공해주는 몇개의 함수, 이벤트, 프라퍼티를 배워보자,
1.프로그램의 기능 분석
이번 예제에서 무엇을 알수 있나
서버가 접속된 클라이언트 각각에 메시지를 어떻게 보내는가?
클라이언트의 접속상황(연결, 해제)을 서버가 어떻게 알수 있는가?
서버의 상태를 어떻게 보여줄수 있는가?
이번 프로그램은 서버의 기능과 클라이언트의 기능을 겸하는 프로그램을 작성하는 의미도 있지만 실질적으로 TCP/IP를 다루기 위해서 보내는 함수와 받는 함수말고도 많은 함수들과 여러 테크닉들이 필요하다. 이를 좀더 익히기 위해서 두개의 컴포넌트에서 제공해주는 여러기능중에서 보내기받기 함수말고 가장 필요한 몇가지 사항에 대해서 좀더 알아보자.
이 프로그램을 실행시킨뒤 서버로 작동하게 하면 서버가 제대로 작동중인지 메시지가 메모컴포넌트에 뜬다. 이를 리슨이라 함은 앞에서 설명했었다. 그리고 서버로 들어오는 메시지가 있으면 이를 메모장에 출력한다. 그리고 메시지를 받았다는 의미로 클라이언트에 ok메시지를 보낸다. 그리고 클라이언트가 접속 요청을 해오면 클라이언트 소켓의 ID 를 리스트 박스 컴포넌트에 보관하다. 소켓ID는 클라이언트가 접속을 요청해서 접속요청이 끝날때까지 같은 번호를 계속유지한다. 즉 소켓이 만들어질때마다 서로 다른 소켓 ID 가 생성되는 것은 아니다. 소켓이 전달될때마다 소켓 ID 가 다르다면 각 소켓이 어느 클라이언트에서 전송된 것인지 프로그램에서 구분해내기가 까다로워진다. 그리고 메모장 옆에는 현재 접속된 클라이언트의 소켓ID 가 있다. 이 소켓중 하나를 선택해서 에디트 컨트롤에 메시지를 적고 Enter키를 치면 이 메시지가 특정 클라이언트로만 전송된다. 또 클라이언트가 접속을 해제하면 이 목록에서 소켓ID가 제거된다. 그리고 클라이언트에서 혹시 에러가 발생하면 에러 메시지가 출력된다.
반대로 클라이언트로 작동하게 되면 클라이언트가 서버와연결되었는지를 표시한다.그리고 서버와 연결이 성공적으로 되면 server found 메시지를 출력한다. 그리고 클라이언트를 닫으면 close메시지를 출력한다. 그리고 메모컴포넌트에는 서버가 보낸 메시지를 출력한다.
2.폼디자인
화면을 보면 알겠지만 이 프로그램은 크게 세 부분으로 나뉘어져 있다.
Server Information panel 부분
listening 버튼: 서버 소켓을 오픈한다.. ( btSOpen) 괄호안은 컴포넌트의 name속성이다.
Cloe 버튼 서버 소켓을 클로우즈한다. (btSClose)
Receive Message 메모 : 서버로 들어오는 메시지나 서버에서 발생한 메시지, 서버의 접속상황들을 출력한다. ( moServer)
Client List 리스트 박스 : 현재 접속되어 있는 클라이언트 소켓의 ID 를 보관하다. (lbClient)
Client Information panel부분
connect 버튼 서버에 연결한다. (btCOpen)
Close 버튼 : 서버와 연결을 끊는다. (btCClose)
Receive Message 메모 : 클라이언트로 들어오는 메시지나 서버에서 발생한 메시지, 서버의 접속상황들을 출력한다. (moClient)
그리고 서버 소켓과 클라이언트 소켓모두를 사용한다. ( client, server)
SendMessage 부분
에디트 컨트롤 : 현재 프로그램이 서버로 진행중이면 리스트 박스에서 선택된 클라이언트에게 메시지를 보내고 클라이언트면 서버에게 메시지를 전송한다. (edit1)
< 그림4 > 클라이언트/서버 폼화면
3.이벤트
여기서 사용하는 이벤트는 10개이다. 이벤트를 정리하면 아래와 같다.
에디트 컴포넌트
OnKeyDown : 현재 실행중인 프로그램이 서버용인지 클라이언트 용인지 구분해서 클라이언트가 서버에게 메시지를 보낼지 서버가 특정 클라이언트에게 메시지를 보낼지를 판단해서 소켓을 전송한다.
서버 컴포넌트
OnClientConnect : 클라이언트가 접속 요청을 해오면 리스트 박스에 접속된 클라이언트의 소켓ID 를 기록한다.
OnClientDisConnect : 클라이언트가 접속을 끊을때 리스트 박스에서 끊을려고 하는 클라이언트 소켓 ID 를 삭제한다.
OnClientError : 클라이언트에서 에러가 발생하면 이벤트가 발생한다.
OnClientRead : 클라이언트에서 날아온 데이타가 있으면 이벤트가 발생한다.
OnListen : 서버가 리슨에 성공하면 이벤트가 발생한다.
클라이언트 컴포넌트
OnDisConnect 접속을 끊을경우(명시적으로 client.close를 호출해도 이 이벤트가 호출되고 프로그램을 종료시켜도 이 이벤트가 발생된다.)
OnConnect : 클라이언트가 서버 접속에 성공하면 이 메시지가 발생한다.
OnError : 에러가 발생하면 이벤트가 발생한다.
OnRead : 서버로부터온 데이타가 있으면 이벤트가 발생한다.
참고로 서버프로그램을 종료하면 서버는 접속되어 있는 모든 클라이언트를 자동으로 close한다.
10개의 이벤트 중에서 중요한 5개의 이벤트를 보자 먼저 에디트 컴포넌트에서 발생한 키보드 이벤트이다.
procedure TForm1.Edit1KeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
var
i : integer;
begin
if key = vk_return then
if client.active then
client.Socket.SendText(edit1.text)
else
for i := 1 to lbClient.items.count do
if Server.Socket.Connections[i-1].handle =
strtoint(lbClient.items[lbClient.ItemIndex]) then
Server.Socket.Connections[i-1].SendText(edit1.text);
end;
현재 프로그램이 서버모드로 작동중인지 클라이언트 모드로 작동중인지를 알수 있는 방법을 제시하고 있다. 변수 하나를 만들어서 현재 서버로 실행되고있는지 클라이언트로 실행되고 있는지를 나타내도 되지만 client의 active프라퍼티와 server의 active프라퍼티는 둘중하나만 open될수 있다. 즉 둘다 open될수는 없다. 이 프라퍼티가 배타적으로 사용되기 때문에 이 것으로 서버모드인지 클라이언트 모드인지를 구분할수 있다. 클라이언트 모드면 단순히 입력된 문자열을 서버로 보내면 된다. 하지만 서버 모드일경우에는 좀 복잡해보일지도 모르겠다. 이 부분의 전체적인 구조를 보기전에 처음보는 프라퍼티먼저 살펴보자.
지금까지 두개의 소켓 컴포넌트를 사용하면서 느낄수 있을지도 모르겠지만 TServerSocket , TClientSocket컴포넌트간에 차이가 없다. 이것을 구지 두개로 구분할필요가 없지 않느냐하고 생각할수도 있고 이것은 맞는 생각이다. 원래 소켓은 서버용과 클라이언트용이 구분되어있지 않다. 하지만 델파이에서 이 두 소켓 클래스가 구분되어 있는 것은 여러가지 이유가 있다. 그중 여기서 살펴볼것은 다중 클라이언트를 관리할수 있는 기능이 TServerSocket에는 들어가 있다는점이다. 현재 접속되어 있는 클라이언트 소켓은 Socket.Connections[]에 있다. Connections[0](접속된 첫번째 클라이언트)은 지금까지 계속 보아왔던 TCustomWinSocket타입이다. 그래서 지금까지메시지를 전달하기 위해서 사용했던 SendText함수를 사용할수 있다. 이 부분의 코딩은 현재 클라이언트에 접속되어 있는 소켓의 핸들을 하나씩 비교해서 선택된 소켓의 핸들을 알아낸다, 그다음 이 소켓의 SendText함수를 호출한것이다. 현재 접속된 클라이언트 소켓의 핸들값은 리스트 박스에 모두 저장되어있다. 사용자가 서버모드에서 리턴키를 치면 리스트 박스에서 선택된 핸들값을 찾아야 하기 때문에 for문과 listbox.items.indexof함수,listobx1.itemindex를 사용했다. 이를 좀더 확장해서 클라이어트에서 클라이언트로 메시지를 전달할수도 있다. 이에 대한 좀더 확장된 예는 채팅프로그램을 구현하면서 알아보자. 그리고 채팅 프로그램에서 다시 애기하겠지만 한사람이 전달한 메시지는 모든 클라이언트에게 전송되어야 한다. 이것을 구현하려면 역시 Socket.Connections[]을 사용하면 된다.
다음은 클라이언트가 접속되었을때 서버에서 호출되는 이벤트이다.
procedure TForm1.serverClientConnect(Sender: TObject;
Socket: TCustomWinSocket);
begin
moServer.lines.add('client connect');
lbClient.items.add(inttostr(socket.handle));
end;
여기서 하는일은 클라이언트가 접속되었을때 접속되었다는 메시지를 출력하고 접속된 소켓의 핸들을 리스트 박스에 추가한다. 이렇게 추가된 소켓 핸들을 가지고 서버는 특정 클라이언트 소켓을 구분해낼수 있다. 그래서 특정 클라이언트에게만 메시지를 전달해줄수 있다. 여태까지는 메시지를 받는 소켓에게만 메시지를 전달할수 있었다.
다음은 클라이언트가 연결을 해제하였을 경우 서버에서 호출되는 이벤트이다.
procedure TForm1.serverClientDisconnect(Sender: TObject;
Socket: TCustomWinSocket);
var
i : integer;
begin
i := lbClient.items.IndexOf(inttostr(socket.handle));
lbClient.items.Delete(i);
moServer.lines.add('client close');
end;
클라이언트 접속이 해제되었을 경우 여기서 하는일은 리스트 박스에서 해제된 소켓의 핸들을 지우는 작업이다. 접속 해제된 소켓의 핸들값을 기억하고 있는 리스트 박스의 항목을 알아내기 위해서 items.indexof를 사용했고 아이템을 제거하기위해서 items.Delete함수를 사용했다.
아래는 특별한 역할은 하지 않지만 클라이언트가 연결이 끊어졌을때 이런 이벤트가 발생되고 아래와 같이 처리해주면 되는구나 하는것을 보여주기위한 예이다. 참고하기 바란다.
procedure TForm1.ClientDisconnect(Sender: TObject;
Socket: TCustomWinSocket);
begin
moClient.Lines.add('close');
end;
프로그램실행시키기
<그림5> 세번째 프로그램 실행화면
그림5에 실행화면이 있다. 위에서 부터 첫번째 윈도우가 서버이고, 밑에 있는 두개의 프로그램은 클라이언트이다. 서버 프로그램은 listening버튼을 누르면 된다. 네트웍이 제대로 동작중이면 메모컴포넌트에 listening이라 표시된다. 그리고 서버 프로그램의 리스트 박스를 보면 두개의 클라이언트가 접속되어 있고, 각 클라이언트의 핸들값이 있는 것을 볼수 있다.
V.여러가지 소켓 컴포넌트
DWinSock컴포넌트
얻을수 있는 곳 : http://www,aait.com/dwinsock
기능및 특징
델파이 2.0과 3.0에서 사용할수 있는 쉐어웨어 컴포넌트이다. 2개의 기본적인 서버 소켓과 클라이언트 소켓뿐만 아니라 8개(텍스트 서버/클라이언트, 바이너리 서버/클라이언트)의 전문화된 컴포넌트를 제공한다.
WinSock 컴포넌트
얻을수 있는곳 : 하이텔 vtool 동우회
기능및 특정 :
윈속을 컴포넌트로 구현한 기본적인 컴포넌트이다. 하나의 소켓 컴포넌트만 지원한다. 그래서 서버와 클라이언트용이 따로구분되어 있지 않다. 하지만 다른 컴포넌트와는 달리 소스가 포함되어 있기 때문에 TCP/IP내부 과정을 모두 제어해볼수 있기 때문에 TCP/IP를 공부하는데 많은 도움이 된다.
VI.마치기전에
프로그램을 테스트 할때와 실제로 서버와 클라이언트를 만들어서 운용할때 IP주소와 port주소가 다를수 있다. 테스트 할때는 자기 컴퓨터에서 서버와 클라이언트를 모두 띄워놓고 하는 것이 빠르기 때문이다. 이럴경우 IP주소를 자꾸 바꿔주어야 하는데 이를 쉽게 해결해줄수 있는 방법은 INI를 이용하는 방법이다.
TCPINFO.ini
[value]
address=127.0.0.1
port=10000
TCPINFO.ini는 위와 같이 구성하고 클라이언트와 서버의 IP주소및 port를 지정하기위해서 아래과 같은 함수를 호출하면 된다.
SetClientSocket(client, getexecpath + ꅭnitcpinfo.ini
SetServerSocket(server, getexecpath + ꅭnitcpinfo.ini
여기서 getexecpath + ꅽcpipinfo.ini ꍊ箚
여기서 getexecpath함수는 프로그램이 실행된 디렉토리 위치를 반환하는 함수이다. 이 두개의 함수와 getexecpath함수는 tcplib.pas에 정의되어 있다. 참고로 Getexecpath는 아래처럼 정의되어 있다.
result := ExtractFilePath(paramstr(0));
사용된 함수모두 델파이 기본 함수이므로 도움말을 찾아보면 쉽게 알수 있을것이다.
소스 구성
첫번째 예제
ex1.dpr
uex1.pas
uex1.dfm
두번째 예제
ex02.dpr
ex02.pas
ex02.dfm
세번째 예제
clisvr.dpr
clisvr.pas
clisvr.dfm
기타 파일
tcpinfo.ini
tcplib.pas
소켓프로그래밍
특정 클라이언트에게만 데이타 전송하려면 어떠한 방법을 사용할수 있을까? 이에 대한 분석은 다음호의 채팅프로그램을 제작하면서 귓속말하기를 구현하면서 알수 있을것이다.
또하나 소켓프로그램밍을 하면서 생기는 문제점은 많은 데이타를 클라이언트에 보낼때이다. 이때 발생하는 문제점은 없는지, 그리고 패킷을 전달할때 특정한 포맷을 가져야 하는지 이런 문제들을 생각해야 한다. 실제로 TCP/IP를 가지고 프로그래밍을 하고자 할때 미쳐생각하지 못했던 많은 문제점이 생겨난다. 소켓프로그램을 하고자 할때 발생할수 있는 문제점과 그 해결방을 채팅 프로그램과 TCP/IP SQL서버를 제작하면서 계속 알아보도록 하자.
참고 자료:
Internet WinSock - 에프원출판사
WinSock - 마소 95년 6월~9월
2회원고 : 채팅 클라이언트/서버
첫번째 예제와 두번째 예제는 풀소스를 실어야 하고 나머지는 디스켓으로 해도 상관없습니다. 가장 간다한 예제이지만 개념을 잡는데 가장 중요한 예제입니다. 실프로그램에 대한소스는 중간중간 중요한 부분만 발췌합니다.
연재를 5회로 구성했으면 합니다. 추가되는 부분은 3회부분으로 TCP/IP프로토콜을 이용한 애플리케이션 프로토콜에 대해서 알아본다면 델파이 네트웍프로그래밍이란 주제가 더 확실해 질것 같습니다. 1회 TCP/IP개념, 2회 TCP/IP 기본 예제 3회 TCP/IP잘알려진 프로토콜 4회, 5회가 TCP/IP응용으로 나뉘어지는 거죠.(3회 제목 : TCP/IP 애플리케이션 포로토콜)
2회 목차
I.채팅프로그램 설계
II.서버
III.클라이언트
IV.더 나은 기능들
마치기전에
박스기사 : Del2Java
참고기사 : 델파이에서 링크트 리스트 구현
2회 : 채팅 클라이언트 서버
지난호에서 소켓프로그래밍을 개관함으로써 네트웍 프로그래밍의 기본은 배웠다. 이번에는 소켓프로그램의 기본 예제라 할수 있는 채팅 프로그램을 만들어보자. 채팅 프로그램은 소켓을 가지고 네트웍 프로그램을 하기 위한 가장 좋은 예제이다. 실용성면에서나 응용성면 에서나 TCP/IP 에서 클라이언트 서버모델에 대한 이해를 이 예제를 통해서 습득할수 있다. 그리고 다른 애플리케이션 프로토콜을 만들때에도 상당히 도움을 준다. 지난호의 목적이 델파이에서 소켓프로그램작성이라면 이번달은 네트웍에서 클라이언트 서버모델에 대한 이해를 목적으로 한다.
그리고 채팅프로그램을 응용하면 화상통신이나, 머드게임같은 것도 만들수 있다. 이 모든것에 대한 기초가 바로 채팅 프로그램이다. 채팅은 접속이라는 영화에서 채팅 공간을 통해서 서로의 아픔을 넘어선 두 사람의 인연을 만들어 낸다. 그리고 TV 드라마중 채팅을 소재로한 내용이 있다. 형사와 벙어리 여인의 대화 수단으로써 채팅이 이용된다. 이렇듯 채팅은 우리 생활에(미래에는 지금과 같은 모습은 아닐지도 모르지만) 확고한 생활양식으로 잡아가고 있는 것 같다. 채팅은 구현이 비교적 쉽고 사용면에서도 일반인에게 가장 많이 알려져 있는 클라이언트 서버 모델이다. 우선 몇사람이 채팅하는 상황을 체크해보고 채팅프로그램을 어떻게 설계해야 하는지를 알아보자.
채팅프로그램 설계
#철수님이 들어오셨습니다.
철수 : 안녕하세요.
영희 : 어서오세요, 철수님
영수 : 어서오세요. 철수님
철수 : 요새 채팅에 대한 일반인들의 관심이 매우 높아졌습니다.
철수 : 저도 접속이라는 영화를 보고 한번 호기심에 이끌려서 발을 디뎠습니다.
영희 : 그럼 채팅이 처음이신가요?
철수 : 예.
...
철수 : 오늘만나서 반가왔 습니다.
철수 : 다음에 또 볼수 있었으면 좋겠네요.
영희 : 안녕히가세요
영수 : 안녕히가세요
#철수님이 퇴장 하셨습니다.
채팅프로그램을 만드는데 있어서 가장 먼저 이해해야 할것은 채팅서버와 채팅 클라이언트를 왜 만드는가 하는점이다. 대화만을 본다면 철수, 영희, 영수가 사용하는 프로그램이 채팅 클라이언트가된다. 여기서보면 서버의 기능은 없는 것처럼 보인다. 그리고 여러명에서 대화를 할때도 서버의 기능을 필요로 하지 않는다. 하지만 지난번에도 언급했지만 대화하는 사람이 두 사람이라면 서버가 그 기능을 필요로 하지 않지만 세사람 이상이라면 서버의 기능이 필요하다. 서로간에 전화를 하는 상황을 생각해보자. 집에 있는 전화기가 클라이언트가 되고 , 전화국이 서버가 된다. 여기서 전화국이 하는일은 전화와 전화사이의 중계역할만을 한다.
아래 그림1은 3사람이 서버없이 통신을 하는 그림이고, 그림2는 서버(전화국)가 있을 경우의 상황이다. 서버가 없다면 이해하긴 편할지 모르지만 한 클라이언트는 모든 클라이언트와 접속이 되있어야 한다. 접속되어 있지 않으면 그사람과 통신을 할수가없다. 하지만 서버가 있다면 각각의 클라이언트는 서버만을 알면 된다. 비용면에서도 훨씬 적게든다. 이 상황을 잘 분석해야한다. 그러면 채팅 서버를 왜만들어야 하는지 그리고 채팅 클라이언트가 해야 하는일이 무엇인지를 알수 있다. 다시 한번 강조하지만 중요한것은 채팅 서버를 이해하는 일이다.
서버가 할일부터 살펴보자. 서버는 항상 대기중에 있어야 한다. 활성화 시간만 본다면 서버는 항상 동작중이어야 하고 클라이언트는 필요할 경우만 동작하면 된다. 전화국은 항상 돌아가야 하고 우리는 필요할 경우에만 전화를 사용하면 된다. 이렇게 서버가 계속 작동중인상태로 들어가는것을 리스닝이라 했다. 서버가 첫번째로 할일은 바로 리스닝이다. 그러면 어느 순간에 클라이언트가 접속을 해온다.
두번째는 할일은 클라이언트가 접속을 요청해오면(프로그램에서는 서버 소켓에 ClientConnect이벤트가 호출된다.) 요청을 받아들이고, 모든 클라이언트에게 한 사람이 들어왔음을 알려주는 메시지를 보내준다. 그러면 위의 대화에서는 누구누구님이 들어오셨습니다를 현재 접속되어있는 모든 클라이언트에게 메시지를 보낸다. 그다음 철수가 메시지를 서버에게 전달한다. 서버는 이 메시지를 받아서 (OnClientRead) 모든 클라이언트(철수를 포함한 클라이언트)에게 전달한다. 그리고 이 과정이 계속적으로 반복되고 이것이 채팅 프로그램의 전체적인 운영의 흐름입니다. 채팅을 계속하다가 누눈가 한명이 빠져나가면 누구가 퇴장하셨습니다. 라는 메시지가 모든 클라이언트에게 전달된다.
그럼 클라이언트 입장에서 생각해보자. 클라이언트는 서버에 접속을 요청한다. 만약 서버가 작동중이라면 클라이언트는 서버에 접속되었다는 메시지를 받아야 한다. 그래야 접속이 가능한지 알수 있다. 접속에 성공하면 누구누구님이 입장하셨습니다. 라는 메시지를 받는다. 이제 부터는 할말을 서버에게 보내기만 하면 된다. 클라이언트는 몇명의 클라이언트가 있는지 신경쓸 필요가 없다. 단지 서버에 전하고 싶은 메시지만 전하면 된다. 그리고 클라이언트는 서버에서 전달되는 모든 메시지를 받는다. 서버에 비하면 클라이언트가 해야 할일은 지극히 간단하다. 그럴수 밖에 없는 이유는 클라이언트는 자기자신과 서버만 고려하면 되기 때문이다. 이것을 표현한 그림이 그림 3이다. 이 그림을 머릿속에 잘 보관하자.
단순히 여기까지만 해서 채팅 프로그램을 작성한다면 아래 서버 소스와 클라이언트 소스만 가지면 충분하다. 이런 골격의 채팅 프로그램의 구조를 가지고 실질적으로 사용될 수 있는 프로그램을 만들면서 TCP/IP를 다양한 상황에서 어떻게 결합시킬수 있는지를 살펴보아야 한다. 첫번째로 작성할 예제 프로그램과 두번째로 작성할 예제 프로그램은 이런 점에서 분명한 차이가 있다. 첫번째 예제프로그램에서는 TCP/IP를 가지고 채팅(서버, 클라이언트)을 어떻게 구현할수 있는지 기본 구조를 보여주고 두번째 예제 프로그램은 이를 실제로 어떻게 적용하고 응용할수 있는가를 보여준다. 두개의 프로그램을 모두 정확히 이해할 필요가 있다. 우선 첫번째 예제 프로그램의 서버 프로그램부터 보자.
서버 프로그램
unit uex1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
ScktComp;
type
TForm1 = class(TForm)
Server: TServerSocket;
procedure ServerClientRead(Sender: TObject; Socket: TCustomWinSocket);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
procedure TForm1.ServerClientRead(Sender: TObject;
Socket: TCustomWinSocket);
var
txt : string;
i : integer;
begin
txt := Socket.ReceiveText;
i := 0;
while true do
begin
try
Server.Socket.Connections[i].SendText(txt)
inc(i);
except
break;
end;
end;
end;
end.
서버 프로그램은 기본폼화면과 TServerSocket(Server) 컴포넌트만 사용한다. Server 컴포넌트의 포트는 6000으로 하고, 디자인 타임시에 active를 true로 해주었다.
서버 프로그램에서 하는일은 클라이언트로 부터 데이타가 전달되면 (Socket.ReceiveText) 현재 서버에 접속되어 있는 모든 클라이언트에게 이 메시지를 다시 전달하는 역할만 한다.. 이름 그림으로 표현하면 그림 4와 같다.
현재 서버에 접속되어 있는 모든 클라이언트에 대한 정보는 Connections에 있다. Connections프라터티는 TCustomWinSocket타입이다. 하지만 현재 몇개의 클라이언트가 접속되어 있는지는 알수 없기 때문에 try except 코드를 사용했다. 할당되지 않은 소켓에 대해서 위와같은 코드는 예외상황을 발생시키기 때문에 이를 이용하면 클라이언트의 접속, 해제를 체크하지 않고도 몇개의 클라이언트가 접속상태에 있는지 체크해볼수 있다.
다음으로 첫번째 예제 프로그램의 클라이언트 프로그램 소스를 보자.
unit uex2;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, ScktComp;
type
TForm1 = class(TForm)
client: TClientSocket;
Edit1: TEdit;
Memo1: TMemo;
procedure clientRead(Sender: TObject; Socket: TCustomWinSocket);
procedure Edit1KeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
procedure TForm1.clientRead(Sender: TObject; Socket: TCustomWinSocket);
begin
memo1.lines.add(Socket.receivetext);
end;
procedure TForm1.Edit1KeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
if key = vk_return then
begin
Client.Socket.SendText(edit1.text);
edit1.text := '';
end;
end;
end.
클라이언트 프로그램은 기본폼, 메모컴포넌트, 에디트 컴포넌트, 클라이언트 소켓 컴포넌트를 사용한다. 서버에서와 마찬가지로 클라이언트 소켓 포트 6000, address 는 127.0.0.1로 되어 있다. 역시 active는 true로 디자인 타임시 설정되어 있다. 첫번째 이벤트는 서버에서 보낸 데이타가 있을 경우 이를 메모장에 출력하는 이벤트이다. 두번째 이벤트는 에디트 컨트롤에서 엔터키가 입력되면 메시지를 서버로 전송하는 코드이다. 위의 코드들은 지난달에사용되었기 때문에 특별히 어려운점은 없다,.
그리고 이 두개의 프로그램을 실행시킨 화면이 그림5에 있다. 채팅 서버와 채팅 클라이언트를 만드는데 작성한 코드는 두 프로그램 합쳐서 10여줄 밖에 안된다. 그리고 이 10여줄코드에서도 어려운 코드는 하나도 없다. 간단하지 않은가? 네트웍 프로그래밍이 그리 어려운것은 아니다. 하지만 어떻게 돌아가는지 알기가 힘들기 때문에 어려울 뿐이다. 다음달에 알아볼 애플리케이션 프로토콜 TELNET, FTP, POP3, SMTP, NMTP등도 바로 이것을 기본으로 하고 있다. 네트웍 프로그래밍에 대해서 막역한 두려움이 채팅프로그램으로 깨지길바란다.
이번 연제에서 가장 중요한 프로그램소스가 바로 위에서 소개한 두개의 프로그램이다. 실제로 우리가 작성하게될 채팅 프로그램은 이보다 복잡하지만 여기서 부터는 응용이기 때문에 개발자에 따라서 상당히 달라질수 있는 부분이다.
위의 프로그램을 사용해보면 느낄수 있겠지만 이 프로그램에서 가장 부족한 것은 메시지가 누구로부터 전달됐는지 메시지 앞에 표시되지 않는점이다. 앞에서 보았던 실제 채팅 상황에서는 철수가 메시지를 보냈으면 자기이름이 출력되었다. 이를 해결하려면 서버에 메시지를 전달할때 Client.Socket.SendText( ꇤ떼
그럼 이러한 기초적인 지식을 바탕으로 해서 실제로 사용할수 있는 서버 프로그램을 만들어 보도록 하자.
II.서버
1.서버의 기능분석
이번 예제에서 무엇을 알수 있나
클라이언트/서버모델에서 서버가 담당할 일은 무엇인가?
여러개의 클라이언트 관리를 어떻게 할것인가?
귓속말을 어떻게 구현할수 있나
채팅규약을 왜 만들어야 하는가
서버의 기능에 대해서는 앞에서부터 계속 설명했다. 가장 중요한 것은 서버가 왜 필요한가를 이해하는것이다. 위의 몇가지 질문에 대해서 답하면 아래와 같다. 클라이언트/서버 모델에서 서버가 담당해야 할일은 클라이언트간의 중간 역할, 클라이언트의 서비스 요청 처리이다. 채팅 프로그램에서는 첫번째 역할이 주를 차지한다. 이번 채팅 프로그램 가지고는 클라이언트의 서비스 요청이 구체적으로 어떠한 것인지를 탐지하기가 힘들다. 개념적으로 이해했다고 해도 그 기능을 서버에서 지원해야 할지 클라이언트에서 지원해야 할지 확실하지 않을수도 있다. 두번째 서버의 기능은 SQL TCP/IP를 통해서 확인하고 여기서는 첫번째 역할에만 촛점을 맞추자. 두번째 의문에 대한 대답은 위에서본 첫번째 예제에서처 럼 Connections[i]를 통해서 관리하고 있다. 클라이언트의 관리는 서버소켓에 의해서 관鱁된다. 클라이언트 소켓과 서버 소켓의 가장 큰 차임점이 바로 이것이다. 첫번째원고에서도 강조했었지만 원래 소켓이라고 하는 것에는 클라이언트용 소켓이라든가 서버 소켓이라든가 하는 개념은 존재하지 않는다. 그렇지만 현실에서 서버의 기능을 생각한다면 서버는 여러개의 클라이언트를 관리해야 한다. 나머지 두개에 대한 대답은 먼저 구현하고 그 대답을 살펴보도록 하자.
서버폼디자인
서버폼을 디자인해보자. 서버는 사용자에게는 별로 필요가 없다. 실제로 제작되는 여러 서버 프로그램들은 화면이 없을수도 있고 간단한 대화상자로 구성될수도 있다. 이렇게 화면이 간단하거나 없는 경우에는 로그 파일(TXT파일)을 남긴다. 그래서 누가 언제 들어와서 언제 나갔는지를 체크해볼수 있게 할수도 있다.
그림 6 서버의 메인폼 화면
그림6에서 볼수 있듯이 메인 화면은 세 부분으로 구성되어 있다. 화면상단에 보이는 것은 서버의 작동을 제어하는 버튼(TBitBtn)이다. 서버 소켓을 열고 닫는 역할을 한다.
그아래에 있는 스트링 그리드는 현재 접속되어 있는 클라이언트들의 상태를 보여준다. 접속번호, 사용자아이디, 접속주소, 접속시간등을 그리드에 표현한다. 그리드에는 현재 접속되어 있는 클라이언트를 보여준다.
프라퍼티
서버 메인 프로그램에서 특별히 신경써야 할 프라퍼티는 없다. 서버 소켓의 프라퍼티도 디펄트값을 사용한다. 왜나하면 서버가 사용할 포트번호등은 전달에 살펴보았듯이 SetServerSocket함수가 그 역할을 해주기 때문이다. 나머지는 소스를 참조하기 바란다.
이벤트
이 프로그램에서 사용하는 이벤트 중에서 살펴볼 주요 이벤트는 아래와 같다.
# 폼의 OnCreate 이벤트
if not SetServerSocket(Server, GetExecPath + 'tcpinfo.ini') then
ShowMessage('초기화를 할수 없습니다.');
Personal := TPersonal.Create;
INI := TINIFile.create(GetExecpath + 'chatsvr.ini');
if Ini.ReadString('value', 'AutoOn', 'false') = 'true' then
btSOpenClick(Sender);
INI.free;
chatsvr.ini파일
[value]
AutoOn=true
첫번째 if 문은 서버소켓을 오픈한다. 그리고 Personal 클래스는 현재 클라이언트의 여러가지정보(접속시간, 클라이언트주소, 사용자아이디)를 관리한다. 클라이언트 관리에 대해서는 아래단락을 참조하면 된다. 그 아래있는 라인들은 서버 프로그램이 시작되면서 자동으로 서버를 리스닝 모드에 둘지를 결정한다. 서버를 상황에 따라서 제어하고 싶을때 어떤한 방법을 사용할수 있는지를 보여주기 위해서 작성해보았다. 여기서 btSOpen함수는 서버를 오픈시키는 비트버튼을 클릭하면 발생하는 이벤트이다. 많은 부분에서 INI를 충분히 이용할수 있으므로 적용범위를 확대해보자.
# 서버 오픈 버튼 OnClick 이벤트
서버 소켓을 오픈하다. (Server.open) 포트번호는 오픈전에 SetServerSocket함수에 의해 세팅된다.
#서버 소켓의 OnClientConnect 이벤트(클라이언트가 접속요청을 해왔을 경우)
클라이언트가 접속을 요청해왔을 경우 발생하는 이벤트이다. 클라이언트의 접속요청시 클라이언트정보를 Personal에 저장하지는 않는다. 그러므로 그리드에 반영되지 않는다. 그리고 실질적인 클라이언트 접속에 대한처리도 여기서 일어나지 않는다. 다음단락 스크립트처리 부분에서 좀더 자세히 살펴보자.
#서버 소켓의 OnDisConnect(클라이언트가 접속해제할 경우)
클라이언트가 접속을 해제해 왔을경우 발생한다. 하지만 그렇게 중요하지는 않다. 왜냐하면 이 메시지가 발생하는 것은 실질적으로 소켓연결이 끊어지는 시점이기 때문이다. 이 메시지가 발생되기 전에 클라이언트는 서버에게 이젠 접속을 마칠것임을 알린다. 하이텔 채팅이면 /quit가 그 역할을 한다. 물리적인 연결이 끊어지는 시점과 논리적인 연결이 끊어지는 시점에 대한 이해는 다음 단락 스크립트 처리 부분에서 보자.
#서버 소켓의 OnClientRead(클라이언트에서 보내진 데이타가 있을경우)
서버에서 가장 중요한 부분이다. 서버에서 어떤 요청이 들어왔을경우(메시지를 전달하는 것도 하나의 요청이다. 최소한 그렇게 해석할수 있다.)이에 대한 모든 처리를 이 함수가 담담한다. 이 부분이 짜여지는 정도에 따라서 서버의 여러 가지 기능이 추가되고 여러 가지 프로그램이 만들어진다. 좀더 자세한 내용은 다음단락의 스크립트 처리를 보면된다.
5.클라이언트 정보관리
각각의 클라이언트 관리는 TPersonal클래스가 한다. 이 클래스에 대한 선언은 아래와 같다.
TPersonal = class
public
address, userid, time : array[1..MAX_COUNTS] of string;
counts : integer;
constructor Create;
function Add(add, user : string) : boolean;
function Delete(user : string) : boolean;
function GetByUser(user : string) : integer;
function GetByAddress(add : string) : integer;
end;
이들 함수의 내부적인 구현은 소스 코드를 참조하면 된다. 코드가 어렵지는 않기 때문에 쉽게 이해할수 있다. 참고로 Add와 Delete할때 여기서는 배열을 사용한다. 가장 기본적인 구조라할수 있다. 여기서의 문제점은 Delete할때이다. 이를 해결하기 위해서 링크트리스트를 이용하면 된다. 델파이에서 링크트리스트를 만드는 방법은 박스기사에서 참고하기 바란다. 필자가 여기서 배열을 사용한 이유는 클라이언트가 30여개정도라면 한사람이 빠져나감으로써 걸리는 Delete시간이 짧기 때문이다. 에디터같은 것을 만들때 링크트리스트를 이용해야만 하는데 그것은 중간에 끼워넣기, 삭제등이 자주일어나고 옮겨야할 내용도 수만수십만바이트가 되기 때문이다. 함수들을 몇개 살펴보자.
Add(add, user : string) : boolean;
새로운 유저를 추가한다. Add 파라미터는 클라이언트의 주소이고, user는 채팅에 사용할 사용자 이름이다. 클라이언트가 접속요청을 해오면 이 함수를 불러 접속한 클라이언트를 관리한다.
Delete(user : string) : boolean;
유저를 지운다. 클라이언트가 접속을 해제하면 이 함수를 호출해서 더이상 메시지를 보내지 않도록 한다.
GetByUser(user : string) : integer;
사용자이름을 가지고 특정 클라이언트를 찾는다 반환하는 값은 인덱스이다. (첫번째 클라이언트의 인덱스는 0이다) 귓속말 구현에 사용된다.
GetByAddress(add : string) : integer;
클라이언트 주소를 가지고 특정 클라이언트를 찾는다 반환하는 값은 인덱스이다. (첫번째 클라이언트의 인덱스는 0이다)
6.스크립트 처리
이번 강좌의 응용에서 가장 중요한 부분이 바로 스크립트라는 개념을 도입하고, 이를 지원하는 부분이다. 왜 스크립트라는 개념을 도입하는가? 그건 향후 효율성 때문이다. 단지 채팅 프로그램만을 만들거라면 스크립트를 구현할 필요가 없다. 하지만 좀더 효과적인 통신을 한다던가 이를 응용해서 전자칠판, 게임등을 좀더 쉽게 만들기 위해서는 스크립트를 만드는 것이 효과적이다. 그럼 어떠한 방법으로 스크립트를 전달하고 전달받을까?
일반적인 메시지는 아래와같이 구성된다.
사용자 이름 + 전달메시지
하지만 이것을 명령어체계안으로 집어넣어서 아래와 같이 만든다.
Msg() : 사용자 이름 + 전달메시지
둘의 차이점은 앞에 Msg가 붙어있다는 점이다. 메시지만 전달한다면 이렇게 하는 의미가 없겠지만 Msg말고 다른 기능을 하나 추가해보면 장점을 쉽게 확인할수 있다.. 귓속말 함수를 생각해보자. Msg()함수는 그 다음에 나오는 메시지를 모든 클라이언트에게 전송한다. 하지만 귓속말 함수
One(철수): 사용자이름 + 전달메시지
이명령어를 서버로 보내며 Msg함수와는 달리 철수에게만 괄호 다음에 나오는 메시지를 보낸다. 이런 식으로 클라이언트에서 필요한 기능을 생각해서 적적할 함수이름과 파라미터를 생각하면 여러가지 기능을 생각할수 있다. 아래에 기본적으로 지원할수 있는 함수들이 나열되어 있다.
사용자를 등록하고자 할때
예)connect(철수)
클라이언트가 서버에 접속하고자 할때 이명령어를 사용한다. 위에서 클라이언트가 서버 접속요청을 하면 사용자 등록을 하지 않고 이 명령어가 전달될때 사용자 등록을 하게 된다. 서버와 클라이언트가 단지 물리적인 연결이 성립되면 주소는 알수 있지만 사용자 이름을 알수없기 때문에 connect에서 사용자 관리를 하는 것이다.
일반메시지를 전하고자 할때
예)msg()철수 : 안녕하세요
나가고자 할때
예) disconnect(철수)
클라이언트가 접속해제하고자 할때 이 명령어를 사용한다. connect함수와 만찬가지이다.
귓속말을 하고자할때
예) one(철수)영희 : 안녕
현재 접속중인 사용자 명단을 알고싶을때
예) getinfo()
이함수를 서버에 보내면 서버는 클라이언트에게 현재 접속되어 있는 사용자 이름에 대한 정보를 연속적으로 보내게 된다.
이들을 지원한다고 해도 클라이언트 프로그램은 거의 바뀌지 않는다. 클라이언트는 단지 명령어를 보내는 역할만 한다. 메시지를 해석해서 각 명령에 맞는 역할을 하는것은 서버의 몫이다. 필자가 사용하는 명령어 형식은 아래와 같다
명령어(파라미터)메시지
명령어는 우리가 원하는 기능에 대한 이름이다. 파라미터는 귓속말처럼 기능에 추가적인 정보를 주고자할때 사용한다. 메시지는 서버로 전달될 실제 내용이다. 클라이언트에서 이런 형태로 메시지가 들어오면 서버는 아래의 함수를 통해서 메시지를 분석한다.
function GetCommand(str : string) : string;
function GetParam(str : string) : string;
function GetMsg(str : string) : string;
이들 함수의 내부 구현은 소스코드를 참조하면 된다. 이들 함수들은 TCPLIB.PAS에 선언되어 있다.
그림 위에서 설멸할때 지나간 OnClientRead의 소스 코드를 보자.
var
command, param, msg, text, t : string;
i : integer;
begin
text := Socket.ReceiveText;
command := GetCommand(text);
param := GetParam(text);
msg := GetMsg(text);
if command = 'connect' then
begin
Personal.add(Socket.RemoteAddress, param);
end
else if command = 'disconnect' then
begin
Personal.Delete(param);
end
else if command = 'msg' then
begin
i := 0;
while true do
begin
try
Server.Socket.Connections[i].SendText(msg);
inc(i);
except
break;
end;
end
end
else if command = 'one' then
begin
i := Personal.GetByUser(param);
Server.Socket.Connections[i].SendText(msg);
Socket.SendText(msg);
end
else if command = 'getinfo' then
begin
for i := 0 to personal.counts - 1 do
socket.SendText(personal.userid[i]+ ' ');
end;
ReDrawGrid;
end;
여기서는 다섯개의 명령어를 지원하다. 다 살펴볼수는 없고 이중 귓속말하기 부분만 살펴보자. (if command = one부분)
클라이언트가 서버에게 보낸 메시지는 ꒝ne(철수)영희 : 안녕철수야 ꕋ甄
command := one
param := 철수
msg := 영희 : 안녕철수야
소스를 보면 GetByUser(철수)함수를 통해서 클라이언트 소켓에 대한 인덱스를 찾는다. 그리고 나서 찾은 소켓에게만 msg값을 보낸다. 그다음 문장은 메시지를 보낸 영희에게 에코를 시켜주기 위한 라인이다. 그리 어렵지는 않다. 나머지 4개의 명령어로 이런식으로 분색해보면 쉽게 이해가 갈것이다.
그림 7은 접속부터 접속해제까지 스크립트가 오고가고 처리되는 과정이다. 이들 함수를 얼마나 많이 지원하는가, 그리고 어떠한 형태로 지원하는가에 따라 여러가지 프로그램이 만들어 질수 있다. 네트웍 게임 프로그램을 만든다면 이 부분을 확실하게 만들어 두는 것이 프로그램을 편하고 효율적으로 할수 있게 할것이다.
III.클라이언트
1.클라이언트 기능 분석
이번 예제에서 무엇을 알수 있나
클라이언트에서 하는 일이 서버에서 하는일과 가장큰차이점은 무엇인가?
범용성 있는 클라이언트를 어떻게 만들수 있는가?
채팅 클라이언트로 실제로 사용되기 위해서 어떠한 기능들이 필요한가?
클라이언트 기능은 서버가 처리해야할 기능에 비해 구현해야할 기능이 별로 없다. 클라이언트에서 제공되는 것첨 보이는 많은 기능들은 서버에서 구현되어지고 클라이언트는 단지 서버에 명령만 보내면 된다. 서버가 스크립트 처리가 주 작업이라면 클라이언트는 서버에서 들어오는 메시지 출력이 주 작업이다. 어떠한 메시지가 어떻게 해석되어 누구에게 가야할지는 서버가 그 역할을 맏는다. 클라이언트는 들어오는 메시지를 해석할 필요없이 단지 뿌려주기만 하면 된다. 그리고 전달하고 싶은 내용만 서버에 전달하면 된다. 으로써 첫번째 질문에 답을 했다. 채팅 클라이언트가 아닌 범용 클라이언트를 만들기 위해서는 클라이언트에서 서버로 전달할수 있는 스크립트의 사용을 좀더 체계화해야 한다. 여기서는 몇가지 명령을 살펴봄으로써 전자칠판이나 게임에 사용될수 있는 좀더 나은 스크립트를 위한 기초지식을 갖을수 있을것이다. 위의 간단한 채팅서버와 채팅 클라이언트에서 가장 부족했던 기능은 누가 메시지를 전달했는지 표시되지 않는 점이다. 그리고 채팅 서버에 들어왔는데 현재 누가 접속되어있는지 알수없다면 채팅으로서의 기능을 할수 없다. 이 두가지 기능이 채팅 클라이언트로써 갖추어야 할 최소한의 기능이 된다. 이런한 것들 구현할수 있는 방법을 알아보도록 하자.
클라이언트폼디자인
채팅 클라이언트 프로그램은 5개의 폼으로 구성되어 있다.
메인 폼 : 서버에서 들어오는 메시지를 표시하고, 서버에 메시지를 보낸다
클라이언트 기능을 사용하기 위해 윈도우 상단에는 버튼들이 있다.
그 아래는 서버에서 들어오는 메시지를 출력하는 메모장이 있고, 그 아래에는
서버에 전달할 메시지를 입력하는 메모장이 있다.(그림8)
서버명령어 폼 : 서버에 스크립트를 전달한다.(그림9)
사용자이름 입력폼 : 서버에 접속할때 사용할 사용자ID입력대화상자(그림10)
말머리를 입력폼 : 전달할 메시지의 앞에 들어갈 말머리입력 대화상자(그림11)
귓속말 설정 폼 : 귓속말을 할 사용자 ID를 입력한다.(그림12)
그림 8 - 12
프로그램이 시작되면 메인폼이 뜬다. 여기서 서버접속 버튼을 선택하면 사용자이름 입력폼이 뜬다. 여기서 사용할 사용자 이름을 입력하고 확인버튼을 누르면 서버에 접속이 된다. 그리고 메인화면에는 서버에서 발생시킨 메시지가 출력된다. 여기서 귓속말을 하고 싶으면 서버명령어 폼을 띄우고 사용자 리스트 스크립트 명령어를 작생해서 확인 보튼을 누른다. 그러면 현재 사용자를알수 있고 귓속말 설정폼을 띄워서 그 이름을 입력해준다. 이제부터는 메인화면에서 입력하는 내용은 다른 클라이언트에게는 전달되지 않고, 특정인에게만 전달된다. 이러면 둘만의 대화를 즐길수가 있다.(채팅하는 사람들 사이에서는 잠수한다고 한다.그리고 이렇게 유지되는 방을 잠수방이라고 한다.)
서버 명령어 폼을 보면 콤보 박스를 통해서 서버에 보낼수 있는 서비스를 선택하고 해당서비스에 전달할 파라미터가 있으면 파라미터란에 파라미터를 써주고(여기에는 주로 유저 아이디가 들어간다.)그리고 전달할 메시지가 있으면 메시지를 써주면 된다. 어떤 서비스는 파라미터와 메시지 모두를 사용할수 도 있고, 파라미터만 사용할수도 있고, 메시지만 사용할수도 있다. 다른 것들은 간단하므로 화면만 보면 금방알수 있다.
그림 13 귓속말을 설정하는 폼
프라퍼티
특별히 신경써야 하는 컴포넌트는 없다. 클라이언트 소켓에 대한 호스트 주소값과 포트 번호는 서버프로그램에서 작성할때 처럼 라이브러리 함수에 의해서 자동으로 세팅된다.
버튼들은 해당 버튼을 눌렀을 경우 각각의 기능에 맞는 윈도우 폼을 띄우는 역할만 한다.
컴포넌트의 이름은 지금까지 사용했던 방식을 사용한다. 그래서 그림 버튼들은 bb를 사용한다 예를들어 말머리 윈도우를 띄우는 버튼의 이름은 bbHead가 된다. 서버에서 들어오는 메시지를 출력하는 메모컴포넌트는 moMsg이다. 나머지것들은 소스를 확인하기 바란다.
이벤트
#폼의 OnCreate이벤트에서 하는일
if not SetClientSocket(Client, GetExecPath + 'tcpinfo.ini') then
ShowMessage('클라이언트로 초기화 할수 없습니다.');
서버에서와 마찬가지로 클라이언트 소켓을 INI파일로 부터 초기화한다.
#클라이언트가 접속버튼을 클릭했을 경우 하는일
Client.Open;
btCClose.Enabled := true;
btCOpen.Enabled := false;
fmUser.ShowModal;
Client.Socket.SendText('connect(' + fmUser.edUser.text + ')');
버튼을 클릭하고 채팅에 사용될 사용자 이름(영수)을 대화상자에서 입력하면 connect( ꇝ돔
# 클라이언트 접속해제버튼을 클릭했을경우 하는일
Client.Socket.SendText('disconnect(영수)');
Client.Close;
btCClose.Enabled := false;
btCOpen.Enabled := true;
disconnect 명령을 통해서 논리적인 연결을 끊는다. 서버는 이 명령을 받으면 파라미터로 전달되는 영수를 찾아 Personal의 Delete함수를 호출해서 클라이언트를 삭제한다.
# 클라이언트에 전달된 데이타가 있을때 하는일(OnRead)
moMsg.lines.add(Socket.ReceiveText);
클라이언트에서는 서버에서 전달된 데이타를 메모장에 뿌려주기만 하면 된다.
# 에디트 컨트롤에서 엔터키를 입력했을때 하는 일( OnKeyDown)
if key = VK_RETURN then
begin
if spOne.down then
// 귓속말을 설정했을 경우
Client.Socket.SendText('one(' + fmOneMan.edUser.text + ')' + fmUser.edUser.text +
+ fmHead.edHead.text + edChat.text);
else
// 귓속만을 설정하지 않았을 경우
Client.Socket.SendText('msg()' + fmUser.edUser.text + ' ' + fmHead.edHead.text +
edChat.text);
end;
프로그램 실행 .
자이제 프로그램을 실행시켜보자. 그러기 위해서 tcpip.ini파일을 설정하자. 포트로 6000, ADDRESS로 127.0.0.1을 사용하자. 서버가 있을경우 서버IP를 사용하면 서버 프로그램을 서버 컴퓨터에서 실행시켜야 하는 불편함이 있기 때문에 개발할때는 127.0.0.1을 사용하는 것이 좋다. 우선 서버 프로그램을 실행하자. 특별한 에러사항이 발생하지 않으면 그림 과같이 화면이 나오고, 서버는 리스닝 모드로 자동 실행된다. 서버에서 할일을 끝났다. 클라이언트 프로그램을 같은 컴퓨터에서 실행하자. 실행화면이 뜨면 서버 연결 버튼을 누르자. 그럼 사용자 이름을 묻는다. 사용자 이름을 입력하자. 입력을 마치면 서버프로그램의 그리드에 지금 막 접속된 클라이언트정보가 추가된다.
클라이언트가 서버에 접속을 성공하면 그림처럼 서버에 등록되어 있는 공지상항이 전달된다. 여기서 또다른 클라이언트 프로그램을 같은 컴퓨터에서 하나 더 띄우고 위에서 했던 과정을 그대로 해보자. 이렇게 하면 두사람을 채팅할수 있다.
더 나은 기능들
들어가는 말, 나가는말
클라이언트가 서버에 접속하면 누구누구님이 들어오셨습니다라는 메시지가 모든 클라이언트에게 전달된다. 여기서 들어가는 말은 이런 것을 뜻한다. 특별한 인사말을 넣는다거나 할때 사용할수 있는 기능이다. 이를 고정적으로 사용할수도 있지만 아래 chatsvr.ini파일에 EnterMessage, ExitMessage를 정의해놓고 클라이언트 접속시 이 메시지를 뿌려주는것이 항상 똑같은 말이 나오는 것보다는 좋을 것이다. 채팅 프로그램을 꼭 채팅하는데만 사용할 필요는 없지 않은가?
[value]
EnterMessage=어서오세요일심히 채팅하세요
ExitMessage=즐거우셨습니까?안녕히가세요
공지사항
들어가는 말을 이용해서 공지사항을 전달할수 있다. 즉 chatsvr.ini의 EnterMessage에 공지사항을 써주면 된다. 이를 조금 응용하면 공지사항을 DB로 부터 읽어서 INI에 출력 하게 할수도 있다. 그러면 채팅프로그램을 고치지 않고도 상항에 따라서 동적으로 변하는 공지사항을 만들수 있다 하지만 여기서 고려해야 할점이 있다. 공지사항이 한 라인을 넘어갈경우에 대한 처리이다. 위에서와같이 하면 INI저장할때 라인구분을 할수가없다. 현재 등록되어 있는 들어가는 말을 INI에 저장할때 그냥 저장하지 말고 아래와 같이 라인끝에 엔터값대신 특수한 문자(~!)를 대신 써서 저장하면 된다.
for i := 0 to fmconf.moEnter.lines.count - 1 do
str := str + fmconf.moEnter.lines[i] +
INI.WriteString('Value', 'EnterMessage', str);
이값을 읽어서 화면에 뿌려줄때 translate(str,
박스기사 == DEL2JAVA
필자가 웹싸이트를 항해하고 있다가 우연히 이 회사를 알게되었다. 여기서는 델파이로 만들어진 소스를 자바로 바꾸어주는 유틸리티를 판매하고 있었다. (1.6버전)
아래소스는 이 프로그램이 번역한 델파이 프로그램소스중 가장 중요한 부분만 발췌한것이다. 번역해 놓은 것을 보면 프로그램이 어떤식으로 해서 델파이 코드를 자바로 컨버트 하는지 추측할수 있다.(지면상의 이유로 많이 단축시켰고, 라인수를 줄이기 위해 간단한 라인은 합쳤다.) 그리고 델파이로 짠 프로그