네트워킹 Part 2¶
메시지 포맷¶
JSON 메시지¶
아이펀 엔진은 JSON 을 다루기 위하여 아이펀 엔진 API 문서 에 설명된 JSON class 를 이용합니다.
아이펀 엔진은 JSON을 다루기 위해 Newtonsoft.Json 을 사용합니다. 더 자세한 내용은 Newtonsoft.Json 을 참고해주세요.
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"]);
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 을 참고하세요.
JSON 메시지 스키마 자동 검증¶
Google Protocol Buffers 는 구조를 미리 선언하고 사용하기 때문에 serialization / deserialization을 하는 동안 필드들의 적합성 검사를 수행해주지만, JSON 은 임의의 필드들이 추가될 수 있기 때문에 JSON 자체가 적합성 검사를 제공하지는 않습니다.
따라서 JSON 패킷을 전달받은 핸들러에서는 JSON 필드들이 올바른지 검사하는 것이 반드시 필요한데, 이는 매우 번거로운 일 입니다. 아이펀 엔진은 이 작업을 쉽게 할 수 있도록 JSON 스키마를 handler 에 붙여 자동으로 message 의 파라미터를 검사하는 기능을 제공합니다. 만약 message 의 스키마가 올바르지 않으면 로그를 출력하고 핸들러로 넘겨주지 않습니다. 따라서 핸들러에서는 파라미터 검사 없이 항상 정상적인 JSON 만 넘어온다고 가정하고 작업을 할 수 있습니다.
코드 상에서 스키마 지정하기¶
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 문서를 참고해주세요.
별도의 스키마 파일 사용하기¶
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
,sc
나cs 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 문서를 참고해주세요.
Protobuf 메시지¶
여기서는 아이펀 엔진에서 Protobuf 으로 메시지를 주고 받을 수 있도록 기본적인 설명만 합니다. 자세한 설명은 Google Protocol Buffers 서 보실 수 있습니다.
프로젝트를 생성하면 {프로젝트이름}_messages.proto 파일이 자동생성되며
최상위 메시지인 FunMessage
가 포함된 파일을 import 하고 있습니다.
별도의 작업 없이 이 파일에 FunMessage
를 extend 하는 메시지를 추가하면 자동으로 메시지가 빌드되어 사용하실 수 있습니다.
Important
FunMessage
를 extend 할 때 필드 번호 1-15까지는 아이펀 엔진에서 사용하므로 사용하시면 안됩니다.
메시지 정의¶
아래는 설명을 위한 메시지 정의 예제로 캐릭터 정보를 담을 수 있도록 하였습니다.
// 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;
...
}
메시지 생성¶
아래는 위에서 정의한 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" 라는 메시지 타입으로 수신할 수 있습니다.
메시지 읽기¶
아래는 위에서 생성한 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;
}
...
}
네트워크 보안¶
메시지 암호화¶
아이펀 엔진은 서버와 클라이언트간 송수신되는 데이터의 암호화 기능을 제공합니다. 서버에서는 간단한 설정으로, 클라이언트에서는 제공된 플러그인을 이용하여 별도의 작업없이 암호화 기능을 이용할 수 있습니다.
현재 지원하는 암호화 알고리즘은 다음과 같습니다:
Tip
암호화 알고리즘은 계속 추가될 예정이며, 제공되지 않는 다른 암호화 알고리즘을 사용하셔야 한다면 iFun Engine Q&A 게시판 에 남겨 주시기 바랍니다.
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_key
에 private_key
값을 지정해주시면 됩니다.
그리고 클라이언트 플러그인 쪽에는 public key
로 출력한 값을 지정해주시면 됩니다.
Important
위 키 값들은 예제일 뿐이니 절대로 이 값을 복사해서 쓰지 마시고, 저 명령을 직접 돌려서 새로운 키를 만들어서 쓰시기 바랍니다.
Important
ChaCha20 혹은 AES-128 을 사용하려는 경우, 반드시 encryption_ecdh_key
값을
변경하시고, 클라이언트 플러그인에도 해당하는 공개키를 지정해주셔야 합니다.
메시지 리플레이 공격 차단¶
아이펀 엔진은 패킷 리플레이 공격을 막는 기능을 제공합니다.
네트워킹 기능 설정 파라미터 의
use_sequence_number_validation
를 true
로 설정하면 패킷 리플레이 공격을
차단하는 기능이 작동됩니다.
SSL/TLS 설정¶
아이펀 엔진은 TCP, HTTP, Websocket 사용 시 SSL/TLS 통신하도록 설정하는 기능을 제공합니다.
이 기능을 사용하기 위해서는 MANIFEST/SessionService 항목에 SSL/TLS 를 사용하려는 프로토콜에 대한 설정이 필요합니다.
사용하지 않는 프로토콜에 대한 설정은 생략할 수 있습니다.
"SessionService": {
...
tcp_use_ssl: false, // TCP 통신 시 SSL/TLS 사용 설정.
http_use_ssl: false, // HTTP 통신 시 SSL/TLS 사용 설정.
websocket_use_ssl: false, // Websocket 통신 시 SSL/TLS 사용 설정.
ssl_certification_path: "", // SSL/TLS 인증서 파일의 경로.
ssl_passphrase: "", // SSL/TLS 비밀키의 암호.
ssl_private_key_path: "", // SSL/TLS 비밀키 파일의 경로.
...
}
클라이언트 플러그인에서도 각 프로토콜에 대하여 동일하게 설정해 주셔야 합니다. 해당 내용은 플러그인의 트랜스포트 설정 을 참고해 주세요.
메시지 압축¶
네트워크 메시지 전송량을 줄이기 위해서 압축 기능을 사용할 수 있습니다. 해당 기능은 특히 JSON 메시지에 대해서 효과적입니다.
설정하기¶
패킷 압축 기능은 개별 프로토콜 별로 따로 설정합니다.
MANIFEST/SessionService 항목에 압축 기능을 사용하려는 프로토콜에 대해서 다음과 같은 내용을 설정해 주시기 바랍니다.
사용하지 않는 프로토콜에 대한 설정은 생략할 수 있습니다.
...
"tcp_compression": {
"type": "none", // TCP 에서 사용할 압축 알고리즘.
"dictionary": "", // 클라이언트와 미리 공유한 사전 데이터 (zstd 압축 알고리즘 전용).
"threshold": 128 // 압축할 최소 메시지 크기.(Byte) 이보다 작은 메시지는 압축하지 않습니다.
},
"udp_compression": {
},
"http_compression": {
},
"websocket_compression": {
},
...
클라이언트 플러그인에서도 해당 프로토콜에 대해서 같은 압축 설정을 지정해주셔야 합니다. 이 부분은 플러그인의 압축 설정 을 참고해주세요.
압축 알고리즘¶
zstd
: 실시간 전송해야하는 메시지에 적당한 알고리즘입니다.deflate
: 큰 데이터를 지연 시간을 감수하고 전송할 경우 적당한 알고리즘입니다.
공유 사전 사용하기¶
압축 알고리즘 zstd 를 사용하는 경우, 서버와 클라이언트가 사전 데이터를 공유해서 더 빠르고 더 작은 메시지를 주고 받을 수 있습니다.
긴급 메시지¶
Session 으로 전송된 메시지는 엔진 이벤트의 태그 에
설명된 순서에 따라 서버에서 처리됩니다. 만약 이 순서를 무시하고 긴급하게
처리되어야 하는 메시지가 있다면 아래처럼 urgent
메시지로 지정하여 보내면
됩니다.
JSON 을 사용하는 경우
_urgent
필드를 추가하여true
로 설정합니다.Protobuf 를 사용하는 경우
FunMessage
의urgent
필드를true
로 설정합니다.
서버의 IP 주소 알아내기¶
클라이언트가 서버에 접속하기 위해서는 클라이언트가 접속할 수 있는 서버의 IP 를 알아내야 합니다. 라이브 서버의 경우 대부분 공인 IP 가 되겠지만, 내부 개발환경의 경우에는 가상 IP 가 될 수도 있습니다.
그리고 접속 IP 는 실서버에 부여된 IP 를 직접 쓰는 경우도 있고, 앞단에 로드밸런서를 쓰는 경우는 로드밸런서의 IP 를 써야되는 경우도 있고, 개발자 데스크탑에서 가상머신으로 서버를 돌리는 경우처럼 가상네트워크 NAT에서의 특정 IP, 포트로 지정해야 될 수도 있습니다.
아이펀엔진은 이런 경우들에 대해서 손쉽게 서버의 IP 를 알아내거나 직접 지정하는 방법을 제공합니다. 그리고 이런 방법들은 우선 순위에 따라 여러개를 나열할 수 있어서, 개발서버를 라이브로 옮기는 것처럼 다른 환경으로 옮기더라도 설정 파일 수정 없이 여러 환경을 지원할 수도 있습니다.
MANIFEST.json
의 HardwareInfo
콤포넌트에서 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 를 참고해주세요.
HTTP 클라이언트¶
아이펀 엔진은 외부 시스템을 손쉽게 호출할 수 있는 HttpClient 를 지원합니다. 보다 자세한 내용은 아이펀 엔진의 HTTP Client 기능 를 참고하세요.
네트워킹 기능 설정 파라미터¶
아래의 설명과 설정 파일 (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)
websocket_json_port: JSON 패킷을 주고 받을 서버의 WebSocket 포트 번호. 0 이면 비활성화입니다. (type=uint64, default=0)
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)
websocket_protobuf_port: Protobuf 패킷을 주고 받을 서버의 WebSocket 포트 번호. 0 이면 비활성화입니다. (type=uint64, default=0)
네트워크 인터페이스 관련 설정
TCP, HTTP, UDP 혹은 WebSocket 포트를 열면 기본 동작으로 모든 주소에서 들어오는 연결을 받아들입니다. (0.0.0.0
에 바인드합니다)
이런 기본 동작 대신 몇 가지 이유로 특정 인터페이스만 사용하도록 할 수 있습니다.
테스트나 보안 상의 이유로 특정 인터페이스만 사용하려는 경우.
여러 개의 IP 주소를 하나의 NIC 를 통해서 받아들이는 설정에서 UDP 통신을 해야하는 경우.
이 때 다음 설정 값을 변경해서 eth0
나 eno1,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 프로토콜에 적용할 압축 알고리즘 type (type=string, default=”none”), dictionary (type=string, default=””), threshold (type=int32, default=128) 값 설정. (type=object)
"tcp_compression": { "type": "none", // TCP 에서 사용할 압축 알고리즘. "dictionary": "", // 클라이언트와 미리 공유한 사전 데이터 (zstd 전용). "threshold": 128 // 압축할 최소 메시지 크기.(Byte) 이보다 작은 메시지는 압축하지 않습니다. }
udp_compression: UDP 프로토콜에 적용할 압축 설정. 형식은 TCP 와 동일합니다.
http_compression: HTTP 프로토콜에 적용합니다.
websocket_compression: 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: 카운터 기능 사용하기 를 통해서 클라이언트-서버간의 통신량에 대한 정보를 HTTP RESTful 로 제공합니다. 자세한 내용은 카운터 기능 사용하기 를 참고하세요. 개발에 편리하지만 서버에 상당한 과부하를 초래하기 때문에 false 로 설정할 것을 권장합니다. (type=bool, default=false)
json_protocol_schema_dir: 패킷 형식으로 JSON 을 사용할 때, JSON 패킷의 유효성을 검증할 스키마 파일이 들어있는 디렉토리 경로. 자세한 내용은 JSON 메시지 스키마 자동 검증 를 참고해주세요. (type=string, default=””)
tcp_ping: TCP 프로토콜에 적용할 핑의 sampling_interval_in_second (type=uint64, default=0), message_size_in_byte (type=uint64, default=32), timeout_in_second (type=uint64, default=0) 값 설정. (type=object)
"tcp_ping": { "sampling_interval_in_second": 0, // RTT 계산을 위한 ping 샘플링 인터벌을 초단위로 지정합니다. 0 은 동작을 끕니다. "message_size_in_byte": 32, // 전송할 ping 메시지 크기. "timeout_in_second": 0 // 지정된 시간 동안 Ping 응답이 오지 않을 경우 연결을 끊습니다. 0 은 동작을 끕니다. }
websocket_ping: WebSocket 프로토콜에 적용할 핑 설정. 형식은 TCP 와 동일합니다.
직접 설정을 바꿀 일이 거의 없는 설정
close_transport_when_session_close: 세션을 닫을 때 딸려있는 Transport(연결)도 같이 종료함. (type=bool, default=true)
close_session_when_event_timeout: 이벤트 타임아웃 발생 시 연관된 세션을 자동으로 닫고, 세션 닫힘 핸들러가 호출되도록 합니다. 세션 닫힘 핸들러의 이벤트 태그는 Session ID 가 아닌 무작위 값으로 호출됩니다. (type=bool, default=false)
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)
멀티 프로토콜¶
아이펀 엔진은 TCP, UDP, HTTP 를 동시에 사용할 수 있습니다. 예를 들어 HTTP 로 PvE 처리를 하고 TCP 나 UDP로 PvP 처리를 할 수 있습니다. 네트워킹 기능 설정 파라미터 에서 사용하려는 프로토콜의 port 를 0 이 아닌 값으로 설정하면 동시에 사용 가능합니다.
전송 프로토콜 명시적 선택¶
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);
전송 프로토콜 자동 선택¶
프로토콜을 생략하거나 kDefaultProtocol
를 지정하면 아래 우선 순위에 따라 프로토콜이 자동 선택됩니다.
Message Handler 에서 전송할 경우 해당 Message 를 수신한 Protocol.
SetTransport(msgtype, protocol)
함수로 지정된 기본 Protocol.SetTransport(protocol)
함수로 지정된 기본 Protocol.네트워킹 기능 설정 파라미터 에 활성화된 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.
HTTP 교차 출처 자원 공유 (CORS)¶
웹 브라우저 기반의 클라이언트를 개발하는 경우 HTTP 교차 출처 자원 공유 (HTTP Cross-Origin-Resource-Sharing; 이하 CORS) 설정이 필요합니다. (Unity WebGL 등을 사용하는 경우)
MANIFEST.json
의 SessionService
설정을 변경하면 해당 기능을 사용하실 수 있습니다.
"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
처럼 다른 도메인 (서브도메인) 을 사용하는 경우, 접속에 실패합니다.만약, 해당 값이
["*"]
이라면 모든 주소를 허용합니다.*
은 개발 환경 혹은 테스트 환경에서만 쓰는게 좋습니다.
(고급) 아이펀 엔진의 네트워크 스택¶
Note
아래 설명은 아이펀 엔진 서버와 호환되는 클라이언트 모듈을 직접 제작하려는 개발자나 아니면 아이펀 엔진의 네트워킹에 대해서 더 알고 싶어하는 고급 개발자를 위한 것입니다. 일반 유저들은 아이펀팩토리 Github 계정 에 있는 클라이언트 플러그인을 사용하기만 하면 됩니다.
아이펀 엔진은 다양한 네트워크 환경에서 효율적이며, 쉽게 사용할 수 있도록 다양한
프로토콜을 선택적으로 사용 할 수 있습니다. 크게
Transport 계층, Message 계층, Session(Application) 계층으로 계층화 되어
있으며, Transport 계층은 TCP
, UDP
는 물론 모바일 환경에 친숙한 HTTP
를
지원하며 Session/Application 계층은 JSON
과 Google Protocol Buffers
를
지원합니다. 목표하는 네트워크 환경에 맞게 Transport 계층과 Session/Application 계층을
조합하여 사용할 수 있습니다.
Transport 계층으로 TCP, UDP 를 사용할 경우 protocol 의 버전, 암호화 및 메시지의 크기 등 제어정보를 포함하는 별도의 헤더를 앞에 붙여서 Message 계층을 만듭니다. 이 때 헤더 구조는 HTTP 와 유사하게 줄마다 키-밸류를 문자열로 기술하는 형태입니다.
Transport 계층으로 HTTP 를 쓸 경우 아이펀 엔진은 별도의 메시지 계층을 만들지 않고 HTTP 헤더에 필요한 제어 정보를 포함시킵니다.
아래는 Session/Application layer 에 따라 구분된 2 가지의 networking stack diagram 입니다.
Transport 계층¶
Transport layer 는 TCP, UDP, HTTP 를 사용 가능하며, 동시에도 사용 가능합니다. 이는 HTTP 또는 TCP 로 로그인, 결제 등의 빈도가 낮고 중요한 데이터의 송수신을 하고, UDP 로 실시간 데이터 동기화를 하는 등의 기능을 손쉽게 구현 가능하게 하는 장점을 제공합니다.
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 알고리즘을 명시합니다.
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
라는 메시지 타입으로 클라이언트에 전송하게 됩니다. 보다 자세한 내용은 (고급) 아이펀 엔진 세션 상세 을 참고해주세요.
Session/Application 계층 - JSON 메시지 포맷¶
Body 는 JSON 으로 이루어지는데, JSON 안에는 게임 개발자가 임의의 key 와 value 를 넣을 수 있습니다. 이때문에 아이펀 엔진 게임들은 client-server 간에 훨씬 유연하고 높은 자유도를 갖고 연동을 할 수 있습니다.
{
"_msgtype": "패킷 타입",
"_sid": "세션 아이디",
// 게임별 패킷 필드들이 여기 추가됩니다.
}
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;
// }
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"
]
(고급) 아이펀 엔진 세션 상세¶
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
라는 인자로 줄 수 있습니다.