FreePascal Programming Forum
C++Builder  |  Delphi  |  FireMonkey  |  C/C++  |  Free Pascal  |  Firebird
볼랜드포럼 BorlandForum
 경고! 게시물 작성자의 사전 허락없는 메일주소 추출행위 절대 금지
FreePacal 포럼
Q & A
FAQ
팁&트릭
강좌/문서
자료실
볼랜드포럼 홈
헤드라인 뉴스
IT 뉴스
공지사항
자유게시판
해피 브레이크
공동 프로젝트
구인/구직
회원 장터
건의사항
운영진 게시판
회원 메뉴
북마크
델마당
볼랜드포럼 광고 모집

FreePascal 강좌/문서
[7] 라자루스를 이용한 업무 프로그램 개발 - 6
어느좋은날 [freepascal] 666 읽음    2020-06-13 22:42
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;
어느좋은날 [freepascal]   2020-06-25 16:05 X
IdTCPSvrExecute 이벤트 중간부분에 로그인 되었는지 체크하는 구문이 빠져있었네요. ^^;;;
따로 변수를 둘수도 있는데 간단하게 UserID가 공백인지 아닌지로만 체크하게 했습니다.

+ -

관련 글 리스트
7 라자루스를 이용한 업무 프로그램 개발 - 6 어느좋은날 666 2020/06/13
Google
Copyright © 1999-2015, borlandforum.com. All right reserved.