9. 네트워킹 Part 2

9.1. 메시지 포맷

9.1.1. JSON 메시지

아이펀 엔진은 JSON 을 다루기 위하여 아이펀 엔진 API 문서 에 설명된 JSON class 를 이용합니다.
아이펀 엔진은 JSON을 다루기 위해 Newtonsoft.Json 을 사용합니다. 더 자세한 내용은 Newtonsoft.Json 을 참고해주세요.

9.1.1.1. JSON 메시지 생성 및 읽기

아래는 주요 기능을 다루는 간단한 예제 코드입니다. 더 자세한 내용은 위의 링크를 참고하시기 바랍니다.

이 예제에서는 다음과 같은 JSON object 를 가정하겠습니다.

{
  "id": "ifunfactory",
  "level": 99,
  "messages": [
    "hello",
    "world",
    1004,
  ],
  "guild": {
      "name": "legend",
      "score": 1000
  }
}

먼저 위와 같은 JSON object 를 만들기 위해서는 다음처럼 하시면 됩니다.

Json msg;
msg["id"] = "ifunfactory";
msg["level"] = 99;
msg["messages"].PushBack("hello"); // messages 는 array 가 됩니다.
msg["messages"].PushBack("world");
msg["messages"].PushBack(1004);
msg["guild"]["name"] = "legend";
msg["guild"]["score"] = 1000;

LOG(INFO) << msg.ToString();

그리고 JSON object 가 이런 형태로 되어있는지 확인하는 것은 다음처럼 하시면 됩니다.

// 값 type 확인
// 참고. Registering message handler의 JsonSchema 를 이용하면
// 아래와 같이 직접 type 검사를 안해도 됩니다.
if (not msg.HasAttribute("id", Json::kString) ||
    not msg.HasAttribute("level", Json::kInteger) ||
    not msg.HasAttribute("messages", Json::kArray) ||
    not msg.HasAttribute("guild", Json::kObject)) {
  // error..
  return;
}

// JSON 객체에서 값을 읽습니다.
// id_ptr 은 msg 객체가 소멸된 후에는 유효하지 않습니다.
const char *id_ptr = msg["id"].GetString();
string id = msg["id"].GetString();
int level = msg["level"].GetInteger();
string message1 = msg["messages"][0].GetString();
string message2 = msg["messages"][1].GetString();
int message3 = msg["messages"][2].GetInteger();
string guild_name = msg["guild"]["name"].GetString();
int guild_score = msg["guild"]["score"].GetInteger();

// msg2 를 JSON 문자열로 변환합니다.
string json_str;
msg2.ToString(&json_str);

// json 문자열을 불러옵니다.
Json msg3;
msg3.FromString(&json_str);

먼저 위와 같은 JSON object 를 만들기 위해서는 다음처럼 하시면 됩니다.

JObject msg = new JObject ();
msg ["id"] = "ifunfactory";
msg ["level"] = 99;
JArray json_arr = new JArray ();
json_arr.Add ("hello");
json_arr.Add ("world");
json_arr.Add (1004);
msg ["messages"] = json_arr;
msg ["guild"] = new JObject ();
msg ["guild"] ["name"] = "legend";
msg ["guild"] ["score"] = 1000;

Log.Info (msg.ToString());
if (msg ["id"].Type != JTokenType.String)
{
  // error..
}

if (msg ["level"].Type != JTokenType.Integer)
{
  // error..
}

if (msg ["messages"].Type != JTokenType.Array)
{
  // error..
}

if (msg ["guild"].Type != JTokenType.Object)
{
  // error..
}

// JSON 객체에서 값을 읽습니다.
string id = (string) msg ["id"];
// 혹은 다음과 같이 값을 읽을 수도 있습니다.
// string id = msg["id"].Value<String>();

int level = (int) msg ["level"];
string message1 = (string) msg ["messages"] [0];
string message2 = (string) msg ["messages"] [1];
int message3 = (int) msg ["messages"] [2];
string guild_name = (string) msg ["guild"] ["name"];
int guild_score = (int) msg ["guild"] ["score"];

// JSON 문자열을 불러옵니다.
JObject msg2 = JObject.Parse (msg.ToString ());
Log.Info ((string) msg2 ["id"]);

9.1.1.2. JSON 파싱 에러 확인

아이펀 엔진은 JSON 데이터 파싱 실패 시 해당 데이터와 실패 이유 등을 콜백으로 받을 수 있습니다. 아래 예제와 같이 Json::From... 함수의 마지막 인자로 Json::ParseErrorCallback 콜백 타입에 해당하는 함수를 등록하면 됩니다. 여기서는 Lambda 를 이용하여 파싱 에러 로그를 출력해보겠습니다.

...
// key2 마지막에 , 가 들어간 올바르지 못한 json을 문자열 입니다.
const char* json_string = "{ \"key1\": \"value1\", \"key2\": \"value2\", }";
fun::Json json;

json.FromString(json_string,
  [] (const string &json_string /*파싱에 실패한 json 문자열*/,
      const string &error_desc /*파싱 실패 사유*/,
      size_t error_offset /*파싱에 실패한 위치*/) {
    // 실패 사유와 파싱을 시도한 json 문자열을 출력해보겠습니다.
    LOG(INFO) << error_desc << std::endl
               << "json ="
               << json_string;
  }
);

또한 아이펀 엔진은 해당 파싱 에러 출력을 위한 기본 함수를 제공합니다. 아래 예제와 같이 Json 클래스의 From... 함수의 마지막 인자로 fun::Json::kDefaultParseErrorHandler 를 추가하시면 됩니다.

...
// key2 마지막에 , 가 들어간 올바르지 못한 json을 문자열 입니다.
const char* json_string = "{ \"key1\": \"value1\", \"key2\": \"value2\", }";
fun::Json json;
// 마지막 인자를 추가하여 파싱 실패시 로그로 확인 할 수 있습니다.
json.FromString(json_string, fun::Json::kDefaultParseErrorHandler);
E0109 12:31:51.445076 24118 json.cc:197] Missing a name for object member.
json={ "key1": "value1", "key2": "value2",
JsonReaderException 을 참고하세요.

9.1.1.3. JSON 메시지 스키마 자동 검증

Google Protocol Buffers 는 구조를 미리 선언하고 사용하기 때문에 serialization / deserialization을 하는 동안 필드들의 적합성 검사를 수행해주지만, JSON 은 임의의 필드들이 추가될 수 있기 때문에 JSON 자체가 적합성 검사를 제공하지는 않습니다.

따라서 JSON 패킷을 전달받은 핸들러에서는 JSON 필드들이 올바른지 검사하는 것이 반드시 필요한데, 이는 매우 번거로운 일 입니다. 아이펀 엔진은 이 작업을 쉽게 할 수 있도록 JSON 스키마를 handler 에 붙여 자동으로 message 의 파라미터를 검사하는 기능을 제공합니다. 만약 message 의 스키마가 올바르지 않으면 로그를 출력하고 핸들러로 넘겨주지 않습니다. 따라서 핸들러에서는 파라미터 검사 없이 항상 정상적인 JSON 만 넘어온다고 가정하고 작업을 할 수 있습니다.

9.1.1.3.1. 코드 상에서 스키마 지정하기

JsonSchema 체크는 다음과 같이 사용합니다.

JsonSchema(parameter 이름, parameter type, 필수여부)

다음과 같은 형태로 JSON 메시지가 도착해야된다고 가정하겠습니다.

{
  "id":"abcd..",
  "pw":"abcd..",
  "user_info":{
    "name":"abcd..",
    "age":10
  }
}

이런 경우 아래처럼 스키마 체크를 할 수 있습니다.

#include <funapi.h>

// 아래 Installer 는 프로젝트를 만들 때 자동으로 생성됩니다.
class MyServerInstaller : public Component {
  static bool Install(const ArgumentMap &/*arguments*/) {
    ...
    // hello message
    JsonSchema hello_schema(JsonSchema::kObject,
      JsonSchema("id", JsonSchema::kString, true),
      JsonSchema("pw", JsonSchema::kString, false),
      JsonSchema("user_info", JsonSchema::kObject, false,
        JsonSchema("name", JsonSchema::kString, false),
        JsonSchema("age", JsonSchema::kInteger, false)));

    // 아래처럼 처리하고 싶은 메시지 타입과 함수를 연동합니다.
    // 메시지 타입은 JSON 안에서 "msgtype" 키에 해당하는 값입니다.
    HandlerRegistry::Register("hello", OnHello, hello_schema);
    ...
  }
}
마이크로소프트의 Validating JSON with JSON Schema 문서를 참고해주세요.
9.1.1.3.2. 별도의 스키마 파일 사용하기

C++ 코드 상에 JSON schema 를 등록해서 client-server 간의 JSON message 의 유효성 검증을 하는 것이 번잡한 작업이 될 수 있으므로, 아이펀 엔진에서는 JSON protocol 을 텍스트 파일로 기술하는 방법도 지원합니다.

MANIFEST.json 에서 SessionService 콤포넌트에 json_protocol_schema_dir 라는 인자로 스키마 파일들이 있는 디렉토리 경로를 지정할 수 있습니다. 각 스키마 파일의 구조는 다음과 같아야 합니다.

  • 각 파일의 확장자는 대소문자 구분없이 json, txt, text 중 하나여야 합니다.

  • 각 파일은 JSON 문서로 구성되는데,

    {"MESSAGE_TYPE1": {MESSAGE 속성},"MESSAGE_TYPE2": {MESSAGE 속성}, ...}
    

    와 같이 구성됩니다.

  • Message 의 속성으로 가능한 것은 다음과 같습니다.

    • direction: cs , sccs sc 가 가능합니다. 각각 클라이언트에서 서버로 가는 메시지, 서버에서 클라이언트로 가는 메시지, 양쪽에 다 쓰이는 메시지를 의미합니다.

    • properties: message 의 field 들을 나열하기 위해 사용되며,

      "properties": { "FIELD1": { FIELD 속성 }, "FIELD2: {FIELD 속성}, ...}
      

      와 같이 사용합니다.

  • Field 의 속성으로 가능한 것은 다음과 같습니다.

    • type: 해당 field 의 type 을 나타냅니다. bool , integer , string , array , object 가 가능합니다.

    • required: 해당 field 가 필수인지 나태냅니다. true / false 의 boolean 값도 가능하고, cs , sc , 또는 cs sc 처럼 message 의 direction 값도 가능합니다. 만일 true/false 를 쓰게 되면 해당 field 가 전송되는 방향에 상관없이 필수 혹은 선택이라는 뜻이되고, cs, sc, cs sc 를 쓰게 되면 전송되는 방향에 따라 필수 여부가 결정됩니다. 이는 해당 message 가 서버 클라이언트 모두에서 사용되는 경우 전송 방향에 따라 필수 field 가 달라질 때 유용합니다. (예를 들어, 서버에서만 result 나 error_code field 를 보내는 경우에 이 둘을 "required": "sc" 로 정의하실 수 있습니다.

    • properties: filed 의 속성이 object 인 경우에 하위 field 를 기술하기 위해서 사용합니다.

       "properties": { "FIELD1": {FIELD 속성}, "FIELD2": {FIELD 속성}, ...}
      
      와 같이 사용합니다.
      
    • items: field 의 속성이 array 인 경우에 요소들에 대해 기술하기 위해서 사용됩니다.

      "items": { 배열 요소 속성 }
      

      와 같이 사용하는데, 현재 사용 가능한 배열 요소 속성은 다음과 같습니다.

      • type: array 의 요소들의 타입을 기술합니다. bool , integer , string, array , object 가 가능합니다.
      • properties: type 값이 object 인 경우에 하위 field 를 기술하기 위해 사용됩니다. 앞에 언급된 properties 와 동일합니다.
      • items: type 값이 array 인 경우에 해당됩니다. 앞에 언급된 items 와 동일합니다.

예제 1: 다음은 client 에서 server 로 보내지는 Login 메시지를 정의하는데, 필수적으로 AccountKey 라는 문자열 필드가 있어야 함을 의미합니다.

{
  "Login": {
    "direction": "cs",
    "properties": {
      "AccountKey": {
        "type": "string",
        "required": true
      }
    }
  }
}

예제 2: 이에 대해서 server 에서 client 로 전송되는 LoginReply 는 다음과 같이 정의할 수 있습니다. 이때 입력으로 받은 AccountKey 와 더불어, 결과를 나타내는 boolean 형태의 Result 가 필수라고 가정하겠습니다. 그리고 Result 가 false 인 경우 이유를 설명해줄 수 있는 ErrorString 이 선택적으로 주어진다고 하면 다음과 같이 쓸 수 있습니다.

{
  "LoginReply": {
    "direction": "sc",
    "properties": {
      "AccountKey": {
        "type": "string",
        "required": "sc"
      },
      "Result": {
        "type": "bool",
        "required": true
      },
      "ErrorString": {
        "type": "string",
        "required": false
      }
    }
  }
}

예제 3: 조금 더 복잡한 경우를 해보겠습니다. 게임 내의 농장이 있고, 해당 농장의 농지에서 수확을 하기 위한 Harvest 라는 메시지를 정의해보겠습니다. 클라이언트가 이 메시지를 보낼 때, 다음과 같은 JSON message 를 보낼것을 가정해봅시다.

{
  "Harvest": {
    "MapObject": {
      "Position": [1, 2],
      "Output": [{
        "ResourceIndex": 1000,
        "YieldCount": 10,
        "CompletionTime": "2014-08-24 10:00:00"
      }]
    }
  }
}

만일 모든 field 들을 다 채워서 보내야 된다면, JSON schema 는 다음과 같이 쓸 수 있습니다.

{
  "Harvest": {
    "direction": "cs",
    "properties": {
      "MapObject": {
        "type": "object",
        "required": true,
        "properties": {
          "Position": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "required": true
          },
          "Output": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "ResourceIndex": {
                  "type": "integer",
                  "required": true
                },
                "YieldCount": {
                  "type": "integer",
                  "required": true
                },
                "CompletionTime": {
                  "type": "string",
                  "required": true
                }
              }
            }
          }
        }
      }
    }
  }
}

예제 4: 어떤 메시지가 양방향으로 사용되는 경우를 생각해봅시다. BidirectionalMessage 라는 메시지가 클라이언트에서 서버로 갈 때는 RequestString 이라는 필드를 반드시 가져야되고, 서버에서 클라이언트로 갈 때는 ReplyString 이라는 field 를 반드시 가져야 된다고 하면, 다음과 같이 JSON schema 를 기술할 수 있습니다.

{
  "BidirectionalMessage": {
    "direction": "cs sc",
    "properties": {
      "RequestString": {
        "type": "string",
        "required": "cs"
      },
      "ReplyString": {
        "type": "string",
        "required": "sc"
      }
    }
  }
}
마이크로소프트의 Validating JSON with JSON Schema 문서를 참고해주세요.

9.1.2. Protobuf 메시지

여기서는 아이펀 엔진에서 Protobuf 으로 메시지를 주고 받을 수 있도록 기본적인 설명만 합니다. 자세한 설명은 Google Protocol Buffers 서 보실 수 있습니다.

프로젝트를 생성하면 {프로젝트이름}_messages.proto 파일이 자동생성되며 최상위 메시지인 FunMessage 가 포함된 파일을 import 하고 있습니다. 별도의 작업 없이 이 파일에 FunMessage 를 extend 하는 메시지를 추가하면 자동으로 메시지가 빌드되어 사용하실 수 있습니다.

Important

FunMessage 를 extend 할 때 필드 번호 1-15까지는 아이펀 엔진에서 사용하므로 사용하시면 안됩니다.

9.1.2.1. 메시지 정의

아래는 설명을 위한 메시지 정의 예제로 캐릭터 정보를 담을 수 있도록 하였습니다.

// FunMessage 를 extend 합니다.
extend FunMessage {
  // 반드시 16 부터 사용합니다. 1~15 는 아이펀 엔진에서 사용합니다.
  optional Login login = 16;
  optional Logout logout = 17;
  optional CharacterInfo character_info = 18;
  ...
}

// Login, Logout message 는 사용하지 않기 떄문에 생략합니다.

message CharacterInfo {
  enum CharacterType {
    kWarrior = 1;
    kElf = 2;
    kDwarf = 3;
  }

  message Pet {
    required string name = 1;
    required uint32 level = 2;
  }

  required string name = 1;
  required uint32 level = 3;
  required CharacterType type = 2;
  repeated Pet pets = 4;

  ...
}

9.1.2.2. 메시지 생성

아래는 위에서 정의한 CharacterInfo 메시지를 생성하는 예제입니다.

// 아이펀 엔진의 Protobuf 최상위 메시지를 생성합니다.
Ptr<FunMessage> message(new FunMessage);

// character_info 메시지를 FunMessage 에 생성하거나, 이미 생성된 경우 얻습니다.
// MutableExtension() 함수의 인자로 FunMessage 의 필드명을 주면 됩니다.
CharacterInfo *char_info = message->MutableExtension(character_info);

// character info 에 값을 채웁니다.

// primitive type 의 경우 set_{필드명}(값) 형식으로 값을 할당할 수 있습니다.
char_info->set_name("example");
char_info->set_level(99);
char_info->set_type(CharacterInfo::kDwarf);

// CharacterInfo 의 pets 필드는 repeated 로 배열과 같습니다.
// add_{필드명}() 으로 추가할 수 있습니다.
CharacterInfo::Pet *pet1 = char_info->add_pets();
pet1->set_name("dog");
pet1->set_level(10);
CharacterInfo::Pet *pet2 = char_info->add_pets();
pet2->set_name("cat");
pet2->set_level(11);

...

// 아래처럼 SendMessage() 함수로 위에서 만든 메시지를 전송할 수 있습니다.
// session->SendMessage("character_info", message);
// 클라이언트에서는 "character_info" 라는 메시지 타입으로 수신할 수 있습니다.
// 아이펀 엔진의 Protobuf 최상위 메시지를 생성합니다.
FunMessage message = new FunMessage ();

// character_info 메시지를 생성하겠습니다.
CharacterInfo char_info = new CharacterInfo();

// character info 에 값을 채웁니다.
// primitive type 의 경우 {필드명} = (값) 형식으로 값을 할당할 수 있습니다.
char_info.name = "example";
char_info.level = 99;
char_info.type = CharacterInfo.CharacterType.kDwarf;

// CharacterInfo 의 pets 필드는 repeated 로 리스트와 같습니다.
// {필드명}.Add({값}) 으로 추가할 수 있습니다.
CharacterInfo.Pet pet1 = new CharacterInfo.Pet();
pet1.name = "dog";
pet1.level = 10;

CharacterInfo.Pet pet2 = new CharacterInfo.Pet();
pet2.name = "cat";
pet2.level = 11;

char_info.pets.Add(pet1);
char_info.pets.Add(pet2);

message.AppendExtension_character_info(char_info);

...

// 아래처럼 SendMessage() 함수로 위에서 만든 메시지를 전송할 수 있습니다.
// session.SendMessage("character_info", message);
// 클라이언트에서는 "character_info" 라는 메시지 타입으로 수신할 수 있습니다.

9.1.2.3. 메시지 읽기

아래는 위에서 생성한 CharacterInfo 메시지를 읽는 예제입니다.

void OnCharacterInfo(const Ptr<Session> &session,
                     const Ptr<FunMessage> &message) {

  // message 는 위 예에서 생성한 것과 같다고 하겠습니다.

  // FunMessage 에 extend 한 character_info 필드에 값이 있는지 확인합니다.
  if (not message->HasExtension(character_info)) {
    // 예외처리
    return;
  }

  // FunMessage 의 character_info 필드를 얻어옵니다.
  // GetExtension() 함수의 인자로 FunMessage 의 필드명을 주면 됩니다.
  const CharacterInfo &char_info = message->GetExtension(character_info);

  // primitive type 의 경우 {필드명}() 형식으로 값을 읽을 수 있습니다.
  string char_name = char_info.name();
  uint32_t char_level = char_info.level();
  CharacterInfo::CharacterType char_type = char_info.type();

  // CharacterInfo 의 pets 필드는 repeated 로 배열과 같습니다.
  // 배열의 길이는 {필드명}_size() 함수로 알 수 있습니다.
  for (size_t i = 0; i < char_info.pets_size(); ++i) {
    // 배열의 element 는 {필드명}(index) 형식으로 얻을 수 있습니다.
    const CharacterInfo::Pet &pet = char_info.pets(i);

    string pet_name = pet.name();
    uint32_t pet_level = pet.level();

    ...
  }

  ...
}
void OnCharacterInfo(Session session, FunMessage message) {

  // message 는 위 예에서 생성한 것과 같다고 하겠습니다.
  CharacterInfo char_info = null;

  // FunMessage 에 extend 한 character_info 필드에 값이 있는지 확인합니다.
  if (!message.TryGetExtension_character_info (out char_info))
  {
    // 예외처리
    return;
  }

  // primitive type 의 경우 {필드명} 형식으로 값을 읽을 수 있습니다.
  string char_name = char_info.name;
  uint char_level = char_info.level;
  CharacterInfo.CharacterType char_type = char_info.type;

  // CharacterInfo 의 pets 필드는 repeated 로 배열과 같습니다.
  foreach (CharacterInfo.Pet pet in char_info.pets)
  {
    string pet_name = pet.name;
    uint pet_level = pet.level;
  }
  ...
}

9.2. 네트워크 보안

9.2.1. 메시지 암호화

아이펀 엔진은 서버와 클라이언트간 송수신되는 데이터의 암호화 기능을 제공합니다. 서버에서는 간단한 설정으로, 클라이언트에서는 제공된 플러그인을 이용하여 별도의 작업없이 암호화 기능을 이용할 수 있습니다.

현재 지원하는 암호화 알고리즘은 다음과 같습니다:

  • ife1: 아이펀 엔진 내부용 1
  • ife2: 아이펀 엔진 내부용 2
  • chacha20: ChaCha20
  • aes128: AES-128

Tip

암호화 알고리즘은 계속 추가될 예정이며, 제공되지 않는 다른 암호화 알고리즘을 사용하셔야 한다면 Funapi support 로 문의 부탁드립니다.

Transport 별로 암호화 알고리즘을 선택할 수 있으며, 여러 암호화 알고리즘을 두고 message 단위로 암호화 알고리즘을 선택할 수도 있습니다. 네트워킹 기능 설정 파라미터 를 참고하여 설정할 수 있으며 사용 예는 아래와 같습니다.

// manifest 에 설정에 따라 기본 암호화 알고리즘으로 암호화 하여 전송
// (기본 암호화 알고리즘이 없으면 암호화 하지 않음)
session->SendMessage("echo", message);

// ChaCha20 알고리즘으로 암호화 하여 전송
session->SendMessage("login", message, kChacha20Encryption);

// AES128 로 암호화 하여 전송
session->SendMessage("login", message, kAes128Encryption);

// ife1 알고리즘으로 암호화 하여 전송
session->SendMessage("login", message, kIFunEngine1Encryption);

// ife2 알고리즘으로 암호화 하여 전송
session->SendMessage("login", message, kIFunEngine2Encryption);
// manifest 에 설정에 따라 기본 암호화 알고리즘으로 암호화 하여 전송
// (기본 암호화 알고리즘이 없으면 암호화 하지 않음)
session.SendMessage ("echo", message);

// ChaCha20 알고리즘으로 암호화하여 전송
session.SendMessage ("login", message, Session.Encryption.kChaCha20);

// ChaCha20 알고리즘으로 암호화하여 전송
session.SendMessage ("login", message, Session.Encryption.kAes128);

// ife1 알고리즘으로 암호화 하여 전송
session.SendMessage ("login", message, Session.Encryption.kIFunEngine1);

// ife2 알고리즘으로 암호화 하여 전송
session.SendMessage ("login", message, Session.Encryption.kIFunEngine2);

Note

TCP 암호화 알고리즘으로 ChaCha20 혹은 AES-128을 사용하는 경우, ECDH 키 교환 알고리즘에서 사용할 비밀키가 필요합니다.

우선 서버 MANIFEST.json 에 아래와 같은 데이터가 있으면, 클라이언트에서는 public_key 에 해당하는 부분을 씁니다.

"SessionService": {
  // Client should use public_key = "161c770..."
  // 클라이언트에서는 public key로 "161c770..." 를 사용해야 합니다.
  "encryption_ecdh_key": "79f4d3c53..."
}

위 데이터가 없거나, 새로 키를 생성하려면 아래와 같은 과정이 필요합니다. 명령행에서 다음 명령을 실행합니다.

$ funapi_key_generator --type=ecdh
private key: e71c121682418194c50baa2bc19f252ca529a5419a731dcbdd1674d2a0352175
public key: cd35cd59fed7ea0fccaa88bed1dc3c74c0047def1d2dcfdd39b0d21a3ad44b15

MANIFEST.json 의 encryption_ecdh_keyprivate_key 값을 지정해주시면 됩니다. 그리고 클라이언트 플러그인 쪽에는 public key 로 출력한 값을 지정해주시면 됩니다.

Important

위 키 값들은 예제일 뿐이니 절대로 이 값을 복사해서 쓰지 마시고, 저 명령을 직접 돌려서 새로운 키를 만들어서 쓰시기 바랍니다.

Important

ChaCha20 혹은 AES-128 을 사용하려는 경우, 반드시 encryption_ecdh_key 값을 변경하시고, 클라이언트 플러그인에도 해당하는 공개키를 지정해주셔야 합니다.

9.2.2. 메시지 리플레이 공격 차단

아이펀 엔진은 패킷 리플레이 공격을 막는 기능을 제공합니다.

네트워킹 기능 설정 파라미터use_sequence_number_validationtrue 로 설정하면 패킷 리플레이 공격을 차단하는 기능이 작동됩니다.

9.3. 메시지 압축

네트워크 메시지 전송량을 줄이기 위해서 압축 기능을 사용할 수 있습니다. 해당 기능은 특히 JSON 메시지에 대해서 효과적입니다.

패킷 압축 기능은 개별 프로토콜 별로 따로 설정합니다. (TCP, UDP, ...) 압축 기능을 사용하려는 프로토콜에 대해서 다음 값을 지정해야 합니다.

  • 압축 알고리즘
  • 압축할 최소 메시지 크기. 해당 값보다 작은 메시지는 압축하지 않습니다.
  • 부가적으로, 압축에 사용할 사전. (미리 클라이언트와 공유해야하며, 값이 다르면 압축을 풀 수 없습니다)

예를 들어 TCP의 경우 네트워크 기능 설정 파라미터

  • tcp_compression
  • tcp_compression_threshold
  • tcp_compression_dictionary

값들이 위 설정에 해당합니다.

클라이언트 플러그인에서도 해당 프로토콜에 대해서 같은 압축 설정을 지정해주셔야 합니다. 이 부분은 플러그인의 압축 설정 을 참고해주세요.

  • zstd : 실시간 전송해야하는 메시지에 적당한 알고리즘입니다.
  • deflate : 큰 데이터를 지연 시간을 감수하고 전송할 경우 적당한 알고리즘입니다.

압축 알고리즘 zstd 를 사용하는 경우, 서버와 클라이언트가 사전 데이터를 공유해서 더 빠르고 더 작은 메시지를 주고 받을 수 있습니다.

9.4. 긴급 메시지

Session 으로 전송된 메시지는 엔진 이벤트의 태그 에 설명된 순서에 따라 서버에서 처리됩니다. 만약 이 순서를 무시하고 긴급하게 처리되어야 하는 메시지가 있다면 아래처럼 urgent 메시지로 지정하여 보내면 됩니다.

  • JSON 을 사용하는 경우 _urgent 필드를 추가하여 true 로 설정합니다.
  • Protobuf 를 사용하는 경우 FunMessageurgent 필드를 true 로 설정합니다.

9.5. 서버의 IP 주소 알아내기

클라이언트가 서버에 접속하기 위해서는 클라이언트가 접속할 수 있는 서버의 IP 를 알아내야 합니다. 라이브 서버의 경우 대부분 공인 IP 가 되겠지만, 내부 개발환경의 경우에는 가상 IP 가 될 수도 있습니다.

그리고 접속 IP 는 실서버에 부여된 IP 를 직접 쓰는 경우도 있고, 앞단에 로드밸런서를 쓰는 경우는 로드밸런서의 IP 를 써야되는 경우도 있고, 개발자 데스크탑에서 가상머신으로 서버를 돌리는 경우처럼 가상네트워크 NAT에서의 특정 IP, 포트로 지정해야 될 수도 있습니다.

아이펀엔진은 이런 경우들에 대해서 손쉽게 서버의 IP 를 알아내거나 직접 지정하는 방법을 제공합니다. 그리고 이런 방법들은 우선 순위에 따라 여러개를 나열할 수 있어서, 개발서버를 라이브로 옮기는 것처럼 다른 환경으로 옮기더라도 설정 파일 수정 없이 여러 환경을 지원할 수도 있습니다.

MANIFEST.jsonHardwareInfo 콤포넌트에서 external_ip_resolvers 를 이용합니다. 아래는 사용 예시입니다. 콤마로 구분해서 우선 순위에 따라 공인 IP를 얻어오는 방법을 나열합니다.

"HardwareInfo": {
  "external_ip_resolvers": "aws,nic:eth0,nat:192.0.2.113:tcp+pbuf=9012:http+json=9018"
}

현재 지원되는 IP 지정방식

  • aws: AWS 상에 가상 머신이 있을 때 AWS 의 관리 API 를 통해 IP 를 얻어내어 이를 클라이언트가 접속할 IP 로 취급합니다. (http://169.254.169.254/latest/meta-data/public-ipv4/ 를 호출합니다. )

  • nic:이름: 네트워크 카드 중 해당 이름을 갖는 네트워크 카드의 IP 주소를 읽어서 이를 클라이언트가 접속할 IP 로 취급합니다.

  • nat:주소:프로토콜=포트:프로토콜=포트:...: 로드밸런서, 방화벽, 공유기 등 Network Address Translation (NAT) 기능을 수행하는 장비 뒤에 서버가 있을 경우, 수동으로 공인 IP 정보를 입력합니다.

    주소에 IP 주소 혹은 DNS 주소를 입력하고, 프로토콜=포트는 선택적으로 줄 수 있는데, NAT 로 인해 공인 포트 번호가 서버의 포트 번화와 다를 때 입력합니다. 가능한 프로토콜로는 tcp+pbuf, tcp+json, udp+pbuf, udp+json, http+pbuf, http+json 이 있습니다.

Tip

위에 나열된 공인 IP 얻는 방법은 먼저 기재된 순으로 처리되며, 만일 실패하면 다음 방법을 시도합니다.

따라서 위의 예시는 aws 에 라이브 서버가 돌고, 내부 공용 개발 서버는 네트워크 카드에 할당된 IP 를 쓰고, 개발자들은 자신의 데스크탑에 가상 서버에 개인 개발서버를 할당 하는 경우에 설정 하나로 모든 경우를 다룰 수 있습니다. 이렇게 하면 나중에 서버를 라이브 서버로 배포할 때 별도로 손대야되는 파일이 줄어들어 편리합니다.

Note

NAT 의 경우 외부로 패킷을 보내보고 그 결과를 반환해서 자신의 IP 를 알려주는 서비스들이 있습니다. 하지만, 이런 경우를 엔진이 자동으로 포함할 경우 해당 사이트가 다운되면 게임 서버도 뜨지 않게되는 문제가 있어서, 아이펀 엔진에서 이 방법은 지원하지 않습니다. 또한 NAT 의 경우 IP 뿐만 아니라 포트까지 맵핑을 하는 경우가 있어서 IP 를 알아내는 것만으로는 어차피 불완전합니다.

그리고 이렇게 얻어진 게임 서버의 IP 와 포트 정보는 include/funapi/system/hardware_info.h 에서 HardwareInfo 클래스 메소드로 알아낼 수 있습니다.

class HardwareInfo : private boost::noncopyable {
  enum FunapiProtocol {
    kTcpPbuf = 0,
    kTcpJson,
    kUdpPbuf,
    kUdpJson,
    kHttpPbuf,
    kHttpJson,
  };

  // 포트 정보를 담는 맵 타입입니다.
  typedef std::map<FunapiProtocol, uint16_t> ProtocolPortMap;

  // 위의 방법으로 얻어낸 공인 IP 를 반환합니다.
  static boost::asio::ip::address GetExternalIp();

  /// 위의 방법으로 얻어낸 공인 포트들을 반환합니다.
  /// NAT 방법으로 명시적으로 포트를 지정한 경우 그 포트 번호가 반환되며,
  /// NAT 를 쓰되 포트를 지정하지 않았거나, NAT 가 아닌 방법을 썼을 때는
  /// Session 서비스에서 열려 있는 포트 번호가 반환됩니다.
  static ProtocolPortMap GetExternalPorts();
};
// 포트 정보를 담는 딕셔너리 타입입니다.
using ProtocolPortMap = Dictionary<HardwareInfo.FunapiProtocol, ushort>;

public static class HardwareInfo
{
  public enum FunapiProtocol
  {
    kTcpPbuf = 0,
    kTcpJson = 1,
    kUdpPbuf = 2,
    kUdpJson = 3,
    kHttpPbuf = 4,
    kHttpJson = 5
  }

  // 위의 방법으로 얻어낸 공인 IP 를 반환합니다.
  public static IPAddress GetExternalIp ();

  /// 위의 방법으로 얻어낸 공인 포트들을 반환합니다.
  /// NAT 방법으로 명시적으로 포트를 지정한 경우 그 포트 번호가 반환되며,
  /// NAT 를 쓰되 포트를 지정하지 않았거나, NAT 가 아닌 방법을 썼을 때는
  /// Session 서비스에서 열려 있는 포트 번호가 반환됩니다.
  public static ProtocolPortMap GetExternalPorts ();
}

Tip

만일 아이펀 엔진의 분산처리 Part 1: ORM, RPC, 로그인 을 사용하는 경우, 클라가 접속할 서버 주소 뿐만 아니라, 서버들이 상호 통신할 IP 를 얻어내야될 수 있습니다. 이 경우는 다른 서버의 공인 IP 를 참고해주세요.

9.6. HTTP 클라이언트

아이펀 엔진은 외부 시스템을 손쉽게 호출할 수 있는 HttpClient 를 지원합니다. 보다 자세한 내용은 아이펀 엔진의 HTTP client 를 참고하세요.

9.7. 네트워킹 기능 설정 파라미터

아래의 설명과 설정 파일 (MANIFEST.json) 상세 를 참고하여 SessionService 관련 설정을 합니다.

포트 관련 설정들

  • tcp_json_port: JSON 패킷을 주고 받을 서버의 TCP 포트 번호. 0 이면 비활성화입니다. (type=uint64, default=8012)
  • udp_json_port: JSON 패킷을 주고 받을 서버의 UDP 포트 번호. 0 이면 비활성화입니다. (type=uint64, default=0)
  • http_json_port: JSON 패킷을 주고 받을 서버의 HTTP 포트 번호. 0 이면 비활성화입니다. (type=uint64, default=8018)
  • tcp_protobuf_port: Protobuf 패킷을 주고 받을 서버의 TCP 포트 번호. 0 이면 비활성화입니다. (type=uint64, default=0)
  • udp_protobuf_port: Protobuf 패킷을 주고 받을 서버의 UDP 포트 번호. 0 이면 비활성화입니다. (type=uint64, default=0)
  • http_protobuf_port: Protobuf 패킷을 주고 받을 서버의 HTTP 포트 번호. 0 이면 비활성화입니다. (type=uint64, default=0)

세션 관리 관련 설정들

  • session_timeout_in_second: 세션을 폐기하기 위해서 기다릴 초단위 유휴 시간. (type=uint64, default=300)

  • use_session_reliability: 세션 수준의 reliability 기능을 켬. 이는 세션을 다시 연결 할 때에도 패킷 손실이 발생하지 않는 것을 보장함. 자세한 내용은 세션의 메시지 전송 안정성 를 참고하세요. (type=bool, default=false)

  • use_sequence_number_validation: 만일 메시지의 시퀀스 번호가 잘못된 경우 메시지를 처리하지 않음. 이는 메시지 replay attack 을 막기 위함임. TCP 와 HTTP 에서만 동작함. 자세한 내용은 메시지 리플레이 공격 차단 를 참고하세요. (type=bool, default=false)

  • session_rate_limit_per_minute: 클라이언트가 세션에 보낼 수 있는 분당 메시지 수 제한. 0이면 제한하지 않습니다. 제한을 초과하는 메시지를 전송하면 다음과 같이 처리합니다.

    • TCP, WebSocket: 메시지가 허용될 때까지 대기한 후에 처리합니다.
    • HTTP: 해당 메시지에 대한 HTTP 응답 코드를 429로 전송합니다. 이 경우 클라이언트 플러그인은 에러 콜백 함수를 호출합니다.
    • UDP: 분당 메시지 수 제한을 초과한 메시지를 무시하고 처리하지 않습니다.

암호화 관련 설정들

암호화 관련해서 자세한 내용은 메시지 암호화 을 참고하세요.

  • use_encryption: 암호화 기능을 끄고 켬. (type=bool, default=false)

  • tcp_encryptions: 암호화 기능이 켜졌을 때, TCP 프로토콜에 적용될 암호화 방식들의 리스트.

    빈값으로 설정하여 암호화 하지 않거나 ife1, ife2, chacha20, aes128 중 하나 또는 모두 사용 가능함. 예) [] 또는 ["ife1", "ife2"], ["chacha20"]

  • udp_encryptions: 암호화 기능이 켜졌을 때, UDP 프로토콜에 적용될 암호화 방식들의 리스트.

    빈값으로 설정하여 암호화 하지 않거나 ife2 사용 가능. 예) [“ife2”]

  • http_encryptions: 암호화 기능이 켜졌을 때, HTTP 프로토콜에 적용될 암호화 방식들의 리스트

    빈값으로 설정하여 암호화 하지 않거나 ife2 사용 가능. 예) [“ife2”]

  • encryption_ecdh_key: 암호화 기능이 켜졌을 때, AES 와 ChaCha20 의 세션 키 교환을 위해 사용되는 서버 측 비밀키

압축 관련 설정들

압축 알고리즘은 기본 값이 "none" 이며, "zstd""deflate" 를 사용할 수 있습니다.

  • tcp_compression: TCP 에서 사용할 압축 알고리즘
  • tcp_compression_threshold: 압축할 최소 메시지 크기. 이보다 작은 메시지는 압축하지 않습니다.
  • tcp_compression_dictionary: 클라이언트와 미리 공유한 사전 데이터 (zstd 전용).
  • udp_compression, udp_compression_threshold, udp_compression_dictionary : TCP 와 유사한 옵션. UDP 프로토콜에 적용합니다.
  • http_compression, http_compression_threshold, http_compression_dictionary : HTTP 프로토콜에 적용합니다.
  • websocket_compression, websocket_compression_threshold, websocket_compression_dictionary : WebSocket 프로토콜에 적용합니다.

TCP 관련된 설정들

  • disable_tcp_nagle: TCP 세션을 사용할 때 TCP_NODELAY 소켓 옵션을 세팅함으로써 Nagle 알고리즘 을 끔. (type=bool, default=true)

디버깅 및 모니터링 관련된 설정들

  • enable_http_message_list: 이 옵션이 켜져 있으면, HTTP 를 사용해서 GET /v1/messages 을 할 때 RegisterHandler() 함수로 등록된 message type 들을 볼 수 있게 해줍니다. 자세한 내용은 (고급) 아이펀 엔진의 네트워크 스택 의 HTTP 관련 내용을 참고하세요. 개발에 편리하지만 보안상 이유로 라이브 단계에서는 false 로 설정할 것을 권장합니다. (type=bool, default=true)

  • session_message_logging_level: 세션 메시지 로그 레벨. 0 은 로그를 남기지 않음. 1 은 패킷 타입과 길이 정보만 남김. 2는 패킷의 내용까지 남김. (type=uint64, default=0)

    Tip

    2 로 세팅하면 개발과정 중에 주고 받는 메시지를 확인할 때 유용합니다. 단, 서버에 부하를 줄 수 있으므로 상용 서비스에서는 권장하지 않습니다.

  • enable_per_message_metering_in_counter: Counter 를 통해서 클라이언트-서버간의 통신량에 대한 정보를 HTTP RESTful 로 제공합니다. 자세한 내용은 Counter 를 참고하세요. 개발에 편리하지만 서버에 상당한 과부하를 초래하기 때문에 false 로 설정할 것을 권장합니다. (type=bool, default=false)

  • json_protocol_schema_dir: 패킷 형식으로 JSON 을 사용할 때, JSON 패킷의 유효성을 검증할 스키마 파일이 들어있는 디렉토리 경로. 자세한 내용은 JSON 메시지 스키마 자동 검증 를 참고해주세요. (type=string, default=””)

  • ping_sampling_interval_in_second: RTT 계산을 위한 ping 샘플링 인터벌을 초단위로 지정합니다. 0 은 동작을 끕니다. 세션 Ping(RTT) 을 참고해주세요. (type=uint64, default=0)

  • ping_message_size_in_byte: 전송할 ping 메시지 크기. 세션 Ping(RTT) 을 참고해주세요. (type=uint64, default=32)

  • ping_timeout_in_second: 만일 지정된 시간 동안 Ping 응답이 오지 않을 경우 연결을 끊습니다. 0 은 동작을 중단함. 세션 Ping(RTT) 을 참고해주세요. (type=uint64, default=0)

직접 설정을 바꿀 일이 거의 없는 설정들

  • close_transport_when_session_close: 세션을 닫을 때 딸려있는 Transport(연결)도 같이 종료함. (type=bool, default=true)

  • send_session_id_as_string: 클라이언트-서버 통신 중에 세션 ID 를 보낼 때 이를 binary 로 할지 문자열로 할지 결정. (type=bool, default=true)

    Important

    이 기능을 쓰기 위해서는 클라이언트 플러그인 버전이 다음 이상이여야 합니다.

    • Unity3D: 190
    • Unreal4: 35
    • Cocos2d-x: 35
  • send_session_id_only_once: 클라이언트-서버 TCP, UDP 통신 중에 세션 ID 를 첫 메시지에서만 보내고 그 이후 메시지에는 생략하여 네트워크 트래픽을 줄일지 결정. (type=bool, default=false)

  • network_io_threads_size: 클라이언트-서버 패킷 처리를 담당할 쓰레드 갯수. (type=uint64, default=4)

9.8. 멀티 프로토콜

아이펀 엔진은 TCP, UDP, HTTP 를 동시에 사용할 수 있습니다. 예를 들어 HTTP 로 PvE 처리를 하고 TCP 나 UDP로 PvP 처리를 할 수 있습니다. 네트워킹 기능 설정 파라미터 에서 사용하려는 프로토콜의 port 를 0 이 아닌 값으로 설정하면 동시에 사용 가능합니다.

9.8.1. 전송 프로토콜 명시적 선택

Session::SendMessage() 또는 AccountManager::SendMessage() 를 호출할 때 아래와 같이 kTcp, kUdp, kHttp 중 하나로 전송 Protocol 을 지정할 수 있습니다.

// HTTP 로 "echo" message 를 보냅니다.
session->SendMessage("echo", message, kDefaultEncryption, kHttp);

// TCP 로 "echo" message 를 보냅니다.
session->SendMessage("echo", message, kDefaultEncryption, kTcp);

// UDP 로 "echo" message 를 보냅니다.
session->SendMessage("echo", message, kDefaultEncryption, kUdp);
// HTTP 로 "echo" message 를 보냅니다.
session.SendMessage("echo", message, Session.Encryption.kDefault, Session.Transport.kHttp);

// TCP 로 "echo" message 를 보냅니다.
session.SendMessage("echo", message, Session.Encryption.kDefault, Session.Transport.kTcp);

// UDP 로 "echo" message 를 보냅니다.
session.SendMessage("echo", message, Session.Encryption.kDefault, Session.Transport.kUdp);

9.8.2. 전송 프로토콜 자동 선택

프로토콜을 생략하거나 kDefaultProtocol 를 지정하면 아래 우선 순위에 따라 프로토콜이 자동 선택됩니다.

  1. Message Handler 에서 전송할 경우 해당 Message 를 수신한 Protocol.
  2. SetTransport(msgtype, protocol) 함수로 지정된 기본 Protocol.
  3. SetTransport(protocol) 함수로 지정된 기본 Protocol.
  4. 네트워킹 기능 설정 파라미터 에 활성화된 Port 가 하나인 경우 해당 Protocol.(예, tcp_json_port 만 0 이 아닌 값이면 kTcp)

2, 3 번에 해당하는 우선순위를 아래 함수로 지정할 수 있습니다. 서버가 시작될 때 설정하는 것이 좋습니다.

SetTransport(msg_type, protocol)
SetTransport(protocol)

예제: TCP 와 UDP 를 동시에 사용할 때

bool Install(...) {
  // "login" 메시지 핸들러를 등록합니다. 이 핸들러에서 전송할 경우 메시지를
  // 수신한 프로토콜이 선택됩니다. 위에 설명된 우선순위 1 번에 해당합니다.
  HandlerRegistry::Register("login", OnLogin);

  // "buy_item" message 는 TCP 로 보내도록 설정합니다.
  SetTransport("buy_item", kTcp);

  // "update" message 는 UDP 로 보내도록 설정합니다.
  SetTransport("update", kUdp);

  // 그 외의 지정되지 않은 message 는 TCP 로 보내도록 설정합니다.
  SetTransport(kTcp);
}

// 이 함수는 "login" 메시지 핸들러이며 클라이언트는 TCP 로 이 메시지를
// 전송합니다.
void OnLogin(const Ptr<Session> &session, const Json &message) {
  ...
  // "login" 메시지는 TCP 로 수신했기 때문에 TCP 로 전송됩니다.
  session->SendMessage("login", reply_message);

  // 주의) 이 함수에서 Event::Invoke() 하여 실행되는 함수에서 보내면
  // TCP 우선순위가 없어집니다.
}

void BuyItem(...) {
  ...
  // "buy_item" 메시지는 SetTransport("buy_item", kUdp) 에 의해 UDP 가
  // 기본값 입니다. 따라서 이 메시지는 UDP 로 전송됩니다.
  session->SendMessage("by_item", reply_message);
}

void SendSomething(...) {
  ...
  // "something" 메시지는 지정된 전송 프로토콜이 없습니다.
  // 따라서 SetTransport(kTcp) 에 의해 TCP 로 전송됩니다.
  session->SendMessage("something", reply_message);
}

Warning

프로토콜을 자동으로 선택할 수 없는 경우 아래와 같은 로그가 출력됩니다. 이 때에는 SetTransport() 로 적당한 전송 프로토콜을 지정하거나, SendMessage() 함수에 명시적으로 전송 프로토콜을 지정해야합니다. ambiguous transport protocol for sending '...' message.

9.9. (고급) 아이펀 엔진의 네트워크 스택

Note

아래 설명은 아이펀 엔진 서버와 호환되는 클라이언트 모듈을 직접 제작하려는 개발자나 아니면 아이펀 엔진의 네트워킹에 대해서 더 알고 싶어하는 고급 개발자를 위한 것입니다. 일반 유저들은 아이펀팩토리 Github 계정 에 있는 클라이언트 플러그인을 사용하기만 하면 됩니다.

아이펀 엔진은 다양한 네트워크 환경에서 효율적이며, 쉽게 사용할 수 있도록 다양한 프로토콜을 선택적으로 사용 할 수 있습니다. 크게 Transport 계층, Message 계층, Session(Application) 계층으로 계층화 되어 있으며, Transport 계층은 TCP, UDP 는 물론 모바일 환경에 친숙한 HTTP 를 지원하며 Session/Application 계층은 JSONGoogle Protocol Buffers 를 지원합니다. 목표하는 네트워크 환경에 맞게 Transport 계층과 Session/Application 계층을 조합하여 사용할 수 있습니다.

Transport 계층으로 TCP, UDP 를 사용할 경우 protocol 의 버전, 암호화 및 메시지의 크기 등 제어정보를 포함하는 별도의 헤더를 앞에 붙여서 Message 계층을 만듭니다. 이 때 헤더 구조는 HTTP 와 유사하게 줄마다 키-밸류를 문자열로 기술하는 형태입니다.

Transport 계층으로 HTTP 를 쓸 경우 아이펀 엔진은 별도의 메시지 계층을 만들지 않고 HTTP 헤더에 필요한 제어 정보를 포함시킵니다.

아래는 Session/Application layer 에 따라 구분된 2 가지의 networking stack diagram 입니다.

아이펀 엔진 네트워킹 스택 - JSON 메시지

그림 1) 아이펀 엔진 네트워킹 스택 - JSON 메시지

_images/funapi_networking_stack_protobuf.png

그림 2) 아이펀 엔진 네트워킹 스택 - Google Protobuf 메시지

9.9.1. Transport 계층

Transport layer 는 TCP, UDP, HTTP 를 사용 가능하며, 동시에도 사용 가능합니다. 이는 HTTP 또는 TCP 로 로그인, 결제 등의 빈도가 낮고 중요한 데이터의 송수신을 하고, UDP 로 실시간 데이터 동기화를 하는 등의 기능을 손쉽게 구현 가능하게 하는 장점을 제공합니다.

9.9.2. Message 계층

Message layer 는 Transport layer 로 TCP, UDP 를 사용할 경우에만 사용하게 됩니다. 메시지 계층은 추가적인 헤더를 붙여서 프로토콜의 버전 식별, 암호화 등의 역할을 수행합니다. 아래는 메시지 계층의 구조를 나타냅니다.

HEADER_KEY1:HEADER_VALUE1
HEADER_KEY2:HEADER_VALUE2
HEADER_KEY3:HEADER_VALUE3

{세션 계층으로 넘어갈 데이터}

Message 는 ‘header’ 와 ‘body(payload)’ 로 이루어지는데, header 는 HTTP 의 header 와 유사하게 각 라인이 KEY:VALUE 형태를 이루며, header 와 body 는 공란으로 구분됩니다. 현재 사용되는 header 는 다음 세 가지가 있습니다.

  • VER: 아이펀 엔진 Message 계층의 version 을 뜻합니다. 현재로서는 반드시 1이어야 합니다.
  • LEN: Header 를 제외하고 순수하게 body 의 길이를 뜻합니다. (Session/Application layer message 인 json 또는 protobuf 의 크기)
  • ENC: encryption 알고리즘을 명시합니다.

9.9.3. Session/Application 계층

Session/Application 계층은 개발이 쉬운 JSON 와 효율적인 Google Protocol Buffers 2 가지의 message format 을 지원합니다. 필요에 따라 하나를 선택하거나, 동시에 사용할 수 있습니다.

Session 계층의 패킷은 session 식별을 위한 “_sid”, 패킷 타입 식별을 위한 “_msgtype” 2 개의 header 를 항상 포함합니다.

  • msgtype: client-server 패킷 타입을 문자열 형태로 정의합니다. 아이펀 엔진은 이 타입 값에 따라 등록된 패킷 핸들러를 호출하게 됩니다.

    Important

    패킷 타입 중 밑줄 (underscore 또는 _) 로 시작하는 타입들은 아이펀 엔진에 의해서 사용되니 게임에서 사용해서는 안됩니다. 그 몇가지 예는 다음과 같습니다.

    • _session_opened: 새 session id 를 할당하는 경우 서버에서 클라이언트로 전송됩니다.(단, Transport lyaer 가 HTTP 이면 별도의 _session_opened 메시지를 전송하지 않습니다. request 에 대한 response 에 sid 가 포함됩니다) 클라이언트는 여기서 알게된 session id 를 이후에 서버로 보내는 메시지에 사용해야 합니다.
    • _session_closed: 서버에서 클라이언트로 보내는 메시지 타입으로, 이미 닫힌 session 임을 알려줍니다.
  • sid: session 을 구분하는 id 를 정의합니다. TCP 처럼 연결 지향적인 protocol 을 사용하면 클라이언트가 연결을 잃어버리는 경우 게임은 이를 복구할 수 있어야 합니다. 아이펀 엔진은 이런 연결 복원을 자동적으로 수행하는데, 그때 참고가 되는 것이 이 sid 입니다. 같은 sid 는 같은 session 으로 인식되며, session 이 idle 상태로 timeout 될때까지 client 는 연결을 복원할 수 있습니다. client 가 최초로 접속할 때는 이 sid 를 생략할 수 있습니다. 아이펀 엔진은 sid 가 없는 경우 새로 session id 을 할당하고 이를 _session_opened 라는 메시지 타입으로 클라이언트에 전송하게 됩니다. 보다 자세한 내용은 (고급) 아이펀 엔진 세션 상세 을 참고해주세요.

9.9.3.1. Session/Application 계층 - JSON 메시지 포맷

Body 는 JSON 으로 이루어지는데, JSON 안에는 게임 개발자가 임의의 key 와 value 를 넣을 수 있습니다. 이때문에 아이펀 엔진 게임들은 client-server 간에 훨씬 유연하고 높은 자유도를 갖고 연동을 할 수 있습니다.

{
  "_msgtype": "패킷 타입",
  "_sid": "세션 아이디",

  // 게임별 패킷 필드들이 여기 추가됩니다.
}

9.9.3.2. Session/Application 계층 - Google Protocol Buffers 메시지 포맷

Body 는 FunMessage 를 extend 하여 자유롭게 구성할 수 있습니다.

// 최상위 레벨의 protobuf 입니다. 게임 패킷들은 이 protobuf 안에 실려가야됩니다.
message FunMessage {
  optional string sid = 1;
  optional string msgtype = 2;
  extensions 16 to max;
}

// 게임 패킷 은 FunMessage 의extension 형태여야 합니다.
// 예를 들어, 다음과 같은 메시지가 있다고 가정하겠습니다.
//   message MyMessage {
//    ...
//   }
//
// 이제 MyMessage 를 FunMessage 에 다음처럼 실어 보낼 수 있습니다.
//   extend FunMessage {
//     optional MyMessage mymessage = 16;
//   }

9.9.3.3. HTTP

HTTP 는 Message 계층을 사용하는 TCP, UDP 와 달리 HTTP 헤더를 직접 사용하며 HTTP 고유의 특징을 이용하여 다음과 같이 상위 layer 의 일부 기능을 특수하게 처리할 수 있습니다.

http://server-url/v{version}/messages/{message-type}

  • version: Message 계층의 “VER” 값과 같습니다. 현재로서는 반드시 1이어야 합니다.
  • message-type: Session 계층의 “_msgtype” 값과 같습니다.(optional)

예)

  • http://mygame.com:8018/v1/messages/login (msgtype 이 “login” 으로 처리됩니다.)
  • http://mygame.com:8018/v1/messages/buy (msgtype 이 “buy” 으로 처리됩니다.)

또는, http://server-url/v{version} 로 모든 message 를 보낼 수 있습니다. 하지만 이렇게 보낼 때에는 http body 에 반드시 _msgtype 을 포함해야합니다.

http://server-url/v{version}/messages 에서 RegisterHandler() 함수로 등록한 모든 message type 을 볼 수 있습니다.

Note

여기 에 설명된 enable_http_message_list 옵션을 true 로 설정해야 작동합니다.

예) http://mygame.com:8018/v1/messages/

[
  "echo",
  "login",
  "join",
  "buy"
]

9.10. (고급) 아이펀 엔진 세션 상세

Note

아래 설명은 아이펀 엔진에서 세션이 어떻게 동작하는지에 관심이 있는 개발자를 위한 것입니다. 만일 아이펀팩토리 Github 계정 에 제공되는 클라이언트 플러그인을 사용하신다면 아래 내용은 그냥 지나치셔도 됩니다.

모바일 환경에서는 핸드폰의 위치 변경으로 기지국이 변경되거나, WiFi 망과 3G/LTE 망 간을 이동하는 경우 IP 가 변경될 수 있습니다. 이 때문에 IP 와 port 로 client 를 구분하는 전통적인 방법은 문제가 됩니다. 이 때문에, 아이펀 엔진은 다양한 transport 프로토콜들 위에 session 계층을 제공합니다.

아이펀 엔진의 session 은 IP, port 로 client 를 구분하는 대신 유니크한 session id 를 이용해 client 를 구현합니다. 이를 위해서 앞에서 설명된 메시지 타입에서 JSON body 부분에 “_sid” 라는 예약된 key가 사용됩니다. 클라이언트가 처음 접속을 맺는다거나 하는 경우는 sid 를 모르기 때문에 sid 를 보내지 않아도 되지만, 이후에 서버에서 sid 를 알려주게 되면 그 sid 를 계속 써야됩니다. 예를 들어 Tutorial 의 hello world 서버를 보면, 클라이언트 와 서버가 주고 받는 메시지는 다음과 같습니다.

Note

아래 예제에서 OS 에 따라 telnet 의 end of line 이 다를 수 있습니다. 아래 예는 CR/LF (2 bytes) 의 경우입니다. LEN 값 계산에 주의하시기 바랍니다.

$ telnet localhost 8012
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
VER:1
LEN:26

{
 "msgtype":"hello"
}

이렇게 client 가 server 에게 sid 없이 “hello” 라는 메시지를 전송했습니다. server 는 sid 를 할당하고 “_session_opened” 라는 메시지를 client 쪽으로 전송하고, client 가 요청한 “hello” 메시지를 처리해서 “world” 라는 메시지를 돌려줍니다.

VER: 1
LEN: 91

{
    "_msgtype" : "_session_opened",
    "_sid" : "d507b0ee-6960-4c7d-b976-1452cc946cd0"
}VER: 1
LEN: 81

{
    "_msgtype" : "world",
    "_sid" : "d507b0ee-6960-4c7d-b976-1452cc946cd0"
}

이 때 “world” message 에 _sid 가 포함되어있다는 것에 유의해주세요. 이 sid 가 client 를 특징 짓는 키가 됩니다. client 가 연결을 끊었다가 다시 맺는 경우를 보겠습니다.

$ telnet localhost 8012
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
VER:1
LEN:78

{
 "_msgtype":"hello",
 "_sid" : "d507b0ee-6960-4c7d-b976-1452cc946cd0"
}

이번에는 client 가 앞에서 받은 sid 를 같이 보냅니다. server 는 다음과 같이 응답합니다.

VER: 1
LEN: 81

{
    "_msgtype" : "world",
    "_sid" : "d507b0ee-6960-4c7d-b976-1452cc946cd0"
}

HTTP transport 를 사용하면 다음과 같습니다.

$ telnet localhost 8018
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
POST /v1 HTTP/1.1
Content-Type: application/json
Content-Length: 27

{
  "_msgtype":"hello"
}

응답은 다음과 같습니다.

HTTP/1.1 200 OK
Content-Length: 81
Content-Type: application/json

{
    "_msgtype" : "world",
    "_sid" : "9902b1dc-7737-4c84-832a-2f25929bbfd7"
}

또는 msgtype 을 url 에 포함하여 아래와 같이 보낼 수 있습니다.

$ telnet localhost 8018
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
POST /v1/hello HTTP/1.1
Content-Type: application/json
Content-Length: 4

{}

응답은 다음과 같습니다.

HTTP/1.1 200 OK
Content-Length: 81
Content-Type: application/json

{
    "_msgtype" : "world",
    "_sid" : "9902b1dc-7737-4c84-832a-2f25929bbfd7"
}

Session 은 ‘현재 접속한 사용자’를 의미 하므로, 일정 시간 아무것도 안하는 (즉, 아무 패킷도 전송하지 않은) 클라이언트는 자동으로 session timeout 을 하게 됩니다. 이 timeout 값은 MANIFEST.json 안의 SessionService component 의 session_timeout_in_second 라는 인자로 줄 수 있습니다.