Deep Studying

[프라우드넷] RPC 통신 이해하기(1) - 용어 및 개념 정의 본문

게임서버

[프라우드넷] RPC 통신 이해하기(1) - 용어 및 개념 정의

miniSeop 2022. 1. 9. 21:10

프라우드넷은 게임 서버입니다. 게임 클라이언트와 통신하여 서비스를 제공하는 것이 프라우드넷의 역할이죠. 그러므로 서버와 클라이언트는 어떤 방법으로든 통신이 이루어져야합니다. 하지만 웹서버와 브라우저가 통신하기 위해서는 http라는 프로토콜이 필요한 것처럼 게임 클라이언트와 게임 서버 사이에도 통신을 하기 위해 특별한 약속이 필요합니다. 게임서버, 그 중에 프라우드넷에서는 어떤 방식으로 클라이언트와 통신하는지 알아보도록 하겠습니다.

 

1.1 RPC ( Remote Procedure Call )

 분산 컴퓨팅에서 remote procedure call ( RPC )는 다른 address space( 일반적으로 네트워크 상에 존재하는 다른 컴퓨터를 지칭 합니다. )에 존재하는 프로시저 ( 혹은 subroutine )을 실행할 때 사용하는 용어입니다. 마치 local에 존재하는 프로시저를 호출하는 것 처럼 코드를 작성할 수 있게 해주며, 프로그래머가 통신에 대한 디테일한 로직을 이해할 필요 없이 이를 처리할 수 있게 해줍니다. 이러한 프로그래밍 방법을 RPC라고 통칭합니다.

 

특정 프로토콜이나 특정 라이브러리를 의미하는 것이 아닌 이러한 방법론을 지칭하기 때문에 광범위한 용어입니다.

 

 

1.2 RMI ( Remote Method Invocation )

 객체 지향 프로그래밍에서 distributed object communication의 방법입니다. RPC는 그 범위가 다른 address space의 프로시저를 실행하는 폭 넓은 의미였다면 RMI는 다른 address space에 있는 객체에 한정하여 원격으로 메서드를 호출하는 것을 의미합니다. 모든 프로시저를 대상으로 하지 않고 객체-메서드에 한정한 RPC라고 할 수 있습니다. 

 

사진 출처: 위키피디아

 

 클라이언트 ( Caller )는 Stub 객체를 가지고, 서버 ( Executor )는 Skeleton 객체를 가지고 있으며 아래 방식을 이용하여 다른 address space에 있는 객체의 메서드를 실행합니다.

종종 Stub / Skeleton 이란 용어가 아니라 Client Stub / Server Stub 이라는 용어로 사용하기도 합니다.

 

  1. 클라이언트는 stub에 의해 재정의된 로컬 프로시저를 실행합니다.

  2. stub이 request message에 call type(프로시저 종류)과 파라미터를 넣고 marshall ( serialize )합니다.

  3. stub이 skeleton으로 메세지를 전송하고 현재 스레드를 블럭시킵니다.

  4. skeleton이 메세지를 unmarshall ( deserialize )하여 call type과 파라미터를 얻어옵니다.

  5. 4에서 얻은 파라미터와 call type을 통해 적절한 로컬 프로시저를 실행합니다.

  6. skeleton이 프로시저의 실행 결과를 패키징하여 response message에 넣습니다.

  7. skeleton이 stub에게 메세지를 전송합니다.

  8. stub이 결과를 언패키징하여 5에서 실행한 프로시저의 결과를 얻습니다.

  9. 처음 프로시저를 호출한 caller에게 결과를 전달해줍니다. 스레드의 블럭을 풀고 이후 명령을 실행합니다.

 

 위와같은 과정을 통해 클라이언트는 로컬 프로시저를 실행시키고 결과를 리턴받지만, 실제 실행은 외부의 프로시저를 실행시킨 결과를 얻을 수 있습니다.

 marshall과 unmarshall이란 용어가 새로 나왔는데 이는 RMI에 대한 설명 이후 말씀드리겠습니다.

 

 

1.3 Java RMI

 위에서 설명한 Stub, Skeleton을 이용한 통신은 그 자체로 RMI라 부를 수 있습니다. 하지만 실제 RMI라는 용어를 주로 사용하는곳은 Java 언어에 한정되어있습니다. 그 이유는 무엇일까요?

 Stub - Skeleton 방식을 차용한 프로토콜은 여러 언어에서 지원합니다. 하지만 각자 이를 다른 이름으로 부르고 있습니다. Object-C에서는 Portable Distributed Object ( PDO ) 라고 부르고 닷넷 환경에서는 .Net Remoting 이라고 부릅니다. 그러다보니 RMI라 하면 위의 RMI 에서 설명한 개념과 함께 Java의 RMI 라이브러리를 의미하는 경우가 많습니다.

 

 

1.4 MSRPC ( Microsoft Remote Procedure Call )

 Microsoft에서도 RMI를 위한 라이브러리를 제공합니다. Stub - Skeleton 구조를 사용하여 원격으로 프로시저를 실행한다는 점은 똑같지만 사용하는 용어가 다릅니다.

 MSRPC를 이용하여 원격으로 접근 가능한 대표적인 객체는 COM ( Component Object Model )입니다. 이것은 MSRPC에서 사용하는 일종의 규격입니다. 여기서 중요한 내용은 아니니 똑같이 "객체" 라는 이름으로 사용하겠습니다.

 

클라이언트( Caller )는 Proxy라는 객체를 통해 서버로 RPC 통신을 하고, 서버는 Stub이라는 객체를 통해 이를 처리합니다. 나머지 주요 내용은 위에 설명한 RMI와 똑같습니다.

 

 

1.5 ProudNet RMI

 프라우드넷에서는 RMI 라는 이름의 프로토콜을 사용하며 Proxy ( Caller )와 Stub ( Executor )을 통해 RPC를 구현했습니다. RMI라는 이름은 Java RMI가 아닌 1.2에서 설명한 RMI에서 왔다고 할 수 있습니다. 또한 Java RMI와 다르게 Stub과 Skeleton이란 용어가 아닌 Proxy와 Stub 이란 용어를 사용합니다. 이는 MSRPC를 기반으로 만들었기 때문에 이런 용어를 사용하는 것 같습니다. Java를 

 앞으로의 포스트에서 "RMI"는 ProudNet에서 사용하는 RMI 라이브러리를 의미하는 용어로 사용하겠습니다.

 

 

2. Marshalling

 마샬링은 한 객체의 메모리에서 표현방식을 저장 또는 전송에 적합한 다른 데이터 형식으로 변환하는 과정이다. 또한 이는 데이터를 컴퓨터 프로그램의 서로 다른 부분 간에 혹은 한 프로그램에서 다른 프로그램으로 이동해야 할 때도 사용한다.

 마셜링의 반대 개념으로 언마셜링(unmarshalling)이 있다. ( 디마셜링, demarshalling 이라고도 불린다. )

마셜링은 프로세스간 또는 스레드간 데이터 전송에 필요한 RPC 메커니즘의 구현에 사용된다.

 

 위는 위키피디아의 마샬링 문서에서 가져온 내용입니다. 간단히 설명하면 객체의 정보를 온전히, 효율적으로 전달하기 위해서 데이터 형식을 변경하는 일종의 컨버팅 과정이라고 할 수 있습니다.

 

즉 Proxy에서 Stub으로 데이터를 전송하기 전에 마셜링을 하고, Stub에서는 받은 데이터를 언마셜링 합니다.

 

 

프라우드넷의 예제 코드에 포함되어있는 Simple_proxy.cs 파일의 내용을 한 번 봐볼까요?

 

public bool Chat(Nettention.Proud.HostID remote,Nettention.Proud.RmiContext rmiContext, String a, int b, float c)
{
	// 1. Message 객체를 만들고
	Nettention.Proud.Message __msg=new Nettention.Proud.Message();
	__msg.SimplePacketMode = core.IsSimplePacketMode();
    
	// 2. Message의 ID ( call type )은 Chat으로 하고
	Nettention.Proud.RmiID __msgid= Common.Chat;
	__msg.Write(__msgid);
    
	// 3. Marshaler라는 객체를 이용해 파라미터를 입력하고
	// ( 파라미터를 마셜링 하고 )
	Nettention.Proud.Marshaler.Write(__msg, a);
	Nettention.Proud.Marshaler.Write(__msg, b);
	Nettention.Proud.Marshaler.Write(__msg, c);
	
	// 4. 호출하는 호스트의 정보를 넣고
	Nettention.Proud.HostID[] __list = new Nettention.Proud.HostID[1];
	__list[0] = remote;
	
	// 5. ProudNet RMI라이브러리를 사용하여 전송한다.
	return RmiSend(__list,rmiContext,__msg,
		RmiName_Chat, Common.Chat);
}

 

    1. Message 객체를 만듭니다.

    2. Message의 call type을 Chat으로 정합니다.

    3. 파라미터를 마셜링합니다.

    4. 호출하는 호스트의 정보를 넣습니다.

    5. RmiSend라는 함수를 이용하여 전송합니다.

 

코드 내용은 아직 몰라도 위와 같은 순서로 코드가 진행되는 것을 볼 수 있습니다.

 

 이제 온전히 Proxy의 역할을 이해할 수 있게 되었습니다. Proxy를 사용하는 클라이언트에서는 Chat이라는 함수의 구현 없이 인터페이스만 가져와서 RPC통신을 하고, Chat이라는 함수의 구현은 서버가 맡는 것이죠.

 

 

3. IDL ( Interface Definition Language )

 Proxy와 Stub이 원격으로 메서드 호출을 할 수 있게 해준다는 사실은 알았지만, 같은 메모리상에서 동작하는 프로그램이 아니기 때문에 서버와 클라이언트 서로 어떤 메서드를 사용할지에 대한 합의가 필요합니다. 최소한 메서드를 정의할 때 필요한 리턴타입, 파라미터, 메서드 이름정도는 공유를 하고 있어야 원활한 작업이 가능할 것입니다. 그리고 서버에서 맘대로 이를 바꾼다거나, 클라이언트에서 이를 맘대로 바꾸어서도 안되죠.

 

 IDL은 서버와 클라이언트가 RPC 통신을 하기 위해 필요한 인터페이스를 정의해둔 문서입니다. IDL에는 위에서 언급한 메서드의 형태를 정의해두고, IDL파일을 이용해서 동일한 형식을 가진 Proxy와 Stub을 만들어냅니다. 이를 각각 클라이언트와 서버가 나누어 사용하면 항상 같은 형식으로 메서드를 호출할 수 있게됩니다.

 

PIDL 파일

프라우드넷은 PIDL이라는 확장자로 이러한 IDL을 정의합니다. 아마 Proudnet + IDL의 약자같습니다.

결국 프라우드넷에서 사용하는 RMI인터페이스를 정의해둔 문서가 되는 셈이겠죠?

 

 

번 외 Serialize ( 틀릴 수 있는 내용 )

 여러 글에서 Marshalling과 Serializing를 혼용하여 사용하는 것을 볼 수 있으며, 거의 유사한 기능이라고 정의되어있습니다. 둘 다 객체에 해당하는 메모리 값을 전송하기 좋게 변형하는 과정입니다.

 Python에서는 둘은 같으며, 혼용할 수 있다고 하고 Java에서는 둘은 비슷하지만 다르다고 정의되어있습니다.

 

RFC2713

To "marshal" an object means to record its state and codebase(s) in such a way that when the marshalled object is "unmarshalled," a copy of the original object is obtained, possibly by automatically loading the class definitions of the object

 Java에서 Marshalled Object ( java.rmi.Remoteinterface 를 implement한 Object )라는 객체를 구분하기 위해 marshalling과 serializing을 다르게 정의한 것 같습니다.

 일반적인 객체 뿐 아니라 IDL 안에 정의된 것은 int, string 등 기본 형식이어도 serialize 할 수 있으며 이를 통해 Stub과 Skeleton이 온전히 데이터를 패키징 / 언패키징 할 수 있습니다. 이 내용은 위에 설명한 marshalling과 같습니다.

 다만 Java에서 Marshalled Object를 Serialize할 때, 객체의 state 뿐만 아니라 Codebase도 함께 컨버팅 하는 것을 따로 Marshal 한다고 표현하는 것으로 보입니다. 이를 통해 Skeleton이 언패키징한 정보를 통해 class의 정의를 가져와서 온전한 객체로 만들 수 있다고 합니다.

 

( Java RMI를 써보지 않고 글만 번역하여 옮긴 것이기 때문에 틀릴 수 있는 내용입니다. )

 

 

다음 포스트에서는 실제 코드를 작성하여 실습해보도록 하겠습니다.

 

다음 포스트: RPC 통신 이해하기(2) - PIDL 파일에 대해