분산처리 Part 1: ORM, RPC, 로그인

게임 서비스는 대량의 요청을 처리하기 위해서 여러 서버들로 구성됩니다. 따라서 확장 가능한 게임 서비스를 디자인하고 구현하는 일은 게임 서비스가 여러 서버로 구성되어있음에도 하나의 서버로 동작하는 것처럼 보이게끔, 원활한 서버간 통신과 서버간 부하 분산 기능을 필요로 합니다. 일반적으로 이를 효율적으로 구현하기 위해서는 많은 시간과 노력이 필요합니다. 그러나 아이펀 엔진은 간단한 설정만으로 강력한 분산 처리 기능을 제공합니다.

분산 환경에서 ORM 동작

ORM Part 1: Overview 에서는 별도의 DB 처리 없이도, 아이펀 엔진 ORM이 자동으로 DB 작업을 처리한다고 설명했습니다. 그럼 만일 같은 오브젝트를 여러 서버에서 접근하는 경우는 어떨까요? 가령 게임 유저의 친구에게 선물을 한다거나 하는 경우 여러 서버에서 친구의 인벤토리라는 같은 게임 오브젝트를 접근해야되는 경우가 생길겁니다.

DB 를 동기화 포인트로 이용하는 간단한 방법을 생각할 수 있지만 이 방법은 금새 DB 가 병목이 될 수 밖에 없습니다. 그리고 이는 서버당 처리 가능한 동접이 낮아짐을 의미합니다.

따라서 가장 효율적인 방법은 DB 까지 갈 필요없이 게임 서버들 간에 오브젝트에 대한 조율을 끝내는 것입니다. 하지만 이를 위해서는 서버간 RPC, 오브젝트 접근에 따른 서버간 deadlock 방지 등 복잡한 구현이 필요합니다. 아이펀 엔진은 분산처리 기능관련한 설정들 를 통해 분산 처리 기능을 켜는 것만으로 간단하게 이런 기능들을 제공합니다.

그리고 ORM 에 어떤 수정도 하실 필요가 없습니다. 예를 들어, Fetch 함수를 호출하는 경우 대상 오브젝트가 이미 DB 에서 다른 서버의 캐시로 로딩되었다면, ORM 은 해당 서버에게 RPC 메시지를 통해 오브젝트를 빌려오고 반환하는 작업을 자동으로 수행해줍니다.

Important

ORM 사용시 모든 서버들이 동일한 오브젝트 모델 정의를 사용해야 됩니다. 또한 모든 서버가 동일한 DB 서버에 연결되어야 합니다.

또한, 서버들이 같은 서버군 안에 포함되기 위해서 다음과 같이 MANIFEST.json 의 AppInfo 섹션에 같은 app_id 를 사용해야됩니다. 이 app ID 는 클라이언트 app id 가 아니라 서버군을 구분하기 위한 ID 이며, 따라서 서버군 안에서 공유되는 임의의 문자열로 지정하시면 됩니다.

{
  ...
  "AppInfo": {
    "app_id": "my_server_app_id_shared_among_all_the_servers"
  }
  ...
}

분산 서버 관리

태그로 분산 서버 구분

많은 경우에 특정한 목적의 서버군을 구분할 필요가 있습니다. 예를 들어 room-lobby 방식의 게임에서 lobby 기능을 담당하는 서버군과 room 서버군을 구분하는 경우나 특정 서버는 초보 던전들만 돌리게 하는 것과 같은 경우를 생각할 수 있습니다.

iFun Engine 은 이런 경우를 단순화하기 위해서 RPC 서버 단위로 태그(Tag)를 다는 기능을 제공합니다. 태그는 프로그래머가 서버를 구분하기 위해서 편의상 다는 닉네임 같은 것이고, 어떤 태그를 어떤 서버에 달지 그리고 각 태그가 어떤 의미를 갖는지는 전적으로 프로그래머가 결정합니다. 태그를 다는 방법은, 아래 예제 코드처럼 코드 상에서 바로 호출하거나 아래의 분산처리 기능관련한 설정들rpc_tags 에 tag 리스트를 나열할 수도 있습니다.

Tip

Flavor 를 사용하면, 해당 서버의 RPC 태그에는 그 flavor 이름이 자동으로 포함됩니다.

서버는 하나 이상의 태그를 가질 수 있고, 여러 서버가 같은 태그를 가질 수도 있습니다.

아래 예제는 Server1 과 Server2 가 모두 lobby 서버군에 포함되며, Server1 이 이 중 마스터 역할을 담당하는 경우입니다. 이를 위해 두 서버에 공통적으로 “lobby” 라는 태그를 부여하고, Server1 에는 추가로 “master” 라는 태그를 부여합니다.

Server1 의 코드

Rpc::AddTag("lobby");
Rpc::AddTag("master");
Rpc.AddTag ("lobby");
Rpc.AddTag ("master");

Server2 의 코드

Rpc::AddTag("lobby");
Rpc.AddTag ("lobby");

이제 다른 서버에서 lobby 서버 리스트가 필요할 때 이를 검색하면 Server1 과 Server2 가 반환되게 됩니다. 그리고 master 라는 태그를 검색하면 Server1 만 반환됩니다.

Server3 의 코드

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// distribution/rpc.h
// typedef boost::asio::ip::tcp::endpoint PeerEndpoint;
// typedef std::map<PeerId, PeerEndpoint> PeerMap;

Rpc::PeerMap peers;
Rpc::GetPeersWithTag(&peers, "lobby");

Rpc::PeerMap masters;
Rpc::GetPeersWithTag(&masters, "master");

// master 라는 태그가 다른 목적으로도 사용될 수 있어서,
// 명시적으로 lobby 태그의 서버들 중에서 master 를 찾고 싶다면 다음처럼 할 수 있습니다.
for (Rpc::PeerMap::iterator it = peers.begin(); it != peers.end(); ++it) {
  Rpc::Tags tags;
  GetPeerTags(&tags, it->first);
  if (tags.find("master") != tags.end()) {
    // Found.
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Dictionary<Guid, System.Net.IPEndPoint> peers;
Rpc.GetPeersWithTag(out peers, "lobby");

Dictionary<Guid, System.Net.IPEndPoint> masters;
Rpc.GetPeersWithTag(out masters, "master");

// master 라는 태그가 다른 목적으로도 사용될 수 있어서,
// 명시적으로 lobby 태그의 서버들 중에서 master 를 찾고 싶다면 다음처럼 할 수 있습니다.
foreach (var pair in peers)
{
  SortedSet<string> tags;
  Rpc.GetPeerTags(out tags, pair.Key);

  if (tags.Contains ("master"))
  {
    // Found.
  }
}

서버 리스트 추출

Rpc::GetPeers(): 모든 서버 리스트 추출

// distribution/rpc.h
// typedef boost::asio::ip::tcp::endpoint PeerEndpoint;
// typedef std::map<PeerId, PeerEndpoint> PeerMap;

static size_t Rpc::GetPeers(Rpc::PeerMap *ret, bool include_self=false)
public static UInt64 Rpc.GetPeers (out Dictionary<Guid, PeerEndpoint> ret, bool include_self = false)

Rpc::GetPeersWithTag(): 특정 태그의 서버 리스트 추출

// distribution/rpc.h
// typedef boost::asio::ip::tcp::endpoint PeerEndpoint;
// typedef std::map<PeerId, PeerEndpoint> PeerMap;

static size_t GetPeersWithTag(Rpc::PeerMap *ret, const Tag &tag, bool include_self=false)
public static UInt64 Rpc.GetPeersWithTag (out Dictionary<Guid, PeerEndpoint> ret, Rpc.Tag tag, bool include_self = false)

다른 서버의 공인 IP

앞의 서버의 IP 주소 알아내기 에서 로컬 서버의 공인 IP 를 얻어오는 방으로 HardwareInfo::GetExternalIp()HardwareInfo::GetExternalPorts() 을 소개했습니다.

유사하게 다른 서버의 IP 와 포트를 얻어내기 위한 Rpc::GetPeerExternalIp()Rpc::GetPeerExternalPorts() 를 제공합니다.

static boost::asio::ip::address Rpc::GetPeerExternalIp(const Rpc::PeerId &peer)
public static System.Net.IPAddress Rpc.GetPeerExternalIp (Rpc.PeerId peer)
static HardwareInfo::ProtocolPortMap Rpc::GetPeerExternalPorts (const Rpc::PeerId &peer)
public static Dictionary<HardwareInfo.FunapiProtocol, ushort> Rpc.GetPeerExternalPorts (Rpc.PeerId peer)

서버 간 상태/정보 공유

경우에 따라 서버의 상태 정보를 서버들 간에 공유해야되는 경우가 있을 수 있습니다. 예를 들어 게임 서버간 로드 밸런싱은 당연히 각 서버의 동접 숫자를 알아야 됩니다. 유사하게, 게임 서비스를 모니터링 하는 툴 역시 모든 서버의 상태값을 알아야 됩니다.

아이펀 엔진은 서버간에 임의의 상태값을 손쉽게 공유할 수 있는 방법을 제공하고 있습니다. Rpc::SetStatus() 함수로 서버의 상태나 정보를 지정할 수 있습니다.

static void Rpc::SetStatus(const Json &status);
public static void Rpc.SetStatus (JObject status)

Rpc::GetPeerStatus() 로 다른 서버에서 지정한 상태값을 얻어올 수 있습니다.

static Json Rpc::GetPeerStatus(const Rpc::PeerId &peer);
public static JObject Rpc.GetPeerStatus (Rpc.PeerId peer)

Tip

Rpc::SetStatus() 함수를 호출하면 즉시 다른 서버들로 전달됩니다. 따라서 매우 잦게 업데이트되는 정보들(동시접속자, 방의 수 등)은 값이 변할 때 매번 Rpc::SetStatus() 를 호출하는 것 보다는 타이머 를 이용하여 주기적으로 업데이트 하는 것을 권장합니다.

예제: 서버간 방 개수 정보 공유하기

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
int64_t g_match_room_count;

void UpdateServerStatus(const Timer::Id &, const WallClock::Value &) {
  Json status;
  status["room_count"] = g_match_room_count;

  Rpc::SetStatus(status);
}

static bool Start() {
  ...
  Timer::ExpireRepeatedly(WallClock::FromSec(10), UpdateServerStatus);
  ...
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
static UInt64 the_match_room_count = 0;

public static void UpdateServerStatus(UInt64 id, DateTime at)
{
  JObject status = new JObject ();
  status["room_count"] = the_match_room_count;

  Rpc.SetStatus (status);
}

public static bool Start()
{
  ...
  Timer.ExpireRepeatedly (WallClock.FromSec (10), UpdateServerStatus);
  ...
}

예제: PvP 서버 중 방 개수가 가장 적은 서버 선택하기

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Rpc::PeerMap servers;
Rpc::GetPeersWithTag(&servers, "pvp");

Rpc::PeerId target;
int64_t minimum_room_count = std::numeric_limits<int64_t>::max();
for (const auto &pair: servers) {
  const Rpc::PeerId &peer_id = pair.first;

  Json status = Rpc::GetPeerStatus(peer_id);
  if (status.IsNull()) {
    continue;
  }

  if (not status.IsObject() ||
      not status.HasAttribute("room_count", Json::kInteger)) {
    LOG(ERROR) << "wrong server status: " << status.ToString();
    continue;
  }

  if (status["room_count"].GetInteger() < minimum_room_count) {
    minimum_room_count = status["room_count"].GetInteger();
    target = peer_id;
  }
}

// target is the least overloaded.
...
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
Dictionary<Guid, System.Net.IPEndPoint> servers;
Rpc.GetPeersWithTag(out servers, "pvp");

Log.Info ("Check Server Status");
Log.Info ("FindWith Tags = {0}", servers.Count.ToString());

System.Guid target;
UInt64 minimum_room_count = UInt64.MaxValue;
foreach (var pair in servers)
{
  System.Guid peer_id = pair.Key;

  Log.Info ("peer id = {0}", peer_id.ToString());
  JObject status = Rpc.GetPeerStatus (peer_id);
  if (status == null) {
    Log.Info("Status is null");
    continue;
  }

  if (status ["room_count"] == null)
  {
    Log.Error ( "wrong server status: {0}", status.ToString());
    continue;
  }

  if (status ["room_count"].Type != JTokenType.Integer)
  {
    Log.Error ( "wrong server status: {0}", status.ToString());
    continue;
  }

  if ( (UInt64) status ["room_count"] < minimum_room_count) {
    minimum_room_count =  (UInt64) status ["room_count"];
    target = peer_id;
  }
}

// target 변수가 가장 적은 부하를 받고 있으니 이 서버를 이용하도록 합니다.
...

분산 환경에서 클라이언트 관리

클라이언트와 아이펀 세션 연동 / 해제 (로그인 / 로그아웃)

로그인 기능을 통해서 사용자 ID 와 아이펀 세션을 연결해 줌으로써 상호 참조 가능한 상태로 만들 수 있습니다.

아이펀 세션 생성 시 사용자 ID 와 연동하기 (로그인)

// 연동할 사용자 ID
string id = "target_id";
if (not AccountManager::CheckAndSetLoggedIn(id, session)) {
  LOG(WARNING) << id << " is already logged in";
  return;
}
// 연동할 사용자 ID
string id = "target_id";
if (!AccountManager.CheckAndSetLoggedIn (id, session))
{
  Log.Warning ("{0} is already logged in", id);
  return;
}

Session 종료 시 account ID 와 연동 해제하기 (로그아웃)

AccountManager::SetLoggedOut(session);

// account id 로도 해제할 수 있습니다.
// string id = "target_id";
// AccountManager::SetLoggedOut(id);
AccountManager.SetLoggedOut (session);

// account id 로도 해제할 수 있습니다.
// string id = "target_id";
// AccountManager.SetLoggedOut (id);

Note

AccountManager::CheckAndSetLoggedInAsync()AccountManager::SetLoggedOutAsync() 를 이용하여 비동기로 처리할 수 있습니다. AccountManager::CheckAndSetLoggedInAsync() 에 max_retry 인자를 설정하여 로그인을 실패한 경우 지정한 횟수만큼 자동으로 재시도 하도록 처리할 수 있습니다. 로그인 실패 시 해당 account id 로그아웃 후 다시 로그인을 시도합니다. 이 때 로그아웃이 성공하면 로그아웃 콜백을 호출합니다. 이 후 로그인이 성공하거나 최대 재시도 횟수를 넘어가면 로그인 콜백을 호출합니다. 함수들의 자세한 내용은 API 문서 를 참고하세요.

Important

위 함수들은 원하지 않는 롤백 감지 에 설명된 대로 ASSERT_NO_ROLLBACK 으로 태깅되어있습니다. 이 때문에 이 함수들은 롤백이 발생할 수 있는 상황에서 사용되면 assertion 을 발생시킵니다.

클라이언트가 접속한 서버 찾기

앞서 설명한 **로그인 처리**를 통해서 사용자 ID 와 아이펀 세션을 정상적으로 연결했다면, 다음과 같이 세션을 찾아낼 수 있습니다.

Account ID 로 찾기

// 검색할 사용자 ID
string id = "target_id";
Rpc::PeerId peer_id = AccountManager::Locate(id);

if (not peer_id.is_nil()) {
  LOG(INFO) << id << " is connected to " << peer_id;
}
// 검색할 사용자 ID
string id = "target_id";
System.Guid peer_id = AccountManager.Locate (id);
if (peer_id != Guid.Empty)
{
  Log.Info("{0} is connected to {1}", id, peer_id.ToString ());
}

다른 서버의 클라이언트에게 패킷 보내기

`AccountManager::CheckAndSetLoggedIn()`_ 을 통해 세션과 연결된 account ID 에게 패킷을 보낼 수 있습니다. 다른 서버가 아니고 자기자신이어도 무방합니다.

아래 예제에서 어딘가에서 AccountManager::CheckAndSetLoggedIn(“target_account_id”) 가 실행되었다고 가정하겠습니다.

1
2
3
4
5
Json msg;
msg["message"] = "hello!";
msg["from"] = "my_id";

AccountManager::SendMessage("chat", msg, "target_account_id");
1
2
3
4
5
JObject msg = new JObject ();
msg ["message"] = "hello!";
msg ["from"] = "my_id";

AccountManager.SendMessage ("chat", msg, "target_account_id");

Important

`AccountManager::CheckAndSetLoggedIn()`_ 을 통해 세션에 어카운트가 지정된 경우에만 가능합니다.

Important

AccountManager::SendMessage()원하지 않는 롤백 감지 에 설명된 대로 ASSERT_NO_ROLLBACK 으로 태깅되어있습니다. 이 때문에 이 함수는 롤백이 발생할 수 있는 상황에서 사용되면 assertion 을 발생시킵니다.

Tip

(고급) RPC 를 이용한 서버간 통신 에 설명된 기능을 이용하면, 다른 서버에서 플레이하고 있는 유저에게 패킷을 보내는 것을 직접 구현할 수도 있습니다.

모든 클라이언트에게 패킷 보내기

로그인 여부 상관없이 서버의 모든 세션에 패킷 보내기

로그인 여부와 상관없이 특정 서버집단에 연결된 모든 세션에 메시지를 전송하기 위해서는 Session::BroadcastGlobally() 함수를 사용합니다. 함수 인자중에 TransportProtocolkTcpkUdp 타입만 사용할 수 있습니다.

아래 예제에서는 모든 서버에 붙어있는 세션들에 패킷을 보냅니다. 만일 모든 서버가 아니라 game 이라는 태그를 갖는 서버들에 붙어있는 모든 세션에 패킷을 보내기 위해서는 7번째 줄을 Rpc::GetPeersWithTag(&peers, "game", true); 로 바꿔주면 됩니다.

1
2
3
4
5
6
7
8
9
void BroadcastToAllSessions() {
  Json msg;
  msg["message"] = "hello!";

  Rpc::PeerMap peers;
  Rpc::GetPeers(&peers, true);

  Session::BroadcastGlobally("world", msg, peers, kDefaultEncryption, kTcp);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public static void BroadcastToAllSessions()
{
  JObject msg = new JObject ();
  msg ["message"] = "hello";

  Dictionary<Guid, System.Net.IPEndPoint> peers;
  Rpc.GetPeers(out peers, true);

  Session.BroadcastGlobally ("world",
                             msg,
                             peers,
                             Session.Encryption.kDefault,
                             Session.Transport.kTcp);
}

Tip

만약 로컬 서버에 접속한 모든 세션에 메시지를 보내고 싶은 경우에는 서버에 연결된 모든 세션에게 메시지 보내기 에 설명된 Session::BroadcastLocally() 를 참고하세요.

Important

Session::BroadcastGlobally()Session::BroadcastLocally()원하지 않는 롤백 감지 에 설명된 대로 ASSERT_NO_ROLLBACK 으로 태깅되어있습니다. 이 때문에 이 두 함수는 롤백이 발생할 수 있는 상황에서 사용되면 assertion 을 발생시킵니다.

서버의 로그인한 모든 클라이언트에 패킷 보내기

아이펀 엔진에서 사용자가 로그인되어있다는 것은 `AccountManager::CheckAndSetLoggedIn()`_ 가 호출되고 아직 `AccountManager::SetLoggedOut()`_ 이 불리지 않은 것을 의미합니다.

로그인한 모든 클라이언트에 패킷을 보내는 것은 AccountManager::BroadcastLocally()AccountManager::BroadcastGlobally() 를 이용합니다. 전자는 현재 서버에 접속되어있는 클라이언트들에게만 패킷을 보내고 후자는 여러 서버에 연결된 모든 클라이언트들에게 패킷을 보냅니다.

함수 인자중에 TransportProtocolkTcpkUdp 타입만 사용할 수 있습니다.

예제: 로컬 서버에 로그인한 모든 클라이언트에 패킷 보내기

1
2
3
4
5
6
void BroadcastToAllLocalClients() {
  Json msg;
  msg["message"] = "hello!";

  AccountManager::BroadcastLocally("world", msg, kDefaultEncryption, kTcp);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public void BroadcastToAllLocalClients()
{
  JObject msg = new JObject();
  msg["message"] = "hello";

  AccountManager.BroadcastLocally("world",
                                  msg,
                                  Session.Encryption.kDefault,
                                  Session.Transport.kTcp);
}

예제: 모든 서버의 모든 클라이언트에 패킷 보내기

1
2
3
4
5
6
7
8
9
void BroadcastToAllClients() {
  Json msg;
  msg["message"] = "hello!";

  Rpc::PeerMap peers;
  Rpc::GetPeers(&peers, true);

  AccountManager::BroadcastGlobally("world", msg, peers, kDefaultEncryption, kTcp);
}

Important

위 예제에서 만일 “game” 이라는 태그를 갖는 서버에게만 패킷을 보내고 싶으면 6번째 줄을 Rpc::GetPeersWithTag(&peers, "game", true); 로 바꾸면 됩니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public void BroadcastToAllClients()
{
  JObject msg = new JObject();
  msg["message"] = "hello";

  Dictionary<Guid, System.Net.IPEndPoint> peers;
  Rpc.GetPeers(out peers, true);

  AccountManager.BroadcastGlobally("world",
                                   msg,
                                   peers,
                                   Session.Encryption.kDefault,
                                   Session.Transport.kTcp);
}

Important

위 예제에서 만일 “game” 이라는 태그를 갖는 서버에게만 패킷을 보내고 싶으면 7번째 줄을 Rpc.GetPeersWithTag(out peers, "game", true); 로 바꾸면 됩니다.

Important

AccountManager::BroadcastGlobally()AccountManager::BroadcastLocally()원하지 않는 롤백 감지 에 설명된 대로 ASSERT_NO_ROLLBACK 으로 태깅되어있습니다. 이 때문에 이 두 함수는 롤백이 발생할 수 있는 상황에서 사용되면 assertion 을 발생시킵니다.

클라이언트를 다른 서버로 옮기기

MANIFEST.json 설정

MANIFEST.json 의 AccountManager 항목에 redirection_secret 값을 지정합니다. 32 bytes 의 랜덤 16진수 문자열을 hex 문자열로 변환해서 입력하시면 됩니다. (64 글자)

{
  "AccountManager": {
    // hex 형식으로 표현한 비밀 값
    "redirection_secret": "a29fd424579997bf91e3..."
  }
}

다음 명령을 이용하면 쉽게 생성할 수 있습니다.

$ python -c "import os; print ''.join('%02x' % ord(c) for c in os.urandom(32))"

해당 값은 클라이언트 이동을 위한 token 을 생성할 때 랜덤 시드로 이용됩니다.

Important

redirection_secret 값은 모든 서버가 같은 값을 사용하게 설정되어야 합니다. 이 값이 외부에 노출되지 않게 안전하게 보관하세요. 필요시 MANIFEST.json 내용 암호화 을 참고하세요.

클라이언트를 다른 서버로 이동하기

매치메이킹 후 게임 서버로 이동하는 것처럼, 클라이언트가 한 서버에서 다른 서버로 옮겨가야할 경우에 AccountManager::RedirectClient() 를 함수를 통하여 클라이언트를 다른 서버로 이동 시킬 수 있습니다.

1
2
3
4
5
6
7
class AccountManager : private boost::noncopyable {
  ...
  static bool RedirectClient(
      const Ptr<Session> &session, const Rpc::PeerId &peer_id,
      const string &extra_data) ASSERT_NO_ROLLBACK;
  ...
};
1
2
3
4
5
6
class AccountManager {
  ...
  public static bool RedirectClient(
      Session session, System.Guid peer_id, string extra_data);
  ...
}

새로 연결하는 서버로 세션의 정보를 전달하려면 extra_data 필드를 통해 전달 할 수 있습니다.

AccountManager::RedirectClient() 함수 사용시에 아래 내용을 유의해주세요.

Warning

정상적으로 클라이언트를 다른 서버로 이동하기 위해서는 옮겨갈 클라이언트가 `AccountManager::CheckAndSetLoggedIn()`_ 함수를 이용해서 로그인한 상태여야 합니다.

Warning

extra_data 값은 클라이언트를 통해서 전달되기 때문에, 클라이언트를 통해 공유해서 안되는 정보는 Rpc를 통해서 서버간에 직접 전송해야 합니다.

Warning

AccountManager::RedirectClient() 함수 호출 후 로그아웃, 로그인 과정은 엔진 내부에서 처리하고 있으므로 별도로 추가적인 로그인, 로그아웃 관련 작업은 필요하지 않습니다.

Important

게임 서버가 TCP (권장) 혹은 UDP를 사용하게끔 설정되어야 합니다. HTTP 는 요청-응답 형태의 프로토콜이므로 클라이언트가 요청하지 않은 패킷을 서버가 먼저 보낼 수 없어서 지원되지 않습니다.

예제: 특정 서버로 클라이언트를 이동 시키기

1
2
3
4
5
6
7
Rpc::PeerId destination_server = ...  // Selected from the result of Rpc::GetPeers().

std::string extra_data = "";

if (not AccountManager::RedirectClient(session, destination_server, extra_data)) {
  return;
}
1
2
3
4
5
6
7
8
System.Guid destination_server = ... // Selected from the result of Rpc.GetPeers().

string extra_data = "";

if (!AccountManager.RedirectClient (session, destination_server, extra_data))
{
  return;
}

이동 메시지 처리 과정

RedirectClient 함수가 호출 되고 서버 이동이 진행되는 과정은 아래와 같습니다.

우선, 기존 서버에서 유저를 로그아웃 (`AccountManager::SetLoggedOut()`_ 함수에 해당) 시킵니다. 로그아웃이 정상적으로 성공한다면 클라이언트 측으로 이동할 서버의 정보 및 랜덤 인증 토큰이 담긴 메시지(_sc_redirect) 를 전송 한 뒤 세션을 종료합니다. 이동 메시지 처리 과정 중 로그아웃, 로그인 과정은 엔진 내부에서 처리하고 있으므로 별도로 추가적인 로그인, 로그아웃 관련 작업은 필요하지 않습니다.

클라이언트 측은 이동 메시지를 받고 난 뒤 기존 서버와의 연결을 해제하고 이동 메시지에 포함된 새 서버의 정보를 통해 연결을 시도하고 기존 서버로부터 받은 랜덤 인증 토큰을 이용해서 새 서버에서 인증을 시도해야합니다.

클라이언트가 정상적으로 이동했다면, 이동할 서버는 랜덤 인증 토큰을 이용하여 클라이언트를 검증합니다. 검증이 정상적으로 끝났다면 서버는 다시 클라이언트를 로그인 시킵니다.

Note

서버로부터 받은 이동 메시지는 아이펀 엔진이 제공하는 플러그인에서 자동으로 처리하기 때문에 클라이언트에서 수작업으로 처리하실 필요는 없습니다.

참고로 플러그인은 다음과 같은 작업을 처리합니다.

  1. 기존 서버와의 연결을 해제

  2. 새 서버와 연결

  3. 기존 서버로부터 받은 랜덤 인증 토큰을 이용해서 새 서버에서 인증 시도

클라이언트 플러그인은 2단계를 처리하는 동안에 호출될 콜백을 지원합니다. 예를 들어, 암호화 타입 지정하기, 넘겨 받은 flavor 정보에 따라 추가적인 설정하기 등의 작업을 할 수 있습니다.

자세한 내용은 클라이언트 플러그인 설명 중 서버간 이동 를 참고하세요.

새 서버에서 옮겨온 클라이언트에 대한 처리

클라이언트는 새 서버에 접속 후, 이전 서버가 보내준 랜덤 토큰으로 인증 과정을 거칩니다. 이 인증 과정은 아이펀 엔진이 자체적으로 수행하지만, 그 결과에 따른 후속 처리는 게임 서버에서 직접해야됩니다.

인증 결과를 받기 위해서는 다음처럼 콜백함수를 설정해야됩니다.

1
2
3
4
5
bool MyProject::Start() {
  ...
  AccountManager::RegisterRedirectionHandler(OnClientRedirected);
  ...
}
1
2
3
4
5
6
public static bool Start ()
{
  ...
  AccountManager.RegisterRedirectionHandler (OnClientRedirected);
  ...
}

이제 클라이언트가 이동해서 들어오는 경우 아이펀 엔진은 앞에서 등록된 콜백함수를 호출해줍니다. 이 때 원래 서버에서 AccountManager::RedirectClient() 에 인자로 넘긴 extra_data 를 클라이언트로부터 받아서 같이 넘겨줍니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void OnClientRedirected(const std::string &account_id,
                        const Ptr<Session> &session,
                        bool success,
                        const std::string &extra_data) {
  if (success) {
    // Authentication succeeded.
    ...
  } else {
    // Authenticated failed.
    ...
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public static void OnClientRedirected (string account_id,
                                       Session session,
                                       bool success,
                                       string extra_data)
{
  if (success)
  {
    // Authentication succeeded.
    ...
  }
  else
  {
    // Authenticated failed.
    ...
  }
}

Zookeeper 설정 및 관리

아이펀 엔진은 분산 시스템 구현을 위해 Zookeeper 를 이용합니다.

이 섹션에서는 Zookeeper 사용에 대한 개략적인 내용을 설명합니다. 보다 자세한 내용은 Zookeeper 공식 사이트 를 참고하시기 바랍니다.

Important

CentOS 8 과 윈도우즈에서는 분산 처리를 위해서 Zookeeper 를 사용하지 않습니다. Rpc Service 의 MANIFEST 설정rpc_backendRedis 로 지정해서 쓰셔야 됩니다.

Zookeeper 설치하기

Zookeeper 는 아래의 명령어로 설치할 수 있습니다.

Tip

최신 버전을 설치하기 위해서는 Zookeeper 공식 사이트 에서 다운로드하여 설치하세요.

Ubuntu:

$ sudo apt-get update
$ sudo apt-get install zookeeper zookeeperd
$ sudo service zookeeper start

CentOS 7:

$ sudo yum install zookeeper
$ sudo systemctl enable zookeeper
$ sudo systemctl start zookeeper

Command-line 툴 사용하기

아이펀 엔진이 생성한 Zookeeper 데이터를 보려면 zkCli.sh 를 이용합니다. 아래의 명령으로 cli 를 이용하여 접속할 수 있으며 ?(물음표) 명령어로 사용 가능한 명령어를 볼 수 있습니다.

$ cd /usr/share/zookeeper/bin/
$ ./zkCli.sh
...
[zk: localhost:2181(CONNECTED) 1]

아이펀 엔진이 만드는 Zookeeper 디렉토리

아이펀 엔진은 Zookeeper 에 다음과 같은 directory 를 생성합니다. 다른 디렉토리는 임의 생성/수정/삭제하면 안됩니다.

  • /{{ProjectName}}/servers

  • /{{ProjectName}}/keys

  • /{{ProjectName}}/objects

  • /{{ProjectName}}/active_accounts

Zookeeper 처리 프로파일링

아이펀 엔진에서는 오브젝트 공유를 위해 이용하는 Zookeeper 관련 처리시간 통계를 측정하여 제공합니다. 이 기능을 사용하려면 다음의 것들이 활성되어 있어야 합니다.

통계를 보기 위해서는 다음과 같이 제공되는 API 를 호출합니다.

GET http://{server ip}:{api-service-port}/v1/counters/funapi/distribution_profiling/

통계 결과는 Zookeeper 명령 처리에 걸린 시간이며 종류와 의미는 다음과 같습니다.

통계 종류

설명

all_time

누적된 통계

last1min

1분전 통계

execution_count

해당 명령이 처리된 횟수

execution_time_mean_in_sec

평균 처리 시간

execution_time_stdev_in_sec

처리 시간 표준편차

execution_time_max_in_sec

최대 실행 시간

통계 결과 예시

{
    "zookeeper": {
        "nodes": "localhost:2181",
        "client_count": 10,
        "all_time": {
            "execution_count": 105213,
            "execution_time_mean_in_sec": 0.00748,
            "execution_time_stdev_in_sec": 0.026617,
            "execution_time_max_in_sec": 0.249311
        },
        "last1min": {
            "execution_count": 0,
            "execution_time_mean_in_sec": 0.0,
            "execution_time_stdev_in_sec": 0.0,
            "execution_time_max_in_sec": 0.0
        }
    }
}

Zookeeper 상태 확인하기

1) Zookeeper 서버로부터 통계 얻어오기:

$ echo stat | nc localhost 2181

Zookeeper version: 3.4.5--1, built on 06/10/2013 17:26 GMT
Clients:
 /0:0:0:0:0:0:0:1:38670[0](queued=0,recved=1,sent=0)
 /0:0:0:0:0:0:0:1:38457[1](queued=0,recved=9469,sent=9469)

Latency min/avg/max: 0/31/334
Received: 1177235
Sent: 1417245
Connections: 2
Outstanding: 0
Zxid: 0x80eb3a9
Mode: standalone
Node count: 10

2) Zookeeper 통계 초기화하기:

$ echo srst | nc localhost 2181

Server stats reset.

3) Zookeeper 상태 확인하기:

imok 는 “I’m OK” 의 의미로 정상동작을 의미합니다.

$ echo ruok | nc localhost 2181

imok

Zookeeper 설정 가이드라인

다음 권장 사항을 참고해서 실 서비스에 적용할 Zookeeper 클러스터를 구성하시기 바랍니다.

권장 노드 수

Zookeeper 클러스터의 최소 서버 수는 3대이며, 홀수 대수로 설치합니다. 대수를 늘리면 성능이 증가하는 것이 아니라 장애 처리 능력이 올라갑니다. 대신 내부 동기화 때문에 성능은 떨어집니다.

권장 기계 사양

CPU 4코어 / 메모리는 “예상되는 동접수 x 유저 1명당 평균 데이터 사이즈” 의 두 배 이상 (게임 마다 유저 1명당 평균 데이터 사이즈와 예상되는 동접 수가 다르기 때문에 저희가 정확한 값을 드리기 곤란합니다.)

Note

Zookeeper 성능은 게임 서버에서 생성하는 ORM 오브젝트 수가 많을 수록 중요해 집니다. 시간당 생성되는 오브젝트가 많을 수록 고성능의 기계로 적은 수의 zookeeper 노드만을 유지하는 편이 유리합니다.

각 노드 별 권장 디스크 구성

  • 물리적으로 독립된 디스크 2개가 필요합니다.

  • 디스크는 각각 Zookeeper data 와 Zookeeper transaction log 용으로 쓰게 됩니다. 각각 Zookeeer 설정에서 dataDirdataLogDir 에 대응됩니다.

  • OS 가 깔려 있는 디스크를 같이 써야된다면, Zookeeper transaction log (즉 dataLogDir) 이 더 많은 디스크 I/O 를 만들기 때문에, Zookeeper data (즉 dataDir) 와 OS 가 같은 디스크를 쓰게 하는 것이 좋습니다.

  • 만일 SSD 가 있다면, transaction log (즉, dataLogDir) 용으로 쓰는 것이 좋습니다.

JVM 설정

JVM Heap 은 시스템 메모리보다 일정 수준 작게 설정되어야 합니다. 그렇지 않을 경우 메모리 swap 이 발생하는데, 이렇게 되면 전체 성능이 급격히 떨어지게 됩니다. 힙 사이즈를 포함한 JVM 설정은 Ubuntu 의 경우 /etc/default/zookeeper, CentOS의 경우 /etc/zookeeper/java.env 파일에서 설정할 수 있습니다.

권장 Zookeeper 옵션

Note

자세한 설명은 Zookeeper Configuration 을 참고하세요.

globalOutstandingLimit

Zookeeper 서버의 큐 길이를 일정 수로 제한합니다. 기본값은 1000 개 입니다. Zookeeper 서버에서 처리량보다 유입량이 더 많은 경우 에는 큐 길이 제한에 따라 Zookeeper 서버로 보내는 요청 자체가 지연될 수 있습니다. Zookeeper 프로파일링 의 결과를 확인 하고 처리가 지연되고 있다면 이 값을 증가시키는게 좋습니다. 다만 많이 증가시킬 경우 큐잉되는 요청 수를 늘리는 것이기 때문에 더 많은 메모리를 쓰게 되고, 이는

Zookeeper 서버에서 Out of memory 문제를 발생시킬 수도 있으니 주의하시기 바랍니다. /etc/zookeeper/conf/zoo.cfg 파일에 다음과 같이 입력하시면 됩니다.

globalOutstandingLimit=1000
forceSync

Zookeeper 가 fsync() 할지 여부를 지정합니다. 해당 값을 no 로 지정하면 ZooKeeper 처리 속도가 빨라집니다. 해당 옵션이 no 인 경우 Zookeeper 서비스가 돌고있는 서버가 크래시할 때 디스크에 쓰지 못하는 경우가 생길 수 있으나, 아이펀엔진에서는 영속적인 데이터를 쓰지 않기 때문에 문제가 생기지 않습니다.

/etc/zookeeper/conf/zoo.cfg 파일에 다음과 같이 입력하시면 됩니다.

forceSync=no
-Xmx{heap-size}m

Zookeeper 서버의 최대 힙 사이즈를 적절한 값으로 설정해야 합니다. 그렇지 않으면 디스크에 swap 파일을 쓰게 되는데 이는 성능에 영향을 줄 수 있습니다. /etc/zookeeper/conf/environment 파일에 JAVA_OPTS 라는 Java 옵션을 입력할 수 있는 변수가 있는데 다음과 같이 적절한 최대 힙 사이즈를 MB 단위로 입력합니다.

# MB 단위이며 여기서는 6GB 를 입력했습니다.
JAVA_OPTS="-Xmx6000m"

만약 물리 메모리 사이즈가 4GB 라면 3GB 정도로 입력하면 됩니다. 단, 물리 메모리 사이즈보다는 작게 입력해야 합니다. 이 값을 결정함에 있어서 가장 좋은 방법은 부하 테스트를 통해 메모리 사용량을 측정하고 이를 토대로 값을 설정하는 것입니다.

autopurge.snapRetainCount

지정된 개수만큼의 가장 최근 스냅샷 파일은 제외하고 나머지 스냅샷 파일들을 제거합니다. autopurge.purgeInterval 에 설정한 시간마다 수행됩니다.

이 옵션을 적용하지 않으면 모든 스냅샷 파일이 남아 있게 되어 디스크 용량 부족 문제가 발생할 수 있습니다. 이렇게 되면 스냅샷 파일을 제거하는 별도의 메인터넌스 작업을 고려하셔야 합니다.

기본값은 3 개이며 최소값은 3 개 입니다. /etc/zookeeper/conf/zoo.cfg 파일에 다음과 같이 입력하시면 됩니다.

autopurge.snapRetainCount=3
autopurge.purgeInterval

지정된 시간마다 자동으로 스냅샷 파일을 제거하는 Zookeeper task 가 동작합니다. 기본값은 0 이며 이 경우에는 동작하지 않습니다. 단위는 시간입니다. autopurge.snapRetainCount 만큼의 가장 최근 스냅샷 파일은 제외하고 나머지 파일을 제거합니다.

/etc/zookeeper/conf/zoo.cfg 파일에 다음과 같이 입력하시면 됩니다.

# 여기서는 3개를 남기고 모두 지우도록 입력했습니다.
autopurge.snapRetainCount=3

# 1 시간 마다 동작합니다.
autopurge.purgeInterval=1

(고급) RPC 를 이용한 서버간 통신

아이펀 엔진은 서버간 통신을 위해서 RPC 기능을 지원합니다. 먼저 사용할 RPC 메시지를 Protobuf 로 정의하고, 해당 RPC 메시지를 받았을 때 호출될 핸들러 함수를 등록하면 됩니다.

RPC 메시지 정의하기

서버간 통신은 Protobuf 으로 이루어집니다. 프로젝트를 생성하면 src 디렉터리에 {{ProejctName}}_rpc_messages.proto 파일이 함께 생성됩니다. 여러분이 만들 RPC 메시지는 FunRpcMessage 를 extend 하는 형태로 정의되어야 합니다.

Note

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

Important

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

아래는 문자열을 보내는 MyRpcMessageEchoRpcMessage 를 정의했습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
message MyRpcMessage {
  optional string message = 1;
}

message EchoRpcMessage {
  optional MyRpcMessage request = 1;
  optional MyRpcMessage reply = 2;
}

extend FunRpcMessage {
  optional MyRpcMessage my_rpc = 32;
  optional EchoRpcMessage echo_rpc = 33;
}

메시지 핸들러 작성하기

Message 를 받아 처리할 handler 함수를 정의합니다. Handler 는 RPC 응답을 명시적으로 보낼 필요가 있는지 없는지에 따라 두 가지 형태가 될 수 있습니다.

명시적인 응답을 보내지 않는 message 에 대한 handler

RPC 메시지를 받고 응답을 보낼 필요가 없는 경우 아래와 같은 형태로 핸들러를 만들 수 있습니다.

void OnMyRpc(const Rpc::PeerId &sender, const Rpc::Xid &xid,
             const Ptr<const FunRpcMessage> &request) {
  BOOST_ASSERT(request->HasExtension(my_rpc));
  const MyRpcMessage &msg = request->GetExtension(my_rpc);

  LOG(INFO) << msg.message() << " from " << sender;
}
public static void OnMyRpcHandler(Guid sender, Guid xid, FunRpcMessage request) {
  MyRpcMessage msg = null;

  if (!request.TryGetExtension_my_rpc (out msg))
  {
    return;
  }
  Log.Info ("{0} from {1}", msg.message, sender);
}

Note

응답을 명시적으로 보내지 않는 형태로 작성하게 되면 아이펀 엔진이 내부적으로 dummy 응답을 보내게 됩니다.

명시적으로 응답을 보내야 되는 message 에 대한 handler

응답을 보내야되는 핸들러는 마지막 인자로 Rpc::ReadyBack 형태의 finisher 를 넘겨 받습니다. 핸들러 안에서 처리를 다 끝내고, RPC 응답과 함께 이 finisher 를 반드시 호출해줘야 됩니다. 그렇지 않으면 RPC 응답을 계속 기다리는 상태가 됩니다.

Handler for “echo”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
void OnEchoRpc(const Rpc::PeerId &sender, const Rpc::Xid &xid,
               const Ptr<const FunRpcMessage> &request,
               const Rpc::ReadyBack &finisher) {
  BOOST_ASSERT(request->HasExtension(echo_rpc));
  const EchoRpcMessage &echo = request->GetExtension(echo_rpc);
  const MyRpcMessage &echo_req = echo.request();

  LOG(INFO) << echo_req.message() << " from " << sender;

  Ptr<FunRpcMessage> reply(new FunRpcMessage);
  reply->set_type("echoreply");
  EchoRpcMessage *echo2 = reply->MutableExtension(echo_rpc);
  MyRpcMessage *echo_reply = echo2->mutable_reply();
  echo_reply->set_message(echo_req.message());

  finisher(reply);
}

Handler for “echoreply”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void OnEchoReplyRpc(const Rpc::PeerId &sender, const Rpc::Xid &xid,
                    const Ptr<const FunRpcMessage> &reply) {
  if (not reply) {
    LOG(ERROR) << "rpc call failed";
    return;
  }

  const EchoRpcMessage &echo = reply->GetExtension(echo_rpc);
  const MyRpcMessage &echo_reply = echo.reply();
  LOG(INFO) << "reply " << echo_reply.message() << " from " << sender;
}

Handler for “echo”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void OnEchoRpcHandler(Guid sender, Guid xid, FunRpcMessage request, Rpc.ReadyBack finisher)
{
  Log.Info ("OnEchoRpcHandler");

  EchoRpcMessage echo_req = null;

  if (!request.TryGetExtension_echo_rpc (out echo_req))
  {
    return;
  }

  Log.Info ("{0} from {1}", echo_req.request.message, sender);

  FunRpcMessage reply = new FunRpcMessage();
  reply.type = "echoreply";
  EchoRpcMessage echo2 = new EchoRpcMessage();
  echo2.reply = new MyRpcMessage();
  echo2.reply.message = echo_req.request.message;
  reply.AppendExtension_echo_rpc(echo2);

  finisher (reply);
}

Handler for “echoreply”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public static void OnEchoReplyRpc(Guid sender, Guid xid, FunRpcMessage reply)
{
  if (reply == null)
  {
    Log.Error ("rpc call failed");
    return;
  }

  EchoRpcMessage echo = null;

  if (!reply.TryGetExtension_echo_rpc(out echo))
  {
    return;
  }

  Log.Info ("{0} from {1}", echo.reply.message, sender);
}

Note

RPC 요청이 만들어질 때 배정되는 트랜잭션 ID (XID) 를 이용해서 RPC 요청의 응답을 판단하기 때문에, RPC 요청과는 달리 응답의 경우 타입 문자열이 그다지 중요하지 않습니다. 공백이 아닌 임의의 문자열을 넣으시면 됩니다. 요청과 응답에 사용되는 XID 는 아이펀 엔진에 의해 자동으로 세팅됩니다.

메시지 핸들러 등록하기

끝으로 RPC 타입에 따라 핸들러를 매핑하고 등록합니다. 이는 서버의 Install() 함수에서 아래와 같이 코드를 추가합니다.

응답이 없는 handler 의 경우

Rpc::RegisterVoidReplyHandler("my", OnMyRpc);
Rpc.RegisterVoidReplyHandler ("my", OnMyRpc);

응답이 있는 handler 의 경우

Rpc::RegisterHandler("echo", OnEchoRpc);
Rpc.RegisterHandler ("echo", OnEchoRpc);

메시지 받을 서버 ID 알아내기

서버 리스트 추출 이나 클라이언트와 아이펀 세션 연동 / 해제 (로그인 / 로그아웃) 에 설명된 방법을 이용하여 RPC 메시지를 받을 서버의 ID 를 알아냅니다.

메시지 전송하기

위에 설명된 방법으로 보낼 서버의 PeerId 를 알아냈다면 아래와 같이 message 를 보낼 수 있습니다.

응답을 받지 않는 메시지의 경우

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Rpc::PeerMap peers;
Rpc::GetPeers(&peers);
Rpc::PeerId target = peers.begin()->first;

Ptr<FunRpcMessage> request(new FunRpcMessage);
// type 은 RegisterHandler 에 등록된 type 과 같아야합니다.
request->set_type("my");
MyRpcMessage *msg = request->MutableExtension(my_rpc);
msg->set_message("hello!");
Rpc::Call(target, request);
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Dictionary<Guid, System.Net.IPEndPoint> peers;
Rpc.GetPeers (out peers);

Guid key = peers.First ().Key;

FunRpcMessage request = new FunRpcMessage ();
request.type = "my";
MyRpcMessage echomsg = new MyRpcMessage ();
echomsg.message = "hello";

request.AppendExtension_echo_rpc (echomsg);
Rpc.Call (key, request);

응답을 받는 메시지의 경우

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Rpc::PeerMap peers;
Rpc::GetPeers(&peers);
Rpc::PeerId target = peers.begin()->first;

Ptr<FunRpcMessage> request(new FunRpcMessage);
request->set_type("echo");
EchoRpcMessage *echo = request->MutableExtension(echo_rpc);
MyRpcMessage *echo_request = echo->mutable_request();
echo_request->set_message("hello!");
Rpc::Call(target, request, OnEchoReplyRpc);
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Dictionary<Guid, System.Net.IPEndPoint> peers;
Rpc.GetPeers (out peers);

Guid key = peers.First ().Key;

FunRpcMessage reply_request = new FunRpcMessage ();
reply_request.type = "echo";
EchoRpcMessage reply_msg = new EchoRpcMessage ();
reply_msg.reply.message = "hello";

reply_request.AppendExtension_echo_rpc (reply_msg);
Rpc.Call (key, reply_request, OnEchoReplyRpc);

Important

Rpc::Call()원하지 않는 롤백 감지 에서 설명한 대로 ASSERT_NO_ROLLBACK 으로 태깅되어있습니다. 이 때문에 이 함수는 롤백이 발생할 수 있는 상황에서 사용되면 assertion 을 발생시킵니다.

분산처리 기능관련한 설정들

Important

하나의 분산 기능 그룹으로 묶여서 통신하는 서버들은 RPC 백엔드 호스트 주소를 포함하여 동일한 설정을 가지고 있어야 합니다.

Important

하나의 기계에 2 개 이상의 서버를 실행할 때는 MANIFEST.json 의 SessionService, RpcService, ApiService 설정에서 포트가 겹치지 않도록 해야합니다.