데디케이티드 서버 RPC

이 문서는 아이펀엔진 으로 제작된 게임 서버와 Unity, Unreal Engine 4 클라이언트 엔진으로 제작한 데디케이티드 서버 사이에 통신을 지원하는 데디케이티드 서버 RPC 기능에 대해서 설명합니다.

데디케이티드 서버 RPC 기능은 연결 프로토콜은 Tcp, 메시지 인코딩은 Protobuf 을 사용합니다.

게임 서버

아이펀엔진 으로 제작하는 게임 서버의 설정과 데디케이티드 서버 RPC 기능을 사용하기 위해서 메시지를 정의하고 게임 서버 코드에서 기능을 사용하는 방법에 대해서 설명합니다.

기능 활성화 및 설정

이 기능을 사용하기 위해서는 분산처리 기능관련한 설정들 항목을 참고하시어, RpcServer 기능 을 먼저 활성화 해 주시기 바랍니다. 데디케이티드 서버 RPC 기능을 사용하기 위해서 MANIFEST.json 파일 안에 DedicatedServerRpcService 를 추가 또는 수정하여 기능을 켤 수 있습니다.

...
"DedicatedServerRpcService": {

  // true 로 설정하여 이 기능을 켤 수 있습니다.
  "dedicated_server_rpc_enabled": true,

  // 응답 콜백을 처리할 스레드 수를 결정합니다.
  "dedicated_server_rpc_threads_size": 4,

  // 데디케이티드 서버로부터 접속을 받아들일 TCP 포트를 결정합니다.
  "dedicated_server_rpc_port": 8016,

  // TCP 접속을 받아들일 NIC 이름을 입력합니다. 이 NIC 의 IP 로 binding 하게
  // 됩니다. 비워두면 자동으로 결정되지만, 올바르지 않을 수 있습니다.
  "dedicated_server_rpc_nic_name" : "",

  // Public IP 로 TCP 접속을 받아들입니다. 위 NIC 설정보다 우선합니다.
  "dedicated_server_rpc_use_public_address": false,

  // 송수신하는 메시지를 로그로 출력합니다. (0 은 로그를 남기지 않음.
  // 1 은 패킷 타입과 길이 정보만 남김. 2 는 패킷의 내용까지 남김)
  "dedicated_server_rpc_message_logging_level": 0,

  // true 이면 TCP Nagle 알고리즘을 사용하지 않습니다.
  "dedicated_server_rpc_disable_tcp_nagle": true,

  // 데디케이티드 서버가 연결할 엔진 서버를 RPC 태그로 설정합니다.
  // RPC 로 연결된 엔진 서버 중에 설정한 태그를 포함하면서 Dedicated_server_rpc_tags_to_connect 설정값이
  // 같은 엔진 서버로만 연결합니다.
  // 값이 비워져있는 경우에 데디케이티드 서버는 처음 연결을 맺는 엔진 서버로만 연결합니다.
  "dedicated_server_rpc_tags_to_connect": [""]

},
...

Tip

RpcService/rpc_tags 에 특정 태그를 추가하고, 해당 태그를 dedicated_server_rpc_tags_to_connect 에 설정하면 데디케이티드 서버가 해당 태그를 갖는 엔진 서버들과 연결을 맺도록 할 수 있습니다.

Tip

서버의 flavor 이름은 기본적으로 RPC 태그로도 사용할 수 있습니다. 만약 lobby, game 서버 flavor 가 모두 데디케이티드 서버 RPC 피어로 연결되기를 원한다면, 두 서버가 같은 dedicated_server_rpc_tags_to_connect 설정값을 가져야 하므로 두 서버 모두 해당값을 "lobby", "game" 으로 지정해야합니다.

Tip

데디케이티드 서버와 엔진 서버가 연결되면 데디케이티드 서버는 엔진 서버와 RPC 로 연결된 다른 엔진 서버들과도 연결하게 되므로 데디케이티드 서버 하나와 엔진 서버 하나만 연결하기 원하는 경우 dedicated_server_rpc_tags_to_connect 값을 비워두면 됩니다.

연결

연결의 방향은 데디케이티드 서버가 게임 서버로 연결을 함으로써 이루어집니다. 다음 두 핸들러를 등록하여 연결이 될 때, 연결이 끊길 때 통지 받을 수 있습니다.

// 서버의 Install 함수에서 핸들러를 등록합니다.
...
static bool Install(...) {
  DedicatedServerRpc::RegisterConnectHandler(OnDediServerConnected);
  DedicatedServerRpc::RegisterDisconnectHandler(OnDediServerDisconnected);
}

void OnDediServerConnected(const DedicatedServerRpc::PeerId &peer_id) {
  // 데디케이티드 서버가 연결되면 이 함수가 불립니다.
  // peer id 로 새롭게 연결된 데디케이티드 서버를 식별할 수 있습니다.
  ...
}

void OnDediServerDisconnected(const DedicatedServerRpc::PeerId &peer_id) {
  // 데디케이티드 서버와 연결이 끊기면 이 함수가 불립니다.
  // peer id 로 연결이 끊어진 데디케이티드 서버를 식별할 수 있습니다.
  ...
}
// 서버의 Install 함수에서 핸들러를 등록합니다.
...
public static bool Install(...)
{
  DedicatedServerRpc.RegisterConnectHandler(OnDedicatedServerRpcConnected);
  DedicatedServerRpc.RegisterDisconnectHandler(OnDedicatedServerRpcDisconnected);
}

public static void OnDedicatedServerRpcConnected (Guid peer_id)
{
  // 데디케이티드 서버가 연결되면 이 함수가 불립니다.
  // peer id 로 새롭게 연결된 데디케이티드 서버를 식별할 수 있습니다.
  ...
}

public static void OnDedicatedServerRpcDisconnected (Guid peer_id)
{
  // 데디케이티드 서버와 연결이 끊기면 이 함수가 불립니다.
  // peer id 로 연결이 끊어진 데디케이티드 서버를 식별할 수 있습니다.
  ...
}

데디케이티드 서버 RPC 연결 목록

다음과 같은 방법으로 데디케이티드 서버 RPC 기능을 호출하기 위해 연결되어 있는 데디케이티드 서버 목록을 얻을 수 있습니다.

DedicatedServerRpc::PeerMap peers;
DedicatedServerRpc::GetPeers(&peers);

DedicatedServerRpc::PeerMap::const_iterator itr = peers.begin();
DedicatedServerRpc::PeerMap::const_iterator itr_end = peers.end();

for (; itr != itr_end; ++ itr) {
  const DedicatedServerRpc::PeerId &peer_id = itr->first;
  LOG(INFO) << "PeerID=" << peer_id;
}
Dictionary<Guid, System.Net.IPEndPoint> peers;
DedicatedServerRpc.GetPeers(out peers);

foreach (var pair in peers)
{
  Log.Info("PeerID={0}", pair.Key);
}

데디케이티드 서버 태그 활용

RPC 를 지원하는 데디케이티드 서버는 각자 태그를 정의할 수 있으며, 아이펀 엔진으로 제작된 게임 서버는 특정 데디케이티드 서버의 태그를 읽어오거나, 같은 태그를 갖는 데디케이티드 서버 목록을 얻을 수 있습니다.

string tag = "navi";

// 태그로 서버 목록을 가져옵니다.
{
  DedicatedServerRpc::PeerMap peers;
  DedicatedServerRpc::GetPeersWithTag(&peers, tag);
}

// 태그를 달고 있는지 알아냅니다.
{
  DedicatedServerRpc::PeerId peer_id = ...;
  if (DedicatedServerRpc::HasTag(peer_id, tag)) {
    ...
  }
}

// 태그를 읽습니다
{
  DedicatedServerRpc::PeerId peer_id = ...;
  string peer_tag = DedicatedServerRpc::GetPeerTag(peer_id);
  ...
}
string tag = "navi";

// 태그로 서버 목록을 가져옵니다.
{
  Dictionary<Guid, System.Net.IPEndPoint> peers;
  DedicatedServerRpc.GetPeersWithTag(out peers, tag);
}

// 태그를 달고 있는지 알아냅니다.
{
  Guid peer_id = ...;
  if (DedicatedServerRpc.HasTag(peer_id, tag)) {
    ...
  }
}

// 태그를 읽습니다
{
  Guid peer_id = ...;
  string peer_tag = DedicatedServerRpc.GetPeerTag(peer_id);
  ...
}

요청 보내기

프로젝트를 생성하면 {프로젝트이름}_dedicated_server_rpc_messages.proto 파일이 생성됩니다. FunDedicatedServerRpcMessage 를 extend 하여 필요한 메시지를 정의할 수 있습니다.

Note

Google Protobufextension 과 문법에 대해서는 Google Protocol Buffers 의 설명을 참고해주세요.

Important

FunDedicatedServerRpcMessageextend 할 때 필드 번호는 32번 부터 사용해야 됩니다. 0번 부터 31번까지는 아이펀 엔진에 의해 사용됩니다.

아래 설명은 예시로 간단한 에코 처리를 위한 “echo”, 길찾기를 위한 “nav” 를 다룹니다. 이 예시에 대한 데디케이티드 서버 구현은 메시지 핸들러 에 설명되어 있습니다.

프로젝트를 생성하면 proto 파일에는 아래와 같이 EchoDedicatedServerRpcMessage 가 예시로 추가 되어 있습니다.

// Your specific message class.
message EchoDedicatedServerRpcMessage {
    optional string message = 1;
}

extend FunDedicatedServerRpcMessage {
    ////////////////////////////////////////////////////////////////////
    // CAUTION: EXTENSIONS FROM 8 THROUGH 31 ARE RESERVED BY THE ENGINE.
    // GAME DEVELOPER SHOULD USE FROM 32.
    ////////////////////////////////////////////////////////////////////

    optional EchoDedicatedServerRpcMessage echo_ds_rpc = 32;
}

여기에 추가로 길찾기를 위한 NavRequest, NavReply 를 추가하면 아래와 같습니다.

// Your specific message class.
message EchoDedicatedServerRpcMessage {
    optional string message = 1;
}

message NavVector3 {
  required float x = 1;
  required float y = 2;
  required float z = 3;
}

message NavRequest {
  required NavVector3 source = 1;
  required NavVector3 destination = 2;
}

message NavReply {
  repeated NavVector3 waypoints = 1;
}

extend FunDedicatedServerRpcMessage {
    ////////////////////////////////////////////////////////////////////
    // CAUTION: EXTENSIONS FROM 8 THROUGH 31 ARE RESERVED BY THE ENGINE.
    // GAME DEVELOPER SHOULD USE FROM 32.
    ////////////////////////////////////////////////////////////////////

    optional EchoDedicatedServerRpcMessage echo_ds_rpc = 32;
    optional NavRequest nav_request = 33;
    optional NavReply nav_reply = 34;
}

위 예시와 같이 메시지를 정의 했다면 다음과 같이 서버 코드에서 데디케이티드 서버로 “echo” 요청을 보낼 수 있습니다.

void Request() {
  // 요청을 보낼 데디케이티드 서버 peer 를 결정합니다.
  DedicatedServerRpc::PeerId peer = ...;

  Ptr<FunDedicatedServerRpcMessage> request(
      new FunDedicatedServerRpcMessage());

  // 데디케이티드 서버는 type 문자열로 호출할 메시지 핸들러를 구분합니다.
  request->set_type("echo");

  // MutableExtension() 의 인자로 정의한 메시지의 변수 이름을 전달합니다.
  EchoDedicatedServerRpcMessage *echo =
      request->MutableExtension(echo_ds_rpc);

  echo->set_message("hello!!!");

  // 요청을 보냅니다. 3 번째 인자로 응답을 수신했을 때 호출할 콜백을
  // 입력합니다.
  DedicatedServerRpc::Call(peer, request, OnDediServerEcho);

  // 또는 PeerID 를 입력하지 않으면 무작위로 하나의 데디케이티드
  // 서버로 요청을 전송합니다.
  // DedicatedServerRpc::CallRandomly(request, OnDediServerEcho);
}

void OnDediServerEcho(
    const DedicatedServerRpc::PeerId &peer_id,
    const DedicatedServerRpc::Xid &xid,
    const Ptr<const FunDedicatedServerRpcMessage> &reply) {
  if (not reply) {
    // 응답 수신 전 해당 데디케이티드 서버와 연결이 끊긴 경우입니다.
    return;
  }

  const EchoDedicatedServerRpcMessage &echo =
      reply->GetExtension(echo_ds_rpc);
  LOG(INFO) << "reply: " << echo.message();
}
public static void Request()
{
  // 요청을 보낼 데디케이티드 서버 peer 를 결정합니다.
  Guid peer_id = ...

  FunDedicatedServerRpcMessage request = new FunDedicatedServerRpcMessage();
  request.type = "echo";

  EchoDedicatedServerRpcMessage echo = new EchoDedicatedServerRpcMessage();
  echo.message = "hello!!!";

  request.AppendExtension_echo_ds_rpc(echo);

  DedicatedServerRpc.Call(peer_id, request, OnDediServerEcho);
  // 또는 PeerID 를 입력하지 않으면 무작위로 하나의 데디케이티드
  // 서버로 요청을 전송합니다.
  // DedicatedServerRpc.CallRandomly(request, OnDediServerEcho);
}

public static void OnDediServerEcho(Guid peer_id, Guid sid, FunDedicatedServerRpcMessage reply)
{
  if (reply == null)
  {
    // 응답 수신 전 해당 데디케이티드 서버와 연결이 끊긴 경우입니다.
    return;
  }

  EchoDedicatedServerRpcMessage echo;
  if (!reply.TryGetExtension_echo_ds_rpc(out echo))
  {
    Log.Error ("OnPbufEcho: Wrong message.");
    return;
  }
  Log.Info (echo.message);

}

“nav” 요청은 아래와 같이 보낼 수 있습니다.

void Request(){
  Ptr<FunDedicatedServerRpcMessage> request(
      new FunDedicatedServerRpcMessage());

  // 데디케이티드 서버는 type 문자열로 호출할 메시지 핸들러를 구분합니다.
  request->set_type("nav");

  // MutableExtension() 의 인자로 정의한 메시지의 변수 이름을 전달합니다.
  NavRequest *req = request->MutableExtension(nav_request);

  // (0,0,0) 에서 (100,100,100) 으로 가는 길을 찾는 요청을 만듭니다.
  req->mutable_source()->set_x(0.0f);
  req->mutable_source()->set_y(0.0f);
  req->mutable_source()->set_z(0.0f);
  req->mutable_destination()->set_x(100.0f);
  req->mutable_destination()->set_y(100.0f);
  req->mutable_destination()->set_z(100.0f);

  // 임의의 데디케이트 서버로 요청을 보냅니다.
  // 2 번째 인자로 응답을 수신했을 때 호출할 콜백을 입력합니다.
  DedicatedServerRpc::CallRandomly(request, OnNavReplyReceived);
}

void OnNavReplyReceived(const DedicatedServerRpc::PeerId &peer_id,
                        const DedicatedServerRpc::Xid &xid,
                        const Ptr<const FunDedicatedServerRpcMessage> &reply) {
  if (not reply) {
    // 응답 수신 전 해당 데디케이티드 서버와 연결이 끊긴 경우입니다.
    return;
  }

  LOG(INFO) << "Nav reply received.";

  const NavReply &repl = reply->GetExtension(nav_reply);

  // 경로를 출력합니다.
  for (size_t i = 0; i < repl.waypoints_size(); ++i) {
    LOG(INFO) << "#" << i << " : "
      << repl.waypoints(i).x() << ", "
      << repl.waypoints(i).y() << ", "
      << repl.waypoints(i).z();
  }
}
void Request()
{
  FunDedicatedServerRpcMessage request = new FunDedicatedServerRpcMessage();

  // 데디케이티드 서버는 type 문자열로 호출할 메시지 핸들러를 구분합니다.
  request.type = "nav";

  // MutableExtension() 의 인자로 정의한 메시지의 변수 이름을 전달합니다.
  NavRequest req = new NavRequest();

  // (0,0,0) 에서 (100,100,100) 으로 가는 길을 찾는 요청을 만듭니다.
  req.source.x = 0.0f;
  req.source.y = 0.0f;
  req.source.z = 0.0f;
  req.destination.x = 100.0f;
  req.destination.y = 100.0f;
  req.destination.z = 100.0f;

  // 임의의 데디케이트 서버로 요청을 보냅니다.
  // 2 번째 인자로 응답을 수신했을 때 호출할 콜백을 입력합니다.
  DedicatedServerRpc.CallRandomly(request, OnNavReplyReceived);
}

void OnNavReplyReceived(Guid peer_id,
                        Guid xid,
                        FunDedicatedServerRpcMessage reply)
{
  if (reply == null)
  {
    // 응답 수신 전 해당 데디케이티드 서버와 연결이 끊긴 경우입니다.
    return;
  }

  Log.Info("Nav reply received.");

  NavReply repl;
  if (!reply.TryGetExtension_nav_reply(out repl))
  {
    Log.Error ("OnNavReplyReceived: Wrong message.");
  }

  // 경로를 출력합니다.
  for (int i = 0; i < repl.waypoints.Count; ++i)
  {
    Log.Info("#{0} : {1}, {2}, {3}",
             i, repl.waypoints[i].x, repl.waypoints[i].y, repl.waypoints[i].z);
  }
}

데디케이티드 서버(Unity)

Unity 엔진 으로 제작하는 데디케이티드 서버가 아이펀 엔진과 약속된 RPC 기능 을 사용하기 위한 방법에 대해서 설명합니다.

데디케이티드 서버 RPC 플러그인은 Funapi Unity 플러그인dedicated-server-plugin 이라는 이름으로 포함되어 있습니다.

Note

Funapi Unity 플러그인GitHub/UnityPlugin 에서 받으실 수 있습니다.

유니티 데디케이티드 서버 프로젝트에서 Funapi DediServer RPC 기능 을 사용하기 위해서는 Assets 폴더에 있는 Funapi 폴더와 Plugins 폴더를 작업중인 프로젝트의 Assets 폴더로 복사하면 됩니다.

객체 생성 및 옵션

데디케이티드 서버 RPC 기능 을 사용하기 위해서는 FunapiDedicatedServerRpc 객체를 사용합니다. 이 객체를 통해서 게임 서버와 메시지를 주고 받을 수 있습니다.

DSRpcOption option = new DSRpcOption();
option.Tag = "Test";
option.AddHost("127.0.0.1", 8012);
option.AddHost("127.0.0.1", 8013);

FunapiDedicatedServerRpc rpc = new FunapiDedicatedServerRpc(option);

FunapiDedicatedServerRpc 객체 생성시 DSRpcOption 을 매개 변수로 전달합니다. DSRpcOption 의 항목은 아래와 같습니다.

public class DSRpcOption
{
    // 데디케이티드 서버의 태그 이름입니다.
    // 게임 서버에서 태그 이름으로 특정 데디케이티드 서버를 조회할 수 있습니다.
    public string Tag = "";

    // nagle 옵션을 사용할지 여부를 결정합니다.
    // 기본값은 true 로 nagle 을 사용하지 않습니다.
    public bool DisableNagle = true;

    // 연결할 게임서버의 주소와 포트 목록입니다.
    public List<KeyValuePair<string, ushort>> Addrs;

    // 서버 목록에 주소와 포트를 추가하는 함수입니다.
    // 여러 개를 입력하는 경우 처음 연결되는 서버로부터 다른
    // 서버의 정보보를 전달 받습니다. 이 서버를 master 서버라고 부릅니다.
    public void AddHost (string hostname_or_ip, ushort port);
}

메시지 핸들러

게임 서버로부터 받게 될 각각의 메시지에 대해 메시지 핸들러를 등록해야 합니다.

메시지 핸들러의 원형은 아래와 같습니다.

FunDedicatedServerRpcMessage handler (string type, FunDedicatedServerRpcMessage request);

메시지 타입과 게임 서버로부터 받은 메시지가 함수 인자로 전달됩니다. 리턴 값은 게임 서버로 전송할 메시지를 반환합니다. 메시지가 null 일 경우 응답 메시지를 보내지 않습니다.

아래는 메시지 핸들러를 등록하고 각각의 핸들러를 구현하는 예제입니다.

FunapiDedicatedServerRpc rpc = new FunapiDedicatedServerRpc(option);
rpc.SetHandler("echo", onEcho);
rpc.SetHandler("nav", onNavRequest);
// 'echo' 메시지에 대한 핸들러입니다.
// 메시지에 "hello" 를 넣어 응답 메시지를 반환합니다.
FunDedicatedServerRpcMessage onEcho (string type, FunDedicatedServerRpcMessage request)
{
    EchoDedicatedServerRpcMessage echo = new EchoDedicatedServerRpcMessage();
    echo.message = "hello";

    return FunapiDSRpcMessage.CreateMessage(echo, MessageType.echo_ds_rpc);;
}

// 'nav' 메시지에 대한 핸들러입니다.
// 길찾기 요청을 받고 응답으로 Vector3 배열을 반환합니다.
FunDedicatedServerRpcMessage onNavRequest (string type, FunDedicatedServerRpcMessage request)
{
    NavRequest req = FunapiDSRpcMessage.GetMessage<NavRequest>(request, MessageType.nav_request);
    // NavMeshAgent.CalculatePath(req.destination, path);

    NavReply reply = new NavReply();
    // For the test
    Vector3[] corners = new [] { Vector3.zero, Vector3.one, Vector3.back };
    for (int i = 0; i < corners.Length; ++i)
    {
        NavVector3 point = new NavVector3();
        point.x = corners[i].x;
        point.y = corners[i].y;
        point.z = corners[i].z;
        reply.waypoints.Add(point);
    }

    return FunapiDSRpcMessage.CreateMessage(reply, MessageType.nav_reply);
}

연결

객체 생성과 핸들러 등록이 모두 완료되었다면 Start 함수로 연결을 시작할 수 있습니다. Start 함수를 호출하면 DSRpcOption 에 설정했던 게임 서버들과 연결을 시작하고, 가장 먼저 연결에 성공한 서버를 master 로 설정합니다. 이 master 서버로부터 나머지 게임 서버 목록을 받아 추가로 연결을 수행합니다.

rpc.Start();

내부적으로 하나의 GameObjectDontDestroyOnLoad 타입으로 생성해 Updater 로 사용하고 있습니다. 그러므로 Scene 이 변경되어도 FunapiDedicatedServerRpc 객체를 새로 생성할 필요없이 하나의 객체를 여러 Scene 에서 공유해서 사용할 수 있습니다.

디버깅

디버깅을 위해 서버 수행 중 기록된 FunapiDedicatedServerRpc 관련 로그를 파일로 저장할 수 있습니다. 로그를 저장하기 위해서는 Unity Player Settings 에서 Symbols 란에 아래 define 을 추가해야 합니다.

ENABLE_LOG;ENABLE_DEBUG;ENABLE_SAVE_LOG

ENABLE_DEBUG 는 생략할 수 있습니다. ENABLE_DEBUG 를 추가하면 주고 받는 메시지에 대한 로그까지 추가되어 로그 양이 많아집니다.

로그 파일은 기본적으로 앱이 종료되는 시점에 저장됩니다. 로그 양이 일정 크기를 넘어가면 중간중간 저장이 이루어집니다. 파일은 앱의 /Logs 폴더에 날짜와 시간을 포함하는 파일명으로 저장됩니다.

데디케이티드 서버(UE)

이 설명은 추후 추가될 예정입니다.