10. 联网Part 2¶
10.1. 消息格式¶
10.1.1. JSON消息¶
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.
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字段是否正确,只是这很繁琐。iFun引擎为了便于上述作业,提供了将JSON schema添加到handler中,并自动检查message参数的功能。如果message的模式不正确,就会输出日志,不上传到处理器中。所以在处理器中可以假设没有参数检验而一直仅接收正常JSON的条件来进行作业。
10.1.1.3.1. 在代码中指定Schema¶
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
,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 문서를 참고해주세요.
10.1.2. Protobuf消息¶
本部分仅作基本的介绍,以确保能够在iFun引擎中通过Protobuf传输消息。具体介绍可查看 Google Protocol Buffers 。
创建项目后,将自动生成{项目名}_messages.proto文档,并输出包含最上级消息
FunMessage
的文档。无需其他操作,只要在该文档中添加对
FunMessage
进行extend的消息,消息即可自动构建并进行使用。
Important
在对 FunMessage
进行extend时,字段编号1-15均会用于iFun引擎中,所以不得使用。
10.1.2.1. 定义消息¶
以下为用于说明的包含character信息的消息定义示例。
// 扩展FunMessage。
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. 消息加密¶
iFun引擎提供了对服务器和客户端之间的收发数据进行加密的功能。在服务器中简单设置,在客户端利用提供的插件,无需单独的操作,即可使用加密功能。
目前支持的加密算法如下:
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
当使用ChaCha20或AES-128作为TCP加密算法时,需要在ECDH密钥交换算法中使用的密钥。
在命令行中执行以下命令。
$ 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
值,并须要为客户端插件指定相应公开密钥。
10.3. Message Compression¶
iFunEngine may compress messages to save bandwidth. Message compression is quite effective against JSON messages.
Message compression is appliable to each transport. (TCP, UDP, …) To apply message compression to a certain transport, you may specify the following:
Compression algorithm
Minimal message size to be compressed. A message shorter than the size would not be compressed.
Optional, shared dictionary between client and server.
For example, to compress messages sent on TCP, set following parameters in <network-configuration> .
tcp_compression
tcp_compression_threshold
tcp_compression_dictionary
(optional)
Also, you should provide the same configuration to client plugin. Please refer to Message Compression to configure client plugin.
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.
10.4. 紧急消息¶
通过Session传输的消息根据 引擎事件的标签 中所介绍的顺序,在服务器中依次处理。如果存在须要忽略该顺序并紧急处理的消息,按如下所示,将其指定为 urgent
消息发送即可。
使用JSON时,添加
_urgent
字段,设置为true
。使用Protobuf时,将
FunMessage
的urgent
字段设置为true
。
10.5. 获取服务器的IP地址¶
为了让客户端访问服务器,须要获取客户端可以访问的服务器的IP。对于live server,大部分都为公网IP,但对于内部环境,有可能是虚拟IP。
另外,还有所访问的IP有直接使用实际执行服务器上的IP的情况,也有在前端使用负载均衡器时须要使用负载均衡器IP的情况,同时,也有在开发人员的桌面上通过虚拟机执行服务器等须要指定虚拟网络NAT中的特定IP、端口的情况。
针对这些情况,iFun引擎提供了可以轻松获取服务器的IP或直接指定IP的方法。这种方法可以根据优先顺序罗列出多个值,就像将开发服务器移动到live上一样,即使转移到其他环境,也无需修改配置文件而支持多个环境。
在 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中有live服务器且内部公用开发服务器使用分配给网卡的IP,同时开发人员在自己的桌面上为虚拟服务器中分配个人开发服务器时,可以通过一种设置来处理所有情况。如此,后续把服务器作为live服务器发布时,由于需要单独处理的文档减少,所以十分便利。
Note
对于NAT,还具有向外部发送数据包并返回其结果,从而获取自身IP的功能。然而,当引擎自动包含该种情况时,若相应网站瘫痪,会导致游戏服务器也无法运行,所以iFun引擎不支持该方法。同时,对于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 를 지원합니다. 보다 자세한 내용은 iFun引擎的HTTP client 를 참고하세요.
Warning
CentOS 7 운영체제를 사용할 경우 libcurl 패키지를 7.65.0 버전 이상으로 업데이트해야합니다.
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)
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_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: 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.
加密相关设置
与加密有关的具体内容,请参考 消息加密 。
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: 加密功能已打开时,为了与AESChaCha20更换会话密钥而使用的服务器端密钥。
Compression settings
Default compression algorithm is "none"
. You may specify either "zstd""
or "deflate"
.
tcp_compression: Compression algorithm for TCP.
tcp_compression_threshold: Minimal message size to be compressed.
tcp_compression_dictionary: Shared dictionary data. (for zstd only).
udp_compression, udp_compression_threshold, udp_compression_dictionary : Similar to TCP, but for UDP.
http_compression, http_compression_threshold, http_compression_dictionary : For HTTP.
websocket_compression, websocket_compression_threshold, websocket_compression_dictionary : For WebSocket.
TCP相关设置
disable_tcp_nagle: 使用TCP会话时,通过设置
TCP_NODELAY
套接口选项来关闭Nagle算法。(type=bool, default=true)
调试及监控相关设置
enable_http_message_list: 若打开该选项,当使用HTTP
GET /v1/messages
时,可查看已通过RegisterHandler()
函数上传的message type 。具体内容请参考 (高级)iFun引擎的网络堆栈 中与HTTP有关的内容。虽然在开发上较为便利,但由于安全原因,建议在live阶段设置为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 。虽然利于开发,但会对服务器造成相当大的超负荷,因此建议设置为flase。(type=bool, default=false)
json_protocol_schema_dir: 通过数据包的形式使用JSON时,包含对JSON数据包的有效性进行验证的模式文件(schema file)的目录路径。具体内容请参考 自动验证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. 多协议¶
iFun引擎可以同时使用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
,可根据以下优先顺序自动选择协议。
在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.
10.9. (高级)iFun引擎的网络堆栈¶
Note
以下内容是向想要直接制作与iFun引擎服务器兼容的客户端模块,或是想要更深入了解iFun引擎联网功能的高级开发人员介绍的内容。普通用户只要使用 iFun Factory Github账户 中的客户端插件即可。
iFun引擎在多种网络环境中有效,可以轻松选择使用多种协议。其大体分为Transport层、Message层、Session(Application)层,Transport层支持 TCP
, UDP
及移动环境中经常用的 HTTP
,Session/Application层支持 JSON
和 Google Protocol Buffers
。可根据目标网络环境,组合使用Transport层和Session/Application层。
当将TCP、UDP用作Transport层时,把包含protocol的版本、加密及消息的大小等控制信息在内的单独标头(header)粘贴在前方,创建Message层。此时,标头的结构与HTTP类似,为每行通过字符串叙述Key-value的形式。
当将HTTP用作Transport层时,iFun引擎不会创建单独的消息层,而是会把必要的控制信息包含在HTTP标头中。
以下是按照Session/Application layer区分的2种networking stack diagram。
![아이펀 엔진 네트워킹 스택 - JSON 메시지](_images/funapi_networking_stack_json.png)
图1)iFun引擎网络堆栈 - JSON消息¶
![_images/funapi_networking_stack_protobuf.png](_images/funapi_networking_stack_protobuf.png)
图2) iFun引擎网络堆栈 - Google Protobuf消息¶
10.9.1. Transport层¶
Transport layer可使用TCP、UDP、HTTP,并且亦可同时使用。它具有轻松实现通过HTTP或TCP收发登录、支付等频度低但却重要的数据,以及通过UDP同步实时数据的功能。
10.9.2. Message层¶
Message layer仅在将TCP、UDP作为Transport layer时使用。消息层具有额外添加标头,进行协议版本的识别、加密等作用。以下显示了消息层的结构。
HEADER_KEY1:HEADER_VALUE1
HEADER_KEY2:HEADER_VALUE2
HEADER_KEY3:HEADER_VALUE3
{向会话层传输的数据}
Message由‘header’和‘body(payload)’组成,header与HTTP的header类似,各line组成了KEY:VALUE形式,header和body用空格分隔。目前使用的header有以下三种。
VER: 表示iFun引擎Message层的version。目前必须为1
LEN: 表示除了Header以外,单纯的body长度。(作为Session/Application layer message的json或protobuf的大小)
ENC: 显示encryption算法。
10.9.3. Session/Application 계층¶
Session/Application层支持易于开发的 JSON 和高效的 Google Protocol Buffers 等两种message format。可根据需要选择一种使用,也可同时使用。
Session层的数据包通常包括用于识别session的“_sid”、用于识别数据包类型的“_msgtype”等2个header。
msgtype: 以字符串的形式定义client-server数据包类型。iFun引擎将根据该类型值调用已注册的数据包处理器。
Important
在数据包类型中以下划线(underscore或 _)开头的类型通过该iFun引擎使用,因此不得在游戏中使用。几个相关示例如下。
_session_opened: 分配新的session id时,从服务器向客户端传输。(但当Transport lyaer为HTTP时,不传输单独的_session_opened消息。对request作出response当中则包括sid。)客户端须要将此处获取的session id用于后续向服务器发送的消息当中。
_session_closed: 它是服务器向客户端发送的消息类型,表示相应session为已关闭的session。
sid: 定义用于区分session的id。若使用TCP等连接指向型protocol,在客户端失去连接时,游戏须要对其进行修复。iFun引擎会自动执行这种连接修复,此时就会参考该sid。相同的sid会被识别为相同的session,在session以idle状态timeout之前,client可以修复连接。client首次访问时可以省略该id。当不存在sid时,iFun引擎会分配新的session id,并将其通过名为 _session_opened 的消息类型传输给客户端。更详细的内容请参考 (高级)iFun引擎会话详情 。
10.9.3.1. Session/Application 계층 - JSON 메시지 포맷¶
Body由JSON组成,游戏开发人员可以在JSON中放入任意key和value。因此,iFun引擎游戏可在client-server之间进行更灵活、自由度更高的联动。
{
"_msgtype": "패킷 타입",
"_sid": "세션 아이디",
// 在此处添加各游戏的数据包字段。
}
10.9.3.2. Session/Application 계층 - Google Protocol Buffers 메시지 포맷¶
Body可以扩展FunMessage,自由组成。
// 最上级的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.9.3.3. HTTP¶
HTTP不同于使用Message层的TCP和UDP,它直接使用HTTP标头,可利用HTTP固有的特征对上级layer的部分功能进行特殊处理,如下所示。
http://server-url/v{version}/messages/{message-type}
version: 与Message层的“VER”值类似。目前必须为1。
message-type: 与Session层的“_msgtype”值类似。(optional)
例)
http://mygame.com:8018/v1/messages/login
(msgtype通过“login”处理。)http://mygame.com:8018/v1/messages/buy
(msgtype通过“buy”处理。)
同时,可通过 http://server-url/v{version}
发送所有message。但这样发送时,http body中必须要包含 _msgtype 。
可以在 http://server-url/v{version}/messages
中查看已通过 RegisterHandler()
函数进行注册的所有message type。
Note
只有当 此处 介绍的enable_http_message_list选项为true时,才会运行。
예) http://mygame.com:8018/v1/messages/
[
"echo",
"login",
"join",
"buy"
]
10.10. (高级)iFun引擎会话详情¶
Note
以下内容是向对iFun引擎中会话的运行方式感兴趣的开发人员介绍的内容。若您使用 iFun Factory Github账户 中提供的客户端插件,可忽略以下内容。
在移动环境中,当因手机位置变更而导致基站发生变更时,或是在WiFi网和3G/LTE网之间进行切换时,IP可能会变更。因此,传统的通过IP和port来区分client的方法就会引发问题。所以iFun引擎在 多种transport协议 上,提供了session层。
iFun引擎的session层运用唯一的session id实现client,以此来代替用IP和port来区分client。为此,在前面介绍的消息类型中,对JSON body部分会使用名为 “_sid” 的保留key。客户端首次连接时由于不知道sid,所以虽然可以不发送sid,但是此后当服务器告知sid时,须持续使用该sid。例如,对于 Tutorial的hello world 服务器,客户端和服务器收发的信息如下。
Note
在以下示例中,telnet的end of line根据OS而不同。以下示例是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在没有sid的情况下,向server发送了“hello”消息。server在分配sid后,向client传输“_session_opened”消息,并对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特征的KEY。下面看下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
因子。