분산처리 Part 2: 서버간 데이터 공유¶
아이펀 엔진은 Redis 를 통해 서버간에 JSON 데이터를 쉽게 공유할 수 있는 CrossServerStorage
라는 콤포넌트를 제공합니다.
이 콤포넌트를 이용하면 손쉽게 아래 예시와 같은 응용이 가능합니다.
인증키 발급을 통해 서버 이동 시 인증된 클라이언트인지 확인
각 게임 지역 또는 서버마다 유입되는 플레이어 수, 거래량 등을 공유
기타 게임 관련 정보 공유
이 챕터에서는 예제를 통해 인증키 발급을 통해 서버 이동 시 인증된 클라이언트인지 확인 하는 것을 다뤄보겠습니다.
Note
예제에서는 본 콤포넌트의 모든 인터페이스를 설명하고 있지 않습니다. 자세한 내용은 funapi/service/cross_server_storage.h 파일과 CrossServerStorage class API 문서 를 참고하시기 바랍니다.
예제 - 서버 이동시 클라이언트 인증 정보 공유하기¶
다음과 같은 시나리오를 가정하겠습니다.
클라이언트가 게임 서버에 로그인
대전 참여 요청
게임 서버는
CrossServerStorage
를 이용하여 key 를 발급하고 대전에 필요한 유저 정보를 저장한 뒤 Key 를 클라이언트로 전달클라이언트가 대전 서버에 접속하고 게임 서버로부터 받은 Key 를 전달
대전 서버는 해당 Key 로 대전에 필요한 유저 정보를 얻고 대전 처리 진행
(optional) 대전 서버에서 key 제거
즉, 게임 서버가 key 라는 인증키를 발급하고 이것으로 대전 서버가 인증된 클라이언트인지 검증한 뒤 대전을 할 수 있게끔 처리합니다.
Note
이 예제에서는 서버 측 코드만 제시하고 클라이언트 처리는 다루지 않습니다.
Important
쉬운 설명을 위해 동기 함수들만 다루고 있습니다. 실 서비스에서는 성능을 위해 비동기 함수를 사용하는 것이 좋습니다.
Important
예제에서 다루지 않는 CrossServerStorage::Write()
나 CrossServerStorage::WriteSync()
함수는 서로 다른 서버가 동일한 key 로 write 할 때 처리 순서를 보장하지 않습니다. 그때문에 먼저 쓴 데이터가 덮어씌여질 수 있다는 점에 주의하세요.
프로젝트 준비¶
Flavor 를 통해 게임, 대전 서버 등록¶
CMakeLists.txt
파일에서 APP_FLAVORS
항목을 다음처럼 추가합니다.
set(APP_FLAVORS game pvp)
Note
Flavor 관련해서는 Flavor: 역할에 따라 서버 구분하기 를 참고하세요.
여기까지 진행한 상태에서 빌드를 하면 src/MANIFEST.game.json
과
src/MANIFEST.pvp.json
파일이 생성됩니다.
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"
},
...
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")
{
}
}
}
|
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")
{
// 대전 서버의 메시지 핸들러를 등록합니다.
}
}
|
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()
함수에서 접속할 대전 서버를 하드코딩하는 대신
여러 대전 서버들중 부하량이 낮은 서버를 선택할 수 있습니다.
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));
}
}
|
CrossServerStorage 기능의 설정 파라미터¶
enable_cross_server_storage: 해당 기능을 활성화시킬지 여부. redis-manifest 의
enable_redis
도 true 로 세팅해야됨.(type=bool, default=false)redis_tag_for_cross_server_storage: 서버간 데이터 공유를 위한 스토리지로 사용될 Redis 서버의 태그 값. (태그(Tag) 로 레디스 서버 구분 참조) (type=string, default=””)