TIL_066 : RPC, 가상함수테이블

김펭귄·2025년 11월 20일

Today What I Learned (TIL)

목록 보기
66/104

오늘 학습 키워드

  • RPC

  • 가상함수테이블

1. RPC

  • Remote Procedure Call의 줄임말로, 함수를 호출하는 PC와 호출받은 함수가 실행되는 PC가 달라도 되게끔 해주는 통신기법

  • 예를들어, 화면에 문자를 출력하는 함수를 서버에서 호출하면 해당 출력시키는 함수는 각 클라이언트들에서 실행되게끔도 해줌

  • 게임에 큰 영향을 끼치지 않는 간단한 동작들(사운드, 파티클 등)에 주로 사용

  • 즉, RPC를 사용하면 다른 PC에 있는 해당 함수를 즉시 실행시켜줌

1.1. Call vs Invoke

  • 둘 다 함수나 메서드를 호출하는거지만 약간의 차이가 존재

Call

  • 명시적으로 직접 호출을 의미
void greet() { /* ... */ }
greet(); // call (직접 호출)

Invoke

  • 함수의 직접 호출보다는 실행하라는 동작 자체를 의미

  • 함수나 메서드가 다른 함수의 인수로 전달되어 실행되거나, 레퍼런스, 델리게이트, 콜백 등을 통해 간접적으로 실행될 때 사용

void executeCallback(void (*callback)()) {
    callback(); // invoke (간접 호출)
}
  • 그래서 RPC를 invoked했다라는 표현을 사용

1.2. Owned

Client-owned actor

  • 이전 TIL에서 공부했듯이, 서버에서 스폰되고 클라로 복제된 액터는 ClientConnction의 패밀리에 들어가야 서버와 송수신 가능

  • ClientConnection이 소유하는 PlayerController가 소유(Own)하는 액터들은 이 패밀리에 들어가게 되고, Client-owned actor

  • 즉, 클라에 존재하는 Autonomous Proxy인 액터 = Client-owned actor
    그리고 이 액터들만 서버에 RPC 및 요청 가능

  • 서버의 경우, 이러한 액터들(Remote Role이 Autonomous Proxy인 액터) 역시 Client-owned actor라고 함

Server-owned actor

  • ClientConnection패밀리에 없는 액터, 즉 특별한 클라이언트 Owner가 없이 서버에 스폰되고, 클라에 복제만 된 액터들

  • 위에 해당하는 액터로, 서버, 클라에 존재하는 액터 둘 다 Server-owned actor라고 함

  • Server-owned actor는 Authority가 전적으로 서버가 가지므로, 서버에서 동기화만 당하고 서버로 요청을 보내는 것은 제한적

Unowned actor

  • Unowned actor는 클라이언트와 서버 어느 쪽에서도 소유자가 명확히 지정되지 않은 액터

  • 임시적으로 존재하긴 함

  • Server-owned actor와 동작자체는 동일하지만, 네트워크와는 연관성이 거의 없음

1.3. RPC 매크로 종류

NetMulticast

  • 모든 PC에게 RPC를 실행시켜달라는 요청 (서버에서 호출해야 함)

  • 클라에서 호출하면 자기만 실행됨 (밑에 총정리 사진에 다 나와있음)

  • 서버의 Pawn의 NetMulticast RPC를 호출하면, 본인(서버)포함 모든 PC의 Pawn에서 해당 RPC 실행

  • 마찬가지로 컨트롤러에서 똑같이 한다면, 서버 컨트롤러와 해당 컨트롤러를 가지고 있는 PC에서만 해당 RPC 실행됨

  • 부하가 심해 자주사용하면 안 됨

Server

  • 클라이언트에서 Server RPC를 호출하면, 서버의 RPC가 실행됨

  • 이때, 당연히 해당 RPC를 요청하는 액터는 Client-owned actor여야만 함

  • Dedicated Server의 경우 이 Server RPC를 무조건 수락하므로, _Validate()함수로 RPC요청을 수락할지 말지 결정함

Client

  • 서버에서 클라이언트들에게 RPC를 실행시켜달라는 요청

  • 해당 액터를 직접 소유하는 클라이언트에서만 해당 RPC가 실행됨

1.4. RPC 사용을 위한 요건

  1. Actor 에서 호출되어야 함
  2. Actor 는 빈드시 replicated여야 함
  3. 서버에서 호출되고 클라이언트에서 실행되는 RPC 의 경우, 해당 Actor 를 실제 소유하고 있는 클라이언트에서만 함수가 실행됨
  4. 클라이언트에서 호출되고 서버에서 실행되는 RPC 의 경우, 클라이언트는 RPC 가 호출되는 Actor 를 소유해야함 (Client-owned actor)
  5. 서버에서 호출하는 Multicast RPC는 서버뿐만 아니라 현재 연결된 모든 클라이언트에서도 실행

1.5. 총정리

  • 각 서버나 클라에서 RPC를 호출했을 때, 호출한 액터의 종류에 따른 동작 결과를 정리한 표

예시 코드

// CXPlayerController.h
UCLASS()
class CHATX_API ACXPlayerController : public APlayerController
{
	// ... //
    
	UFUNCTION(Client, Reliable)		// Client RPC
	void ClientRPCPrintChatMessageString(const FString& InChatMessageString);

	UFUNCTION(Server, Reliable)		// Server RPC
	void ServerRPCPrintChatMessageString(const FString& InChatMessageString);
};
// CXPlayerController.cpp
#include "EngineUtils.h"

// 채팅 입력시 호출되는 함수
void ACXPlayerController::SetChatMessageString(const FString& InChatMessageString)
{
	if (IsLocalController())
    	// 서버 RPC 호출
		ServerRPCPrintChatMessageString(InChatMessageString);
}

// 모든 RPC들은 함수명 뒤에 _Implementation붙여서 구현부를 생성
void ACXPlayerController::ClientRPCPrintChatMessageString_Implementation(
										const FString& InChatMessageString)
{
	// 화면에 문자열 출력해주는 함수
	PrintChatMessageString(InChatMessageString);
}

void ACXPlayerController::ServerRPCPrintChatMessageString_Implementation(
										const FString& InChatMessageString)
{
	// 서버에 존재하는 모든 컨트롤러 가져오기
	for (TActorIterator<ACXPlayerController> It(GetWorld()); It; ++It) {
		ACXPlayerController* CXPlayerController = *It;
        
        // 클라이언트 RPC호출
		if (IsValid(CXPlayerController))
			CXPlayerController->ClientRPCPrintChatMessageString(InChatMessageString);
	}
}

2. RPC 신뢰성 검증

2.1. WithValidation

  • 서버실행 로직은 무조건 동작하므로, RPC의 경우 이 매크로를 이용하여 신뢰성을 검증해야함

  • UFUNCTION과 함께 사용되는 매크로로, 이 매크로가 붙은 RPC는 _Implementation() 함수와 _Validate() 함수로 나뉘어 구현해야함

  • 전자 함수는 실제 실행부분이고, 후자부는 검증부분으로 true이면 전자함수를 실행, false이면 drop함

2.2. Reliable vs Unreliable

Reliable

  • UFUNCTION에 사용하는 매크로로, Reliable을 사용하면, 원격 PC에서 무조건 실행됨

  • 호출이 누락되어 전송이 실패하면 재전송을 하여 무조건 실행됨을 보장해줌

  • 데미지 로직, 스폰 등 중요한 게임 로직에 사용

Unreliable

  • 아무것도 사용 안 하면 Unreliable이 기본 키워드로 동작하여 딱 한 번만 호출요청하고, 누락되면 재전송 안해 신뢰성이 없음

  • Unreliable 키워드를 통해 네트워크의 과부하를 줄임

  • 사운드, 이펙트처럼 게임에 큰 영향 없는 로직에 사용

3. 가상함수테이블 (VTable)

  • OOP의 특징중 하나인 다형성에는 오버라이딩과 오버로딩이 있었다

  • 오버로딩의 경우, 컴파일타임에 함수가 결정되는 정적함수이지만, 오버라이딩은 런타임에 호출될 함수가 결정되는 동적 함수이다

  • 그래서 컴파일러는 가상함수테이블을 이용하여 동적으로 함수를 호출한다

  • 이 가상함수테이블은 컴파일러마다 조금 다르지만, 일반적으로 컴파일러가 메모리의 data부분에서도 rdata에 생성함.

  • 이부분은 초기화된 전역변수와 static변수가 저장되는 data와 다름

  • data는 읽기/쓰기가 가능하지만 rdata는 읽기만 가능한 부분

  • vptrvtable을 직접 접근해 사용은 못 함. 우회하면 가능한 방법도 있으나 위험하므로 접근하지 말기

3.1. 가상함수테이블 생성 순서

class Parent {
    virtual void foo1() {}
    void foo2() {}
    virtual void foo3() {}
};

class Child : public Parent {
    virtual void foo3() override {}
    virtual void foo4() {}
};
  1. 컴파일타임에 컴파일러가 virtual키워드를 가지는 모든 클래스마다 가상함수테이블을 .rdata에 생성하고, 가상함수테이블을 가리키는 포인터변수를 클래스에 생성

  2. 당연히 부모클래스가 virtual을 가지면 자식도 물려받으니까 자식에게도 해당.
    vptr은 객체내의 메모리에서 가장 앞에 위치

  1. 각 클래스는 자신의 vtable에다가 가상함수가 선언된 순서대로, 가상함수의 포인터주소를 저장함
  1. 순수가상함수의 경우는 컴파일러마다 다르지만, NULL을 적기도하고, 아니면 _purecall같은 에러핸들러의 주소를 저장

  2. 자식클래스도 부모클래스의 가상함수테이블을 기본으로하여 만듦

  3. 만약, 자식클래스에서 오버라이딩을 했다면, 오버라이딩한 해당 가상함수포인터만 오버라이딩한 함수포인터로 저장

  1. 그리고 각 클래스의 가상함수테이블포인터변수는 생성자에서 각 객체의 vtable 주소로 초기화됨.
    보통 가상함수를 가지는 객체는 하나의 vtable포인터 변수를 가짐. 부모, 자식 둘 다 가상함수를 가져도 마찬가지. 근데, 다중상속의 경우는 vtable을 가지는 부모 클래스 수만큼 vptr을 가짐
  1. 따라서 런타임에 가상함수를 호출하게 되면, 해당 객체의 vptr에 접근하고, 해당 가상함수가 선언된 순서로 인덱스로 접근하여 가상함수포인터를 읽고 메모리에 접근하게 된다
Parent* myClass = new Child;
myClass->foo3();
// myClass의 vptr은 Child의 vtable을 가리킴
// foo3은 2번째이므로, vptr + 1의 위치에(64bit니깐 8byte) 있는 함수포인터에 접근
  • 참고로, GCC의 경우는 RTTI(Runtime Type Info)정보를 담은 객체를 가리키는 포인터가 첫번째 원소. MSVC는 이 정보를 전역으로 따로 관리
profile
반갑습니다

0개의 댓글