18. 分布式处理Part 2: 跨服共享数据

iFun引擎通过Redis提供了可以轻松在服务器之间共享JSON数据的 CrossServerStorage 组件。

使用该组件可轻松进行如下应用。

  1. 通过颁发认证密钥来确认移动服务器时是否为已认证的客户端

  2. 共享各游戏地区及各服务器流入的玩家数、交易量等信息

  3. 共享与游戏有关的其他信息

本章运用示例介绍了 通过颁发认证密钥来确认移动服务器时是否为已认证的客户端 的方法。

Note

示例中未说明本组件的所有接口。 具体内容请参考 /usr/include/funapi/service/cross_server_storage.h 文件或 CrossServerStorage API文件

18.1. 示例-跨服移动时共享客户端认证信息

假设如下脚本。

  1. 客户端登录游戏服务器

  2. 请求参与对战

  3. 游戏服务器运用``CrossServerStorage``颁发key,保存对战时所需的 用户信息后,将Key传输给客户端

  4. 访问客户端对战服务器,传输从游戏服务器获得的Key

  5. 对战服务器通过该Key获取对战时所需的用户信息,进行对战处理

  6. (optional) 在对战服务器中删除key

即,游戏服务器颁发名为key的认证密钥,通过它来验证客户端是否为已验证对战服务器的 客户端,然后让其参与对战。

Note

本示例中仅提出了服务器端的代码,未对客户端进行处理。

Important

为便于说明,只处理同步函数。在实际Service中,为了确保性能最好使用非同步函数。

Important

对于示例中未处理的 CrossServerStorage::Write()CrossServerStorage::WriteSync() 函数,当不同的服务器通过相同的key写入时,不会保证处理顺序。因此先写入的数据会被覆盖,敬请注意。

18.1.1. 准备项目

18.1.1.1. 通过Flvaor注册游戏、对战服务器

CMakeLists.txt 文件中按如下所示添加 APP_FLAVORS 项。

set(APP_FLAVORS game pvp)

Note

关于Flavor请参考 Flavor: 按不同作用来区分服务器

在当前状态下进行构建,即可生成 src/MANIFEST.game.jsonsrc/MANIFEST.pvp.json 文件。

18.1.1.2. 修改MANIFEST.json

按如下所示修改MANIFEST文件。

src/MANIFEST.game.json:

...

"SessionService": {
  "tcp_json_port": 8012,
  "udp_json_port": 0,
  "http_json_port": 0,
  "tcp_protobuf_port": 0,
  "udp_protobuf_port": 0,
  "http_protobuf_port": 0,
  ...
},
"Object": {
  "cache_expiration_in_ms": 3000,
  "copy_cache_expiration_in_ms": 700,
  "enable_database" : true,
  "db_mysql_server_address" : "tcp://10.10.10.10:3306",
  "db_mysql_id" : "funapi",
  "db_mysql_pw" : "funapi",
  "db_mysql_database" : "funapi",
  ...
},
"ApiService": {
  "api_service_port": 0,
  "api_service_event_tags_size": 1,
  "api_service_logging_level": 2
},
"Redis": {
  "enable_redis": true,
  "redis_mode": "redis",
  "redis_servers": {
    "cross_server_storage": {
      "address": "10.10.10.10:6379",
      "auth_pass": ""
    }
  },
  "redis_async_threads_size": 4
},
"RpcService": {
  "rpc_enabled": true,
  "rpc_threads_size": 4,
  "rpc_port": 8015,
  ...
},
"CrossServerStorage": {
  "enable_cross_server_storage": true,
  "redis_tag_for_cross_server_storage": "cross_server_storage"
},

...

src/MANIFEST.pvp.json:

...

"SessionService": {
  "tcp_json_port": 9012,
  "udp_json_port": 0,
  "http_json_port": 0,
  "tcp_protobuf_port": 0,
  "udp_protobuf_port": 0,
  "http_protobuf_port": 0,
  ...
},
"Object": {
  "cache_expiration_in_ms": 3000,
  "copy_cache_expiration_in_ms": 700,
  "enable_database" : true,
  "db_mysql_server_address" : "tcp://10.10.10.10:3306",
  "db_mysql_id" : "funapi",
  "db_mysql_pw" : "funapi",
  "db_mysql_database" : "funapi",
  ...
},
"ApiService": {
  "api_service_port": 0,
  "api_service_event_tags_size": 1,
  "api_service_logging_level": 2
},
"Redis": {
  "enable_redis": true,
  "redis_mode": "redis",
  "redis_servers": {
    "cross_server_storage": {
      "address": "10.10.10.10:6379",
      "auth_pass": ""
    }
  },
  "redis_async_threads_size": 4
},
"RpcService": {
  "rpc_enabled": true,
  "rpc_threads_size": 4,
  "rpc_port": 9015,
  ...
},
"CrossServerStorage": {
  "enable_cross_server_storage": true,
  "redis_tag_for_cross_server_storage": "cross_server_storage"
},

...

18.1.1.3. 不同Flavor的处理准备

为了按照不同的Flavor进行处理,先按如下所示修改文件。

src/event_handlers.cc

 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
// PLEASE ADD YOUR EVENT HANDLER DECLARATIONS HERE.

#include "event_handlers.h"

#include <funapi.h>
#include <glog/logging.h>

#include "test_loggers.h"
#include "test_messages.pb.h"

DECLARE_string(app_flavor);


namespace test {

void SendErrorMessage(const Ptr<Session> &session, const string &msg_type,
                      const string &error_message) {
  Json response;
  response["error"] = true;
  response["error_message"] = error_message;
  session->SendMessage(msg_type, response);
}


void RegisterEventHandlers() {
  if (FLAGS_app_flavor == "game") {
  } else if (FLAGS_app_flavor == "pvp") {
  }
}

}  // namespace test

mono/server.cs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
namespace Test
{

  public static void SendErrorMessage(
      Session session, string msg_type, string error_message)
  {
    JObject response = new JObject();
    response ["error"] = true;
    response ["error_message"] = error_message;
    session.SendMessage(msg_type, response);
  }


  public static void Install(ArgumentMap arguments)
  {
    ...
    if (Flags.GetString ("app_flavor") == "game")
    {
    }
    else if (Flags.GetString ("app_flavor") == "pvp")
    {
    }
  }
}

18.1.2. 在Game flavor中注册登录消息处理器

假设客户端向服务器发送 cs_game_server_login 消息,注册对此进行处理的 OnGameServerLogin() 处理器。

src/event_handlers.cc

 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
void OnGameServerLogin(const Ptr<Session> &session, const Json &message) {
  const string user_name = message["user_name"].GetString();

  Ptr<User> user = User::FetchByName(user_name);
  if (not user) {
    user = User::Create(user_name);
    if (not user) {
      SendErrorMessage(session, "sc_game_server_login", "Already exists.");
      return;
    }

    user->SetLevel(1);
  }

  LOG_ASSERT(user);

  if (not AccountManager::CheckAndSetLoggedIn(user_name, session)) {
    SendErrorMessage(session, "sc_game_server_login", "Already logged in");
    return;
  }

  Json response;
  response["error"] = false;
  response["level"] = user->GetLevel();
  session->SendMessage("sc_game_server_login", response);
}


void RegisterEventHandlers() {
  if (FLAGS_app_flavor == "game") {
    HandlerRegistry::Register("cs_game_server_login", OnGameServerLogin);
  } else if (FLAGS_app_flavor == "pvp") {
    // 대전 서버의 메시지 핸들러를 등록합니다.
  }
}

mono/server.cs

 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
40
41
42
43
44
45
46
47
48
public static void OnGameServerLogin(
    Session session , JObject message)
{
  string user_name = (string) message ["user_name"];

  User user = User.FetchByName (user_name);
  if (user == null)
  {
    user = User.Create (user_name);

    if (user == null)
    {
      SendErrorMessage (session, "sc_game_server_login", "Already exists");
      return;
    }

    user.SetLevel (1);
  }

  Log.Assert (user != null);

  if (!AccountManager.CheckAndSetLoggedIn (user_name, session))
  {
    SendErrorMessage(session, "sc_game_server_login", "Already logged in");
    return;
  }

  JObject response = new JObject();
  response["error"] = false;
  response["level"] = user.GetLevel();
  session.SendMessage("sc_game_server_login", response);
}

public static void Install(ArgumentMap arguments)
{
  ...
  if (Flags.GetString ("app_flavor") == "game")
  {
    NetworkHandlerRegistry.RegisterMessageHandler (
        "cs_game_server_login",
        new NetworkHandlerRegistry.JsonMessageHandler (
            OnGameServerLogin));
  }
  else if (Flags.GetString ("app_flavor") == "pvp")
  {
    // 대전 서버의 메시지 핸들러를 등록합니다.
  }
}

18.1.3. 在Game flavor中注册进入PVP模式的消息处理器

假设客户端向服务器发送 cs_enter_pvp 消息, 注册对此进行处理的 OnEnterPvp() 消息处理器。

 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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
void OnEnterPvp(const Ptr<Session> &session, const Json &message) {
  const string user_name = AccountManager::FindLocalAccount(session);
  if (user_name.empty()) {
    session->Close();
    return;
  }

  Ptr<User> user = User::FetchByName(user_name);
  if (not user) {
    SendErrorMessage(session, "sc_enter_pvp", "No user data");
    return;
  }

  Json user_data;
  user_data["user_name"] = user_name;
  user_data["level"] = user->GetLevel();

  // 클라이언트에게 전달할 인증키를 생성하고 CrossServerStorage 에 저장합니다.
  CrossServerStorage::Key auth_key;
  const CrossServerStorage::Result result = CrossServerStorage::CreateSync(
      user_data,
      WallClock::FromSec(10),  // 만료시간을 10 초로 지정합니다.
      &auth_key);

  if (result != CrossServerStorage::kSuccess) {
    LOG(ERROR) << "CrossServerStorage::CreateSync() failed: "
               << "result=" << result;
    SendErrorMessage(session, "sc_enter_pvp", "Server error");
    return;
  }

  const string pvp_server_ip = "10.10.10.10";
  const int64_t pvp_server_port = 9012;

  Json response;
  response["error"] = false;
  response["pvp_server_ip"] = pvp_server_ip;
  response["pvp_server_port"] = pvp_server_port;
  response["auth_key"] = boost::lexical_cast<string>(auth_key);
  session->SendMessage("sc_enter_pvp", response);

  // 이제 클라이언트는 대전 서버에 접속하여 인증키를 전달할 것입니다.
  // 대전 서버는 전달받은 인증키로 인증받은 클라이언트인지 확인할 것입니다.
}


void RegisterEventHandlers() {
  if (FLAGS_app_flavor == "game") {
    HandlerRegistry::Register("cs_game_server_login", OnGameServerLogin);
    HandlerRegistry::Register("cs_enter_pvp", OnEnterPvp);
  } else if (FLAGS_app_flavor == "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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public static void OnEnterPvp(Session session , JObject message)
{
  string user_name = AccountManager.FindLocalAccount (session);
  if (user_name == String.Empty) {
    session.Close ();
    return;
  }

  User user = User.FetchByName (user_name);
  if (user == null) {
    SendErrorMessage (
        session, "sc_enter_pvp", "No user data");
    return;
  }

  JObject user_data = new JObject ();
  user_data["user_name"] = user_name;
  user_data["level"] = user.GetLevel ();

  // CrossServerStorage 를 이용해서 저장합니다.
  // 이때 인증키를 발급받아 클라이언트에게 전달합니다.
  Guid auth_key;
  CrossServerStorage.Result result = CrossServerStorage.CreateSync(
      user_data,
      WallClock.FromSec(10),  // 만료시간을 10 초로 지정합니다.
      out auth_key);

  if (result != CrossServerStorage.Result.kSuccess) {
    Log.Error ("CrossServerStorage::CreateSync() failed: result= {0}",
               result);
    SendErrorMessage (session, "sc_enter_pvp", "Server error");
    return;
  }

  string pvp_server_ip = "127.0.0.1";
  short pvp_server_port = 9012;

  JObject response = new JObject ();
  response ["error"] = false;
  response ["pvp_server_ip"] = pvp_server_ip;
  response ["pvp_server_port"] = pvp_server_port;
  response ["auth_key"] = auth_key.ToString();
  session.SendMessage ("sc_enter_pvp", response);

  // 이제 클라이언트는 대전 서버에 접속하여 인증키를 전달할 것입니다.
  // 대전 서버는 전달받은 인증키로 인증받은 클라이언트인지 확인할 것입니다.
}

public static void Install(ArgumentMap arguments)
{
  ...

  if (Flags.GetString ("app_flavor") == "game")
  {
    NetworkHandlerRegistry.RegisterMessageHandler (
        "cs_game_server_login",
        new NetworkHandlerRegistry.JsonMessageHandler (
            OnGameServerLogin));

    NetworkHandlerRegistry.RegisterMessageHandler (
        "cs_enter_pvp",
        new NetworkHandlerRegistry.JsonMessageHandler (
            OnEnterPvp));
  }
  else if (Flags.GetString ("app_flavor") == "pvp")
  {
    // 대전 서버의 메시지 핸들러를 등록합니다.
  }
}

Tip

如果使用分布式处理功能 跨服共享状态和信息 , 则可以在多个对战服务器中选择负载量较低的服务器,而不是在 OnEnterPvp() 函数中 对要访问的对战服务器进行硬编码。

18.1.4. 在PvP flavor中注册PvP参与消息处理器

假设客户端向服务器发送 cs_transfer_pvp_server 消息, 注册对此进行处理的 OnTransferPvpServer() 消息处理器。

 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
40
41
42
43
44
45
46
47
48
49
50
51
void OnTransferPvpServer(const Ptr<Session> &session, const Json &message) {
  if (not message.HasAttribute("auth_key", Json::kString) ||
      message["auth_key"].GetString().empty()) {
    session->Close();
    return;
  }

  CrossServerStorage::Key auth_key = CrossServerStorage::Key();
  try {
    auth_key =
        boost::lexical_cast<CrossServerStorage::Key>(
            message["auth_key"].GetString());
  } catch (const boost::bad_lexical_cast &) {
    session->Close();
    LOG(ERROR) << "인증키 얻기 실패. auth_key=" << message["auth_key"].GetString();
    return;
  }

  Json user_data;
  CrossServerStorage::Result result =
      CrossServerStorage::ReadSync(auth_key, &user_data);

  if (result != CrossServerStorage::kSuccess) {
    SendErrorMessage(session, "sc_transfer_pvp_server", "Failed to authenticate");
    return;
  }

  const string user_name = user_data["user_name"].GetString();
  const int64_t level = user_data["level"].GetInteger();

  LOG(INFO) << "user_name=" << user_name << ", level=" << level;

  // 이 예제에서는 인증키 만료시간이 10초이므로 명시적으로 삭제하지 않습니다.

  // 만약 명시적으로 인증키를 삭제해야 한다면 다음 함수로 삭제할 수 있습니다.
  // CrossServerStorage::DeleteSync(auth_key);

  // 인증키를 계속 사용할 수도 있습니다. (게임 서버가 대전 결과가 필요한 경우)
  // 아래 함수를 이용하면 인증키 만료 시간을 갱신할 수 있습니다.
  // CrossServerStorage::RefreshSync(auth_key, WallClock::FromSec(60));
}


void RegisterEventHandlers() {
  if (FLAGS_app_flavor == "game") {
    HandlerRegistry::Register("cs_game_server_login", OnGameServerLogin);
    HandlerRegistry::Register("cs_enter_pvp", OnEnterPvp);
  } else if (FLAGS_app_flavor == "pvp") {
    HandlerRegistry::Register("cs_transfer_pvp_server", OnTransferPvpServer);
  }
}
 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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public static void OnTransferPvpServer(
    Session session , JObject message)
{
  if (message ["auth_key"] == null)
  {
    session.Close ();
    return;
  }
  else if ( (string) message ["auth_key"] == String.Empty)
  {
    session.Close ();
  }

  Guid auth_key;

  try {
    auth_key = new Guid ((string) message ["auth_key"]);
  } catch (Exception exception) {
    session.Close();
    Log.Error ("인증키 얻기 실패. auth_key= {0}",
               (string)  message ["auth_key"]);
    return;
  }

  JObject user_data;
  CrossServerStorage.Result result =
      CrossServerStorage.ReadSync(auth_key, out user_data);

  if (result != CrossServerStorage.Result.kSuccess)
  {
    SendErrorMessage (session, "sc_transfer_pvp_server", "Failed to authenticate");
    return;
  }

  string user_name = (string) user_data ["user_name"];
  ulong level = (ulong) user_data ["level"];

  Log.Info ("user_name= {0}, level= {1}", user_name, level);

  // 이 예제에서는 인증키 만료시간이 10초이므로 명시적으로 삭제하지 않습니다.

  // 만약 명시적으로 인증키를 삭제해야 한다면 다음 함수로 삭제할 수 있습니다.
  // CrossServerStorage.DeleteSync(auth_key);

  // 인증키를 계속 사용할 수도 있습니다. (게임 서버가 대전 결과가 필요한 경우)
  // 아래 함수를 이용하면 인증키 만료 시간을 갱신할 수 있습니다.
  // CrossServerStorage.RefreshSync(auth_key, WallClock.FromSec(60));
}

public static void Install(ArgumentMap arguments)
{
  ...

  if (Flags.GetString ("app_flavor") == "game")
  {
    NetworkHandlerRegistry.RegisterMessageHandler (
        "cs_game_server_login",
        new NetworkHandlerRegistry.JsonMessageHandler (
            OnGameServerLogin));

    NetworkHandlerRegistry.RegisterMessageHandler (
        "cs_enter_pvp",
        new NetworkHandlerRegistry.JsonMessageHandler (
            OnEnterPvp));
  }
  else if (Flags.GetString ("app_flavor") == "pvp")
  {
    NetworkHandlerRegistry.RegisterMessageHandler (
        "cs_transfer_pvp_server",
        new NetworkHandlerRegistry.JsonMessageHandler (
            OnEnterPvp));
  }
}

18.2. CrossServerStorage功能的设置参数

  • enable_cross_server_storage: 是否激活相应功能。Redis 기능 설정 파라미터enable_redis 也须设置为true.(type=bool, default=false)

  • redis_tag_for_cross_server_storage: 作为跨服共享数据储存库使用的Redis服务器标签值。(参考 Tag 로 Redis Server 구분 )(type=string, default=””)