9. 联网Part 1: 会话

在iFun引擎中,可以通过会话来表示与客户端的连接。一个会话可以拥有TCP、UDP、HTTP中的一种以上的传输协议,也可以同时使用两种以上的传输协议。例如,登录和支付消息可以通过HTTP传输,游戏内消息可以通过TCP传输。

会话在新的客户端访问时生成,在调用关闭会话的函数或因超时关闭之前一直有效。利用会话,可以通过JSON或Protobuf等任意的协议传输消息。

iFun引擎的会话提供了游戏服务时所需的以下功能。

  • 消息(数据包)传输功能

  • TCP连接修复功能

  • Ping测量功能

  • 紧急消息功能

  • 加密功能

  • 消息重放攻击屏蔽功能

还提供除此以外的其他功能,在本章和下一章中可以熟悉相关使用方法。

Note

如果想要深入理解iFun引擎的网络协议,请参考 (高级)iFun引擎的网络堆栈

9.1. 注册会话处理器

可以上传打开新的会话或关闭会话时所调用的处理器(handler)函数。在处理器中,新的会话或已关闭的会话通过因子传输。

#include <funapi.h>

...

// 아래 Installer 는 프로젝트를 만들 때 자동으로 생성됩니다.
class MyServerInstaller : public Component {
  static bool Install(const ArgumentMap &/*arguments*/) {
    ...
    // OnSessionOpened 함수와 OnSessionClosed 함수를 등록합니다.
    HandlerRegistry::Install2(OnSessionOpened, OnSessionClosed);
    ...
    // OnTcpConnected 함수를 등록합니다. TCP 연결을 맺은 후 호출합니다.
    HandlerRegistry::RegisterTcpTransportAttachedHandler(OnTcpConnected);
    // OnTcpDisconnected 함수를 등록합니다. TCP 연결이 끊기면 불립니다.
    HandlerRegistry::RegisterTcpTransportDetachedHandler(OnTcpDisconnected);
    ...
  }
}

void OnSessionOpened(const Ptr<Session> &session) {
  // 새로운 클라이언트가 접속하여 세션이 만들어졌습니다.
  // 필요하다면 여기서 세션 초기화 처리를 할 수 있습니다.
  // (여기서 메시지 전송도 가능합니다)
  LOG(INFO) << "New session opened: " << session->id();
}

void OnSessionClosed(const Ptr<Session> &session, SessionClosedReason reason) {
  // 세션이 닫혔습니다.
  // (이미 닫힌 후에 불리기 때문에 메시지 전송은 불가능합니다.)
  LOG(INFO) << "Session closed: " << session->id();

  // 아래처럼 닫힌 원인에 따라 처리할 수 있습니다.
  if (reason == kClosedForServerDid) {
    // Session::Close() 함수를 호출하여 닫았습니다. 또는 비정상 클라이언트로
    // 판단되어 자동으로 Session::Close() 가 불렸을 수 도 있습니다.
    ...
  } else if (reason == kClosedForIdle) {
    // 세션이 타임아웃되어 닫혔습니다.
    ...
  }
}

void OnTcpConnected(const Ptr<Session> &session) {
  // TCP 연결을 맺었습니다.
  ...
}

void OnTcpDisconnected(const Ptr<Session> &session) {
  // 세션의 TCP 연결이 끊겼습니다.
  // 실시간 대전 등 TCP 연결에 민감한 처리가 있었다면 여기서 예외처리 할 수
  // 있습니다. (AI 전환 또는 대전에서 강퇴)
  ...
}

Tip

웹소켓 프로토콜은 HandlerRegistry::RegisterWebSocketTransportAttachedHandlerHandlerRegistry::RegisterWebSocketTransportDetachedHandler 함수를 사용해야 합니다.

using funapi;

public class Server
{
  public static void Install(ArgumentMap arguments)
  {
    ...
    // OnSessionOpened 함수와 OnSessionClosed 함수를 등록합니다.
    NetworkHandlerRegistry.RegisterSessionHandler (
      new NetworkHandlerRegistry.SessionOpenedHandler (OnSessionOpened),
      new NetworkHandlerRegistry.SessionClosedHandler (OnSessionClosed));
    ...
    // OnTcpConnected 함수를 등록합니다. TCP 연결을 맺은 후 호출합니다.
    NetworkHandlerRegistry.RegisterTcpTransportAttachedHandler(OnTcpConnected);
    // OnTcpDisconnected 함수를 등록합니다. TCP 연결이 끊기면 불립니다.
    NetworkHandlerRegistry.RegisterTcpTransportDetachedHandler(OnTcpDisconnected);
    ...
  }

  public static void OnSessionOpened(Session session)
  {
    // 새로운 클라이언트가 접속하여 세션이 만들어졌습니다.
    // 필요하다면 여기서 세션 초기화 처리를 할 수 있습니다.
    // (여기서 메시지 전송도 가능합니다)
    Log.Info ("Session opened.");
  }

  public static void OnSessionClosed(Session session)
  {
    // 세션이 닫혔습니다.
    // (이미 닫힌 후에 불리기 때문에 메시지 전송은 불가능합니다.)
    Log.Info ("Session closed.");
  }

  public static void OnTcpConnected(Session session)
  {
    // TCP 연결을 맺었습니다.
    ...
  }

  public static void OnTcpDisconnected(Session session)
  {
    // 세션의 TCP 연결이 끊겼습니다.
    // 실시간 대전 등 TCP 연결에 민감한 처리가 있었다면 여기서 예외처리 할 수
    // 있습니다. (AI 전환 또는 대전에서 강퇴)
    ...
  }
}

Tip

웹소켓 프로토콜은 NetworkHandlerRegistry.RegisterWebSocketTransportAttachedHandlerNetworkHandlerRegistry.RegisterWebSocketTransportDetachedHandler 함수를 사용해야 합니다.

9.2. 从会话接收数据包

在通过iFun引擎接收消息(数据包)时,为了区分出消息为何种消息,将会共同传输消息类型的字符串。例如,可以是登录“login”、购买道具“buy_item”、人物移动“move”信息。

为接收消息,须要注册对已接收的消息进行处理的函数。该函数被称为消息处理器。消息处理器可按照不同消息类型注册。该函数把发送消息的会话、已接收的消息作为因子传输后通过某种传输协议(TCP、UDP、HTTP)接收后调用。在消息处理器函数中,如未单独指定发送消息时的传输协议,则通过接收消息的传输协议发送。

以下是接收“hello”消息并对其进行处理的OnHello函数注册示例。

Note

当使用 iFun Factory Github账户 中的客户端插件记录消息类型时,会自动设置,所以也可以不作单独考虑。

9.2.1. JSON消息处理器

HandlerRegistry::Register(메시지타입, 핸들러함수) 를 이용하여 JSON 메시지 핸들러를 등록할 수 있습니다.

#include <funapi.h>

// 아래 Installer 는 프로젝트를 만들 때 자동으로 생성됩니다.
class MyServerInstaller : public Component {
  static bool Install(const ArgumentMap &/*arguments*/) {
    ...
    // 아래처럼 처리하고 싶은 메시지 타입과 함수를 연동합니다.
    // 메시지 타입은 JSON 안에서 "_msgtype" 이라는 키 값에 들어있는 값입니다.
    HandlerRegistry::Register("hello", OnHello);
    ...
  }
}

void OnHello(const Ptr<Session> &session, const Json &message) {
  ...
  // hello 메시지를 수신하면 이 함수가 불립니다.

  // 예를 들어 hello 메시지가 아래와 같은 데이터를 포함했다면
  //
  // {
  //   "user_id": "my_user",
  //   "character": {
  //     "name": "my_character",
  //     "level": 99,
  //     ...
  //   }
  // }
  //
  // 아래와 같이 읽을 수 있습니다.
  // string user_id = message["user_id"].GetString();
  // string character_name = message["character"]["name"].GetString();
  // int64_t character_level = message["character"]["level"].GetInteger();
  ...
}

HandlerRegistry::RegisterMessageHandler(메시지타입, 핸들러함수) 를 이용하여 JSON 메시지 핸들러를 등록할 수 있습니다.

using funapi;

public class Server
{
  public static void Install(ArgumentMap arguments)
  {
    ...
    NetworkHandlerRegistry.RegisterMessageHandler (
        "hello",
        new NetworkHandlerRegistry.JsonMessageHandler (OnHello));
    ...
  }

  public static void OnHello (Session session, JObject message)
  {
    ...
    // hello 메시지를 수신하면 이 함수가 불립니다.

    // 예를 들어 hello 메시지가 아래와 같은 데이터를 포함했다면
    //
    // {
    //   "user_id": "my_user",
    //   "character": {
    //     "name": "my_character",
    //     "level": 99,
    //     ...
    //   }
    // }
    //
    // 아래와 같이 읽을 수 있습니다.
    // JObject character = (JObject) message["character"];

    // message 안의 user_id를 가져오겠습니다.
    // string user_id = (string) message["user_id"];
    //
    // 또, 다음과 같이 가져올 수도 있습니다.
    // string user_id = message["user_id"].Value<String>();

    // string character_name = (string) character["name"];
    // Int64 character_level = (Int64) character["level"];
    ...
  }

9.2.2. Protobuf消息处理器

HandlerRegistry::Register2(메시지타입, 핸들러함수) 를 이용하여 Google Protocol Buffers 메시지 핸들러를 등록할 수 있습니다.

// FunMessage 는 아이펀엔진의 Google Protocol Buffers 최상위 메시지입니다.
extend FunMessage {
  optional HelloMessage hello_message = 16;
  ...
}
message HelloMessage {
  required string user_id = 1;
  required Character character = 2;
}
message Character {
  required string name = 1;
  required int64 level = 2;
}
#include <funapi.h>

class MyServerInstaller : public Component {
  // Installer 는 프로젝트를 만들 때 자동으로 생성됩니다.
  static bool Install(const ArgumentMap &/*arguments*/) {
    ...
    // 아래처럼 처리하고 싶은 메시지 타입과 함수를 연동합니다.
    HandlerRegistry::Register2("hello", OnHello);
    //
    // 또는 아래와 같이 정수형 메세지 타입을 사용할 수 있습니다.
    // HandlerRegistry::Register2(1000, OnHello);
    //
    // C++에서는 다음과 같이 생성된 메세지 구조체를 메세지 타입으로 지정할 수 있습니다.
    // HandlerRegistry::Register2(hello_message, OnHello);
  }
}

void OnHello(const Ptr<Session> &session, const Ptr<FunMessage> &message) {
  ...
  // hello 메시지를 수신하면 이 함수가 불리며 아래와 같이 읽을 수 있습니다.
  //
  // if (not message->HasExtension(hello_message)) {
  //   LOG(ERROR) << "wrong message";
  //   ...
  //   return;
  // }
  //
  // const HelloMessage &hello = message->GetExtension(hello_message);
  //
  // string user_id = hello.user_id();
  // string character_name = hello.character().name();
  // int64_t character_level = hello.character().level();
  ...
}

NetworkHandlerRegistry.RegisterMessageHandler(메시지타입, 핸들러함수) 를 이용하여 Google Protocol Buffers 메시지 핸들러를 등록할 수 있습니다.

// FunMessage 는 아이펀엔진의 Google Protocol Buffers 최상위 메시지입니다.
extend FunMessage {
  optional HelloMessage hello_message = 16;
  ...
}
message HelloMessage {
  required string user_id = 1;
  required Character character = 2;
}
message Character {
  required string name = 1;
  required int64 level = 2;
}
using funapi;

public class Server
{
  public static void Install(ArgumentMap arguments)
  {
    ...
    NetworkHandlerRegistry.RegisterMessageHandler (
        "hello",
        new NetworkHandlerRegistry.ProtobufMessageHandler (OnHello));
    //
    // 또는 아래와 같이 정수형 메세지 타입을 지정할 수 있습니다.
    // NetworkHandlerRegistry.RegisterMessageHandler (
    //     1000,
    //     new NetworkHandlerRegistry.ProtobufMessageHandler (OnHello));
    ...
  }

  public static void OnHello(Session session, FunMessage message)
  {
    ...
    // hello 메시지를 수신하면 이 함수가 불리며 아래와 같이 읽을 수 있습니다.
    //
    // HelloMessage hello_message;
    // if (!message.TryGetExtension_hello_message (
    //     out hello_message))
    // {
    //   Log.Error ("OnEchoPbuf: Wrong message.");
    //   return;
    // }
    //
    // string user_id = hello_message.user_id;
    // string character_name = hello_message.character.name;
    // long character_level = hello_message.character.level;
    ...
  }
}

9.3. 向会话发送数据包

为了向会话发送消息(数据包),使用 Session::SendMessage() 或支持分布式服务器的 AccountManager::SendMessage() 函数即可。 (AccountManager::SendMessage() 请参考 将数据包发送给其他服务器的客户端

消息传输函数具有如下因子。

Session::SendMessage(消息类型, 消息 [, 加密方式] [, 传输协议])
  • 메세지 타입: 메세지 구분 식별자로 사용됩니다. 문자열 또는 정수 형태로 사용 가능합니다.

  • 메세지: 보낼 메세지를 지정합니다. Json 또는 FunMessage 형태로 지정할 수 있습니다.

  • 加密方式: 可省略,或在指定 kDefaultEncryption 时使用默认值。具体说明请参考 消息加密

  • 传输协议: 可以是 kTcp, kUdp, kHttp 中的任意一个,若省略,将自动选择。具体说明请参考 多协议

Note

ambiguous transport protocol for sending 'testtype' message. candidates: Tcp, Http 等错误日志并无法传输时,请参考 多协议 的说明。

Note

:code:`SendMessage ignored. no ‘Tcp’ 日志是相应传输协议未连接的情况(试图通过kTcp发送但TCP连接断开等),在正常情况下也经常发生。

Tip

如使用 Session::SendBackMessage() 函数,则可以省略消息类型,并自动通过最后接收的消息类型进行发送。例如,在接收“login”消息的处理器中,若调用 SendBackMessage() ,则按照 SendMessage("login", ...) 运行。

9.3.1. 传输JSON消息

이 예제에서는 아래 JSON 메시지를 “world” 라는 메시지 타입으로 전송합니다.

{
   "user_id": "my_user",
   "character": {
     "name": "my_character",
     "level": 99,
     ...
   }
 }
Json message;
message["user_id"] = "my_user";
message["character"]["name"] = "my_character";
message["character"]["level"] = 99;

session->SendMessage("world", message);

// 명시적으로 HTTP 로 전송
session->SendMessage("world", message, kDefaultEncryption, kHttp);

// ChaCha20 으로 암호화하여 전송
session->SendMessage("world", message, kChacha20Encryption);

// 명시적으로 TCP 로 AES128 로 암호화하여 전송
session->SendMessage("world", message, kAes128Encryption, kTcp);

// 정수형 메세지 타입을 지정한 TCP 전송
session->SendMessage(1000, message, kDefaultEncryption, kTcp);

// C++에서는 메세지 타입으로 Protobuf 구조체도 사용할 수 있습니다.
session->SendMessage(hello_message, message, kDefaultEncryption, kTcp);

이 예제에서는 아래 JSON 메시지를 “world” 라는 메시지 타입으로 전송합니다.

{
   "user_id": "my_user",
   "character": {
     "name": "my_character",
     "level": 99,
     ...
   }
 }
JObject message = new JObject();
message["user_id"] = "my_user";
message["character"] = new JObject();
message["character"]["name"] = "my_character";
message["character"]["level"] = 99;

session.SendMessage ("world", message);

// 명시적으로 HTTP 로 전송
session.SendMessage (
    "hello",
    message,
    Session.Encryption.kDefault,
    Session.Transport.kHttp);

// ChaCha20 으로 암호화하여 전송
session.SendMessage (
    "hello", message, Session.Encryption.kChaCha20);

// 명시적으로 TCP 로 AES128 로 암호화하여 전송
session.SendMessage (
    "hello",
    message,
    Session.Encryption.kAes128,
    Session.Transport.kTcp);

9.3.2. 传输Protobuf消息

이 예제에서는 아래 HelloMessage 메시지를 “world” 라는 메시지 타입으로 전송합니다.

extend FunMessage {
  optional HelloMessage hello_message = 16;
  ...
}
message HelloMessage {
  required string user_id = 1;
  required Character character = 2;
}
message Character {
  required string name = 1;
  required int64 level = 2;
}
Ptr<FunMessage> message(new FunMessage);
HelloMessage *hello = message->MutableExtension(hello_message);
hello->set_user_id("my_user");
Character *character = hello->mutable_character();
character->set_name("my_character");
character->set_level(99);

session->SendMessage("world", message);

// 명시적으로 HTTP 로 전송
session->SendMessage("world", message, kDefaultEncryption, kHttp);

// ChaCha20 으로 암호화하여 전송
session->SendMessage("world", message, kChacha20Encryption);

// 명시적으로 TCP 로 AES128 로 암호화하여 전송
session->SendMessage("world", message, kAes128Encryption, kTcp);

// 정수형 메세지 타입을 지정한 TCP 전송
session.SendMessage (1000, message, kDefaultEncryption, kTcp);

이 예제에서는 아래 HelloMessage 메시지를 “world” 라는 메시지 타입으로 전송합니다.

extend FunMessage {
  optional HelloMessage hello_message = 16;
  ...
}
message HelloMessage {
  required string user_id = 1;
  required Character character = 2;
}
message Character {
  required string name = 1;
  required int64 level = 2;
}
FunMessage message = new FunMessage();

HelloMessage hello_message = new HelloMessage();
hello_message.user_id = "my_user";
hello_message.character = new Character();
hello_message.character.name = "my_character";
hello_message.character.level = 99;
message.AppendExtension_hello_message (hello_message)
session.SendMessage ("world", message);

// 명시적으로 HTTP 로 전송
session.SendMessage ("world", message, kDefaultEncryption, kHttp);

// ChaCha20 으로 암호화하여 전송
session.SendMessage ("world", message, kChacha20Encryption);

// 명시적으로 TCP 로 AES128 로 암호화하여 전송
session.SendMessage ("world", message, kAes128Encryption, kTcp);

Important

事务 中所述,iFun引擎的对象子系统会对所有lock进行解锁,通过操作回滚的方式防止死锁。“操作回滚”是指数据包处理器(消息处理器)可以多次反复执行。但这种反复执行会给执行无法回滚的操作的函数带来问题。 SendMessage() 函数就属于这种无法回滚的操作。为了避免该问题,将不得重复执行的代码放在可回滚的代码后即可。

iFun引擎为了防止这种函数因为失误而被放置在可回滚的代码前,造成意料之外的重复执行,采用了在发生回滚后产生assertion的设计。而在开发过程中可以轻松发现这种assertion,可通过更改代码位置,避免该问题的发生。

9.3.3. 向所有会话传输消息

아이펀 엔진에서는 모든 세션에 메시지를 전송하는 기능을 제공하고 있습니다. 로컬 서버에 접속한 모든 세션에 메시지를 전송하기 위해서는 Session::BroadcastLocally() 함수를 사용하면 됩니다. 함수 인자는 向会话发送数据包 에서 설명하는 Session::SendMessage() 함수와 동일합니다. 함수 인자중에 TransportProtocolkTcpkUdp 타입만 사용할 수 있습니다.

Tip

분산 환경에서 로컬 서버뿐만 아니라 다른 서버에 접속한 모든 세션에도 메시지를 전송하기 위해서는 无视登录与否,向服务器的所有会话发送数据包 에 설명된 Session::BroadcastGlobally() 함수를 사용하면 됩니다.

Note

Session::BroadcastLocally()Session::BroadcastGlobally() 는 로그인 처리 완료 여부와 상관없이 모든 세션에 메시지를 전송합니다. 로그인 처리된 유저들에게만 메시지를 전송하는 것은 向服务器上已登录的所有客户端发送数据包。 를 참고하세요.

JSON 메시지 전송

아래 코드는 로컬 서버에 접속한 모든 세션에 다음의 JSON 메시지를 전송하는 예제입니다.

{
  "user_id": "my_user",
  "character": {
    "name": "my_character",
    "level": 99,
    ...
  }
}
void BroadcastToAllLocalSessions() {
  Json message;
  message["user_id"] = "my_user";
  message["character"]["name"] = "my_character";
  message["character"]["level"] = 99;

  // TCP 로 접속한다고 가정하겠습니다.
  // 만약 UDP 로 접속한다면 kUdp 로 지정하세요.
  Session::BroadcastLocally("world", message, kDefaultEncryption, kTcp);
  // 또는 다음과 같이 정수형 메세지 타입을 사용할 수 있습니다.
  // Session::BroadcastLocally(1000, message);
  // C++에서는 Protobuf 구조체를 메세지 타입으로 사용할 수 있습니다.
  // Session::BroadcastLocally(hello_message, message);
}
public void BroadcastToAllLocalSessions()
{
  JObject message = new JObject ();
  message["user_id"] = "my_user";
  message["character"] = new JObject ();
  message["character"]["name"] = "my_character";
  message["character"]["level"] = 99;

  // TCP 로 접속한다고 가정하겠습니다.
  // 만약 UDP 로 접속한다면 kUdp 로 지정하세요.
  Session.BroadcastLocally ("world",
                            message,
                            Session.Encryption.kDefault,
                            Session.Transport.kTcp);
  // 또는 다음과 같이 정수형 메세지 타입을 사용할 수 있습니다.
  // Session.BroadcastLocally (1000,
  //                           message,
  //                           Session.Encryption.kDefault,
  //                           Session.Transport.kTcp);
}

Protobuf 메시지 전송

아래 코드는 로컬 서버에 접속한 모든 세션에 다음의 Protobuf 메시지를 전송하는 예제입니다.

extend FunMessage {
  optional HelloMessage hello_message = 16;
  ...
}
message HelloMessage {
  required string user_id = 1;
  required Character character = 2;
}
message Character {
  required string name = 1;
  required int64 level = 2;
}
void BroadcastToAllLocalSessions() {
  Ptr<FunMessage> message(new FunMessage);
  HelloMessage *hello = message->MutableExtension(hello_message);
  hello->set_user_id("my_user");
  Character *character = hello->mutable_character();
  character->set_name("my_character");
  character->set_level(99);
  // TCP 로 접속한다고 가정하겠습니다.
  // 만약 UDP 로 접속한다면 kUdp 로 지정하세요.
  Session::BroadcastLocally("world", message);
}
public void BroadcastToAllLocalSessions()
{
  FunMessage message = new FunMessage ();
  HelloMessage hello = new HelloMessage ();
  hello.user_id = "my_user";
  hello.character = new Character ();
  hello.character.level = 99;
  hello.character.name = "my_character";
  message.AppendExtension_hello_message (hello);

  // TCP 로 접속한다고 가정하겠습니다.
  // 만약 UDP 로 접속한다면 kUdp 로 지정하세요.
  Session.BroadcastLocally ("world",
                            message,
                            Session.Encryption.kDefault,
                            Session.Transport.kTcp);
}

Important

Session::BroadcastLocally() 함수는 오브젝트 트랜잭션 롤백이 발생할 경우 assertion 을 일으키는 코드를 내장하고 있습니다. 자세한 내용은 事务 을 참고하세요.

9.4. 结束会话

会话可因以下三种原因关闭。

  1. 调用 void Session::Close() 函数,会话将立即关闭。

  2. 当在指定的时间内没有消息的收发时,将自动关闭。(可以通过 网络功能设置参数 中介绍的session_timeout_in_second来设置该时间。)

  3. 当判断为非正常客户端时,将自动关闭。

会话关闭后,将调用 注册会话处理器 中介绍的会话关闭处理器。通过会话关闭处理器传输的关闭原因就是,第1种和第3种情况为 kClosedForServerDid, 第2种情况为 kClosedForIdle

同时,可通过 bool Session::IsOpened() const 查看是否已经打开。由于调用消息处理器时传输的会话已经打开,因此无需单独的检测,但将会话保存在全局变量中使用时,有时需要查看是否已经打开。(但是,此时也无需单独的检测,最好在会话关闭处理器中,对全局变量中保存的会话进行整理。)

Note

若通过已关闭的会话传输消息,将输出 SendMessage ignored. closed session 日志,消息不会被传输,将直接被省略。(在正常情况下也有可能发生,它并不是表示错误的日志。)

保留会话,仅结束已附在其中的连接

可以保留会话,仅结束已附在其中的连接。此时,若在会话超时之前客户端重新连接,则可继续使用相应会话。

应用 void Session::CloseTransport([要关闭的 传输 协议]) 函数。用于关闭的传输协议因子可以为TCP、UDP、HTTP,若省略,将关闭所有传输协议。

可以通过 bool Session::IsTransportAttached([传输 协议]) const 函数查看传输协议是否已经连接。若已省略传输协议因子,则可确认是否已通过任意协议连接。

对于TCP,可以注册连接断开时所调用的函数。请参考 注册会话处理器 的说明。

Ptr<Session> session = ...;

// session 에 TCP 연결이 있는지 확인합니다.
if (session->IsTransportAttached(kTcp)) {
  ...
}

// session 에 TCP, UDP, HTTP 가 있는지 확인합니다.
if (session->IsTransportAttached()) {
  ...
}

// session 에 TCP 연결이 있다면 끊습니다.
session->CloseTransport(kTcp);

// session 의 TCP, UDP, HTTP 를 모두 닫습니다.
session->CloseTransport();
session = ...;

// session 에 TCP 연결이 있는지 확인합니다.
if (session.IsTransportAttached (Session.Transport.kTcp)) {
  ...
}

// session 에 TCP, UDP, HTTP 가 있는지 확인합니다.
if (session.IsTransportAttached ()) {
  ...
}

// session 에 TCP 연결이 있다면 끊습니다.
session.CloseTransport (Session.Transport.kTcp);

// session 의 TCP, UDP, HTTP 를 모두 닫습니다.
session.CloseTransport ();

9.5. 会话数据

9.5.1. 会话标签

可以给会话添加标签,通过标签来搜索会话。

  • 添加标签: void Session::Tag(const string &tag)

  • 删除标签: void Session::Untag(const string &tag)

  • 确认有无标签: bool Session::HasTag(const string &tag)

  • 提取标签列表: std::set<string> Session::GetTags()

  • 提取带有特定标签的全部会话: static SessionsSet Session::FindWithTag(const string &tag)

以下是运用标签功能向参与PvP对战的用户发送公告的示例。

// PvP 대전이 시작될 때 불리는 핸들러라고 하겠습니다.
void OnPvPStarted(const Ptr<Session> &session, const Json &message) {
  ...
  // PvP 대전 태그를 붙입니다.
  session->Tag("pvp");
}

// 운영자가 PvP 대전을 진행중인 유저에게 공지를 보내는 함수라고 하겠습니다.
void NoticeToPvP(const string &notice_message) {
  // SessionsSet 은 boost::unordered_set<Ptr<Session>> 의 typedef 입니다.
  Session::SessionsSet sessions = Session::FindWithTag("pvp");
  for (const Ptr<Session> &session: sessions) {
    // 공지 메시지를 전달합니다.
    session->SendMessage(...);
  }
}
// PvP 대전이 시작될 때 불리는 핸들러라고 하겠습니다.
public void OnPvPStarted(Session session, JObject message)
{
  // PvP 대전 태그를 붙입니다.
  session.Tag("pvp");
}

// 운영자가 PvP 대전을 진행중인 유저에게 공지를 보내는 함수라고 하겠습니다.
public void NoticeToPvP(string notice_message)
{
  List<Session> sessions = Session.FindWithTag("pvp");
  foreach(Session session in sessions)
  {
    // 공지 메시지를 전달합니다.
    session.SendMessage (...);
  }
}

9.5.2. 会话context

각 세션 별로 고유의 상태, 데이터를 저장할 수 있습니다.

  • 컨텍스트 쓰기: void Session::SetContext(const Json &context)

  • 컨텍스트 읽기: Json &Session::GetContext()

Important

위 두 함수는 thread-safe 하지 않기 때문에 주의 하셔야 합니다. 필요할 경우 아래 함수들로 뮤텍스 락을 걸 수 있습니다.

  • void Session::LockContext()

  • void Session::UnlockContext()

또는

  • boost::mutex &Session::GetContextMutex() 로 직접 뮤텍스 객체를 얻을 수 있습니다.

더 편한 방법은 세션 객체를 뮤텍스로 사용하는 것이며, 아래 예제는 이것을 이용해 세션이 닫힐 때 세션 콘텍스트로 로그인 상태인지 판단하여 로그아웃 처리를 합니다.

각 세션 별로 고유의 상태, 데이터를 저장할 수 있습니다.

  • 컨텍스트 객체 얻기: JObject Session.Context()

Important

위 함수는 thread-safe 하지 않기 때문에 주의 하셔야 합니다. 필요할 경우에 아래처럼 동기화 하여 사용합니다.

lock (session)
{
  ...
}
// 이 함수는 로그인 요청이 올 때 불린다고 하겠습니다.
void OnLogin(const Ptr<Session> &session, ...) {
  ...
  // 로그인 처리를 합니다.
  ...

  // 로그인이 완료 되었으면 아래 처리를 합니다.
  {
    // 뮤텍스 락을 걸어 동시에 쓰는 것을 보호합니다.
    boost::mutex::scoped_lock lock(*session);
    // 또는 이렇게도 가능합니다. (데드락 주의)
    // session->LockContext() 반드시 sesion->UnlockContext() 도 불러야합니다.

    // 세션이 로그인 상태임을 저장합니다.
    session->GetContext()["login"] = true;
  }
}

// 이 함수는 세션이 닫힐 때 불리는 핸들러라고 하겠습니다.
void OnSessionClosed(const Ptr<Session> &session) {
  ...

  // 로그인 되어 있던 세션이면 로그아웃 처리를 합니다.
  bool logged_in = false;
  {
    // 뮤텍스 락을 걸어 동시에 쓰는 것을 보호합니다.
    boost::mutex::scoped_lock lock(*session);

    const Json &ctxt = session->GetContext();
    if (ctxt.HasAttribute("login", Json::kBoolean)) {
      logged_in = ctxt["login"].GetBool();
    }
  }

  if (logged_in) {
    // 로그인 상태의 세션입니다. 로그아웃 처리를 합니다.
    ...
  }
}
// 이 함수는 로그인 요청이 올 때 불린다고 하겠습니다.
public void OnLogin(Session session, ...)
{
  ...
  // 로그인 처리를 합니다.
  ...

  // session의 context를 직접 제어할 때는 thread-safe하지 않으므로
  // 다음과 같이 해당 세션을 대상으로 락을 잡아서 처리합니다.
  lock (session)
  {
    session.Context ["login"] = true;
  }
}

// 이 함수는 세션이 닫힐 때 불리는 핸들러라고 하겠습니다.
public void OnSessionClosed(Session session)
{
  ...

  // 로그인 되어 있던 세션이면 로그아웃 처리를 합니다.
  bool logged_in = false;

  lock (session)
  {
    if (session.Context ["login"] != null)
    {
      JToken token = session.Context.GetValue ("login");
      if (token.Type == JTokenType.Boolean)
      {
        logged_in = true;
      }
      else
      {
        Log.Error ("wrong json type 'login'. type= {0}",
                   token.Type.ToString());
      }
    }
    else
    {
      Log.Error ("wrong json attribute 'login'. json string= {0}",
                 sesson.Context.ToString());
    }
  }

  if (logged_in) {
    // 로그인 상태의 세션입니다. 로그아웃 처리를 합니다.
    ...
  }
}

Tip

使用该函数,可以更加便利地使用会话context。不仅thread-safe,而且可用于在context JSON中保存的key上写入值后进行读取。

  • void Session::AddToContext(const string &key, const string &value)

  • void Session::AddToContext(const string &key, int64_t value)

  • bool Session::GetFromContext(const string &key, string *ret)

  • bool Session::GetFromContext(const string &key, int64_t *ret)

  • bool Session::DeleteFromContext(const string &key)

이 함수들을 이용하여 위 예제를 아래처럼 간단하게 할 수 있습니다.

// 이 함수는 로그인 요청이 올 때 불린다고 하겠습니다.
void OnLogin(const Ptr<Session> &session, ...) {
  ...
  // 로그인 처리를 합니다.
  ...

  // 로그인이 완료 되었으면 아래 처리를 합니다.
  session->AddToContext("login", 1);
}

// 이 함수는 세션이 닫힐 때 불리는 핸들러라고 하겠습니다.
void OnSessionClosed(const Ptr<Session> &session) {
  ...

  // 로그인 되어 있던 세션이면 로그아웃 처리를 합니다.
  int64_t logged_in = 0;
  if (session->GetFromContext("login", &logged_in) && logged_in == 1)
  {
    // 로그인 상태의 세션입니다. 로그아웃 처리를 합니다.
    ...
    session->DeleteFromContext("login");
  }
}
// 이 함수는 로그인 요청이 올 때 불린다고 하겠습니다.
public void OnLogin(Session session, ...)
{
  ...
  // 로그인 처리를 합니다.
  ...

  // 로그인이 완료 되었으면 아래 처리를 합니다.
  session.AddToContext ("login", 1);
}

// 이 함수는 세션이 닫힐 때 불리는 핸들러라고 하겠습니다.
public void OnSessionClosed(Session session)
{
  ...

  Int64 logged_in = 0;
  if (session.GetFromContext ("login", out logged_in) && logged_in == 1)
  {
    // 로그인 상태의 세션입니다. 로그아웃 처리를 합니다.
    ...
    session.DeleteFromContext ("login");
  }
}

9.6. 会话Ping(RTT)

iFun引擎提供了获取与客户端之间的round-trip time(RTT)的功能。同时,还提供了在一定时间内未对PING请求作出响应的客户端连接进行断开的功能。

Note

当前版本仅在TCP Transport 中运行。

9.6.1. 측정

可通过 Session::SetPingSamplingInterval() 函数设置测量周期。默认值为以下 网路功能设置参数ping_sampling_interval_in_second 。0表示不使用。

9.6.2. 获取值

可通过 Session::GetPing() 函数获取Ping(RTT)值。

9.6.3. 断开未响应的连接

通过 Session::SetPingTimeout() 函数设置的时间内无响应时,就会断开连接。这并不是关闭会话,仅仅是断开TCP连接。默认值为以下 网路功能设置参数ping_timeout_in_second 。0表示不使用。

9.6.4. 示例

// 서버 Install 함수입니다.
static bool Install(const ArgumentMap &) {
  // 세션 열림 핸들러를 등록합니다.
  HandlerRegistry::Install2(OnSessionOpened, ...);

  // TCP 연결 끊김 핸들러를 등록합니다.
  HandlerRegistry::RegisterTcpTransportDetachedHandler(OnTcpDisconnected);

  HandlerRegistry::Register(...);
  ...
}

// 세션이 열리면 불리는 핸들러입니다.
void OnSessionOpened(const Ptr<Session> &session) {
  // 10 초 주기로 RTT 를 측정하며 5 초 이상 응답하지 않으면 연결을 닫습니다.
  session->SetPingSamplingInterval(10);  // 이 코드는 MANIFEST 의 기본 값을 설정해두면 필요 없습니다.
  session->SetPingTimeout(5);

  ...
}

// TCP 연결이 끊기면 불리는 핸들러입니다.
void OnTcpDisconnected(const Ptr<Session> &session) {
  // TCP 연결이 끊기면 이 함수가 불립니다.
  ...
}

// 임의의 사용자 함수
void OnXYZ(const Ptr<Session> &session, ...) {
  // 필요할 때 아래와 같이 RTT 값을 얻을 수 있습니다.
  Session::Ping ping = session->GetPing();
  if (ping.second == 0) {
    // 측정된 rtt 가 하나도 없습니다.
    return;
  }

  int64_t rtt_in_ms = ping.first / 1000;

  LOG(INFO) << "rtt=" << rtt_in_sec << " ms";
}
// 서버 Install 함수입니다.
public static void Install(ArgumentMap arguments)
{
  // 세션 열림 핸들러를 등록합니다.
  NetworkHandlerRegistry.RegisterSessionHandler (
    new NetworkHandlerRegistry.SessionOpenedHandler (OnSessionOpened),
    new NetworkHandlerRegistry.SessionClosedHandler (OnSessionClosed));

  // TCP 연결 끊김 핸들러를 등록합니다.
  NetworkHandlerRegistry.RegisterTcpTransportDetachedHandler(OnTcpDisconnected);

  NetworkHandlerRegistry.Register(...);
  ...
}

// 세션이 열리면 불리는 핸들러입니다.
public static void OnSessionOpened(Session session)
{

  // 10 초 주기로 RTT 를 측정하며 5 초 이상 응답하지 않으면 연결을 닫습니다.
  session.SetPingSamplingInterval(10);  // 이 코드는 MANIFEST 의 기본 값을 설정해두면 필요 없습니다.
  session.SetPingTimeout(5);
}

// TCP 연결이 끊기면 불리는 핸들러입니다.
public static void OnTcpDisconnected(Session session)
{
  // TCP 연결이 끊기면 이 함수가 불립니다.
  ...
}

// 임의의 사용자 함수
public static void OnXYZ(Session session)
{
  // 필요할 때 아래와 같이 RTT 값을 얻을 수 있습니다.
  Session.Ping ping = session.GetPing();
  TimeSpan rtt_span = ping.RoundtripTime;

  if (ping.SamplingCount <= 0)
  {
    // 측정된 rtt 가 하나도 없습니다.
    return;
  }

  Log.Info ("rtt= {0}ms", rtt_span.Milliseconds);
}

9.7. 세션 메시지 핸들러 후킹

핸들러 후킹 함수를 이용하면 메세지 핸들러가 호출되기 전(Pre)/후(Post) 시점에서 핸들러 호출을 제어할 수 있으며 Protobuf와 Json 핸들러 모두 지원합니다.

호출되기 전(Pre) 핸들러는 bool 타입을 리턴 값으로 받는데 이 값이 false일 경우 핸들러와 호출 후(Post) 핸들러가 불리지 않습니다. 또한 두 핸들러 모두 NULL일 경우에는 에러가 발생합니다.

typedef function<bool(
    const Ptr<Session> &/*session*/,
    const Ptr<const FunMessage> &/*message*/,
    const string &/*message type*/)> ProtobufPreMessageHandlerHook;

typedef function<void(
    const Ptr<Session> &/*session*/,
    const Ptr<const FunMessage> &/*message*/,
    const string &/*message type*/)> ProtobufPostMessageHandlerHook;

typedef function<bool(
    const Ptr<Session> &/*session*/,
    const Ptr<const FunMessage> &/*message*/,
    const int32_t /*message_type*/)> ProtobufPreMessageHandlerHook2;

typedef function<void(
    const Ptr<Session> &/*session*/,
    const Ptr<const FunMessage> &/*message*/,
    const int32_t /*message_type*/)> ProtobufPostMessageHandlerHook2;

typedef function<bool(
    const Ptr<Session> &/*session*/,
    const Json &/*message*/,
    const string & /*message type*/)> JsonPreMessageHandlerHook;

typedef function<void(
    const Ptr<Session> &/*session*/,
    const Json &/*message*/,
    const string &/*message type*/)> JsonPostMessageHandlerHook;

void InstallProtobufMessageHandlerHook(
    const ProtobufPreMessageHandlerHook &protobuf_pre_message_handler_hook,
    const ProtobufPostMessageHandlerHook &protobuf_post_message_handler_hook);

void InstallProtobufMessageHandlerHook2(
    const ProtobufPreMessageHandlerHook2 &protobuf_pre_message_handler_hook,
    const ProtobufPostMessageHandlerHook2 &protobuf_post_message_handler_hook);

void InstallJsonMessageHandlerHook(
    const JsonPreMessageHandlerHook &json_pre_message_handler_hook,
    const JsonPostMessageHandlerHook &json_post_message_handler_hook);
class Session
{
  ...
  public delegate bool ProtobufPreMessageHandlerHook(Session session,
                                                     FunMessage message,
                                                     string message_type);

  public delegate void ProtobufPostMessageHandlerHook(Session session,
                                                      FunMessage message,
                                                      string message_type);

  public delegate bool JsonPreMessageHandlerHook(Session session,
                                                 JObject message,
                                                 string message_type);

  public delegate void JsonPostMessageHandlerHook(Session session,
                                                  JObject message,
                                                  string message_type);

  public static void InstallProtobufMessageHandlerHook (
      ProtobufPreMessageHandlerHook pre_hook,
      ProtobufPostMessageHandlerHook post_hook);

  public static void InstallJsonMessageHandlerHook (
      JsonPreMessageHandlerHook pre_hook,
      JsonPostMessageHandlerHook post_hook);
  ...
}

Note

InstallProtobufMessageHandlerHook2 함수는 1.0.0-2874 Experimental 버전 이상에서만 사용할 수 있습니다.

아래 예제는 핸들러 호출 전 Json 오브젝트를 검사하여 test 라는 항목이 있을 경우에만 호출하는 코드입니다.

...
bool OnJsonPreMessageHandle(
    const Ptr<fun::Session> &session,
    const fun::Json &json,
    const string &message_type) {
  if (json.HasAttribute("test")) {
    return true;
  }

  return false;
}
...

void RegisterEventHandlers() {
  ...
  InstallJsonMessageHandlerHook(OnJsonPreMessageHandle, NULL);
  ...
}
public class Server
{
  ...
  static bool OnPreMessageHandle(Session session,
                                 JObject json,
                                 string message_type) {
    if (json["test"] != null) {
      return true;
    }

    return false;
  }
  ...
  public static bool Install(ArgumentMap arguments)
  {
    ...
    Session.InstallJsonMessageHandlerHook (OnPreMessageHandle, null);
    ...
  }
}

9.8. 会话消息传输hooking

可以以统计服务器性能为目的,注册调用SendMessage()时所调用的函数。利用该函数及 最后消息的接收时间 ,可以测量接收消息后作出响应所需的时间。

可通过以下函数注册hook函数。

typedef function<void(const Ptr<Session> &/*session*/,
                      const Ptr<const FunMessage> &/*message*/,
                      const string &/*message_type*/,
                      size_t message_size)> ProtobufMessageSendHook;

typedef function<void(const Ptr<Session> &/*session*/,
                      const Json &/*message*/,
                      const string &/*message_type*/,
                      size_t message_size)> JsonMessageSendHook;

void InstallProtobufMessageSendHook(const ProtobufMessageSendHook &hook);
void InstallJsonMessageSendHook(const JsonMessageSendHook &hook);
class Session
{
  ...
  public delegate void ProtobufMessageSendHook(Session session,
                                               FunMessage message,
                                               string message_type,
                                               ulong message_size);

  public delegate void JsonMessageSendHook(Session session,
                                           JObject message,
                                           string message_type,
                                           ulong message_size);

  public static void InstallProtobufMessageSendHook(
      ProtobufMessageSendHook hook);

  public static void InstallJsonMessageHook(
      JsonMessageSendHook hook);
  ...
}

以下是通过日志输出消息响应时间的示例。在该示例中,假设客户端所发送的消息的消息类型为 cs_ prefix,服务器发送的为 sc_ prefix。

auto send_hook = [](const Ptr<Session> &session, const Json &message,
                    const string &message_type, size_t message_size) {
  // 클라와 서버간 메시지 타입이 prefix 로 인해 다르기 때문에
  // prefix 를 제거하여 공통 메시지 타입을 얻습니다.
  const char *common_message_type = &message_type[3];

  // 메시지 수신 시간을 얻어 응답하는데 걸린 시간을 출력합니다.
  WallClock::Value request_time;
  if (session->GetLastReceiveTime(string("cs_") + common_message_type,
                                  &request_time)) {
    string ip_address;
    session->GetRemoteEndPoint(kHttp, &ip_address);
    size_t response_time_in_ms = (WallClock::Now() - request_time).total_milliseconds();
    LOG(INFO) << "Response: ip_address=" << ip_address
              << ", msgtype=" << common_message_type
              << ", response_time=" << response_time_in_ms
              << ", response_size=" << message_size
              << ", error_code=" << message["error_code"].GetInteger();
  } else {
    // request 없는 send
  }
};

// 서버 Install 시점에 이 함수를 등록합니다.
fun::InstallJsonMessageSendHook(send_hook);

C# 예제는 추후 업데이트 됩니다.

9.9. 会话消息传输的稳定性

在移动环境中,网络断线重连的情况较为频繁,所以为了保证稳定的游戏服务,需要对此作出应对措施。iFun引擎实现了不仅玩家不会感觉到网络瞬间重连,而且游戏开发人员也不会感觉到网络瞬间重连。

当TCP连接断线时,重新连接其实并不难。然而,在重连的过程中发送的消息会丢失,并且难以去掌握是哪些数据包发生了丢失。如果无法恢复已丢失的数据包,用于进行游戏的context就会损坏,导致无法正常游戏。(须要通过主菜单强制发送,或重启应用程序。)通常,找出这些丢失的数据包并进行修复是一件不容易的事情,而且它也会让游戏服务器和客户端之间的源代码变得非常复杂。

若激活以下 configuration 和所提供的客户端插件的 session reliability 选项,iFun引擎即可自动在重连后依次传输丢失的数据包。

9.10. 会话的便利功能

9.10.1. 搜索会话

利用以下函数,可通过会话ID获取会话。

static Ptr<Session> Session::Find(const SessionId &session_id)
static Session Find(System.Guid session_id)

会话ID可通过以下函数获取。

const SessionId &Session::id() const
System.Guid Id

9.10.2. 会话地址

利用以下函数,可获取与已连接在会话上的客户端的IP/Port。

bool Session::GetRemoteEndPoint(TransportProtocol protocol, string *ip, uint16_t *port = NULL)

port 因子可以省略。

以下是在登录时通过日志输出客户端IP地址的示例。

// 로그인 시 불리는 핸들러라고 하겠습니다.
void OnLogin(const Ptr<Session> &session, ...) {
  ...
  string ip;
  if (session->GetRemoteEndPoint(kTcp, &ip)) {
    LOG(INFO) << "client_ip_address=" << ip;
  } else {
    // 이 핸들러 처리 중 TCP 연결이 끊긴 경우입니다.
  }
}
// 로그인 시 불리는 핸들러라고 하겠습니다.
void OnLogin(Session session, ...) {
  ...
  string ip;
  if (session.GetRemoteEndpoint (Session.Transport.kTcp, out ip)) {
    Log.Info ("client_ip_address={0}", ip);
  } else {
    // 이 핸들러 처리 중 TCP 연결이 끊긴 경우입니다.
  }
}

9.10.3. 最后收发的消息类型

通过以下函数可获取最后收发的消息类型。

const string &Session::LastSentMessageType() const
const string &Session::LastReceivedMessageType() const
string LastSentMessageType
string LastReceivedMessageType

9.10.4. 最后消息的接收时间

通过以下函数可获取已保存的消息类型的最后接收时间。该功能仅在注册 会话消息传输hooking 中所介绍的hook函数时运行。

bool Session::GetLastReceiveTime(const string &msg_type, WallClock::Value *receive_time) const

该功能用于统计,详情请参考 会话消息传输hooking 中的说明。

9.10.5. Changing session timeout value

You can change timeout value for specific session with OverrideSessionTimeout() . Please note that this function is available in 1.0.0-4006 experimental and after.

Followings are the rules to determine the timeout.

  • The overridden timeout value will be used if OverrideSessionTimeout() was called.

  • The default value(session_timeout_in_second in MANIFEST.json) will be used for the session that was not overridden by OverrideSessionTimeout() function.

  • The timeout checking will be disabled if session_timeout_in_second or overridden value is 0.

    Important

    Please be careful when disabling session’s timeout. The session will remain forever until you close it manually.

Followings are the interface.

void OverrideSessionTimeout(
    const Ptr<Session> &session, const int64_t timeout_in_ms);
Not supported yet.