LazarusCompleteGuide:11.1
TCP/IP 프로그래밍
본 장은 TCP 클라이언트와 서버 프로그래밍에 대한 간략한 지침을 제시한다. 필수 부분을 강조하기 위해 코드는 간소화하였다. 따라서 전체적 오류 검사는 실제 생산 품질 전문 소프트웨어에서 실행되어야 하므로 여기서는 실행되지 않았다.
시냅스 소켓(Synapse sockets) 라이브러리를 기본 TCP 라이브러리로서 사용할 것이다. 이 라이브러리는 아래 주소에서 다운로드 가능하다:
본 장의 완전한 소스 코드는 본 저서에 따라오는 디스크의 sources 폴더 내 tcp 하위폴더에 제공된다.
TCP/IP 네트워크에서 모든 컴퓨터는 유일한 IP 주소로 식별된다. 컴퓨터에 실행되는 서버 프로세스는 특정 포트에 대한 클라이언트의 요청을 듣는다. TCP 포트는 양의 정수로 식별된다. 하나의 서버 프로세스는 한 번에 주어진 포트만 들을 수 있다. 그러면 클라이언트 프로세스는 서버 프로세스가 듣는 포트 번호 및 서버 컴퓨터의 IP 주소가 주어진 서버 프로세스로 연결을 개방할 수 있다. 본 장에서는 클라이언트로 간단한 정수 계산 기능을 (Add, Subtract, Multiply, Divide) 제공하기 위해 포트 1234 상에 들을 수 있는 간단한 서버 프로세스를 빌드할 것이다. 따라서 클라이언트 요청은 원하는 수치 연산이 무엇인지를 명시하고, 작동해야 할 적당한 수치 인수(numerical argument)를 제공해야 할 것이다. 우리는 이러한 클라이언트 요청을 표시하기 위해 데이터 구조의 정의부터 시작하고자 한다:
<Code ID = 1>
TIntType = ShortInt;
TChar = ansichar;
TCalcRequest = packed record
Operation: TChar;
ArgumentA: TIntType;
ArgumentB: TIntType;
end;
</Code>
그리고 서버의 응답을 나타내기 위한 또 다른 데이터 구조는 아래와 같다:
<Code ID = 2>
TCalcResponse = packed record
Operation: TChar;
ArgumentA: TIntType;
ArgumentB: TIntType;
ResultArg: TIntType;
end;
</Code>
응답 데이터 구조는 쿼리의 항목, 그리고 계산 결과를 포함하는 ResultArg 를 포함한다. 서버와 클라이언트 간 상호작용은 아래와 같이 발생한다:
- 서버가 클라이언트 연결을 듣는다.
- 클라이언트가 서버로 연결한다.
- 클라이언트가 TCalcRequest 요청을 나타내는 버퍼 스트림을 서버로 전송한다.
- 서버가 버퍼 스트림을 요청 변수 TCalcRequest 로 읽어온다.
- 서버가 계산을 실행하고 요청 데이터와 계산 결과를 결과 변수 TCalcResponse 에 저장한다.
- 서버가 결과 변수를 클라이언트에게 리턴한다.
- 클라이언트가 결과를 읽고 표시한다.
- 클라이언트가 서버로부터 연결을 끊는다.
(3)번 항목부터 (7)번항목 사이에서 동작 순서는 클라이언트가 원하는 대로 반복할 수 있다. 필요한 데이터 구조는 service_intf.pas 파일에서 정의된다.
unit service_intf;
{$mode objfpc}{$H+}
interface
type
TIntType = ShortInt;
TChar = ansichar;
TCalcRequest = packed record
Operation: Char;
ArgumentA: TIntType;
ArgumentB: TIntType;
end;
TCalcResponse = packed record
Operation: Char;
ArgumentA: TIntType;
ArgumentB: TIntType;
ResultArg: TIntType;
end;
implementation
end.
클라이언트 프로그램
클라이언트 프로그램은 계산을 실행하고 결과를 인쇄하기 위해 두 개의 피연산자(operand)와 하나의 연산자(operator)를 취하는 간단한 프로그램이다. 실행 세션과 프로그램의 코드는 아래와 같다:
<Session ID = 1>
E:\book\tcp\client>client.exe
Client demo program.
Enter first operand ( -120 < x < 120 ) : 12
Enter second operand ( -120 < x < 120 ) : 10
Enter operator ( +, -, *, / ) : +
12 + 10 = 22
Continue (y/n) ? : y
Enter first operand ( -120 < x < 120 ) : 12
Enter second operand ( -120 < x < 120 ) : 10
Enter operator ( +, -, *, / ) : -
12 - 10 = 2
Continue (y/n) ? : n
</Session>
<Code ID = 3>
1 program client;
2 {$mode objfpc}{$H+}
3 uses
4 Classes, SysUtils,
5 service_intf, client_imp;
6 var
7 a, b, c: TIntType;
8 op: Char;
9 s: string;
10 begin
11 InitConnection();
12 try
13 WriteLn('Client demo program.');
14 while True do begin
15 Write('Enter first operand ( -120 < x < 120 ) : ');
16 ReadLn(a);
17 Write('Enter second operand ( -120 < x < 120 ) : ');
18 ReadLn(b);
19 Write('Enter operator ( +, -, *, / ) : ');
20 ReadLn(op);
21 if ( op in ['+', '-', '*', '/'] ) and
22 not ( ( op = '/' ) and ( b = 0 ) )
23 then begin
24 c := Compute(a,b,op);
25 WriteLn();
26 WriteLn( ' ', a, ' ', op, ' ', b, ' = ', c);
27 end else begin
28 WriteLn('Wrong arguments and/or operation.');
29 end;
30 WriteLn('');
31 Write('Continue (y/n) ? : '); ReadLn(s);
32 if ( UpperCase(s) <> 'Y' ) then
33 Break;
34 WriteLn();
35 end;
36 finally
37 CloseConnection();
38 end;
39 end.
</Code>
코드는 서버로의 연결을 준비하고 개방하는 것으로 시작해 (11번 행) 연결을 닫는 것으로 완료된다 (37번 행). 연결은 client_imp.pas에서 선언된 전역 변수 SocketCnx 이다. 이 연결을 열고 닫는 루틴, 즉 InitConnection 과 CloseConnection 은 client_imp.pas 파일에서 찾을 수 있다. InitConnection 은 단순히 연결이 구축되었는지를 검사하고, 연결되지 않았다면 연결을 생성 및 초기화한다.
<Code ID = 4>
1 var
2 SocketCnx: TTCPBlockSocket = nil;
3
4 procedure InitConnection();
5 var
6 cnx: TTCPBlockSocket;
7 begin
8 if ( SocketCnx = nil ) then begin
9 cnx := TTCPBlockSocket.Create();
10 try
11 cnx.RaiseExcept := True;
12 cnx.Connect(SERVER_ADDRESS, IntToStr(SERVER_PORT));
13 SocketCnx := cnx;
14 except
15 on e : Exception do begin
16 cnx.Free();
17 raise;
18 end;
19 end;
20 end;
21 end;
</Code>
직렬화(serialization), 서버와의 통신, 역직렬화(deserialization)에 대한 연결의 세부내용은 client_imp.pas에 정의된 Compute 루틴에 의해 “line 24 code ID 3”에서 처리된다.
<Code ID = 5>
1 function Compute(
2 const A,B: TIntType;
3 const AOp: Char
4 ): TIntType;
5 var
6 RequestBuffer: TCalcRequest;
7 ResponseBuffer: TCalcResponse;
8 cnx: TTCPBlockSocket;
9 bufferLen: Integer;
10 begin
11 RequestBuffer.ArgumentA := A;
12 RequestBuffer.ArgumentB := B;
13 RequestBuffer.Operation := AOp;
14 cnx := SocketCnx;
15 cnx.SendBuffer(@RequestBuffer,SizeOf(RequestBuffer));
16 cnx.ExceptCheck();
17 bufferLen := cnx.RecvBufferEx(
18 @ResponseBuffer, SizeOf(ResponseBuffer),100 );
19 cnx.ExceptCheck();
20 if ( bufferLen <> SizeOf(ResponseBuffer) ) then
21 raise Exception.Create('Invalid server response.');
22 Result := ResponseBuffer.ResultArg;
23 end;
</Code>
Compute 루틴은 아래 순서로 명령어를 실행한다:
- 요청 버퍼가 채워진다 (11-13번 행).
- 요청 버퍼가 서버로 전송되고 (15번 행) 오류가 검사된다 (16번 행).
- 서버에 의해 전송된 요청 버퍼가 읽히고 (17-18번 행) 오류가 검사된다 (19번 행).
- 수신된 버퍼 길이와 응답 크기가 비교되고, 일치하지 않을 시 예외가 발생한다 (21번 행).
- 마지막으로 함수의 결과가 서버의 응답 ResultArg 로 채워진다 (22번 행).
클라이언트 코드 컴파일하기
본 장의 소스 코드는 세 개의 폴더로 조직된다.
- client: 이 폴더는 클라이언트 특정적 코드를 포함한다;
- share: 공유된 폴더가 서버와 클라이언트 사이에서 공유된 코드를 포함하는데, 주로 클라이언트와 서버 사이에 전달된 데이터 구조를 정의하는 service_intf.pas 인터페이스 유닛이 해당한다.
- Server: 이 폴더는 서버 특정적 코드를 포함한다.
client 폴더는 하나의 라자루스 프로젝트 파일, client.lpi를 포함한다. 우선 프로젝트를 컴파일하기 전에 Synapse 라이브러리를 다운로드하여 설치할 필요가 있다 (synapse.zip 파일을 명시된 폴더에 압축 해제하여). Synapse 라이브러리는 http://www.ararat.cz/synapse/ 에서 자유롭게 이용할 수 있다. ...\synapseXYZ\source\lib\ 폴더를 포함시키기 위해 프로젝트의 유닛 검색 폴더도 업데이트할 필요가 있는데, 해당 폴더의 \synapseXYZ\에서 XYZ는 다운로드한 Synapse 배포판의 해당 버전 번호로 변경되어야 한다.
서버 프로그램
서버 프로그램은 클라이언트의 연결과 요청을 위해 정의된 포트를 듣는다(listen on). 우리가 사용한 소켓 라이브러리-Synapse-는 블로킹(blocking) 알고리즘을 사용하므로 모든 클라이언트는 각자 전용 스레드에서 처리될 필요가 있다. 서버 프로세스는 클라이언트 연결 요청을 기다리는 listening 스레드를 가진다. 클라이언트가 연결을 요청하게 되면 서버 listening 스레드는 클라이언트를 독점적으로 처리할 스레드를 생성한다. 서버 프로그램 코드를 아래에 표시하였다:
<Code ID = 6>
1 program tcp_server;
2 {$mode objfpc}{$H+}
3 uses
4 Classes, SysUtils,
5 service_intf, server_imp;
6 var
7 serverThread: TServerListnerThread;
8 begin
9 WriteLn('Starting the server listening thread ...');
10 serverThread :=
11 TServerListnerThread.Create('vmwinxp',1234);
12 WriteLn('Listening on port # 1234');
13 WriteLn('Hit <enter> to stop.');
14 ReadLn;
15 WriteLn('Terminating ...');
16 serverThread.Terminate();
17 Sleep(500);
18 serverThread.Free();
19 WriteLn('Finished.');
20 end.
</Code>
프로그램은 listening 스레드를 생성함으로써 시작하는데, 동시에 그것이 듣게 될 포트를 명시한다. 서버 프로세스에는 하나의 listening 스레드만 있을 수 있다. 우선 생성된 listener 스레드는 자동으로 시작한다. 아래 코드는 listener 스레드의 Execute 메소드를 보여준다:
<Code ID = 7>
1 procedure TServerListenerThread.Execute();
2 var
3 ClientSock: TSocket;
4 begin
5 FSocketObject.RaiseExcept := True;
6 FSocketObject.CreateSocket();
7 FSocketObject.SetLinger(True,10);
8 FSocketObject.Bind(FIP,IntToStr(FPort));
9 FSocketObject.Listen();
10 while not Terminated do begin
11 if FSocketObject.CanRead(DefaultTimeOut) then begin
12 ClientSock := FSocketObject.Accept();
13 TClientHandlerThread.Create(ClientSock);
14 end;
15 end;
16 end;
</Code>
서버 소켓이 준비된 후 (5-8번 행) 듣기를 시작한다 (9번 행). 이후 스레드 루프가 시작되고 (10번 행), 클라이언트의 연결 요청을 기다린다. 클라이언트가 연결을 요청하면 서버와의 통신을 처리하는 스레드가 생성된다 (11-13번 행). 클라이언트 처리 스레드는 우선 생성되고 나면 자동으로 시작한다. 아래 코드는 클라이언트 처리 스레드의 메인 실행 메소드이다:
<Code ID = 8>
1 procedure TClientHandlerThread.Execute();
2 begin
3 FSocketObject := TTCPBlockSocket.Create();
4 try
5 FSocketObject.Socket := FSocketHandle;
6 FSocketObject.GetSins();
7 while not Terminated do begin
8 ClearBuffers();
9 if ReadInputBuffer() then begin
10 Compute();
11 SendOutputBuffer();
12 ClearBuffers();
13 end;
14 end;
15 finally
16 FreeAndNil(FSocketObject);
17 end;
18 end;
</Code>
클라이언트 처리 스레드는 소켓 객체를 준비한 후 실행 루프로 진입한다. 9번 행에서 스레드는 소켓으로부터 읽기를 시도하는데, 이는 원격 클라이언트가 계산 요청을 했는지 여부에 따라 채워질 수도, 채워져 있지 않을 수도 있다. 읽기 연산이 성공하면 아래 시퀀스가 발생한다:
- 입력 버퍼가 채워진다;
- 계산이 완료되고 출력 버퍼가 채워진다;
- 출력 버퍼가 다시 원격 클라이언트로 전송된다;
- 입력과 출력 버퍼가 삭제된다.
서버 코드 컴파일하기
클라이언트 프로그램의 경우, 당신은 Synapse 라이브러리의 복사본을 그 고유 폴더에 설치해야 한다. server 폴더는 하나의 라자루스 프로젝트, file tcp_server.lpi 를 포함한다. 프로젝트를 컴파일하기 전에 프로젝트의 search 폴더를 업데이트하여 Synapse library 폴더를 포함하도록 한다.
멀티바이트 타입을 사용 시 주목해야 할 크로스 플랫폼 문제
공유 인터페이스 파일, service_intf.pas에 정의된 요청 및 응답 데이터 구조는 packed record이며, 그 구성인자(member)는 단일 바이트 타입으로 되어 있다 (ShortInt 또는 ShortInt aliase). 만일 Word, LongInt, Double과 같은 멀티바이트 타입을 사용했다면 프로그램의 기본이 되는 프로세서의 엔디안(endianness)에 의존하지 않도록 그러한 데이터 구성인자(data member)의 바이트 순서를 주의해야 할 것이다.
Raw TCP/IP 프로그래밍은 컴파일러로 인해 오브젝트 파스칼 개발자들에게 자연스러워 보이는 아래와 같은 일부 기능들을 사용해야 하는 필요성을 해결하기 힘들어진다는 기술적 문제도 존재한다:
- 멀티바이트 타입 (Word, LongInt, Cardinal, Int64, Single, Double, …),
- 긴 문자열 (AnsiString, WideString),
- 동적 배열,
- 클래스 인스턴스,
- 열거형(enumerations).
개발자는 다음과 같은 내용을 처리해야 할 것이다:
- 프로세서의 엔디안(endianness), 크로스 플랫폼 애플리케이션의 경우, 멀티바이트 타입을 처리하기 위해: "on the wire" 버퍼의 바이트 순서; 가장 자주 사용되는 두 가지는 ARM, PowerPC, SPARC 프로세서에서 찾을 수 있는 Big-Endian과 (낮은 메모리 주소에서 최상위 바이트), x86, x86-64에서 찾을 수 있는 Little-Endian이 (낮은 메모리 주소에서 최하위 바이트) 있다.
- 오브젝트 파스칼의 관리된 타입을 (문자열, 배열) 적절한 포맷으로 직렬화,
- 포인터 기반 타입 또는 포인터를 이용한 타입, 직렬화된 데이터로부터 직렬화된 포맷을 설치할 수 있도록 한다. 이는 클래스의 인스턴스, 포인터 멤버가 있는 레코드를 포함한다.
다음 절에서 다루게 될 "웹 서비스 툴킷", 짧게 말해 WST는 사실상의 산업 표준을 (SOAP과 XmlRPC, JSON과 함께 XML) 활용하거나 WST 커스텀 크로스 플랫폼 바이너리 포맷을 이용하여 크로스 플랫폼 방식으로 파라미터의 직렬화를 처리함으로써 이러한 문제들을 모두 해결한다. WST는 이에 더해 HTTP 프로토콜과 커스텀 동적 연결 (Windows DLL/(x)nix SO) 기반의 프로토콜 (라이브러리 모듈에서 관리되는 서비스) 사용을 허용한다.