데디케이티드 서버 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 Protobuf 의 extension 과 문법에 대해서는 Google Protocol Buffers 의 설명을 참고해주세요.
Important
FunDedicatedServerRpcMessage
를 extend 할 때 필드 번호는 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();
내부적으로 하나의 GameObject 를 DontDestroyOnLoad 타입으로 생성해 Updater 로 사용하고 있습니다. 그러므로 Scene 이 변경되어도 FunapiDedicatedServerRpc 객체를 새로 생성할 필요없이 하나의 객체를 여러 Scene 에서 공유해서 사용할 수 있습니다.
디버깅¶
디버깅을 위해 서버 수행 중 기록된 FunapiDedicatedServerRpc 관련 로그를 파일로 저장할 수 있습니다. 로그를 저장하기 위해서는 Unity Player Settings 에서 Symbols 란에 아래 define 을 추가해야 합니다.
ENABLE_LOG;ENABLE_DEBUG;ENABLE_SAVE_LOG
ENABLE_DEBUG
는 생략할 수 있습니다. ENABLE_DEBUG
를 추가하면 주고 받는
메시지에 대한 로그까지 추가되어 로그 양이 많아집니다.
로그 파일은 기본적으로 앱이 종료되는 시점에 저장됩니다. 로그 양이 일정 크기를 넘어가면 중간중간 저장이 이루어집니다. 파일은 앱의 /Logs 폴더에 날짜와 시간을 포함하는 파일명으로 저장됩니다.
데디케이티드 서버(UE)¶
이 설명은 추후 추가될 예정입니다.