1-4. 서버 프로그램 선언부 및 이벤트
DB에서 조회한 Data는 XML 형태로 주고 받는다. "TXMLXSDExporter" 객체("fpXMLXSDExport.pas")를 사용하면 DataSet의 내용을 파일로 저장이 가능하다. 그런데 우리가 필요한건 파일 형태가 아니라 메모리 안에서 처리하기위한 Stream 형태이기때문에 이 유닛을 참고해서 "TCustomBufDataset"에서 사용가능한 Stream 형태의 XML로 반환하는 객체("TXMLStreamExporter")를 만들어야 한다.("xml_data2stream.pas" 첨부파일 참조)
이제 "TdmnSIMLaz" 소스에 지금까지 만든 유닛들과 날짜계산을 위한 "dateutils", Indy 컴퍼넌트의 "TIdContext"와 "Exception" 처리를 위한 "IdContext", "IdException" 유닛을 uses에 추가한다. 참고로 라자루스는 컴퍼넌트를 추가할 때 직접적인 연관이 있는 유닛이 아니라면 자동으로 추가해주지 않는다. 불편하지만 컴퍼넌트와 직접적인 연관은 없지만 코드에 사용되는 항목들이 포함된 유닛은 직접 찾아서 등록해줘야한다.
uses
..., xml_data2stream, simlaz_lib, simlaz_log, simlaz_db, dateutils,
..., IdContext, IdException;
"TdmnSIMLaz"에 사용할 변수들과 함수들을 선언한다.
private
u_HostName, u_Database, u_UserID, u_Password: string; // DB 접속정보
u_KillTime: Int32; // 접속유지시간(분)
u_Log: TSIMLazLog; // 로그
u_TestMode: Boolean; // Test Mode 여부
u_Encoding: IIdTextEncoding;
procedure UP_SIMLazLog(ALog: string; ACont: string = ''); // 로그 처리
procedure UP_SIMLazExec(ACnxt: TIdContext; var ACont: TIdBytes); // Query 실행
procedure UP_SIMLazFile(ACnxt: TIdContext; var ACont: TIdBytes); // 파일 가져오기
procedure UP_SIMLazKill(AAll: Boolean = True); // 클라이언트 죽이기
procedure UP_SIMLazLogin(ACnxt: TIdContext; var ACont: TIdBytes); // 로그인
procedure UP_SIMLazOpen(ACnxt: TIdContext; var ACont: TIdBytes); // 데이터 가져오기
procedure UP_SIMLazSendB(ACnxt: TIdContext; ACmd: UInt8; var ACont: TIdBytes); // Bytes 전송
procedure UP_SIMLazSendS(ACnxt: TIdContext; ACmd: UInt8; ACont: string); // string 전송
procedure UP_SIMLazUsrLst(ACnxt: TIdContext); // 사용자 목록
procedure UP_SIMLazVer(ACnxt: TIdContext; var ACont: TIdBytes); // 버전 정보
서비스 시작시 로그를 열고 ini 파일에서 서버가 사용할 Port와 최대 접속자 수, 접속유지 제한 시간 및 테스트 모드 여부를 지정하는 변수와 DB 관련 정보를 가져온 후 Indy TCP Server 및 킬타이머를 구동시킨다. 서비스 형태로 만들어지면 디버깅하기가 쉽지않기때문에 테스트 모드를 설정해서 개발시 필요한 부분에 로그를 남기는 용도로 활용하고 실제 서비스할 때는 테스트 모드를 끄도록 한다.
procedure TdmnSIMLaz.DataModuleStart(Sender: TCustomDaemon; var OK: Boolean);
begin
u_Log := TSIMLazLog.Create('SIMLaz');
u_Encoding := IndyTextEncoding_UTF8;
with TIniFile.Create(ExtractFilePath(ParamStr(0)) + 'simlaz_server.ini') do
begin
try
IdTCPSvr.DefaultPort := ReadInteger('Setup', 'Port', 11111);
IdTCPSvr.MaxConnections := ReadInteger('Setup', 'MaxConn', 300);
u_KillTime := ReadInteger('Setup', 'KillTime', 10);
u_TestMode := ReadBool('Setup', 'TestMode', False);
u_HostName := ReadString('Setup', 'Host', '127.0.0.1');
u_Database := ReadString('Setup', 'Database', '');
u_UserID := ReadString('Setup', 'UserID', 'sysdba');
u_Password := ReadString('Setup', 'Password', 'masterkey');
finally
Free;
end;
end;
IdTCPSvr.TerminateWaitTime := u_KillTime * 60000; // 분 단위체크
IdTCPSvr.Active := True; // TCP Server 시작
tmKill.Enabled := True; // Kill Timer On
UP_SIMLazLog('* BEGIN');
OK := True;
end;
※ "simlaz_server.ini" File 내용 예시
[Setup]
Port=11111
MaxConn=300
KillTime=10
TestMode=1
Host=127.0.0.1
Database=D:\Data\TEST-UNI.FDB
UserID=sysdba
Password=masterkey
서비스 종료시에는 킬타이머를 끄고 Indy TCP Server를 종료시킨 후 종료 로그를 남기고 사용한 로그 객체를 Free 시킨다.
procedure TdmnSIMLaz.DataModuleStop(Sender: TCustomDaemon; var OK: Boolean);
begin
tmKill.Enabled := False; // Kill Timer Off
if IdTCPSvr.Active then
begin
UP_SIMLazKill; // 모든 접속자 Kill
IdTCPSvr.Active := False; // TCP Server 종료
end;
UP_SIMLazLog('* END');
FreeAndNil(u_Log);
OK := True;
end;
Indy 서버에 클라이언트가 접속되면 각 클라이언트가 사용할 DB 접속용 객체를 생성하고 접속 로그를 남긴 다음 접속이 잘 되었다는 응답을 보낸다.
procedure TdmnSIMLaz.IdTCPSvrConnect(AContext: TIdContext);
var
l_SIMLazDB: TSIMLazDB;
begin
try
// 각 클라이언트별 DB 접속용 객체 생성
l_SIMLazDB := TSIMLazDB.Create(AContext.Connection, AContext.Connection.Socket.Binding.PeerIP);
AContext.Data := l_SIMLazDB;
l_SIMLazDB.SetDBParams(u_HostName, u_Database, u_UserID, u_Password); // DB 접속정보 설정
UP_SIMLazLog(l_SIMLazDB.ConnID + ' connect.');
UP_SIMLazSendS(AContext, smlc_CONNECT, '');
except
on E: Exception do
begin
UP_SIMLazLog(AContext.Connection.Socket.Binding.PeerIP, E.Message);
UP_SIMLazSendS(AContext, smlc_ERROR, E.Message);
if AContext.Connection.Connected then AContext.Connection.Disconnect;
end;
end;
end;
클라이언트의 접속이 종료되면 사용했던 객체를 해제하고 접속종료 로그를 남긴다.
procedure TdmnSIMLaz.IdTCPSvrDisconnect(AContext: TIdContext);
var
s: string;
l_SIMLazDB: TSIMLazDB;
begin
l_SIMLazDB := TSIMLazDB(AContext.Data);
s := l_SIMLazDB.ConnID;
AContext.Data := nil;
FreeAndNil(l_SIMLazDB);
UP_SIMLazLog(s + ' disconnect.');
end;
각종 처리시 발생하는 오류 외에 Indy 자체에서 발생하는 오류를 캡쳐해서 로그로 남긴다.
procedure TdmnSIMLaz.IdTCPSvrException(AContext: TIdContext; AException: Exception);
begin
if not (AException is EIdConnClosedGracefully) then
begin
UP_SIMLazLog(AContext.Connection.Socket.Binding.PeerIP, 'Exception: ' + AException.Message);
if AContext.Connection.Connected then UP_SIMLazSendS(AContext, smlc_ERROR, 'Exception: ' + AException.Message);
end;
end;
procedure TdmnSIMLaz.IdTCPSvrListenException(AThread: TIdListenerThread; AException: Exception);
begin
UP_SIMLazLog('ListenException', AThread.Name + ': ' + AException.Message);
end;
타이머를 이용해서 일정 시간마다 요청시간을 초과한 클라이언트의 접속을 종료시킨다.
procedure TdmnSIMLaz.tmKillTimer(Sender: TObject);
begin
tmKill.Enabled := False;
try
UP_SIMLazKill(False);
tmKill.Enabled := True;
except
on E: Exception do
begin
UP_SIMLazLog('Error', E.Message);
tmKill.Enabled := True;
end;
end;
end;
클라이언트의 메세지를 수신하는 처리는 Indy TCP Server의 Execute Event에서 한다. 수신해야할 데이터의 길이를 Int64 값으로 받은 후, 길이 만큼의 데이터를 TIdBytes에 받는다. 수신된 데이터의 첫자리를 떼어내어 "구분자"로 활용하고 나머지 데이터에대해 암호해독과정을 거친다. 각 클라이언트별 DB 처리용 객체에 있는 요청시간을 현재 일시로 갱신한 후 "구분자"별 procedure에 수신 받은 데이터를 변수로 넘기고 실행한다.
procedure TdmnSIMLaz.IdTCPSvrExecute(AContext: TIdContext);
var
l_Cmd: UInt8;
l_Len: Int64;
l_SIMLazDB: TSIMLazDB;
l_Data, l_Cont: TIdBytes;
begin
l_Len := AContext.Connection.IOHandler.ReadInt64(False);
// Test Mode
if u_TestMode then UP_SIMLazLog(AContext.Connection.Socket.Binding.PeerIP + 'Test - Length = ' + IntToStr(l_Len));
if l_Len < 1 then
begin
UP_SIMLazLog(AContext.Connection.Socket.Binding.PeerIP, ERR_SIMLAZ002);
UP_SIMLazSendS(AContext, smlc_ERROR, ERR_SIMLAZ002);
Exit;
end;
l_Data := nil;
AContext.Connection.IOHandler.ReadBytes(l_Data, l_Len, False);
GP_SIMLazDec(l_Data);
l_Cmd := l_Data[0]; // Command
// Data
l_Len := Length(l_Data) - 1;
SetLength(l_Cont, l_Len);
Move(l_Data[1], l_Cont[0], l_Len);
// Test Mode
if u_TestMode then UP_SIMLazLog(AContext.Connection.Socket.Binding.PeerIP + 'Test - Command = ' + Char(l_Cmd));
l_SIMLazDB := TSIMLazDB(AContext.Data);
l_SIMLazDB.ReqTime := Now;
if l_Cmd = smlc_LOGIN then UP_SIMLazLogin(AContext, l_Cont) // 로그인
else if l_Cmd = smlc_VERSION then UP_SIMLazVer(AContext, l_Cont) // 버전 정보
else if l_Cmd = smlc_GETFILE then UP_SIMLazFile(AContext, l_Cont) // 파일 가져오기
else if l_Cmd in [smlc_DUMMY, smlc_OPENQUERY, smlc_EXECQUERY, smlc_USERLIST] then
begin
if l_SIMLazDB.UserID = '' then
begin
UP_SIMLazLog(AContext.Connection.Socket.Binding.PeerIP, ERR_SIMLAZ001);
UP_SIMLazSendS(AContext, smlc_ERROR, ERR_SIMLAZ001);
Exit;
end;
case l_Cmd of
smlc_DUMMY: UP_SIMLazSendS(AContext, smlc_DUMMY, '');
smlc_OPENQUERY: UP_SIMLazOpen(AContext, l_Cont);
smlc_EXECQUERY: UP_SIMLazExec(AContext, l_Cont);
smlc_USERLIST : UP_SIMLazUsrLst(AContext);
end;
end
else // Command Error!
begin
UP_SIMLazLog(AContext.Connection.Socket.Binding.PeerIP, ERR_SIMLAZ002);
UP_SIMLazSendS(AContext, smlc_ERROR, ERR_SIMLAZ002);
end;
end;