소스가 복잡한 편이라 예제로서는 부적합 할 수 있으나, 현재 작업 중인 과정을 잠시 설명하려고 합니다. 중단했던 미디어 서버 개발이 모두 완료되면 다시 정리해서 올리도록 하겠습니다.
우선 어제 새벽에 그린 상태도는 [그림 1]과 같습니다. 기존의 UML 툴이 제공하는 상태변화의 선행처리와 후행처리 표시가 복잡해 보여서, 네모 박스로 표시했습니다. 문제는 상태가 완전히 변한 후에 처리되느냐, 전에 처리되느냐를 알 수가 없다는 것 입니다. 나중에 좀 더 고민하여 표기법을 정리해보려고 합니다.
[그림 1]은 TCP 소켓을 이용해서 미디어 서버로부터 데이터를 스트림으로 받는 TFFTCPStream라는 클래스의 상태도입니다. TFFTCPStream는 아래와 같은 조건에서 개발 중 입니다.
[소스 1] 패킷의 종류
[소스 2] 주요 클래스의 인터페이스
20: 상태가 변화되서 자신이 선택되었을 경우 실행됩니다. 자신이 선택되었을 때 선행처리를 합니다.
21: 상태가 변화되서 다른 상태가 선택되었을 경우 실행됩니다. 다른 상태로 전이될 때 후행처리를 합니다.
23-33: [그림 1]의 상태도에서 화살표에 그려진 프로세스들을 모두 표기한 것 입니다.
[소스 3] 디버깅을 위한 코드
[소스 4] FFTCPStream.pas의 전체 소스
우선 어제 새벽에 그린 상태도는 [그림 1]과 같습니다. 기존의 UML 툴이 제공하는 상태변화의 선행처리와 후행처리 표시가 복잡해 보여서, 네모 박스로 표시했습니다. 문제는 상태가 완전히 변한 후에 처리되느냐, 전에 처리되느냐를 알 수가 없다는 것 입니다. 나중에 좀 더 고민하여 표기법을 정리해보려고 합니다.
[그림 1] 미디어 서버의 클라이언트 모듈의 일부에 대한 상태도
[그림 1]은 TCP 소켓을 이용해서 미디어 서버로부터 데이터를 스트림으로 받는 TFFTCPStream라는 클래스의 상태도입니다. TFFTCPStream는 아래와 같은 조건에서 개발 중 입니다.
- TFFTCPStream 은 서버에 있는 동영상 파일의 정보 및 데이터를 수신 받아서 버퍼에 저장한다. 이 버퍼는 미디어 플레이어 클래스에서 사용되어 재생된다.
- 안드로이드 플랫폼에서 사용 될 것이다. 다만, 디버깅 효율을 높이기 위해서 델파이로 원도우용 클라이언트를 작성하여 테스트를 진행 할 것이다.
- 스마트 폰의 특성 상 접속이 자주 끊어질 수 있다. 따라서, 접속이 끊어지면 기존의 세션을 복구해야 한다.
- 재생 위치의 빠른 이동을 위해서 서버에서는 인덱싱 정보를 캐싱하고 있다.
- 기타 등등
[소스 1] 패킷의 종류
type
TPacketType = (
// 공통
ptNone, ptPing,
// Server Received
ptOptions, ptOpen, ptReopen, ptAsk, ptClose, ptLogin, ptLogout,
// Client Received
ptPackage, ptOkOpen, ptErOpen, ptOkReopen, ptErReopen,
ptOkLogin, ptErLogin, ptOkLogout, ptErLogout
);
PPacketType = ^TPacketType;
[소스 1]은 통신 할 때 사용하는 패킷의 종류들 입니다. 대부분 이름을 보면 금새 무엇을 하는 넘인지 알 수 있을 것이라 생각합니다. ptReopen은 접속이 끊어졌다가 다시 연결되었을 때, 기존의 세션을 복구하기 위한 패킷입니다. ptAsk는 인덱스 위치에 해당하는 미디어 데이터를 요청합니다.[소스 2] 주요 클래스의 인터페이스
type
TFFTCPStream = class (TComponent)
private
public
function OpenFile(AHost:string; APort:integer; AFileName:string):boolean;
procedure CloseFile;
published
property IsOpened : boolean read FIsOpened;
property OnFileOpened : TFileOpenedEvent read FOnFileOpened write FOnFileOpened;
property OnFileOpenError : TNotifyEvent read FOnFileOpenError write FOnFileOpenError;
property OnFileClosed : TNotifyEvent read FOnFileClosed write FOnFileClosed;
end;
TState = class
private
FFFTCPStream:TFFTCPStream;
public
constructor Create(AFFTCPStream:TFFTCPStream); reintroduce; virtual;
procedure ActionIn(AOldState:TObject); virtual;
procedure ActionOut(ANewState:TObject); virtual;
function OpenFile(AHost:string; APort:integer; AFileName:string):boolean; virtual;
procedure CloseFile; virtual;
procedure on_Timer; virtual;
procedure on_Disconnected; virtual;
procedure pt_Package(AData:pointer; ASize:integer); virtual;
procedure pt_OkOpen(AData:pointer; ASize:integer); virtual;
procedure pt_ErOpen(AData:pointer; ASize:integer); virtual;
procedure pt_OkReopen(AData:pointer; ASize:integer); virtual;
procedure pt_ErReopen(AData:pointer; ASize:integer); virtual;
end;
TSate는 각 상태에 대한 클래스의 추상화된 부모 클래스입니다.20: 상태가 변화되서 자신이 선택되었을 경우 실행됩니다. 자신이 선택되었을 때 선행처리를 합니다.
21: 상태가 변화되서 다른 상태가 선택되었을 경우 실행됩니다. 다른 상태로 전이될 때 후행처리를 합니다.
23-33: [그림 1]의 상태도에서 화살표에 그려진 프로세스들을 모두 표기한 것 입니다.
[소스 3] 디버깅을 위한 코드
{ TState }
procedure TState.CloseFile;
begin
{$IFDEF DEBUG}
OutputDebugString(PChar(Format('%s.CloseFile: ', [Self.ClassName])));
{$ENDIF}
end;
{ TStateConnected }
procedure TStateConnected.CloseFile;
begin
{$IFDEF DEBUG}
inherited;
{$ENDIF}
FFFTCPStream.close_File;
FFFTCPStream.SetState(FFFTCPStream.FStateClosed);
end;
부모 클래스인 TState에는 모든 메소드마다 디버깅 정보를 출력하도록 되어 있습니다. 이를 통해서 실제 동작 할 때 어떤 순서로 동작하는 지, 그리고 [그림 1]의 상태도의 조건대로 프로세스가 진행된느 지를 쉽게 확인 할 수가 있습니다. 자식 클래스에서는 inherited 를 실행하여 자신이 동작 할 때마다 디버그 정보를 [그림 2]와 같이 출력하도록 합니다.
[그림 2] 디버깅 정보 확인
[소스 4] FFTCPStream.pas의 전체 소스
unit FFTCPStream;
interface
uses
FFServerUtils, RyuMpeg, RyuSocketClient, ThreadRepeater,
Windows, Classes, SysUtils, SyncObjs;
const
_TimerInterval = 1 * 1000; // 1초 마다
type
TFileOpenedEvent = procedure (Sender:TObject; AMediaInfo:TMediaInfo) of object;
TFFTCPStream = class (TComponent)
private
FFileName : string;
FOldTick : Cardinal;
FCS : TCriticalSection;
procedure close_File;
procedure fire_OnFileOpened(AMediaInfo:TMediaInfo);
procedure fire_OnFileOpenError;
procedure fire_OnFileClosed;
private
FSocket : TRyuSocketClient;
procedure on_Disconnected(Sender:TObject);
procedure on_Received(Sender:TObject; AData:pointer; ASize:integer);
private
procedure send_Text(APacketType:TPacketType; AText:AnsiString);
procedure send_Open;
procedure send_Reopen;
procedure send_Close;
procedure send_Ask(AIndex:integer);
private
FTimer : TThreadRepeater;
procedure on_Timer(Sender:TObject);
private
FState : TObject;
FStateClosed, FStateConnected, FStateOpened, FStateDisconnected, FStateReconnected : TObject;
procedure SetState(AState:TObject);
private
FOnFileClosed: TNotifyEvent;
FOnFileOpenError: TNotifyEvent;
FOnFileOpened: TFileOpenedEvent;
FIsOpened: boolean;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
function OpenFile(AHost:string; APort:integer; AFileName:string):boolean;
procedure CloseFile;
published
property IsOpened : boolean read FIsOpened;
property OnFileOpened : TFileOpenedEvent read FOnFileOpened write FOnFileOpened;
property OnFileOpenError : TNotifyEvent read FOnFileOpenError write FOnFileOpenError;
property OnFileClosed : TNotifyEvent read FOnFileClosed write FOnFileClosed;
end;
implementation
type
TState = class
private
FFFTCPStream:TFFTCPStream;
public
constructor Create(AFFTCPStream:TFFTCPStream); reintroduce; virtual;
procedure ActionIn(AOldState:TObject); virtual;
procedure ActionOut(ANewState:TObject); virtual;
function OpenFile(AHost:string; APort:integer; AFileName:string):boolean; virtual;
procedure CloseFile; virtual;
procedure on_Timer; virtual;
procedure on_Disconnected; virtual;
procedure pt_Package(AData:pointer; ASize:integer); virtual;
procedure pt_OkOpen(AData:pointer; ASize:integer); virtual;
procedure pt_ErOpen(AData:pointer; ASize:integer); virtual;
procedure pt_OkReopen(AData:pointer; ASize:integer); virtual;
procedure pt_ErReopen(AData:pointer; ASize:integer); virtual;
end;
TStateClosed = class (TState)
private
public
function OpenFile(AHost:string; APort:integer; AFileName:string):boolean; override;
end;
TStateConnected = class (TState)
private
public
function OpenFile(AHost:string; APort:integer; AFileName:string):boolean; override;
procedure CloseFile; override;
procedure on_Timer; override;
procedure on_Disconnected; override;
procedure pt_OkOpen(AData:pointer; ASize:integer); override;
procedure pt_ErOpen(AData:pointer; ASize:integer); override;
end;
TStateOpened = class (TState)
private
public
function OpenFile(AHost:string; APort:integer; AFileName:string):boolean; override;
procedure CloseFile; override;
procedure on_Timer; override;
procedure on_Disconnected; override;
procedure pt_Package(AData:pointer; ASize:integer); override;
end;
TStateDisconnected = class (TState)
private
public
function OpenFile(AHost:string; APort:integer; AFileName:string):boolean; override;
procedure CloseFile; override;
procedure on_Timer; override;
end;
TStateReconnected = class (TState)
private
public
function OpenFile(AHost:string; APort:integer; AFileName:string):boolean; override;
procedure CloseFile; override;
procedure on_Timer; override;
procedure pt_OkReopen(AData:pointer; ASize:integer); override;
procedure pt_ErReopen(AData:pointer; ASize:integer); override;
end;
{ TFFTCPStream }
procedure TFFTCPStream.CloseFile;
begin
TState(FState).CloseFile;
end;
procedure TFFTCPStream.close_File;
begin
SetState(FStateClosed);
FTimer.Stop;
send_Close;
FIsOpened := false;
fire_OnFileClosed;
end;
constructor TFFTCPStream.Create(AOwner: TComponent);
begin
inherited;
FFileName := '';
FIsOpened := false;
FSocket := TRyuSocketClient.Create(Self);
FSocket.OnDisconnected := on_Disconnected;
FSocket.OnReceived := on_Received;
FCS := TCriticalSection.Create;
FStateClosed := TStateClosed.Create(Self);
FStateConnected := TStateConnected.Create(Self);
FStateOpened := TStateOpened.Create(Self);
FStateDisconnected := TStateDisconnected.Create(Self);
FStateReconnected := TStateReconnected.Create(Self);
FState := FStateClosed;
FTimer := TThreadRepeater.Create(Self);
end;
destructor TFFTCPStream.Destroy;
begin
CloseFile;
FreeAndNil(FSocket);
FreeAndNil(FStateClosed);
FreeAndNil(FStateConnected);
FreeAndNil(FStateOpened);
FreeAndNil(FStateDisconnected);
FreeAndNil(FStateReconnected);
FreeAndNil(FTimer);
FreeAndNil(FCS);
inherited;
end;
procedure TFFTCPStream.fire_OnFileClosed;
begin
if Assigned(FOnFileClosed) then FOnFileClosed(Self);
end;
procedure TFFTCPStream.fire_OnFileOpened(AMediaInfo:TMediaInfo);
begin
// TODO : Player.Open(AMediaInfo);
if Assigned(FOnFileOpened) then FOnFileOpened(Self, AMediaInfo);
end;
procedure TFFTCPStream.fire_OnFileOpenError;
begin
if Assigned(FOnFileOpenError) then FOnFileOpenError(Self);
end;
procedure TFFTCPStream.on_Disconnected(Sender: TObject);
begin
TState(FState).on_Disconnected;
end;
procedure TFFTCPStream.on_Received(Sender: TObject; AData: pointer;
ASize: integer);
var
pPacketType : ^TPacketType absolute AData;
begin
case pPacketType^ of
ptPackage: TState(FState).pt_Package(AData, ASize);
ptOkOpen: TState(FState).pt_OkOpen(AData, ASize);
ptErOpen: TState(FState).pt_ErOpen(AData, ASize);
ptOkReopen: TState(FState).pt_OkReopen(AData, ASize);
ptErReopen: TState(FState).pt_ErReopen(AData, ASize);
end;
end;
procedure TFFTCPStream.on_Timer(Sender: TObject);
var
Tick : Cardinal;
Temp : integer;
begin
Tick := GetTickCount;
FCS.Enter;
try
if Tick > FOldTick then Temp := Tick - FOldTick
else Temp := 0;
if Temp >= _TimerInterval then begin
FOldTick := Tick;
TState(FState).on_Timer;
end;
finally
FCS.Leave;
end;
Sleep(1);
end;
function TFFTCPStream.OpenFile(AHost: string; APort: integer;
AFileName: string): boolean;
begin
FIsOpened := false;
FFileName := AFileName;
Result := TState(FState).OpenFile(AHost, APort, AFileName);
if Result then FTimer.Execute(on_Timer);
end;
procedure TFFTCPStream.send_Ask(AIndex: integer);
var
Packet : TPacketAsk;
begin
Packet.PacketType := ptAsk;
Packet.Index := AIndex;
FSocket.Send(@Packet, SizeOf(Packet));
end;
procedure TFFTCPStream.send_Close;
var
PacketType : TPacketType;
begin
PacketType := ptClose;
FSocket.Send(@PacketType, SizeOf(PacketType));
end;
procedure TFFTCPStream.send_Open;
begin
send_Text(ptOpen, FFileName);
end;
procedure TFFTCPStream.send_Reopen;
begin
send_Text(ptReopen, FFileName);
end;
procedure TFFTCPStream.send_Text(APacketType: TPacketType; AText: AnsiString);
var
pPacket : ^TPacket;
iPacketSize : integer;
begin
iPacketSize := SizeOf(TPacketType) + Length(AText) + 1;
GetMem(pPacket, iPacketSize);
try
FillChar(pPacket^, iPacketSize, 0);
pPacket^.PacketType := APacketType;
if Length(AText) > 0 then Move(AText[1], pPacket^.DataStart, Length(AText));
FSocket.SendNow(pPacket, iPacketSize);
finally
FreeMem(pPacket);
end;
end;
procedure TFFTCPStream.SetState(AState: TObject);
var
OldState, NewState : TObject;
begin
FCS.Enter;
try
FOldTick := GetTickCount;
OldState := FState;
NewState := AState;
TState(OldState).ActionOut(NewState);
FState := NewState;
TState(NewState).ActionIn(OldState);
finally
FCS.Leave;
end;
end;
{ TState }
procedure TState.ActionIn(AOldState: TObject);
begin
{$IFDEF DEBUG}
OutputDebugString(PChar(Format('%s.ActionIn: OldState = %s', [Self.ClassName, AOldState.ClassName])));
{$ENDIF}
end;
procedure TState.ActionOut(ANewState: TObject);
begin
{$IFDEF DEBUG}
OutputDebugString(PChar(Format('%s.ActionOut: NewState = %s', [Self.ClassName, ANewState.ClassName])));
{$ENDIF}
end;
procedure TState.CloseFile;
begin
{$IFDEF DEBUG}
OutputDebugString(PChar(Format('%s.CloseFile: ', [Self.ClassName])));
{$ENDIF}
end;
constructor TState.Create(AFFTCPStream: TFFTCPStream);
begin
inherited Create;
FFFTCPStream := AFFTCPStream;
end;
procedure TState.on_Disconnected;
begin
{$IFDEF DEBUG}
OutputDebugString(PChar(Format('%s.on_Disconnected: ', [Self.ClassName])));
{$ENDIF}
end;
procedure TState.on_Timer;
begin
{$IFDEF DEBUG}
OutputDebugString(PChar(Format('%s.on_Timer: ', [Self.ClassName])));
{$ENDIF}
end;
function TState.OpenFile(AHost: string; APort: integer;
AFileName: string): boolean;
begin
{$IFDEF DEBUG}
OutputDebugString(PChar(Format('%s.OpenFile: %s, %d, %s', [Self.ClassName, AHost, APort, AFileName])));
{$ENDIF}
Result := false;
end;
procedure TState.pt_ErOpen(AData: pointer; ASize: integer);
begin
{$IFDEF DEBUG}
OutputDebugString(PChar(Format('%s.pt_ErOpen: ', [Self.ClassName])));
{$ENDIF}
end;
procedure TState.pt_ErReopen(AData: pointer; ASize: integer);
begin
{$IFDEF DEBUG}
OutputDebugString(PChar(Format('%s.pt_ErReopen: ', [Self.ClassName])));
{$ENDIF}
end;
procedure TState.pt_OkOpen(AData: pointer; ASize: integer);
begin
{$IFDEF DEBUG}
OutputDebugString(PChar(Format('%s.pt_OkOpen: ', [Self.ClassName])));
{$ENDIF}
end;
procedure TState.pt_OkReopen(AData: pointer; ASize: integer);
begin
{$IFDEF DEBUG}
OutputDebugString(PChar(Format('%s.pt_OkReopen: ', [Self.ClassName])));
{$ENDIF}
end;
procedure TState.pt_Package(AData: pointer; ASize: integer);
begin
{$IFDEF DEBUG}
OutputDebugString(PChar(Format('%s.pt_Package: ', [Self.ClassName])));
{$ENDIF}
end;
{ TStateClosed }
function TStateClosed.OpenFile(AHost: string; APort: integer;
AFileName: string): boolean;
begin
{$IFDEF DEBUG}
inherited;
{$ENDIF}
FFFTCPStream.FSocket.Host := AHost;
FFFTCPStream.FSocket.Port := APort;
FFFTCPStream.FSocket.Connect;
Result := FFFTCPStream.FSocket.Connected;
if Result then begin
FFFTCPStream.SetState(FFFTCPStream.FStateConnected);
FFFTCPStream.send_Open;
end;
end;
{ TStateConnected }
procedure TStateConnected.CloseFile;
begin
{$IFDEF DEBUG}
inherited;
{$ENDIF}
FFFTCPStream.close_File;
FFFTCPStream.SetState(FFFTCPStream.FStateClosed);
end;
procedure TStateConnected.on_Disconnected;
begin
{$IFDEF DEBUG}
inherited;
{$ENDIF}
FFFTCPStream.SetState(FFFTCPStream.FStateDisconnected);
end;
procedure TStateConnected.on_Timer;
begin
{$IFDEF DEBUG}
inherited;
{$ENDIF}
FFFTCPStream.send_Open;
end;
function TStateConnected.OpenFile(AHost: string; APort: integer;
AFileName: string): boolean;
begin
{$IFDEF DEBUG}
inherited;
{$ENDIF}
raise Exception.Create(ClassName + '.OpenFile: ');
end;
procedure TStateConnected.pt_ErOpen(AData: pointer; ASize: integer);
begin
{$IFDEF DEBUG}
inherited;
{$ENDIF}
FFFTCPStream.fire_OnFileOpenError;
FFFTCPStream.SetState(FFFTCPStream.FStateClosed);
end;
procedure TStateConnected.pt_OkOpen(AData: pointer; ASize: integer);
var
pPacket : ^TPacketOkOpen absolute AData;
begin
{$IFDEF DEBUG}
inherited;
{$ENDIF}
FFFTCPStream.FIsOpened := true;
// TODO : Buffer.Clear;
FFFTCPStream.SetState(FFFTCPStream.FStateOpened);
FFFTCPStream.send_Ask(0);
FFFTCPStream.fire_OnFileOpened(pPacket^.MediaInfo);
end;
{ TStateOpened }
procedure TStateOpened.CloseFile;
begin
{$IFDEF DEBUG}
inherited;
{$ENDIF}
FFFTCPStream.close_File;
FFFTCPStream.SetState(FFFTCPStream.FStateClosed);
end;
procedure TStateOpened.on_Disconnected;
begin
{$IFDEF DEBUG}
inherited;
{$ENDIF}
FFFTCPStream.SetState(FFFTCPStream.FStateDisconnected);
end;
procedure TStateOpened.on_Timer;
begin
{$IFDEF DEBUG}
inherited;
{$ENDIF}
// TODO : FFFTCPStream.send_Ask(Buffer.NextIndex);
end;
function TStateOpened.OpenFile(AHost: string; APort: integer;
AFileName: string): boolean;
begin
{$IFDEF DEBUG}
inherited;
{$ENDIF}
raise Exception.Create(ClassName + '.OpenFile: ');
end;
procedure TStateOpened.pt_Package(AData: pointer; ASize: integer);
begin
{$IFDEF DEBUG}
inherited;
{$ENDIF}
// TODO : Buffer.Add();
end;
{ TStateDisconnected }
procedure TStateDisconnected.CloseFile;
begin
{$IFDEF DEBUG}
inherited;
{$ENDIF}
FFFTCPStream.close_File;
FFFTCPStream.SetState(FFFTCPStream.FStateClosed);
end;
procedure TStateDisconnected.on_Timer;
begin
{$IFDEF DEBUG}
inherited;
{$ENDIF}
FFFTCPStream.FSocket.Connect;
if FFFTCPStream.FSocket.Connected then begin
if FFFTCPStream.FIsOpened then begin
FFFTCPStream.SetState(FFFTCPStream.FStateReconnected);
FFFTCPStream.send_Reopen;
end else begin
FFFTCPStream.SetState(FFFTCPStream.FStateConnected);
FFFTCPStream.send_Open;
end;
end;
end;
function TStateDisconnected.OpenFile(AHost: string; APort: integer;
AFileName: string): boolean;
begin
{$IFDEF DEBUG}
inherited;
{$ENDIF}
raise Exception.Create(ClassName + '.OpenFile: ');
end;
{ TStateReconnected }
procedure TStateReconnected.CloseFile;
begin
{$IFDEF DEBUG}
inherited;
{$ENDIF}
FFFTCPStream.close_File;
FFFTCPStream.SetState(FFFTCPStream.FStateClosed);
end;
procedure TStateReconnected.on_Timer;
begin
{$IFDEF DEBUG}
inherited;
{$ENDIF}
FFFTCPStream.send_Reopen;
end;
function TStateReconnected.OpenFile(AHost: string; APort: integer;
AFileName: string): boolean;
begin
{$IFDEF DEBUG}
inherited;
{$ENDIF}
raise Exception.Create(ClassName + '.OpenFile: ');
end;
procedure TStateReconnected.pt_ErReopen(AData: pointer; ASize: integer);
begin
{$IFDEF DEBUG}
inherited;
{$ENDIF}
FFFTCPStream.close_File;
end;
procedure TStateReconnected.pt_OkReopen(AData: pointer; ASize: integer);
begin
{$IFDEF DEBUG}
inherited;
{$ENDIF}
FFFTCPStream.SetState(FFFTCPStream.FStateOpened);
end;
end.
'소프트웨어 공학 > 디자인패턴' 카테고리의 다른 글
| State Pattern을 이용한 미디어 서버 구축 과정 (0) | 2012/01/31 |
|---|---|
| 양치기 소년 패턴 (Proxy Pattern의 부분적 활용) (0) | 2010/08/26 |


