10. 네트워킹 Part 2

10.1. 메시지 포맷

10.1.1. JSON 메시지

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

10.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"]);

10.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 을 참고하세요.

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

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

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

10.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 문서를 참고해주세요.
10.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 문서를 참고해주세요.

10.1.2. Protobuf 메시지

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

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

Important

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

10.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;

  ...
}

10.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" 라는 메시지 타입으로 수신할 수 있습니다.

10.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;
  }
  ...
}

10.2. 네트워크 보안

10.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 값을 변경하시고, 클라이언트 플러그인에도 해당하는 공개키를 지정해주셔야 합니다.

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

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

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

10.3. 메시지 압축

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

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

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

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

  • tcp_compression
  • tcp_compression_threshold
  • tcp_compression_dictionary

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

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

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

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

10.4. 긴급 메시지

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

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

10.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 를 참고해주세요.

10.6. HTTP 클라이언트

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

10.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)

네트워크 인터페이스 관련 설정

TCP, HTTP, UDP 혹은 WebSocket 포트를 열면 기본 동작으로 모든 주소에서 들어오는 연결을 받아들입니다. (0.0.0.0 에 바인드합니다) 이런 기본 동작 대신 몇 가지 이유로 특정 인터페이스만 사용하도록 할 수 있습니다.

  • 테스트나 보안 상의 이유로 특정 인터페이스만 사용하려는 경우.
  • 여러 개의 IP 주소를 하나의 NIC 를 통해서 받아들이는 설정에서 UDP 통신을 해야하는 경우.

이 때 다음 설정 값을 변경해서 eth0eno1,eno2 같은 특정 네트워크 인터페이스에서만 클라이언트 연결을 받아들이기 합니다. 만약 비어있다면 0.0.0.0 주소를 사용해서 모든 NIC를 활용합니다. 아래의 내용은 모두 , 로 구분한 NIC 목록입니다.

  • tcp_nic: TCP 연결을 받아들일 NIC 목록.
  • udp_nic: UDP 연결을 받아들일 NIC 목록.
  • http_nic: HTTP 연결을 받아들일 NIC 목록.
  • websocket_nic: WebSocket 연결을 받아들일 NIC 목록.

세션 관리 관련 설정

  • 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)

10.8. 멀티 프로토콜

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

10.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);

10.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.

10.9. HTTP 교차 출처 자원 공유 (CORS)

웹 브라우저 기반의 클라이언트를 개발하는 경우 HTTP 교차 출처 자원 공유 (HTTP Cross-Origin-Resource-Sharing; 이하 CORS) 설정이 필요합니다. (Unity WebGL 등을 사용하는 경우)

MANIFEST.jsonSessionService 설정을 변경하면 해당 기능을 사용하실 수 있습니다.

"SessionService": {
  // ...
  // HTTP CORS related settings
  "http_enable_cross_origin_resource_sharing": false,
  "http_cross_origin_resource_sharing_allowed_origins": ["*"]
}
  • http_enable_cross_origin_resource_sharing: HTTP CORS 기능을 활성화할지 여부. 설정한 경우 필요한 HTTP 헤더를 자동으로 전송합니다.

  • http_cross_origin_resource_sharing_allowed_origins: HTTP CORS origin으로 허용한 도메인 목록. 해당 도메인 이하의 URL에서 다운로드한 클라이언트만 아이펀 엔진 게임 서버에 접속하는 것을 허용합니다.

    예를 들어 해당 값을 ["https://app1.example.com", "https://app2.example.com"] 으로 설정한 경우, https://app1.example.com/static/client1.html 에 있는 클라이언트로 게임 서버에 접속할 수 있습니다. 그러나 클라이언트의 주소가 https://other.example.com/static/client.html 처럼 다른 도메인 (서브도메인) 을 사용하는 경우, 접속에 실패합니다.

    만약, 해당 값이 ["*"] 이라면 모든 주소를 허용합니다. * 은 개발 환경 혹은 테스트 환경에서만 쓰는게 좋습니다.

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

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 메시지

10.10.1. Transport 계층

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

10.10.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 알고리즘을 명시합니다.

10.10.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 라는 메시지 타입으로 클라이언트에 전송하게 됩니다. 보다 자세한 내용은 (고급) 아이펀 엔진 세션 상세 을 참고해주세요.

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

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

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

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

10.10.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;
//   }

10.10.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"
]

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

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 라는 인자로 줄 수 있습니다.