11. Networking Part 2

11.1. Message Format

11.1.1. JSON message

To support JSON, iFun Engine uses a JSON class explained in iFun Engine API documents.

To support JSON, iFun Engine uses Newtonsoft.Json. For details, please refer to Newtonsoft.Json.

11.1.1.1. Generating and reading JSON messages

The following are simple examples of code to handle main functions. For more details, please refer to the linked pages.

Assume the following JSON objects in these examples:

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

To create a JSON object like above, you may write code like this:

Json msg;
msg["id"] = "ifunfactory";
msg["level"] = 99;
msg["messages"].PushBack("hello"); // messages becomes an array.
msg["messages"].PushBack("world");
msg["messages"].PushBack(1004);
msg["guild"]["name"] = "legend";
msg["guild"]["score"] = 1000;

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

And to verify the created JSON object has the desired structure, you may write code like this:

// Verifies the value type.
// FYI, you can simplify below code
// using JsonSchema mentioned.
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;
}

// Fetches a value from the JSON object.
// Please be aware that id_ptr becomes invalid after msg object gets destroyed.
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();

// Converts the msg2 object into a string.
string json_str;
msg2.ToString(&json_str);

// Constructs a JSON object from a string.
Json msg3;
msg3.FromString(&json_str);

To create a JSON object like above, you may write code like this:

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

// Fetches a value from the JSON object.
string id = (string) msg ["id"];
// Or you can read a valie like this:
// 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"];

// Converts into a string, and then constructs a JSON object from the string.
JObject msg2 = JObject.Parse (msg.ToString ());
Log.Info ((string) msg2 ["id"]);

11.1.1.2. Checking JSON parsing errors

You can get the data and cause of error for JSON parsing using a callback function.

Like the following example, you may pass the Json::ParseErrorCallback callback function to Json::From... functions. For brevity, lambda function is used to print error log for parsing failure.

...
// It has an erroneous trailing `,` at the end of last string literal.
const char* json_string = "{ \"key1\": \"value1\", \"key2\": \"value2\", }";
fun::Json json;

json.FromString(json_string,
  [] (const string &json_string /* An input JSON string caused parsing failure. */,
      const string &error_desc /* Error description for parsing failure. */,
      size_t error_offset /* Offset of the errneous position in the string */) {
    // Prints cause of error and input string.
    LOG(INFO) << error_desc << std::endl
               << "json ="
               << json_string;
  }
);

Also, iFun Engine provides a default handler to process parsing errors. In the example below, give fun::Json::kDefaultParseErrorHandler to the method From... in the Json class.

...
// This JSON string is invalid as it has a trailing comma.
const char* json_string = "{ \"key1\": \"value1\", \"key2\": \"value2\", }";
fun::Json json;
// Passing the default error handler, we can see the a parsing error log message.
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",

Please refer to JsonReaderException.

11.1.1.3. Automatically verifying JSON message using JSON schema

Since Google Protocol Buffers declares structure in advance, it conducts conformance tests for fields during serialization/deserialization; however, since fields can be added to JSON at will, JSON itself does not offer conformance tests.

For that reason, it is necessary to test whether JSON fields are correct in the handler that transmits JSON packets, and this is cumbersome. iFun Engine adds JSON schema to a handler to easily perform this task and can automatically test message parameters. If the message schema is incorrect, it is output to a log and not delivered to a handler. This task can be performed with the assumption that only normal JSON is delivered without parameter testing in the handler.

11.1.1.3.1. Setting schema in the code

You can do JsonSchema check like this:

JsonSchema(parameter_name, parameter_type, if_required)

Suppose we have received a JSON message like this:

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

Then we can validate the message like this:

#include <funapi.h>

// This Installer method is auto-generated when creating a project.
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)));

    // Associate a JSON message type
    // with schema we have defined above.
    HandlerRegistry::Register("hello", OnHello, hello_schema);
    ...
  }
}

Please refer to Validating JSON with JSON Schema from Microsoft.

11.1.1.3.2. Using separate schema files

We have studied how to specify a schema in C++ for incoming JSON messages from the client. But this in-line approach may reduce code readability and have to recompile when schema changes. Thus, iFun Engine provides another way to validate JSON message using separate schema files.

In MANIFEST.json, we can specify a directory containing JSON schema files by setting json_protocol_schema_dir in the SessionService component. Each schema file is structured according to these rules.

  • File extension must be either json, txt, or text.

  • Each file contains a JSON object that maps JSON message type to its expected properties.

    {"MESSAGE_TYPE1": {MESSAGE properties},"MESSAGE_TYPE2": {MESSAGE properties}, ...}
    
  • For Message properties, belows are possible.

    • direction: Either cs , sc or cs sc is possible. These imply a message is clent-to-server, server-to-client, and both, respectively.

    • properties: Lists fields of a particular message type.

      "properties": { "FIELD1": { FIELD properties }, "FIELD2: {FIELD properties}, ...}
      
  • For Field properties, belows are possible.

    • type: Specifies the type of the given field. Either bool , integer , string , array , or object is possible.

    • required: Specifies if the field is mandatory. Boolean values like true or false is possible. Also, message direction like cs , sc , or cs sc is possible. When using a boolean value, the field becomes mandatory/optional regardless of message direction. When using a message direction value, the field becomes mandatory/optional depending on message direction. This is particularly useful if a message type is shared by client-to-server case and server-to-client case, as required fields are different depending on direction. (For example, say only the server populates fields named result and error_code, then we should set the fields as "required": "sc".

    • properties: If the field is of JSON object, this defines the properties of the JSON object.

      "properties": { "FIELD1": {FIELD properties}, "FIELD2": {FIELD properties}, ...}
      
    • items: If the field is of JSON array, this defines the properties of array elements.

      "items": { array element properties}
      

      Belows are acceptable as an array element property.

      • type: Defines the type of array elements. Either bool , integer , string, array , or object possible.

      • properties: If array element is of JSON object, this defines the properties of the JSON object. It’s the same as the previous properties explained in the field properties.

      • items: If array element is of JSON array, this defines the properties of the JSON array. It’s the same as the previous items explained in the filed properties.

Example 1: Below schema defines a Login message type that is sent from the client to the server. The message must have a string field named AccountKey.

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

Example 2: As a matching message, below schema defines a LoginReply message type that it sent from the server to the client. Suppose, the message must carry a boolean filed named Result as well as the AccountKey field from the Login message. Also, it can have an optional string field named ErrorString to explain what happened in case the Result field is false.

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

Example 3: More complicated example. Suppose the game has a farm and there’s a message named Harvest from the client to the server to notify the player wants to harvest from the farm. The client-to-server message may look like this:

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

If all fields must be filled, we may write a JSON schema like this:

{
  "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
                }
              }
            }
          }
        }
      }
    }
  }
}

Example 4: Suppose a message used for client-to-server and server-to-client. And say its name is BidirectionalMessage. If the message goes from the client to the server, a field named RequestString must be set, while it goes from the server to the client a field named ReplyString must be set. Then, its matching schema may look like this:

{
  "BidirectionalMessage": {
    "direction": "cs sc",
    "properties": {
      "RequestString": {
        "type": "string",
        "required": "cs"
      },
      "ReplyString": {
        "type": "string",
        "required": "sc"
      }
    }
  }
}

Please refer to Validating JSON with JSON Schema from Microsoft.

11.1.2. Protobuf messages

This is only a basic explanation so messages may be sent and received in Protobuf in iFun Engine. For more details, please see Google Protocol Buffers.

When a project is created, a file named {projectname}_messages.proto is automatically generated and the file is imported including the top-level message, which is FunMessage. If messages extending FunMessage are added to the file with no other tasks performed, the message may automatically be built and used.

Important

When extending FunMessage, do not use field numbers 1-15 as they are used by iFun Engine.

11.1.2.1. Message definition

The following is an example of message definition for the sake of explanation and may contain character information.

// Extends the FunMessage.
extend FunMessage {
  // Use only field 16 and above. 1-15 are used by iFun Engine.
  optional Login login = 16;
  optional Logout logout = 17;
  optional CharacterInfo character_info = 18;
  ...
}

// Omitted Login/Logout messages as they are not used in this example.

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;

  ...
}

11.1.2.2. Generating messages

In the following example, the CharacterInfo message defined above is generated.

// Creates the outer-most protobuf used by iFun Engine.
Ptr<FunMessage> message(new FunMessage);

// Get a new instance of character_info
// by passing its identifier (i.e. character_info) to MutableExtension().
CharacterInfo *char_info = message->MutableExtension(character_info);

// Fills the character_info.

// For primitive types, set_{field_name}(value) can be used.
char_info->set_name("example");
char_info->set_level(99);
char_info->set_type(CharacterInfo::kDwarf);

// The pets field is defined as repeated, and so it's the same as array.
// You can add an element to the pets field by calling add_{field_name}().
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);

...

// We ship the message by calling SendMessage() like following:
// session->SendMessage("character_info", message);
// The client will receive the message as "character_info".
// Creates the outer-most protobuf used by iFun Engine.
FunMessage message = new FunMessage ();

// Get a new instance of character_info
CharacterInfo char_info = new CharacterInfo();

// Fills the character_info.
// For primitive types, var.{field_name} = (value) can be used.
char_info.name = "example";
char_info.level = 99;
char_info.type = CharacterInfo.CharacterType.kDwarf;

// The pets field is defined as repeated, and so it's the same as array.
// You can add an element to the pets field by calling {field_name}.Add({value}).
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);

...

// We ship the message by calling SendMessage() like following:
// session.SendMessage("character_info", message);
// The client will receive the message as "character_info".

11.1.2.3. Reading messages

An example of reading the CharacterInfo message generated above follows.

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

  // Suppose the message generated in the previous subsection is passed.

  // Checks if there's a field named character_info that extends FunMessage.
  if (not message->HasExtension(character_info)) {
    // Exception handling..
    return;
  }

  // Fetches the character_info field from the FunMessage instance.
  // You can do it by passing an auto-generated extension identifier to GetExtension().
  const CharacterInfo &char_info = message->GetExtension(character_info);

  // For primitive type, you can read its value by {field_name}().
  string char_name = char_info.name();
  uint32_t char_level = char_info.level();
  CharacterInfo::CharacterType char_type = char_info.type();

  // The pets field is defined as repeated, and so it's the same as array.
  // The length of an array can be got by {field_name}_size().
  for (size_t i = 0; i < char_info.pets_size(); ++i) {
    // To access an element in the array, you can use {field_name}(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) {

  // Suppose the message generated in the previous subsection is passed.
  CharacterInfo char_info = null;

  // Checks if there's a field named character_info that extends FunMessage.
  if (!message.TryGetExtension_character_info (out char_info))
  {
    // Exception handling..
    return;
  }

  // For primitive type, you can read its value by {field_name}.
  string char_name = char_info.name;
  uint char_level = char_info.level;
  CharacterInfo.CharacterType char_type = char_info.type;

  // The pets field is defined as repeated, and so it's the same as array.
  foreach (CharacterInfo.Pet pet in char_info.pets)
  {
    string pet_name = pet.name;
    uint pet_level = pet.level;
  }
  ...
}

11.2. Network Security

11.2.1. Message encryption

iFun Engine can encrypt data between client and server. You can use a simple configuration for the server or a provided plugin for the client to encrypt with no other action required.

The currently supported encryption algorithms are as follows:

  • ife1: Simple, proprietary encryption 1

  • ife2: Simple, proprietary encryption 2

  • chacha20: ChaCha20

  • aes128: AES-128

Tip

We plan to keep adding encryption algorithms, and if you need to use an encryption algorithm other than those provided, please contact iFun Engine support.

You can choose encryption algorithms by transport and also choose one of several encryption algorithms at the message level. Please refer to Networking parameters to configure. Examples of algorithm use are below.

// Encrypts according to the default encryption function set in Manifest.json.
// (If no encryption method is specified, no encryption will be performed.)
session->SendMessage("echo", message);

// Sending after performing encryption using ChaCha20.
session->SendMessage("login", message, kChacha20Encryption);

// Sending after performing encryption using AES128.
session->SendMessage("login", message, kAes128Encryption);

// Sending after performing encryption using ife1.
session->SendMessage("login", message, kIFunEngine1Encryption);

// Sending after performing encryption using ife2.
session->SendMessage("login", message, kIFunEngine2Encryption);
// Encrypts according to the default encryption function set in Manifest.json.
// (If no encryption method is specified, no encryption will be performed.)
session.SendMessage ("echo", message);

// Sending after performing encryption using ChaCha20.
session.SendMessage ("login", message, Session.Encryption.kChaCha20);

// Sending after performing encryption using AES128.
session.SendMessage ("login", message, Session.Encryption.kAes128);

// Sending after performing encryption using ife1.
session.SendMessage ("login", message, Session.Encryption.kIFunEngine1);

// Sending after performing encryption using ife2.
session.SendMessage ("login", message, Session.Encryption.kIFunEngine2);

Note

If ChaCha20 or AES-128 is used as a TCP encryption algorithm, you need a secret key to use for the ECDH key exchange algorithm.

You may find the corresponding public key – which should be used by client – in MANIFEST.json.

"SessionService": {
  // Client should use public_key = "161c770..."
  "encryption_ecdh_key": "79f4d3c53..."
}

If the public key is not present in MANIFEST.json, or if you want to replace the key, run the following command in a command line:

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

You can set the private_key value in the MANIFEST.json encryption_ecdh_key. You can set the output value as a public key on the client plugin side.

Important

The key values above are only examples, so do not copy them for your own use. Please get a new key of your own by running the command.

Important

If using ChaCha20 or AES-128, be sure to change the encryption_ecdh_key value. You must set a public key for the client plugin as well.

11.2.2. Blocking message replay attacks

iFun Engine can block packet replay attacks.

If use_sequence_number_validation in <network-configuration> is set to true, the packet replay attack blocking function is enabled.

11.3. Message Compression

iFunEngine may compress messages to save bandwidth. Message compression is quite effective against JSON messages.

패킷 압축 기능은 개별 프로토콜 별로 따로 설정합니다.

MANIFEST/SessionService 항목에 압축 기능을 사용하려는 프로토콜에 대해서 다음과 같은 내용을 설정해 주시기 바랍니다.

사용하지 않는 프로토콜에 대한 설정은 생략할 수 있습니다.

...
"tcp_compression": {
  "type": "none",   // TCP 에서 사용할 압축 알고리즘.
  "dictionary": "", // 클라이언트와 미리 공유한 사전 데이터 (zstd 압축 알고리즘 전용).
  "threshold": 128  // 압축할 최소 메시지 크기.(Byte) 이보다 작은 메시지는 압축하지 않습니다.
},
"udp_compression": {
},
"http_compression": {
},
"websocket_compression": {
},
...

Currently, it supports two algorithms.

  • zstd: Fast compression algorithm for realtime messages.

  • deflate: Usually slower than the zstd, but compressed size is smaller than zstd.

11.4. Urgent messages

Messages sent to a session are handled by the server in the order explained in Engine event tags. If a message needs to be handled immediately while ignoring this order, you can send it as an urgent message.

  • If using JSON, add the _urgent and set it to true.

  • If using Protobuf, set the FunMessage urgent field to true.

11.5. Detecting Server IP Addresses

For a client to access a server, it must know the server IP used for client access. Most live servers have public IP addresses, but virtual IPs may be used in internal development environments.

The IP used may sometimes be the IP assigned to the production server, or sometimes the IP of the forward load balancer if one is used, or sometimes a specific IP and port must be specified in the virtual network NAT as in cases where the server is switched to a virtual machine on a developer’s desktop.

iFun Engine provides a way to easily discover server IP in these cases or to set it yourself. These methods can be listed according to priority, and several environments are supported without the need to modify the configuration file even if the server is moved to a different environment, as in when a development server goes live.

external_ip_resolvers are used in MANIFEST.json’s HardwareInfo component. The following is an example of use. Methods to get a public IP are listed and according to priority and comma-separated.

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

Currently supported IP discovery methods:

  • aws: If there is a virtual machine on AWS, you can get the IP through the AWS management API to provide it as the IP for the client to contact. (See http://169.254.169.254/latest/meta-data/public-ipv4/)

  • nic:name: Reads the network card IP with the relevant name to provide it as the IP for the client to contact.

  • nat:address:protocol=port:protocol=port:…: Manually enter the public IP if the server is behind equipment running Network Address Translation (NAT) features such as a load balancer, firewall, or router.

    Enter the IP or the DNS hostname into the address and optionally give protocol=port. Enter if the public port number differs from the server port number due to NAT. Available protocol types are tcp+pbuf, tcp+json, udp+pbuf, udp+json, http+pbuf, and http+json.

Tip

The methods listed above to get public IP are handled in the order first specified, and if one fails, the next is attempted.

Therefore, in the example above, one configuration may handle all cases in which a live server turns to AWS, an internal common-use development server uses an IP allocated by a network card, or developers allocate a private development server to a virtual server on their desktops. This is convenient as it reduces the need for separate files when distributing servers as live servers later.

Note

There are services for NAT that indicate one’s own IP address by sending packets outside and returning the results. However, if the engine automatically includes these and their site goes down, the game server cannot start. For that reason, iFun Engine does not support these methods. As well, IP as well as port mapping are involved in some cases with NAT, so discovering IP alone is insufficient.

The IP and port data for the game server obtained in this way can be found with the HardwareInfo class in include/funapi/system/hardware_info.h

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

  // Map type to hold ports information.
  typedef std::map<FunapiProtocol, uint16_t> ProtocolPortMap;

  // Returns a public IP associated with the server.
  static boost::asio::ip::address GetExternalIp();

  /// Returns a public port associated with the server.
  /// If port number is explicitly set in the NAT case, the port number will be returned.
  /// If port number is not set in the NAT case or AWS/NIC are used,
  /// ports specified in the SessionService component in MANIFEST.json will be returned.
  static ProtocolPortMap GetExternalPorts();
};
// Dictionary to hold port information.
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
  }

  // Returns a public IP associated with the server.
  public static IPAddress GetExternalIp ();

  /// Returns a public port associated with the server.
  /// If port number is explicitly set in the NAT case, the port number will be returned.
  /// If port number is not set in the NAT case or AWS/NIC are used,
  /// ports specified in the SessionService component in MANIFEST.json will be returned.
  public static ProtocolPortMap GetExternalPorts ();
}

Tip

When using iFun Engine’s Distributed processing part 1: ORM, RPC, login, we may need the IP address of each server for inter-server communication as well as public IP for client-server traffic. In this case, please refer to Peer servers’ public IPs.

11.6. HTTP Client

iFun Engine provides a HttpClient class that is handy when invoking external services over HTTP. Please see iFun Engine’s HTTP client for details.

11.7. Networking parameters

You may consult to the following explanation and Configuration file (MANIFEST.json) details to configure SessionService.

Port configuration

  • tcp_json_port: Server’s TCP port number for JSON traffic. Disabled if 0. (type=uint64, default=8012)

  • udp_json_port: Server’s UDP port number for JSON traffic. Disabled if 0. (type=uint64, default=0)

  • http_json_port: Server’s HTTP port number for JSON traffic. Disabled if 0. (type=uint64, default=8018)

  • tcp_protobuf_port: Server’s TCP port number for Protobuf traffic. Disabled if 0. (type=uint64, default=0)

  • udp_protobuf_port: Server’s UDP port number for Protobuf traffic. Disabled if 0. (type=uint64, default=0)

  • http_protobuf_port: Server’s HTTP port number for Protobuf traffic. Disabled if 0. (type=uint64, default=0)

Network interface configuration

By default, open TCP, HTTP, UDP or WebSocket socket will listen to all NIC addresses. That is, it will listen to 0.0.0.0.

However, you may choose to bind to certain NIC or certain NICs for following reasons:

  • For better security.

  • If you have multiple IP address on a single NIC.

For these cases you may choose NIC or NIC list – eth0 or eno1,enot2, … – to accept client connections. If the list is empty, it will listen to 0.0.0.0. (Default behaviour) Following flags accept comma seperated list of NIC.

  • tcp_nic: NICs that will accept TCP connections.

  • udp_nic: NICs that will accept UDP datagrams.

  • http_nic: NICs that will accept HTTP requests.

  • websocket_nic: NICs that will accept WebSocket connections.

Session management settings

  • session_timeout_in_second: Idle time in seconds for session to timeout. (type=uint64, default=300)

  • use_session_reliability: Enables reliability function for sessions. This protects against packet loss even when sessions are reconnected. Please refer to Session Message Transmission Stability for more. (type=bool, default=false)

  • use_sequence_number_validation: Does not process messages if the message sequence number is wrong. This prevents message replay attacks. It only works in TCP and HTTP. For more details, see Blocking message replay attacks (type=bool, default=false)

  • session_rate_limit_per_minute: It limits a client to send message less frequently than the limit. A client will not be able to send message more than the limit per minute. If set to 0 – which is a default value – it would not limit the rate. Each transport would react to rate-limited sessions as follow:

    • TCP, WebSocket: It would delay the session message processing until the message is allowed.

    • HTTP: It would send HTTP 429 status code. The client plugin would call an error callback.

    • UDP: The messages violating the rate limit would be discarded without processing.

Encryption settings

For more about encryption, please refer to Message encryption.

  • use_encryption: Enables and disables encryption. (type=bool, default=false)

  • tcp_encryptions: List of encryption methods used in TCP protocol when encryption is enabled.

    You can set to null to disable encryption or use one or all of ife1, ife2, chacha20, aes128. E.g., [], [“ife1”, “ife2”], or [“chacha20”]

  • udp_encryptions: List of encryption methods used in UDP protocol when encryption is enabled.

    You can set to null to disable encryption or use ife2. E.g., [“ife2”]

  • http_encryptions: List of encryption methods used in HTTP protocol when encryption is enabled.

    You can set to null to disable encryption or use ife2. E.g., [“ife2”]

  • encryption_ecdh_key: Server-side secret key used to exchange ChaCha20 and AES session keys when encryption is enabled.

Compression settings

Default compression algorithm is "none". You may specify either "zstd"" or "deflate".

  • tcp_compression: TCP 프로토콜에 적용할 압축 알고리즘 type (type=string, default=”none”), dictionary (type=string, default=””), threshold (type=int32, default=128) 값 설정. (type=object)

    "tcp_compression": {
      "type": "none",   // Compression algorithm for TCP.
      "dictionary": "", // Shared dictionary data. (for zstd only).
      "threshold": 128  // Minimal message size to be compressed.(Byte)
    }
    
  • udp_compression: Similar to TCP, but for UDP.

  • http_compression: For HTTP.

  • websocket_compression: For WebSocket.

TCP settings

  • disable_tcp_nagle: Turns off Nagle algorithm by setting the TCP_NODELAY socket option when TCP sessions are used (type=bool, default=true)

Debugging and monitoring settings

  • enable_http_message_list: When this option is enabled, makes message types registered by RegisterHandler() visible when using HTTP to GET /v1/messages. For more details, please refer to the HTTP information in (Advanced) iFun Engine Network Stack. It is convenient for development, but is best set as false during the live stage for security reasons. (type=bool, default=true)

  • session_message_logging_level: Session message log level. If 0, no logs are kept. If 1, only packet time and length is logged. If 2, packet contents are also logged. (type=uint64, default=0)

    Tip

    Setting this to 2 is helpful to see messages transferred during the development process. However, it can give server load, so it is not recommended for a live service environment.

  • enable_per_message_metering_in_counter: Provides data on client-server traffic through the iFun Engine Counter using HTTP RESTful. For more details, please see iFun Engine Counter. It is convenient for development, but is best set to false as it can considerably overload the server. (type=bool, default=false)

  • json_protocol_schema_dir: Directory path for schema files to verify the validity of JSON packets when using JSON as a packet format. For more details, please refer to Automatically verifying JSON message using JSON schema. (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, // Sets ping sampling intervals for RTT calculations in seconds. Stops at 0. “message_size_in_byte”: 32, // Size of ping message to be transferred. “timeout_in_second”: 0 // Disconnects if there is no ping response in a set period of time.

    }

  • websocket_ping: WebSocket 프로토콜에 적용할 핑 설정. 형식은 TCP 와 동일합니다.

Parameters that rarely needs to be changed:

  • close_transport_when_session_close: Ends related transport (connections) when a session is closed. (type=bool, default=true)

  • close_session_when_event_timeout: 이벤트 타임아웃 발생 시 연관된 세션을 자동으로 닫고, 세션 닫힘 핸들러가 호출되도록 합니다. 세션 닫힘 핸들러의 이벤트 태그는 Session ID 가 아닌 무작위 값으로 호출됩니다. (type=bool, default=false)

  • send_session_id_as_string: Decides whether to send session ID as binary or string during client-server communication. (type=bool, default=true)

    Important

    To use this feature, the client plugin versions must be as follows:

    • Unity3D: 190

    • Unreal4: 35

    • Cocos2d-x: 35

  • send_session_id_only_once: Whether to send a session ID only in the first message of TCP/UDP. This reduces client-server traffic. (type=bool, default=false)

  • network_io_threads_size: Number of threads to handle client-server traffic. (type=uint64, default=4)

11.8. Multiple Protocols

You can use TCP, UDP, and HTTP simultaneously in iFun Engine. For example, you can handle PvE with HTTP and PVP with TCP or UDP. You can use these simultaneously if the value of the protocol port to use is not set to 0 in Networking parameters.

11.8.1. Explicit protocol selection

When invoking Session::SendMessage() or AccountManager::SendMessage() , you can designate one of kTcp, kUdp, or kHttp as the protocol.

// Sends a "echo" message over HTTP.
session->SendMessage("echo", message, kDefaultEncryption, kHttp);

// Sends a "echo" message over TCP.
session->SendMessage("echo", message, kDefaultEncryption, kTcp);

// Sends a "echo" message over UDP.
session->SendMessage("echo", message, kDefaultEncryption, kUdp);
// Sends a "echo" message over HTTP.
session.SendMessage("echo", message, Session.Encryption.kDefault, Session.Transport.kHttp);

// Sends a "echo" message over TCP.
session.SendMessage("echo", message, Session.Encryption.kDefault, Session.Transport.kTcp);

// Sends a "echo" message over UDP.
session.SendMessage("echo", message, Session.Encryption.kDefault, Session.Transport.kUdp);

11.8.2. Automatic protocol selection

If the protocol is omitted or set to kDefaultProtocol, protocols are automatically selected in the order below.

  1. Protocol in which message was received if sent from Message Handler.

  2. Default protocol set via SetTransport(msgtype, protocol).

  3. Default protocol set via SetTransport(protocol).

  4. Protocol if one port is enabled in Networking parameters.(For example, if only tcp_json_port is not 0, then kTcp)

Priority for #2 and 3 can be set with the following function. It’s recommended to set it when starting the server.

SetTransport(msg_type, protocol)
SetTransport(protocol)

Example: When simultaneously using TCP and UDP

bool Install(...) {
  // "login" message handler registered. The protocol that received this message
  // If sent by the handler is chosen. This is part of #1 in the order explained above.
  HandlerRegistry::Register("login", OnLogin);

  // "buy_item" message set to be sent through TCP.
  SetTransport("buy_item", kTcp);

  // "update" message set to be sent through UDP.
  SetTransport("update", kUdp);

  // Other unassigned messages set to be sent through TCP.
  SetTransport(kTcp);
}

// This function is a "login" message handler, and the client sends this message
// via TCP.
void OnLogin(const Ptr<Session> &session, const Json &message) {
  ...
  // Since the "login" message is received through TCP, this is sent through TCP.
  session->SendMessage("login", reply_message);

  // Caution) If sent from a function running Event::Invoke(),
  // TCP priority is lost.
}

void BuyItem(...) {
  ...
  // UDP by SetTransport("buy_item", kUDP) is the default setting for the "buy_item"
  // message. Therefore, this message is sent via UDP.
  session->SendMessage("by_item", reply_message);
}

void SendSomething(...) {
  ...
  // There is no set protocol for the "something" message.
  // Therefore, it is sent through TCP by SetTransport(kTCP).
  session->SendMessage("something", reply_message);
}

Warning

If protocol cannot be automatically selected, a log is output as follows.

ambiguous transport protocol for sending '...' message.

In such cases, a suitable protocol is designated as SetTransport(), or a protocol must be explicitly designated by the SendMessage() function.

11.9. HTTP Cross Origin Resource Sharing

If you develop a game client using web browsers, you need add configuration for HTTP Cross-Origin-Resource-Sharing (CORS).

You can activate the HTTP CORS related functionality from SessionService in MANIFEST.json.

"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: Enables HTTP CORS functionality. If set, iFunEngine will send requried HTTP headers.

  • http_cross_origin_resource_sharing_allowed_origins: Allowed origins for HTTP CORS. Only the clients downloaded from allowed domains will be able to connect the iFunEngine game servers.

    For example, let’s say that it is set to ["https://app1.example.com", "https://app2.example.com"]. A client downloaded from http://app1.example.com/static/client1.html can connect to a game server. But a client downloaded from https://other.example.com/static/client.html will fail to connect to a game server.

    Also, you can allow all domains by specifying ["*"]. But it is not recommended for a production environment.

11.10. (Advanced) iFun Engine Network Stack

Note

The following explanation is for developers who plan to personally create client modules compatible with iFun Engine servers or for advanced developers who want to know more about iFun Engine networking. Most users need only use the client plugins found at iFunFactory Github home.

iFun Engine is efficient in various network environments and can use several protocols that make it easy to use. It is divided into transport layer, message layer, and session (application) layer. The transport layer supports TCP and UDP as well as the familiar HTTP in mobile environments, while the session/application layer supports JSON and Google Protocol Buffers. You can combine the transport layer and session/application layer to suit your target network environment.

If you use TCP and UDP as the transport layer, a separate header including control information such as protocol version, encryption, and message size is added in front to create a message layer. The header format includes key-values as text strings for each line, much like HTTP.

If HTTP is used as the transport layer, iFun Engine does not create a separate message layer, but rather includes the required control information in the HTTP header.

Below are two networking stack diagrams classified by session/application layer.

iFun Engine networking stack - JSON messages

Figure 1) iFun Engine networking stack - JSON messages

iFun Engine networking stack - Google Protobuf messages

Figure 2) iFun Engine networking stack - Google Protobuf messages

11.10.1. Transport layer

TCP, UDP, and HTTP can be used for the transport layer and can be used simultaneously. This has the advantage of allowing HTTP or TCP to be used to transmit uncommon but important data like login or billing, while UDP can be used for real-time data synchronization.

11.10.2. Message layer

The message layer is only used if TCP or UDP is used as the transport layer. The message layer differentiates protocol version or encryption by adding additional headers. The message layer structure is shown below.

HEADER_KEY1:HEADER_VALUE1
HEADER_KEY2:HEADER_VALUE2
HEADER_KEY3:HEADER_VALUE3

{Data passed to the session layer}

A message comprises a header and body (payload). Each line of the header is in KEY:VALUE form, similarly to HTTP, and the header and body are differentiated by a blank space. The following three headers are currently used.

  • VER: Indicates the iFun Engine message layer version. Currently, this must be 1.

  • LEN: Indicates only the body length, regardless of the header. (Size of JSON or Protobuf, which is the session/application layer message)

  • ENC: Specifies the encryption algorithm.

11.10.3. Session/Application layer

Two message formats are supported for the session/application layer: JSON, which is easy to develop with, and Google Protocol Buffers, which is efficient. You can choose one or use both at the same time.

Session layer packets always include two headers: “_sid” to indicate sessions and “_msgtype” to indicate packet type.

  • msgtype: Defines client-server packet type in text string format. Registered packet handlers are invoked based on these type values.

    Important

    Packet types that begin with an underscore or _ are used by iFun Engine and cannot be used in games. Here are a few examples.

    • _session_opened: Sent from the client to the server when a new session ID is allocated.(If the transport layer is HTTP, a separate _session_opened message is not sent. SID is included in the response to the request.) Clients must use the session ID given here in later messages sent to the server.

    • _session_closed: This message type sent from the server to the client notifies the client that the session has been closed.

  • sid: Defines the ID to identify the session. If a connection protocol like TCP is used and the client loses its connection, the game needs to be able to restore this. iFun Engine automatically restores these connections and refers to this SID at this time. The same SID is recognized as the same session, and the client connection can be restored until the idle session times out. This SID may be omitted when the client first accesses the server. If there is no SID, iFun Engine allocates a new session ID and sends it to the client as a message type called _session_opened. For more information, please refer to (Advanced) iFun Engine Session Details.

11.10.3.1. Session/Application layer - JSON message format

The body is in JSON, and game developers may put keys and values in that JSON if they wish. For that reason, iFun Engine games can make client-server connections with a great deal of flexibility and freedom.

{
  "_msgtype": "packet type",
  "_sid": "session ID",

  // Packet handlers for each game are added here.
}

11.10.3.2. Session/Application layer - Google Protocol Buffers message format

The body can be created freely by extending FunMessage.

// Outer-most protobuf. Game packets must be loaded inside this Protobuf.
message FunMessage {
  optional string sid = 1;
  optional string msgtype = 2;
  extensions 16 to max;
}

// Game packets must be formatted as extensions of FunMessage.
// For example, say you have the following message:
//   message MyMessage {
//    ...
//   }
//
// You can now load and send MyMessage on FunMessage as follows:
//   extend FunMessage {
//     optional MyMessage mymessage = 16;
//   }

11.10.3.3. HTTP

Unlike TCP and UDP used for the message layer, HTTP headers are used directly, and the inherent features of HTTP are used for special handling of some top-layer features, as follows.

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

  • version: Matches the message layer’s “VER” value. Currently, this must be 1.

  • message-type: Matches the session layer’s “_msgtype”.(optional)

Example)

  • http://mygame.com:8018/v1/messages/login (msgtype handled as “login”.)

  • http://mygame.com:8018/v1/messages/buy (msgtype handled as “buy”.)

As well, you can send all messages with http://server-url/v{version}. However, when sent in this way, _msgtype must be included in the HTTP body.

You can see all message types registered with RegisterHandler() at http://server-url/v{version}/messages.

Note

The enable_http_message_list option explained here must be set to true.

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

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

11.11. (Advanced) iFun Engine Session Details

Note

The following explanation is for developers interested in how sessions work in iFun Engine. If you are using client plugins from iFunFactory Github home, you can skip this information.

If the base station changes in a mobile environment because the phone’s location has changed or if moving between WiFi and 3G/LTE, IP may change. For this reason, traditional methods of identifying clients through IP and port may cause problems. iFun Engine provides a session layer explained in (Advanced) iFun Engine Network Stack to solve these problems.

iFun Engine sessions handle clients using a unique session ID instead of identifying clients by IP and port. To do this, a reserved key called

“_sid” is included in the JSON body part in the message type explained above. Since the SID is unknown when a client first connects, a SID does not need to be sent, but that SID is continually required after the server notifies the client of it. For example, the messages sent and received between a client and the hello world server in the tutorial would be as follows.

Note

In the following example, telnet’s end of line may vary depending on OS. It is CR/LF (2 bytes) in the following example. Please be aware of this when calculating LEN value.

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

{
 "msgtype":"hello"
}

The client has sent a message called “hello” to the server without a SID. The server allocates a SID, sends a “_session_opened” message to the client, and handles the “hello” message requested by the client to return a message of “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"
}

Note that _sid is included in the “world” message here. This SID is a specific key for the client. In this case, the client disconnects and then reconnects.

$ 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"
}

This time, the client also sends the SID received earlier. The server responds as follows:

VER: 1
LEN: 81

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

Here is how it looks if HTTP transport is used:

$ 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"
}

The response is as follows:

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

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

The message type may be included in the URL and sent as follows:

$ 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

{}

The response is as follows:

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

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

A session means a “currently connected user”, and automatically times out if a client does nothing (i.e. does not send any packets) for a set period of time. This timeout value is given as a parameter called session_timeout_in_second in the SessionService component in MANIFEST.json.