10. Networking part 1: Sessions

Client connections are known as sessions in iFun Engine. A session may use one or more protocols (TCP, UDP, or HTTP). For example, login and billing messages may be transmitted through HTTP while game messages are transmitted through TCP.

A session begins when a new client connects and lasts until the session is closed or times out. Messages can be easily transmitted through the desired protocol, such as JSON or Protobuf, using sessions.

iFun Engine sessions offer the following features necessary for game service:

  • Message (packet) transmission

  • TCP connection restoration

  • Ping sampling

  • Urgent messages

  • Encryption

  • Message replay attack blocking

Several other features are offered, and you can familiarize yourself with their use in this and the next chapter.

Note

If you want a more detailed understanding of iFun Engine’s networking protocol, please read (Advanced) iFun Engine Network Stack.

10.1. Registering session handlers

You can register the handler invoked when a new session is opened or closed. A new session or closed session is transmitted to the handler as a parameter.

#include <funapi.h>

...

// Installer below is auto-generated when creating a project.
class MyServerInstaller : public Component {
  static bool Install(const ArgumentMap &/*arguments*/) {
    ...
    // Registers both OnSessionOpened function and OnSessionClosed function.
    HandlerRegistry::Install2(OnSessionOpened, OnSessionClosed);
    ...
    // OnTcpConnected 함수를 등록합니다. TCP 연결을 맺은 후 호출합니다.
    HandlerRegistry::RegisterTcpTransportAttachedHandler(OnTcpConnected);
    // Registers OnTcpDisconnected function. Will be invoked once TCP gets disconnected.
    HandlerRegistry::RegisterTcpTransportDetachedHandler(OnTcpDisconnected);
    ...
  }
}

void OnSessionOpened(const Ptr<Session> &session) {
  // New session has been created due to a new client connection.
  // You can put a session initialization code here, if required.
  // (Also, it's possible to send out a message to the client.)
  LOG(INFO) << "New session opened: " << session->id();
}

void OnSessionClosed(const Ptr<Session> &session, SessionClosedReason reason) {
  // Session is closed.
  // (This hanlder is invoked after the session is closed. So, cannot send out messages.)
  LOG(INFO) << "Session closed: " << session->id();

  // You may take actions according to the session close reason.
  if (reason == kClosedForServerDid) {
    // Closed due to Session::Close(). This means you may have called the function,
    // or iFun Engine did for detecting a weird behavior.
    ...
  } else if (reason == kClosedForIdle) {
    // Closed due to timeout.
    ...
  } else if (reason == kClosedForEventTimeout) {
    // 세션과 연관된 이벤트에서 타임아웃이 발생했습니다.
    // MANIFEST/SessionService 의 close_session_when_event_timeout 값이
    // true 일 때만 발생합니다.
    ...
  }
}

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

void OnTcpDisconnected(const Ptr<Session> &session) {
  // TCP associated with the session got disconnected.
  // You may want to perform appropriate actions.
  // (Switching to AI, closing a matching session, etc.)
  ...
}

Tip

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

using funapi;

public class Server
{
  public static void Install(ArgumentMap arguments)
  {
    ...
    // Registers both OnSessionOpened function and OnSessionClosed function.
    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)
  {
    // New session has been created due to a new client connection.
    // You can put a session initialization code here, if required.
    // (Also, it's possible to send out a message to the client.)
    Log.Info ("Session opened.");
  }

  public static void OnSessionClosed(Session session)
  {
    // Session is closed.
    // (This hanlder is invoked after the session is closed. So, cannot send out messages.)
    Log.Info ("Session closed.");
  }

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

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

Tip

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

10.2. Receiving Packets from a Session

When iFun Engine transmits messages (packets), they are sent along with a string identifying the message type to distinguish which type of message is being sent. For example, a message may be “login” to log in, “buy_item” to purchase an item, or “move” to move a character.

To receive a message, you need to register the function that handles the received message. This function is called a message handler. Message handlers can be registered for each message type. This function transmits the session that sent the message and the received message as parameters, and is invoked according to the protocol in which the message was received (TCP, UDP, HTTP). If a different protocol is not specified when a message is sent by the message handler, it is automatically sent using the protocol in which it was received.

Below is an example of registration of the OnHello function that receives and handles a message of “hello”.

Note

Message type is automatically filled in if you use client plug-ins at iFunFactory Github page , so there is no need to worry about it.

10.2.1. JSON message handler

You can define a new message type and its handler using HandlerRegistry::Register(msg_type, handler_func).

#include <funapi.h>

// Installer below is auto-generated when creating a project.
class MyServerInstaller : public Component {
  static bool Install(const ArgumentMap &/*arguments*/) {
    ...
    // Maps the message type string to a handler.
    // The type string will be embedded in JSON as "_msgtype".
    HandlerRegistry::Register("hello", OnHello);
    ...
  }
}

void OnHello(const Ptr<Session> &session, const Json &message) {
  ...
  // On the receipt of the hello message, this function is invoked.

  // Suppose, the hello message looks like this:
  //
  // {
  //   "user_id": "my_user",
  //   "character": {
  //     "name": "my_character",
  //     "level": 99,
  //     ...
  //   }
  // }
  //
  // Then you can read fields like this:
  // string user_id = message["user_id"].GetString();
  // string character_name = message["character"]["name"].GetString();
  // int64_t character_level = message["character"]["level"].GetInteger();
  ...
}

You can define a new message type and its handler using HandlerRegistry::RegisterMessageHandler(msg_type, handler_func).

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)
  {
    ...
    // On the receipt of the hello message, this function is invoked.

    // Suppose, the hello message looks like this:
    //
    // {
    //   "user_id": "my_user",
    //   "character": {
    //     "name": "my_character",
    //     "level": 99,
    //     ...
    //   }
    // }
    //
    // Then you can read fields like this:
    // JObject character = (JObject) message["character"];

    // To fetch a user_id in the message:
    // string user_id = (string) message["user_id"];
    //
    // Or you can also do like this:
    // string user_id = message["user_id"].Value<String>();

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

10.2.2. Protobuf message handler

You can define a new message type and its handler using HandlerRegistry::Register2(msg_type, handler_func).

// FunMessage is the outter-most Google Protocol Buffers type used by iFun Engine.
extend FunMessage {
  optional HelloMessage hello_message = 1000;
  ...
}
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 below is auto-generated when creating a project.
  static bool Install(const ArgumentMap &/*arguments*/) {
    ...
    // Maps a message type to its handler like this:
    HandlerRegistry::Register2("hello", OnHello);
    //
    // Or its also possible to use a message type of integer instead of one of string.
    // HandlerRegistry::Register2(1000, OnHello);
    //
    // In C++, it's also possible to use a generated protobuf extension identifier as a message type.
    // HandlerRegistry::Register2(hello_message, OnHello);
    ...
  }
}

void OnHello(const Ptr<Session> &session, const Ptr<FunMessage> &message) {
  ...
  // On the receipt of the hello message, this function is invoked.
  //
  // 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();
  ...
}

You can define a new message type and its handler using NetworkHandlerRegistry.RegisterMessageHandler(msg_type, handler_func).

// FunMessage is the outter-most Google Protocol Buffers type used by iFun Engine.
extend FunMessage {
  optional HelloMessage hello_message = 1000;
  ...
}
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));
    //
    // Or its also possible to use a message type of integer instead of one of string.
    // NetworkHandlerRegistry.RegisterMessageHandler (
    //     1000,
    //     new NetworkHandlerRegistry.ProtobufMessageHandler (OnHello));
    ...
  }

  public static void OnHello(Session session, FunMessage message)
  {
    ...
    // On the receipt of the hello message, this function is invoked.
    //
    // 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;
    ...
  }
}

10.3. Sending packets to a session

To send a message (packet) to a session, you can use Session::SendMessage() or AccountManager::SendMessage(), which supports distributed servers. (For more about AccountManager::SendMessage(), please refer to Sending packets to peer server clients)

The message transfer function has the following parameters:

Session::SendMessage(msg_type, msg_body [, encryption_method] [, protocol])
  • msg_type: Used to distinuguish messages. Either string or integer is allowed.

  • msg_body: Message to be sent. Either JSON or FunMessage is allowed.

  • Enryption method: If omitted or set as kDefaultEncryption, the default value is used. For a detailed explanation, please see Message encryption.

  • protocol: Can be one of kTcp, kUdp, or kHttp and is automatically chosen if omitted. For a detailed explanation, please see Multiple Protocols.

Note

For an error message like ambiguous transport protocol for sending 'testtype' message. candidates: Tcp, Http, please refer to Multiple Protocols.

Note

For an error message like SendMessage ignored. no 'Tcp' transport, it means the session does not have a transport for the specified protocol. (E.g., Protocol is set to kTcp, though the session does not have a TCP transport.) Since transport can be disconnected by the client, this error message can be seen in normal cases.

Tip

If Session::SendBackMessage() is used, message type may be omitted and the message is automatically sent as the last received message type. For example, if a handler having received a “login” message invokes SendBackMessage(), it works the same way as SendMessage("login", ...).

10.3.1. Sending JSON messages

In this example, we will send out a JSON message described below as a message type of “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);

// Explicitly uses HTTP.
session->SendMessage("world", message, kDefaultEncryption, kHttp);

// Sends after encrypting by ChaCha20.
session->SendMessage("world", message, kChacha20Encryption);

// Explicitly uses TCP after encrypting using AES128.
session->SendMessage("world", message, kAes128Encryption, kTcp);

In this example, we will send out a JSON message described below as a message type of “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);

// Explicitly uses HTTP.
session.SendMessage (
    "hello",
    message,
    Session.Encryption.kDefault,
    Session.Transport.kHttp);

// Sends after encrypting by ChaCha20.
session.SendMessage (
    "hello", message, Session.Encryption.kChaCha20);

// Explicitly uses TCP after encrypting using AES128.
session.SendMessage (
    "hello",
    message,
    Session.Encryption.kAes128,
    Session.Transport.kTcp);

10.3.2. Sending Protobuf messages

In this example, we will send out a HelloMessage protobuf described below as a message type of “world”.

extend FunMessage {
  optional HelloMessage hello_message = 1000;
  ...
}
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);

// Explicitly uses HTTP.
session->SendMessage("world", message, kDefaultEncryption, kHttp);

// Sends after encrypting by ChaCha20.
session->SendMessage("world", message, kChacha20Encryption);

// Explicitly uses TCP after encrypting using AES128.
session->SendMessage("world", message, kAes128Encryption, kTcp);

// Uses an integer message type instead of a string one.
session->SendMessage(1000, message, kDefaultEncryption, kTcp);

// In C++, it's possible to use a protobuf identifier as a message type.
session->SendMessage(hello_message, message, kDefaultEncryption, kTcp);

In this example, we will send out a HelloMessage protobuf described below as a message type of “world”.

extend FunMessage {
  optional HelloMessage hello_message = 1000;
  ...
}
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);

// Explicitly uses HTTP.
session.SendMessage ("world", message, kDefaultEncryption, kHttp);

// Sends after encrypting by ChaCha20.
session.SendMessage ("world", message, kChacha20Encryption);

// Explicitly uses TCP after encrypting using AES128.
session.SendMessage ("world", message, kAes128Encryption, kTcp);

// Uses an integer message type instead of a string one.
session.SendMessage (1000, message, kDefaultEncryption, kTcp);

Important

As explained in Transaction, iFun Engine’s object subsystem releases all locks and rolls back tasks to prevent deadlock. Rolling back tasks means that the packet handler (message handler) can be run several times repeatedly. However, this repetition can be a problem for functions performing tasks that can’t be rolled back. This includes tasks performed by the SendMessage() function. To avoid this, code that cannot be repeated should be placed after items that can be rolled back.

If iFun Engine accidentally places these functions ahead of code that can be rolled back and rollback occurs to prevent unintentional repetition, an assertion is raised. These assertions can easily be spotted in the development process, and you can avoid them by changing the code location.

10.3.3. Sending messages to all sessions

iFun Engine provides a way to send out a message to all the sessions. To send to all the session on a local server, you can use Session::BroadcastLocally() The function takes parmeters sames as Session::SendMessage() described in Sending packets to a session does. But only kTcp and kUdp are allowed as TransportProtocol because it’s not possible to send out messages over HTTP without client’s request.

Tip

To send a message to all the sessions globally (i.e., regarldess of server location), you can use Session::BroadcastGlobally() described in Sending packets to all server sessions regardless of login.

Note

Please note that both Session::BroadcastLocally() and Session::BroadcastGlobally() can send a message regardless of client’s login status. To send to all the clients passed the login steps , please refer to Sending packets to all clients logged into servers.

Sending JSON messages

Below example demonstrates how to send a JSON message to all the clients on a local server.

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

  // Say, the clients connect over TCP.
  // If using UDP, use kUdp instead of kTcp.
  Session::BroadcastLocally("world", message, kDefaultEncryption, kTcp);
}
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;

  // Say, the clients connect over TCP.
  // If using UDP, use kUdp instead of kTcp.
  Session.BroadcastLocally ("world",
                            message,
                            Session.Encryption.kDefault,
                            Session.Transport.kTcp);
}

Sending Protobuf messages

Below example demonstrates how to send a Protobuf message to all the clients on a local server.

extend FunMessage {
  optional HelloMessage hello_message = 1000;
  ...
}
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);
  // Say, the clients connect over TCP.
  // If using UDP, use kUdp instead of kTcp.
  Session::BroadcastLocally("world", message);
  // Or you can use an integer as a message type like below.
  // Session::BroadcastLocally(1000, message);
  // In C++, protobuf identifier is also allowed as a message type.
  // Session::BroadcastLocally(hello_message, 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);

  // Say, the clients connect over TCP.
  // If using UDP, use kUdp instead of kTcp.
  Session.BroadcastLocally ("world",
                            message,
                            Session.Encryption.kDefault,
                            Session.Transport.kTcp);
  // Or you can use an integer as a message type like below.
  // Session.BroadcastLocally (1000,
  //                           message,
  //                           Session.Encryption.kDefault,
  //                           Session.Transport.kTcp);
}

Important

Please be aware that Session::BroadcastLocally() raises an assertion if ORM functions are invoked before the function and the ORM functions trigger a rollback. For details, please refer to Transaction.

10.4. Closing sessions

Sessions can be closed for the following 3 reasons:

  1. A session closes immediately when void Session::Close() is invoked.

  2. Sessions are automatically closed when no messages are sent or received for a set period of time. (This time can be set in session_timeout_in_second, as explained in Networking parameters.)

  3. If the client is deemed abnormal, the session is automatically closed.

When a session is closed, the session close handler is invoked, as explained in Registering session handlers. Causes #1 and 3 of session closure transmitted by the session close handler are kClosedForServerDid, while #2 is kClosedForIdle.

You can also check whether bool Session::IsOpened() const has been opened. Since sessions transmitted when the message handler is invoked are open, there is no need for a separate test, but if sessions are saved in global variables, it may sometimes be necessary to check whether they are open. (However, it may be better to clean up sessions saved in global variables in the session close handler without separate tests in these cases as well.)

Note

If a message is sent to a closed session, a SendMessage ignored. closed session log is output and the message is ignored without transmission. (This may occur under ordinary circumstances as well, and this is not an error log.)

Detaching a session’s transport types

You can detach transport types left by a session. In this case, the client can reconnect before session timeout to keep using the session.

Use void Session::CloseTransport([protocol_to_close]). TCP, UDP, or HTTP protocols can be closed, and if omitted, all protocols are closed.

You can check whether protocols are connected with bool Session::IsTransportAttached([protocol]) const. If the transport type is omitted, it checks whether anything is connected, regardless of closure.

You can register the function invoked when closing the session’s TCP connection. Please refer to Registering session handlers.

Ptr<Session> session = ...;

// Checks if the session has a TCP transport.
if (session->IsTransportAttached(kTcp)) {
  ...
}

// Checks if the session has any transport.
if (session->IsTransportAttached()) {
  ...
}

// Closes the TCP transport if attached to the session.
session->CloseTransport(kTcp);

// Closes all the transports associated with the session.
session->CloseTransport();
session = ...;

// Checks if the session has a TCP transport.
if (session.IsTransportAttached (Session.Transport.kTcp)) {
  ...
}

// Checks if the session has any transport.
if (session.IsTransportAttached ()) {
  ...
}

// Closes the TCP transport if attached to the session.
session.CloseTransport (Session.Transport.kTcp);

// Closes all the transports associated with the session.
session.CloseTransport ();

10.5. Session Data

10.5.1. Session tags

You can add tags to a session and search sessions by tags.

  • Adding a tag: void Session::Tag(const string &tag)

  • Removing a tag: void Session::Untag(const string &tag)

  • Checking if has a tag: bool Session::HasTag(const string &tag)

  • Getting a tags list: std::set<string> Session::GetTags()

  • Getting a list of sessions with the given tag: static SessionsSet Session::FindWithTag(const string &tag)

  • Getting the number of sessions with the given tag: static size_t Session::CountWithTag(const string &tag)

The following is an example of sending a notification only to users participating in a PvP battle using the tag function.

// Say this handler is invoked on the start of a PvP match.
void OnPvPStarted(const Ptr<Session> &session, const Json &message) {
  ...
  // Attach a tag meaning a PvP match.
  session->Tag("pvp");
}

// Say this function is to broadcast a message to sessions doing PvP matches.
void NoticeToPvP(const string &notice_message) {
  // SessionsSet is a typedef of boost::unordered_set<Ptr<Session>>.
  Session::SessionsSet sessions = Session::FindWithTag("pvp");
  for (const Ptr<Session> &session: sessions) {
    // Sends a message to the session.
    session->SendMessage(...);
  }
}
// Say this handler is invoked on the start of a PvP match.
public void OnPvPStarted(Session session, JObject message)
{
  // Attach a tag meaning a PvP match.
  session.Tag("pvp");
}

// Say this function is to broadcast a message to sessions doing PvP matches.
public void NoticeToPvP(string notice_message)
{
  List<Session> sessions = Session.FindWithTag("pvp");
  foreach(Session session in sessions)
  {
    // Sends a message to the session.
    session.SendMessage (...);
  }
}

10.5.2. Session context

iFun Engine allows to store per-session status and data as a session context.

  • Writing a context: void Session::SetContext(const Json &context)

  • Reading a context: Json &Session::GetContext()

Important

Please be aware that the functions are not thread-safe. Thus, you need to lock the context using the functions below before calling the previous functions.

  • void Session::LockContext()

  • void Session::UnlockContext()

Otherwise, it’s also possible to access the bare mutex instance using this function:

  • boost::mutex &Session::GetContextMutex().

The session instance can be passed to boost lock functions. So, it’s more convenient to do so. In an example below, we uses the session context to handle logout steps by passing the session instance to a mutex lock function.

iFun Engine allows to store per-session status and data as a session context.

  • Accessing the context: JObject Session.Context()

Important

Please be aware the function is not thread-safe. So, it’s required to synchronize like below:

lock (session)
{
  ...
}
// Say this function is invoked once a login message is arrived from a session.
void OnLogin(const Ptr<Session> &session, ...) {
  ...
  // Process the login steps.
  ...

  // After finishing the login steps, we additionally handle like this:
  {
    // Lock the session context using the session itself.
    boost::mutex::scoped_lock lock(*session);
    // This is also possible. (Please be sure to avoid deadlocks when using this method.)
    // session->LockContext() and sesion->UnlockContext()

    // Marks the session passed the login steps.
    session->GetContext()["login"] = true;
  }
}

// Say this function is invoked once a logout is required. (disconnection or explicit logout message.)
void OnSessionClosed(const Ptr<Session> &session) {
  ...

  // Checks if the session has a login mark in the session context.
  bool logged_in = false;
  {
    // Lock the context using the session instance itself.
    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) {
    // The session has the mark. Performs logout steps.
    ...
  }
}
// Say this function is invoked once a login message is arrived from a session.
public void OnLogin(Session session, ...)
{
  ...
  // Process the login steps.
  ...

  // Acessing the session context is not thread-safe.
  // So, we need to lock the session before accessing it.
  lock (session)
  {
    session.Context ["login"] = true;
  }
}

// Say this function is invoked once a logout is required. (disconnection or explicit logout message.)
public void OnSessionClosed(Session session)
{
  ...

  // Checks if the session has a login mark in the session context.
  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) {
    // The session has the mark. Performs logout steps.
    ...
  }
}

Tip

Use the following function to use session context a little more conveniently. It is thread-safe, and the value is used and read on the key set in the context JSON.

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

You can use these functions to do this easily as in the following example.


// Say this function is invoked once a login message is arrived from a session.
void OnLogin(const Ptr<Session> &session, …) {

… // Process the login steps. …

// Marks the session passed the login steps. session->AddToContext(“login”, 1);

}

// Say this function is invoked once a logout is required. (disconnection or explicit logout message.) void OnSessionClosed(const Ptr<Session> &session) {

// Checks if the session has a login mark in the session context. int64_t logged_in = 0; if (session->GetFromContext(“login”, &logged_in) && logged_in == 1) {

// The session has the mark. Performs logout steps. … session->DeleteFromContext(“login”);

}

}

// Say this function is invoked once a login message is arrived from a session.
public void OnLogin(Session session, ...)
{
  ...
  // Process the login steps.
  ...

  // Marks the session passed the login steps.
  session.AddToContext ("login", 1);
}

// Say this function is invoked once a logout is required. (disconnection or explicit logout message.)
public void OnSessionClosed(Session session)
{
  ...

  Int64 logged_in = 0;
  if (session.GetFromContext ("login", out logged_in) && logged_in == 1)
  {
    // The session has the mark. Performs logout steps.
    ...
    session.DeleteFromContext ("login");
  }
}

10.6. Session Ping (RTT)

iFun Engine provides a function to get round-trip time (RTT) from clients. It also provides a function that disconnects clients that don’t respond to ping requests for a set amount of time.

Note

TCP Transport 와 Websocket Transport 에서 작동됩니다.

10.6.1. 설정하기

MANIFEST/SessionService 항목에 핑 기능을 사용하려는 프로토콜에 대하여 다음과 같은 내용을 설정해 줍니다.

...
"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": {
  "sampling_interval_in_second": 0, // RTT 계산을 위한 ping 샘플링 인터벌을 초단위로 지정합니다. 0 은 동작을 끕니다.
  "message_size_in_byte": 32, // 전송할 ping 메시지 크기.
  "timeout_in_second": 0 // 지정된 시간 동안 Ping 응답이 오지 않을 경우 연결을 끊습니다. 0 은 동작을 끕니다.
},
...

또는 Session 인터페이스를 통해 설정할 수 있습니다.

  • 주기 설정: void Session::SetPingSamplingInterval(size_t seconds, TransportProtocol protocol)

  • 타임 아웃 설정: void Session::SetPingTimeout(size_t seconds, TransportProtocol protocol)

  • 주기 설정: void Session.SetPingSamplingInterval(ulong seconds, Transport transport)

  • 타임 아웃 설정: void Session.SetPingTimeout(ulong seconds, Transport transport)

10.6.2. 동작

설정된 주기마다 핑 메시지를 주고 받으며 설정된 타임 아웃 시간 동안 응답이 안오면 연결을 끊습니다. 세션이 닫히는 것은 아니며 해당 트랜스포트의 연결만 끊습니다.

10.6.3. 값 얻기

  • RTT 값 얻기: Ping Session::GetPing(TransportProtocol protocol) const

typedef std::pair<MonotonicClock::Duration /*ping_time*/, size_t /*sample_count*/> Ping;
  • RTT 값 얻기: Ping Session.GetPing(Transport transport)

public struct Ping
{
  public TimeSpan RoundtripTime;
  public ulong SamplingCount;
}

10.6.4. 예제

// Install method of the server component.
static bool Install(const ArgumentMap &) {
  // Registers a handler to be invoked on session open.
  HandlerRegistry::Install2(OnSessionOpened, ...);

  // Registers a handler to be invoked on TCP disconnection.
  HandlerRegistry::RegisterTcpTransportDetachedHandler(OnTcpDisconnected);

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

// This handler will be invoked on session open.
void OnSessionOpened(const Ptr<Session> &session) {
  // Measures RTT once every 10 seconds, and closes the session if it does not reply for longer than 5 seconds.
  // kTcp 또는 kWebSocket 을 사용할 수 있습니다.
  // 이 코드 대신 MANIFEST/SessionService 의 tcp_ping 또는 websocket_ping 값을
  // 설정해도 됩니다.
  session->SetPingSamplingInterval(10, kTcp);
  session->SetPingTimeout(5, kTcp);

  ...
}

// This handler will be invoked on TCP disconnection.
void OnTcpDisconnected(const Ptr<Session> &session) {
  // TCP got disconnected.
  ...
}

// Say this is a handler for another event.
void OnXYZ(const Ptr<Session> &session, ...) {
  // We can read RTT measurement when required.
  // kTcp 또는 kWebSocket 을 사용할 수 있습니다.
  Session::Ping ping = session->GetPing(kTcp);
  if (ping.second == 0) {
    // No measurement.
    return;
  }

  int64_t rtt_in_ms = ping.first / 1000;

  LOG(INFO) << "rtt=" << rtt_in_sec << " ms";
}
// Install method of the server component.
public static void Install(ArgumentMap arguments)
{
  // Registers a handler to be invoked on session open.
  NetworkHandlerRegistry.RegisterSessionHandler (
    new NetworkHandlerRegistry.SessionOpenedHandler (OnSessionOpened),
    new NetworkHandlerRegistry.SessionClosedHandler (OnSessionClosed));

  // Registers a handler to be invoked on TCP disconnection.
  NetworkHandlerRegistry.RegisterTcpTransportDetachedHandler(OnTcpDisconnected);

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

// This handler will be invoked on session open.
public static void OnSessionOpened(Session session)
{

  // Measures RTT once every 10 seconds, and closes the session if it does not reply for longer than 5 seconds.
  // kTcp 또는 kWebSocket 을 사용할 수 있습니다.
  // 이 코드 대신 MANIFEST/SessionService 의 tcp_ping 또는 websocket_ping 값을
  // 설정해도 됩니다.
  session.SetPingSamplingInterval(10, kTcp);
  session.SetPingTimeout(5, kTcp);
}

// This handler will be invoked on TCP disconnection.
public static void OnTcpDisconnected(Session session)
{
  // TCP got disconnected.
  ...
}

// Say this is a handler for another event.
public static void OnXYZ(Session session)
{
  // We can read RTT measurement when required.
  // kTcp 또는 kWebSocket 을 사용할 수 있습니다.
  Session.Ping ping = session.GetPing(kTcp);
  TimeSpan rtt_span = ping.RoundtripTime;

  if (ping.SamplingCount <= 0)
  {
    // No measurement.
    return;
  }

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

10.7. Hooking Message Handler

Pre/Post-message handler hook allows to control the message handler. these can be available to both Protobuf/Json message handler.

If the pre-message handler return false, the message handler and post-message handler will not be called. Please note that an exception will occur if both handlers are 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

The InstallProtobufMessageHandlerHook2 function is available in version 1.0.0-2874 Experimental and higher.

following code displays that checking test attribute to controll message handler.

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

10.8. Hooking Session Message Transmission

You can register the function invoked when invoking SendMessage() for the sake of gathering server performance statistics. Time taken to receive and respond to messages can be sampled by using this function and the Last message reception time.

You can register the hook function as follows:

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

The following is an example of message response time output as a log. In this example, we suppose message types sent by the client are prefixed with cs_ and those sent by the server are prefixed with sc_.

auto send_hook = [](const Ptr<Session> &session, const Json &message,
                    const string &message_type, size_t message_size) {
  // Strips the prefix
  // to get a common message type string.
  const char *common_message_type = &message_type[3];

  // Gets the message receive time and outputs the handling time.
  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 {
    // Sent without the client's request
  }
};

// In the Install method of the server component, we register this function.
fun::InstallJsonMessageSendHook(send_hook);

Will be supported later in C#

10.9. Session Message Transmission Stability

Internet disconnection and reconnection is common in a mobile environment, and it is necessary to prepare for this to provide a stable game service. iFun Engine is implemented in a manner that ensures Internet reconnection is not noticeable to users or even to game developers.

When TCP is disconnected, it is easy to reconnect. However, it is hard to tell whether messages and some packets sent during the reconnection process have been lost. If lost packets cannot be recovered, gameplay context is lost and games cannot be played normally.(Users must be forced to return to the main menu or restart the app.) It is normally difficult to discover and recover these lost packets, and the game server and client source become very complicated.

When the following Networking parameters and provided client plugin’s session reliability option are enabled, iFun Engine recognizes and reconnects, then sends lost packets in their correct order.

10.10. Additional Convenient Session Features

10.10.2. Session addresses

You can get the IP/port of clients connected to the session by using the following function:

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

port can be omitted.

The example below is the client’s IP during login, output as a log.

// Say this handler will be invoked on login.
void OnLogin(const Ptr<Session> &session, ...) {
  ...
  string ip;
  if (session->GetRemoteEndPoint(kTcp, &ip)) {
    LOG(INFO) << "client_ip_address=" << ip;
  } else {
    // TCP got disconnected while processing this handler.
  }
}
// Say this handler will be invoked on login.
void OnLogin(Session session, ...) {
  ...
  string ip;
  if (session.GetRemoteEndpoint (Session.Transport.kTcp, out ip)) {
    Log.Info ("client_ip_address={0}", ip);
  } else {
    // TCP got disconnected while processing this handler.
  }
}

10.10.3. Last transmitted message type

You can get the last transmitted message type with the two functions below:

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

10.10.4. Last message reception time

You can get the last message reception time for a set message type with the following function. This function only works when registered as a hook function, as explained in Hooking Session Message Transmission.

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

This function is provided for statistical purposes. Please refer to Hooking Session Message Transmission.

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