본문 바로가기
Delphi Tip/통신

소켓 프로그래밍 기법의 활용 5편

by MonoSoft 2021. 12. 26.
728x90
반응형

소켓 프로그래밍 기법의 활용 5편

 

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);

begin

  Listening := False;

end;

 

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);

begin

  ServerSocket1.Close;

  ClientSocket1.Close;

end;

 

Listen 버튼의 OnClick 이벤트에서는 현재의 폼을 서버로 대기하도록 하는 기능을 한다. 

버튼의 OnClick 이벤트 핸들러를 다음과 같이 작성한다.

 

procedure TForm1.Button1Click(Sender: TObject);

begin

  Listening := not Listening;

  if Listening then

  begin

    ClientSocket1.Active := False;

    ServerSocket1.Active := True;

    Statusbar1.Panels[0].Text := 'Listening...';

  end

  else

  begin

    if ServerSocket1.Active then

    ServerSocket1.Active := False;

    Statusbar1.Panels[0].Text := '';

  end;

end;

 

이 버튼을 클릭하면 현재의 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);

begin

  if ClientSocket1.Active then 

    ClientSocket1.Active := False;

 

  if Length(Edit1.Text) > 0 then

  begin

    ClientSocket1.Address := Edit1.Text;

    ClientSocket1.Active := True;

  end;

end;

 

procedure TForm1.Button3Click(Sender: TObject);

begin

  ClientSocket1.Close;

  Button1Click(nil);

end;

 

이제 각 버튼의 OnClick 이벤트 핸들러는 모두 작성하였다. 

이제부터 서버 소켓과 클라이언트 소켓의 이벤트 핸들러를 

하나씩 작성하면서 이들의 역할을 알아보도록 하자.

먼저 서버 소켓의 이벤트 핸들러를 작성하도록 하자. 

 

앞에서도 설명했듯이 서버 소켓에 클라이언트 소켓이 접속을 시도하면 

OnGetSocket 이벤트에 이어서 OnAccept 이벤트가 발생한다. 

 

이 이벤트에서는 서버가 클라이언트의 접속을 받아들이기로 할 때 

발생하는 이벤트이므로 IsServer 변수를 True로 설정하여 

이제 어플리케이션이 서버로 동작하고 있음을 나타내게 하고, 

StatusPanel에 연결된 클라이언트의 IP 주소를 나타내도록 한다. 

 

OnAccept 이벤트의 Socket 파라미터는 클라이언트 소켓을 나타낸다.

 

procedure TForm1.ServerSocket1Accept(Sender: TObject;

Socket: TCustomWinSocket);

begin

  IsServer := True;

  Statusbar1.Panels[0].Text := 'Connected to: ' + Socket.RemoteAddress;

end;

 

이어서 클라이언트가 연결을 완료하면 OnClientConnect 이벤트가 발생한다. 

지금 작성하고 있는 채팅 어플리케이션과 같은 논-블로킹 서버의 경우에는 

이 시점에서 데이터를 읽고, 쓰기를 시작할 수 있다. 

 

Memo2 컴포넌트에는 클라이언트 소켓에서 발생한 메시지를 표시할 것이므로, 

이 시점에서부터 내용을 보여주도록 메모 컴포넌트의 내용을 지우도록 한다. 

 

그리고, OnRead 이벤트는 서버 소켓이 클라이언트 소켓으로부터 데이터를 

전달받을 때 발생하는데 Socket 파라미터의 ReceiveText 프로퍼티에 

클라이언트 소켓에서 전송한 텍스트 값이 들어가 있다. 

 

그러므로 이를 Memo2 컴포넌트에 보여주도록 이벤트 핸들러를 작성한다.

 

procedure TForm1.ServerSocket1ClientConnect(Sender: TObject;

Socket: TCustomWinSocket);

begin

  Memo2.Lines.Clear;

end;

 

procedure TForm1.ServerSocket1ClientRead(Sender: TObject;

Socket: TCustomWinSocket);

begin

  Memo2.Lines.Add(Socket.ReceiveText);

end;

 

마지막으로 연결이 종료될 때에 발생하는 OnClientDisconnect 이벤트 핸들러에서는 

서버 소켓의 Active 프로퍼티를 False로 설정하고, 

처음의 Listening 상태로 들어가기 위해서

Button1Click 이벤트 핸들러를 다시 호출한다. 

 

이때 이를 호출하면 Listening 변수의 값이 변경되므로 

먼저 Listening 변수 값을 변경한 뒤에 호출해야 원래의 값이 보존될 것이다.

 

procedure TForm1.ServerSocket1ClientDisconnect(Sender: TObject;

Socket: TCustomWinSocket);

begin

  ServerSocket1.Active := False;

  Listening := not Listening;

  Button1Click(nil);

end;

 

이번에는 클라이언트 소켓의 이벤트 핸들러를 작성할 차례이다. 

클라이언트 소켓도 서버 소켓과 접속이 되었을 때 OnConnect 이벤트가 발생한다. 

여기에서도 마찬가지로 접속된 서버 소켓이 위치한 컴퓨터의 이름을 나타내도록 하는데, 

Socket 파라미터의 RemoteHost 프로퍼티를 사용하면 

마이크로소프트의 UNC이름에 해당되는 컴퓨터 이름이 나타난다.

 

procedure TForm1.ClientSocket1Connect(Sender: TObject;

Socket: TCustomWinSocket);

begin

  Statusbar1.Panels[0].Text := 'Connected to: ' + Socket.RemoteHost;

end;

 

이어서 나타날 수 있는 OnRead 이벤트 핸들러에서는 서버 소켓을 

Socket 파라미터에서 얻을 수 있다. 

서버의 텍스트 내용을 클라이언트로 동작하고 있는 어플리케이션의 메모 컴포넌트에 

보여주도록 다음과 같이 이벤트 핸들러를 작성한다.

 

procedure TForm1.ClientSocket1Read(Sender: TObject;

Socket: TCustomWinSocket);

begin

  Memo2.Lines.Add(Socket.ReceiveText);

end;

 

클라이언트 소켓의 OnDisconnect 이벤트 핸들러에서는 Button1Click 이벤트 핸들러를 

호출하여 서버 소켓으로 동작할 수 있도록 한다.

 

procedure TForm1.ClientSocket1Disconnect(Sender: TObject;

Socket: TCustomWinSocket);

begin

  Button1Click(nil);

end;

 

그리고, 이런 접속과정에서 에러가 발생할 경우에는 OnError 이벤트가 발생하는데, 

이벤트 핸들러를 다음과 같이 작성하여 발생된 에러 상황을 나타내도록 한다.

 

procedure TForm1.ClientSocket1Error(Sender: TObject;

Socket: TCustomWinSocket; ErrorEvent: TErrorEvent;

var ErrorCode: Integer);

begin

  Memo2.Lines.Add('Error connecting to : ' + Edit1.Text);

  ErrorCode := 0;

end;

 

마지막으로 Memo1 컴포넌트의 OnKeyDown 이벤트 핸들러에서 눌려진 키가 

리턴 키일 경우에 소켓으로 전송하도록 한다. 

 

이때 IsServer 변수를 검사하여 서버일 경우와 클라이언트일 경우에 

서로 다른 소켓에다가 SendText 메소드를 이용하여 텍스트를 전송한다.

 

procedure TForm1.Memo1KeyDown(Sender: TObject; var Key: Word;

Shift: TShiftState);

begin

  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]);

end;

 

여기서 클라이언트 소켓의 경우에는 간단하지만, 

서버 소켓의 경우 여러 개의 클라이언트 소켓과 물릴 수 있기 때문에 

Connections라는 컬렉션 객체가 포함되어 있다. 

 

그렇지만, 우리가 작성한 어플리케이션은 단지 1:1 통신만 지원하므로 

Connections[0]으로 접속된 클라이언트 소켓을 지칭할 수 있다.

 

이것으로 간단한 1:1 통신을 지원하는 채팅 어플리케이션이 완성되었다. 

컴파일하고 실행한 뒤에 서버와 클라이언트 컴퓨터에 각각 띄우도록 한다.

 

그리고, 서버 측에서는 Listen 버튼을 클릭하여 클라이언트 프로그램의 접속을 대기하도록 하고, 

클라이언트 측에서는 에디트 박스에 IP 주소를 적어 넣은 뒤에 

Connect 버튼을 클릭하여 서버에 접속하도록 하자. 

 

성공적으로 접속이 되면, 상태 바에 연결된 컴퓨터의 IP 주소(서버 컴퓨터의 경우) 또는 

연결된 컴퓨터의 컴퓨터 이름(클라이언트 컴퓨터의 경우)이 나타날 것이다. 

이제 메모 컴포넌트에서 통신을 시도하면 다음과 같이 채팅을 할 수 있을 것이다.

728x90
반응형

댓글