본문 바로가기
Delphi Tip/통신

소켓 프로그래밍 기법의 활용 6편 마무리

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

소켓 프로그래밍 기법의 활용 6편 마무리

 

블로킹 연결을 이용한 파일 전송 예제

소켓을 이용한 프로그래밍을 할 때 앞서 설명한 채팅 어플리케이션과 같이 

연결을 유지하면서 통신을 할 필요가 있을 때에는 중단되지 않는 

논-블로킹 연결을 지원하도록 해야 하지만, 

경우에 따라서는 전송하는 측과 전송받는 측의 데이터 전송에 있어서 

서로 비동기 적으로 처리하는 것이 효율적일 때가 많다. 

 

이럴 때에는 블로킹 연결을 이용하게 되는데, 블로킹 연결을 이용하여 

소켓 프로그래밍을 하는 것은 해당되는 연결의 쓰레드를 생성하여 

이를 실행하도록 하는 형태로 제작해야하기 때문에 

논-블로킹 연결을 지원하는 어플리케이션에 비해 다소 까다로운 점이 많다.

 

그러면, 블로킹 연결을 통해 클라이언트에서 

서버로 지정된 파일을 전송하는 예제를 작성해 보도록 하자. 

 

이 예제는 Stig Johansen이 공개한 예제를 바탕으로 작성하였다. 

예제 프로그램의 구조는 클라이언트 어플리케이션에서 파일 열기 대화 상자에서 

지정한 파일을 에디트 박스에 지정한 IP 주소로 전송한다. 

 

그리고, 서버 어플리케이션에서는 클라이언트와 연결되면 서버에 

저장할 파일 이름을 지정하면, 여기에 파일을 저장하도록 한다.

 

먼저, 서버 어플리케이션을 작성하도록 하자. 

폼 위에 다음과 같이 메모 컴포넌트와 서버 소켓, 파일 저장 대화상자 

컴포넌트를 하나씩 추가하고, 메모 컴포넌트의 Align 프로퍼티는

alClient, ScrollBars 프로퍼티는 ssVertical, ReadOnly 프로퍼티는 True로 설정한다. 

 

그리고 ServerSocket1 컴포넌트의 ServerType 프로퍼티는 

stThreadBlocking, Port 프로퍼티는 2001로 설정하도록 하자.

 

 

블로킹 연결을 지원하는 서버를 작성하기 위해서는 TServerClientThread에서 

상속받은 쓰레드 클래스를 이용하는 것이 핵심이다. 

 

여기서는 파일과 소켓의 스트림을 내부적으로 사용하여 파일의 전송을 구현하기 때문에, 

private 섹션에 소켓 스트림과 파일 스트림 필드를 추가하고 외부에서 접근할 수 있도록 

public 섹션에 프로퍼티로 선언하도록 한다.

 

TServerThread = class(TServerClientThread)

private

  FSocketStream: TWinSocketStream ;

  FFileStream: TFileStream;

protected

  procedure ClientExecute; override;

public

  property SocketStream: TWinSocketStream read FSocketStream write FSocketStream ;

  property FileStream: TFileStream read FFileStream write FFileStream ;

end;

 

쓰레드 클래스에서 꼭 오버라이드해서 구현해야 하는 

메소드가 ClientExecute로, 다중 쓰레드를 지원하는 경우에 

Execute 메소드를 오버라이드하는 것과 같은 역할을 한다. 

 

다중 쓰레드 프로그래밍에 대해서는 41장에서 자세히 다루므로 이를 참고하기 바란다.

폼의 OnCreate 이벤트 핸들러에서 서버 소켓의 Active 프로퍼티를 

True로 설정하여 서버 소켓이 클라이언트 소켓과의 연결을

받아들일 수 있도록 이를 열어놓도록 한다.

procedure TForm1.FormCreate(Sender: TObject);

begin

  ServerSocket1.Active := True;

end;

 

그리고 서버 소켓의

OnAccept, OnListen, OnThreadStart, OnThreadStop 이벤트 핸들러를 

다음과 같이 작성하여 클라이언트

소켓과의 연결 상황을 메모 컴포넌트에 나타내도록 한다.

 

procedure TForm1.ServerSocket1Accept(Sender: TObject;

Socket: TCustomWinSocket);

begin

  Memo1.Lines.Add('Accept ' + Socket.RemoteAddress);

end;

 

procedure TForm1.ServerSocket1Listen(Sender: TObject;

Socket: TCustomWinSocket);

begin

  Memo1.Lines.Add('Listening ... ');

end;

 

procedure TForm1.ServerSocket1ThreadStart(Sender: TObject;

Thread: TServerClientThread);

begin

  Memo1.Lines.Add('Start Thread of ' + Thread.ClientSocket.LocalAddress);

end;

 

procedure TForm1.ServerSocket1ThreadEnd(Sender: TObject;

Thread: TServerClientThread);

begin

  Memo1.Lines.Add('End Thread');

end;

 

블로킹 연결에 있어서 가장 중요한 것은 OnGetThread 이벤트 핸들러에서 

서버 쓰레드를 생성하는 작업이다. 

 

서버 쓰레드를 생성할 때 두번째 파라미터를 False로 선언하면 

생성과 동시에 실행되는 것이지만, 다음과 같이 True로 설정하면 

쓰레드 객체의 프로퍼티를 변경한 뒤에 Resume 메소드로 쓰레드가 실행된다.

 

procedure TForm1.ServerSocket1GetThread(Sender: TObject;

ClientSocket: TServerClientWinSocket;

var 

  SocketThread: TServerClientThread);

var

  SocketStream: TWinSocketStream ;

  FileStream: TFileStream ;

  FileName: string;

begin

  FileName := 'Default.file';

  SocketStream := TWinSocketStream.Create(ClientSocket, 20);

  if SaveDialog1.Execute then FileName := SaveDialog1.FileName;

 

  FileStream := TFileStream.Create(FileName, fmCreate or fmShareExclusive);

  SocketThread := TServerThread.Create(True, ClientSocket);

  (SocketThread as TServerThread).SocketStream := SocketStream;

  (SocketThread as TServerThread).FileStream := FileStream;

  SocketThread.FreeOnTerminate := True ;

  SocketThread.Resume ;

end;

 

여기에서 전송되어온 파일의 이름을 대화 상자에서 선택할 수 있도록 하고, 

파일 스트림 객체를 생성하여 쓰레드 객체의 프로퍼티에 대입하고, 

마찬가지로 TWinSocketStream 객체를 생성하여

이를 소켓 스트림 프로퍼티에 대입한다.

 

이제 쓰레드가 실제로 실행되는 ClientExecute 메소드를 구현하면

서버 프로그램이 완성된다. 

 

이를 구현하기에 앞서 소켓 스트림의 전체 내용을 읽어오는 역할을 하는 

ReadStream 함수를 다음과 같이 구현한다.

 

function ReadStream (Stream: TWinSocketStream; Buffer: Pointer;

Count: Integer): Boolean;

var

  P: PChar;

  Total, Delta, TimeOut: Integer;

begin

  if Count = 0 then

  begin

    Result := True;

    Exit;

  end;

 

  TimeOut := 0;

  Result := True;

  Total := 0;

  P := Buffer;

 

  while Total < Count do

  begin

    try

      Delta := Stream.Read(P^, Count - Total);

    except

      Exit;

    end;

 

    if Delta = 0 then

    begin

      Inc(Timeout);

      while not Stream.WaitForData(1000) and (TimeOut < 20) do Inc(TimeOut);

     

      if Timeout >= 20 then

      begin

        Result := False;

        Exit;

      end;

    end

    else

      TimeOut := 0;

      Inc(P, Delta);

      Inc(Total, Delta);

    end;

end;

 

이 루틴은 꽤 유용하게 사용되므로 잘 익혀두기 바란다. 

구현된 내용을 간단히 설명하면 소켓 스트림에서 데이터를 읽어올 때 

버퍼와 시간 제한을 이용하여 적절한 버퍼링을 해주는 것이 주된 내용이다. 

이런 작업이 필요한 이유는 이런 버퍼링 작업이 없이 송신 측에서는 

무조건 데이터를 밀어 넣고, 수신 측에서는 무조건 데이터를 가져올 경우에는 

간혹 손실되는 패킷이 생기기 때문이다. 

 

실제로 뉴스 그룹에서도 이런 문제로 어려움을 겪는 많은 개발자들이 있었는데, 

이런 문제를 이 루틴으로 해결할 수 있다.

 

소스를 보면 쉽게 이해할 수 있을 것이나 간단한 설명을 덧붙이자면, 

스트림의 Read 메소드에 의해 실제 읽어온 데이터의 바이트 수를 

Delta라는 변수에 대입하게 되는데 이때 이 값이 0이면 WaitForData 메소드를 

이용하여 버퍼에 데이터가 들어오는지 기다려 보고, 

이를 여기서는 최대 20번까지 기다려본 후에 그래도 데이터가 전송되어오지 않으면 

연결이 끊어진 것으로 간주하고 실행을 중지하게 된다.

 

이 루틴을 이용하여 TServerThread 객체의 ClientExecute 메소드는 다음과 같이 구현한다.

 

procedure TServerThread.ClientExecute;

var

  FileLength: Integer;

  MemoryStream: TMemoryStream;

begin

  if ReadStream(SocketStream, Addr(FileLength), SizeOf(FileLength)) then

  begin

    MemoryStream := TMemoryStream.Create;

    MemoryStream.SetSize(FileLength);

    ReadStream(SocketStream, MemoryStream.Memory, FileLength);

    FFileStream.CopyFrom(MemoryStream, MemoryStream.Size);

    MemoryStream.Free;

  end;

 

  if Assigned(FSocketStream) then FSocketStream.Free;

  if Assigned(FFileStream) then FFileStream.Free;

  Terminate;

end;

 

여기서 눈여겨 보아야 할 것은 파일 스트림과

소켓 스트림의 원활한 전달을 위해 중간에 

TMemoryStream 형의 변수를 이용한다는 점이다. 

 

그리고, 처음에 일단 ReadStream 메소드를 이용하여 전달된 

파일의 크기를 먼저 받아본다는 점이 중요하다. 

 

이는 클라이언트에서 파일을 전송할 때 먼저 파일의 크기를 전송하고 나서, 

실제 파일을 전송한다는 것을 의미한다. 

 

그리고, 이 값을 이용하여 서버에서 파일을 생성하고 스트림을 복사한다.

이것으로 서버 프로그램이 완성되었다.

 

이번에는 이 서버 프로그램과 연결해서 사용할 클라이언트 프로그램을 작성해보자.

서버와는 달리 클라이언트 프로그램에는 연결한 서버의 IP 주소를 적어넣을 에디트 박스와 

버튼을 하나의 패널에 올려 놓고, 메모 컴포넌트를 

다음과 같이 추가하여 디자인하도록 하자.

 

서버와 같은 2001로 설정한다.

그리고 먼저 클라이언트 소켓의 OnConnect, OnDisconnect, OnError 이벤트 핸들러를 

다음과 같이 작성하여 통신 상황을 나타내도록 한다.

 

procedure TForm1.ClientSocket1Connect(Sender: TObject;

Socket: TCustomWinSocket);

begin

  Memo1.Lines.Add('Connected to ' + Socket.RemoteAddress);

end;

 

procedure TForm1.ClientSocket1Disconnect(Sender: TObject;

Socket: TCustomWinSocket);

begin

  Memo1.Lines.Add('Disconnected to ' + Socket.RemoteAddress);

end;

 

procedure TForm1.ClientSocket1Error(Sender: TObject;

Socket: TCustomWinSocket; ErrorEvent: TErrorEvent;

var ErrorCode: Integer);

begin

  Memo1.Lines.Add('Error Code is ' + IntToStr(ErrorCode));

end;

 

그리고, 서버 프로그램에서의 ReadStream과 마찬가지로 클라이언트에서 

파일의 내용을 파일 스트림에 읽어들인 후, 이를 소켓 스트림에 기록하는 역할을 하는 

WriteStream 함수를 다음과 같이 구현한다.

 

procedure WriteStream(Stream: TWinSocketStream; const Buffer: Pointer;

Count: Integer);

var

  P: PChar;

  Total, Delta, TimeOut: Integer;

begin

  if Count = 0 then Exit;

 

  Total := 0;

  TimeOut := 0;

  Delta := 0;

  P := Buffer;

 

  while Total < Count do

  begin

    try

      Delta := Count - Total;

      if Delta > 16384 then Delta := 16384; //최대값

      Delta := Stream.Write(P^, Delta);

    except

      Exit;

    end;

 

    Inc(P, Delta);

    Inc(Total, Delta);

  end;

end;

 

비교적 간단한 구현 내용이므로 쉽게 이해할 수 있을 것이다. 

참고로 여기서는 한번에 최대 16384 바이트를

하나의 패킷으로 스트림에 기록하도록 하였다. 

 

소켓 연결 상태 등에 따라 크기를 변경할 수도 있겠다.

마지막으로 Button1의 OnClick 이벤트 핸들러를 다음과 같이 작성한다.

 

procedure TForm1.Button1Click(Sender: TObject);

var

  FileStream: TFileStream;

  FileLength: Integer;

begin

  if OpenDialog1.Execute then

  begin

    FileStream := TFileStream.Create(OpenDialog1.FileName, fmOpenReador fmShareDenyNone);

    FileLength := FileStream.Size;

 

    if FileLength > 0 then

    begin

      ClientSocket1.Address := Edit1.Text;

      ClientSocket1.Active := True;

 

      if ClientSocket1.Active then

      begin

        ClientSocket1.Socket.SendBuf(FileLength, SizeOf(FileLength));

        ClientSocket1.Socket.SendStream(FileStream);

      end;

 

      ClientSocket1.Active := False;

    end;

  end;

end;

 

일단 파일 열기 대화상자를 이용하여 전송할 파일을 선택하게 하고, 

이 파일에 대한 파일 스트림 객체를 생성한다. 

 

그리고, 파일 스트림의 크기를 먼저 SendBuf 메소드를 이용하여 전송하고, 

파일 스트림의 내용을 SendStream 메소드를 이용하여 전송한다.

 

이와 같이 소켓을 이용하여 스트림을 전송할 때에는 패킷을 나누어 전송하고, 

스트림의 크기를 먼저 전송하게 하는 것이 에러를 줄일 수 있는 요령이다. 

물론, 독자적인 프로토콜을 정의하여 이를 이용하는 것이 가장 이상적일 것이다.

 

이것으로 클라이언트 어플리케이션이 완성되었다.

이제 클라이언트와 서버 어플리케이션을 띄우고 파일을 선택하여 전송하도록 해보자. 

다음 그림은 서버와 클라이언트 어플리케이션의 실행화면이다.

728x90
반응형

댓글