18. 分布式处理Part 2: 跨服共享数据¶
iFun引擎通过Redis提供了可以轻松在服务器之间共享JSON数据的 CrossServerStorage
组件。
使用该组件可轻松进行如下应用。
通过颁发认证密钥来确认移动服务器时是否为已认证的客户端
共享各游戏地区及各服务器流入的玩家数、交易量等信息
共享与游戏有关的其他信息
本章运用示例介绍了 通过颁发认证密钥来确认移动服务器时是否为已认证的客户端 的方法。
Note
示例中未说明本组件的所有接口。
具体内容请参考 /usr/include/funapi/service/cross_server_storage.h
文件或
CrossServerStorage API文件 。
18.1. 示例-跨服移动时共享客户端认证信息¶
假设如下脚本。
客户端登录游戏服务器
请求参与对战
游戏服务器运用``CrossServerStorage``颁发key,保存对战时所需的 用户信息后,将Key传输给客户端
访问客户端对战服务器,传输从游戏服务器获得的Key
对战服务器通过该Key获取对战时所需的用户信息,进行对战处理
(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.json
和
src/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=””)