11-15: 라인에서는 서버로 접속을 시도하고, 실패하면 프로그램을 종료하도록 되어 있습니다.
예제에 대한 소스는 아래의 주소를 참고하시기 바랍니다. 아래 예제 소스에서는 클라이언트 풀링을 토대로 Provider가 작성되어 있습니다.
지금부터는 실제 코드 내용을 살펴보면서 설명을 이어가도록 하겠습니다. 우선 서버 쪽부터 시작합니다.
VirtualSocket을 사용하기 위해 제일 먼저 해야 할 일은 Provider를 구현하는 하는 것 입니다. Provider의 구현은 아래와 같은 요소 등에 따라 달라 질 수 있습니다. 예제에서는 이미 언급한 대로, 클라이언트 풀링을 사용하며, Indy 9을 래핑해서 사용합니다. 제한 속도 등과 같은 디테일한 설정은 예제에서 제외되어 있습니다. 이와 같은 부분들은 실제 프로젝트의 소스를 소개 할 때 다루도록 하겠습니다.
- 소켓은 어떤 소켓을 사용 할 것 인지?
- 또는 자신이 직접 소켓을 구현 할 것 인지?
- 패킷 전송 정책은 어떻게 할 것 인지? (전송 속도 제한, 풀링 or 푸시 등)
- 패킷의 종류에 따라 우선 순위를 둘 것 인지?
다음으로 작성해야 할 것은 패킷의 종류 마다 VirtualSocket을 구현하는 것 입니다. 예제에서는 Text, Voice, Video로 나누어서 작업이 되어 있지만, 실제 구현이 완료되어 있는 것은 Text 뿐 입니다. Voice와 Video에 대해서는 다른 포스트에서 다루도록 하겠습니다.
VirtualSocket을 구현할 때는, [소스 3]과 같이 TVirtualServerSocket 클래스를 상속받아서 사용합니다. [소스 3]에는 지금까지 설명이 생략된 TChannelInfo 클래스가 나타납니다. 이는 현재 접속한 사용자의 각 채널에 대한 정보를 관리하는 클래스 입니다. 모든 채널에 공동적으로 사용되는 표준적인 방법을 따른다면 구태여 10: 라인처럼 상속하여 새로운 클래스를 작성 할 필요는 없습니다.
TChannelInfo 클래스를 확장하여 사용하는 경우에 대해서는 역시 실제 프로젝트 소스를 통해서 설명하도록 하겠습니다.
[소스 3]
unit TextServer;
interface
uses
VirtualSocketUtils, VirtualServerSocket,
Classes, SysUtils;
type
TTextChannelInfo = class (TChannelInfo)
private
public
end;
TTextServer = class (TVirtualServerSocket)
private
public
protected
function do_CreateChannelInfo(AConnection:IConnection):TChannelInfo; override;
procedure do_OnConnected(AChannelInfo:TChannelInfo); override;
procedure do_OnDisonnected(AChannelInfo:TChannelInfo); override;
procedure do_OnReceived(AChannelInfo:TChannelInfo; AData:pointer; ASize:integer); override;
end;
implementation
{ TTextServer }
function TTextServer.do_CreateChannelInfo(
AConnection: IConnection): TChannelInfo;
begin
Result := TTextChannelInfo.Create(AConnection, FChannel);
end;
procedure TTextServer.do_OnConnected(AChannelInfo: TChannelInfo);
begin
// Todo :
AChannelInfo.Certified := true;
end;
procedure TTextServer.do_OnDisonnected(AChannelInfo: TChannelInfo);
begin
end;
procedure TTextServer.do_OnReceived(AChannelInfo: TChannelInfo; AData: pointer;
ASize: integer);
begin
if ASize > 0 then SendToAll(AData, ASize)
else AChannelInfo.SendData;
end;
end.
19-22: 라인에서는 TVirtualServerSocket 에서 정의한 abstract 메소드를 재 정의하고 있습니다. 이에 대한 설명은 각각 아래와 같습니다.
- do_CreateChannelInfo
- 채널 정보를 관리 할 객체를 생성하여, 부모 클래스가 이를 통해 VirtualSocket 하부 로직을 처리하도록 합니다.
- 32: 라인에서는 TTextChannelInfo 을 생성하고 있지만, 현재 예제의 상태로는 TChannelInfo를 생성하여 리턴하여도 무방합니다.
- AConnection은 실제 접속 정보를 관리하는 객체입니다. 자동으로 생성되어 인자로 넘어옵니다.
- FChannel은 자신의 채널 정보입니다. 0번부터 차례로 채널 번호를 자동으로 부여 받게 됩니다.
- do_OnConnected
- 새로운 커넥션이 접속되었을 때, 발생하는 이벤트 호출 입니다.
- 38: 라인에서는 접속된 커넥션을 바로 인증을 하여 줍니다. (일종의 로그인) 실제로는 로그인 과정을 거쳐서 사용자 인증을 거친 사용자를 따로 관리하고자 할 때 사용 합니다.
- AChannelInfo.Certified := true; 와 같은 코드는 현재 체널이 아닌 전체 커넥션에 대한 인증을 true로 변경합니다.
- do_OnDisonnected
- 접속이 끊어질 때, 발생하는 이벤트 호출 입니다.
- do_OnReceived
- 새로운 패킷이 수신되었을 때 발생하는 이벤트 호출 입니다.
- 50: 라인에서 패킷의 크기가 0보다 크면, 자신을 포함한 전체 사용자에게 같은 메시지를 전달하도록 하고 있습니다.
- SendToAll은 TVirtualServerSocket에 미리 구현된 메소드이며, SendTo, SendToOther 등이 준비되어 있습니다. 이 부분에 대한 설명도 실제 프로젝트 소스 설명을 할 때, 진행하도록 하겠습니다.
- 패킷의 크기가 0보다 작으면, 지금까지 자신의 채널에 쌓인 메시지를 클라이언트에 보내도록 합니다.
- 예제에서 클라이언트는 패킷의 크기가 0인 메시지를 수시로 서버에게 보내면서 자신에게 온 메시지를 풀링 해 가도록 Provider가 작성되어 있습니다.
위에서 등장하는 IConnection의 멤버 구성은 아래와 같습니다.
- procedure Send(AChannel:byte; AData:pointer; ASize:integer);
- 실제 데이터를 클라이언트에 전송합니다.
- 사용자가 직접 사용하는 것보다, TChannelInfo를 통해서 전달 되도록 합니다. 전체 구조를 이해하고 자신만의 로직을 적용하는 것이 아니라면 사용하지 마시기 바랍니다.
- procedure setCertified(AValue:boolean);
- 사용자 인증을 지정합니다. true가 지정되면 해당 커넨셕의 인증은 허가된 상태입니다.
- TVirtualServerSocket.SendToAll() 등은 사용자 인증이 안되어 있는 경우 해당 커넨셕에게는 패킷을 전달 하지 않습니다.
- function getCertified:boolean;
- function isReadyToSend:boolean;
- Provider 쪽에서 패킷의 우선 순위 또는 전송 속도를 제어하고 싶을 때 사용합니다. false가 리턴되면 아직 패킷을 보낼 허가가 안 떨어졌으니 보내지 말라는 의이이며, TChannelInfo에서는 isReadyToSend:boolean에 의존하여 패킷을 보낼 수 있는 지를 확인하고 보내도록 되어 있습니다.
- procedure setUserData(AChannel:byte; AData:pointer);
- IConnection 마다 연결된 TChannelInfo로 생성된 객체를 관리하기 위한 메소드 입니다.
- TVirtualServerSocket 내부에서 사용되며, 사용자가 직접 사용 할 일은 없습니다.
- function getUserData(AChannel:byte):pointer;
- IConnection 마다 연결된 TChannelInfo로 생성된 객체를 관리하기 위한 메소드 입니다.
- TVirtualServerSocket 내부에서 사용되며, 사용자가 직접 사용 할 일은 없습니다.
TChannelInfo 멤버 구성은 아래와 같습니다.
- procedure ClearData;
- 현재 채널에 저장된 패킷을 모두 삭제합니다.
- TChannelInfo은 자신과 연결된 클라이언트에게 보낸 메시지를 버퍼에 간직하고 있다가, 이것을 전달되어야 하는 시기가 되면, 버퍼에서 꺼낸 후 클라이언트에게 보내도록 되어 있습니다.
- procedure AddData(AData:pointer; ASize:integer); virtual;
- 자신과 연결된 클라이언트의 같은 채널에게 보낼 패킷을 버퍼에 저장한다.
- 본 메소드가 virtual로 선언되어 있기 때문에 사용자는 다른 정책을 사용하여 패킷을 처리 할 수가 있습니다. 예를 들어 음성의 경우에는 버퍼에 너무 많은 데이터가 적재되면, 음성 전달이 지연되므로, 적정량 이상의 패킷은 삭제하는 경우가 되겠습니다.
- procedure SendData; virtual;
- 자신과 연결된 클라이언트의 같은 채널에게 패킷을 보낸다.
- [소스 4]의 6: 라인과 12: 라인에 보면, 이미 언급한 바와 같이 IConnection.isReadyToSend를 의존하여 계속 패킷을 전송 할 것인지를 결정하고 있습니다.
- 본 메소드가 virtual로 선언되어 있기 때문에 사용자는 다른 정책을 사용하여 패킷을 처리 할 수가 있습니다.
- property Certified : boolean read GetCertified write SetCertified;
[소스 4]
procedure TChannelInfo.SendData;
var
Data : pointer;
Size : integer;
begin
if not FConnection.isReadyToSend then Exit;
FBuffer.GetPacket(Data, Size);
while (Size > 0) do begin
FConnection.Send(FChannel, Data, Size);
if not FConnection.isReadyToSend then Break;
FBuffer.GetPacket(Data, Size);
end;
end;
클라이언트의 경우에도 서버와 마찬가지로 Provider를 우선 구현해야 합니다. 이후에는 VirtualSocket을 구현해야 합니다. 클라이언트의 VirtualSocket의 경우에는 TVirtualClientSocket 상속 받아서 구현 합니다.
[소스 5]
unit TextClient;
interface
uses
VirtualClientSocket, ObserverList,
Classes, SysUtils, ExtCtrls;
type
TTextClient = class (TVirtualClientSocket)
private
FTimer : TTimer;
FObserverList : TObserverList;
procedure on_Timer(Sender:TObject);
protected
FChannel : byte;
procedure do_OnConnected; override;
procedure do_OnDisonnected; override;
procedure do_OnReceived(AData:pointer; ASize:integer); override;
public
constructor Create;
destructor Destroy; override;
procedure SendText(AText:string);
property ObserverList : TObserverList read FObserverList;
end;
implementation
{ TTextClient }
constructor TTextClient.Create;
begin
inherited;
FObserverList := TObserverList.Create(nil);
FTimer := TTimer.Create(nil);
FTimer.Interval := 5;
FTimer.Enabled := true;
FTimer.OnTimer := on_Timer;
end;
destructor TTextClient.Destroy;
begin
FreeAndNil(FObserverList);
FreeAndNil(FTimer);
inherited;
end;
procedure TTextClient.do_OnConnected;
begin
end;
procedure TTextClient.do_OnDisonnected;
begin
end;
procedure TTextClient.do_OnReceived(AData: pointer; ASize: integer);
var
ssData : TStringStream;
begin
ssData := TStringStream.Create('');
try
ssData.Write(AData^, ASize);
ssData.Position := 0;
FObserverList.AsyncBroadcast(ssData.DataString);
finally
ssData.Free;
end;
end;
procedure TTextClient.on_Timer(Sender: TObject);
begin
// 0바이트 데이터를 보내면 서버에 저장되어 있는 메시지를 풀링 해 온다.
Send(nil, 0);
end;
procedure TTextClient.SendText(AText: string);
begin
if AText <> '' then Send(@AText[1], Length(AText));
end;
end.
17-19: 라인에서는 TVirtualClientSocket에 정의된 abstract 메소들 재 정의하고 있습니다. 이들은 서버와 동일하므로 설명을 생략 합니다.
78-82: 라인에서는 TTimer를 이용하여 주기적으로 서버에게 0 크기의 패킷을 보내서 자신에게 메시지가 온 것이 있는 지 확인하고 있습니다. 서버는 이 패킷을 받게되면 이미 설명한 바와 같이 해당 클라이언트에게 전달해야 할 패킷을 버퍼에서 꺼내서 전송하게 됩니다.
63-76: 라인은 패킷을 수신 받았을 경우 처리하는 과정이 표현되어 있습니다.
72: 라인에서는 FObserverList.AsyncBroadcast() 메소드를 통해서 수신 된 패킷을 임의의 객체에게 비동기식으로 전달합니다. 쓰레드를 통해서 전달 받은 메시지를 동기식으로 처리 할 경우 발생 될 문제를 피하기 위함입니다.
[소스 6]
unit _fmMain;
interface
uses
ValueList, VirtualClient, VirtualClientProvider,
TextClient, VoiceClient, VideoClient,
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TfmMain = class(TForm)
moMsg: TMemo;
edMsg: TEdit;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure edMsgKeyPress(Sender: TObject; var Key: Char);
private
FVirtualClient : TVirtualClient;
FTextClient : TTextClient;
procedure on_Disconnected(Sender:TObject);
public
published
procedure rp_Talk(APacket:TValueList);
end;
var
fmMain: TfmMain;
implementation
{$R *.dfm}
procedure TfmMain.edMsgKeyPress(Sender: TObject; var Key: Char);
begin
if Key = #13 then begin
Key := #0;
FTextClient.SendText(Format('Code=Talk<rYu>Msg=%s', [edMsg.Text]));
edMsg.Text := '';
end;
end;
procedure TfmMain.FormCreate(Sender: TObject);
begin
FTextClient := TTextClient.Create;
FTextClient.ObserverList.Add(Self);
FVirtualClient := TVirtualClient.Create(TVirtualClientProvider.Create);
FVirtualClient.Port := 1234;
FVirtualClient.OnDisconnected := on_Disconnected;
// 클라이언트와 서버의 AddSocket 순서가 동일해야 한다.
FVirtualClient.AddSocket(FTextClient);
FVirtualClient.AddSocket(TVoiceClient.Create);
FVirtualClient.AddSocket(TVideoClient.Create);
FVirtualClient.Host := '127.0.0.1';
FVirtualClient.Port := 1234;
if not FVirtualClient.Connect then begin
Application.Terminate;
end;
end;
procedure TfmMain.FormDestroy(Sender: TObject);
begin
FVirtualClient.Free;
end;
procedure TfmMain.on_Disconnected(Sender: TObject);
begin
Application.Terminate;
end;
procedure TfmMain.rp_Talk(APacket: TValueList);
begin
moMsg.Lines.Add(APacket.Values['Msg'])
end;
end.
46: 라인을 통해서 [소스 5]의 FObserverList.AsyncBroadcast()를 통해서 전송되는 메시지를 Self(fmMain)이 받아서 처리하겠다는 것을 알립니다.
24: 라인은 FObserverList.AsyncBroadcast()에서 전송되는 메시지 중 "Code=Talk" 라는 라인이 있다면, 자신을 실행 시켜 달라는 뜻 입니다. 이때 메소드의 이름은 Code의 Value 값 앞에 "rp_" 붙여서 사용합니다. 파라메터로는 TStringList를 래핑한 TValueList를 사용합니다. 그리고, RTTI를 사용하기 때문에 published에 선언되어야 합니다.
이렇게 ObserverList를 통해서 메시지를 처리하면, 조건문을 사용하지 않고 문자열의 데이터를 토대로 메소드를 검색해서 직접 실행하게 되어 장.단점이 생기게 됩니다. 제가 작성하는 대부분의 패킷 처리 코드는 이렇게 구현되어 있습니다.
38: 라인에서 문자열에 있는 '<rYu>' 경우는 TStringList의 LineBreak를 '<rYu>'로 지정했기 때문에 사용 된 것 입니다. 기본 그대로 리턴 문자를 사용하면, 메시지 내부에서 발생하는 리턴 문자와 메시지의 내용을 구별하는 LineBreak가 서로 충돌하기 때문입니다. 따라서, 사용자가 '<rYu>'를 입력하지 못하도록 UI에서 방어를 하셔야 합니다. 또한, LineBreak는 개발자 스스로가 마음에 드는 것으로 변경해서 사용해도 상관없습니다.
예제는 컴파일 후 서버를 먼저 실행하시고, 클라이언트를 원하는 수 만큼 실행한 후 에디트 창에 메시지를 입력 한 다음 엔터키를 치면, 전체 클라이언트에게 메시지를 전달하면서 채팅을 할 수 있게 됩니다.