네트워킹 Part 1: 세션

아이펀 엔진은 서버와 클라이언트 간 연결을 세션으로 표현합니다. 하나의 세션은 TCP, UDP, HTTP 중 하나 이상의 전송 프로토콜을 관리하며, 이 전송 프로토콜을 각각 트랜스포트 객체로 관리합니다.

예를 들면 로그인, 결제 메시지는 HTTP 트랜스포트를 사용하고, 게임 플레이 시 주고 받을 메시지는 TCP 트랜스포트를 사용할 수 있습니다.

세션은 새 클라이언트가 접속할 때 생성되고, 세션을 닫는 함수를 부르거나, 세션 타임아웃으로 닫힐 때까지 유효합니다. 세션을 이용하면 쉽게 JSON, Protobuf 중 원하는 프로토콜로 메시지를 주고 받을 수 있습니다.

아이펀 엔진의 세션은 게임 서비스를 위해 반드시 필요한 다음의 기능을 제공하고 있습니다.

  • 메시지(패킷) 전송 기능

  • TCP 연결 복원 기능

  • Ping 측정 기능

  • 긴급 메시지 기능

  • 암호화 기능

  • 메시지 리플레이 공격 차단 기능

이 외에도 다양한 기능을 제공하며 이 챕터와 다음 챕터에서 사용 방법을 익히실 수 있습니다.

Note

아이펀 엔진의 네트워크 프로토콜을 자세히 알고 싶다면 (고급) 아이펀 엔진의 네트워크 스택 을 참고하기 바랍니다.

세션 핸들러 등록

새로운 세션이 열리거나, 세션이 닫힐 때 불리는 핸들러 함수를 등록할 수 있습니다. 핸들러에는 새 세션이나 닫힌 세션이 인자로 전달 됩니다.

#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) << "A new session is opened: " << session->id();
}

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

  // 아래처럼 닫힌 원인에 따라 처리할 수 있습니다.
  if (reason == kClosedForServerDid) {
    // Session::Close() 함수를 호출하여 닫았습니다. 또는 비정상 클라이언트로
    // 판단되어 자동으로 Session::Close() 가 불렸을 수 도 있습니다.
    ...
  } else if (reason == kClosedForIdle) {
    // 세션이 타임아웃되어 닫혔습니다.
    ...
  } 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 연결이 끊겼습니다.
  // 실시간 대전 등 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 함수를 사용해야 합니다.

세션으로부터 패킷 수신하기(수신)

세션으로부터 메시지(패킷)를 주고 받을 때는 메시지가 어떤 형태인지 구분하기 위해 메시지 타입 문자열 을 함께 전송하게 됩니다. 예를 들면 로그인은 “login”, 아이템 구매는 “buy_item”, 케릭터 이동 메시지에는 “move” 등을 지정할 수 있습니다.

메시지를 수신하기 위해서는 수신된 메시지를 처리하는 함수를 등록해야합니다. 이 함수를 메시지 핸들러 라고 부릅니다. 메시지 핸들러는 각 메시지 타입별로 등록할 수 있습니다.

메시지 수신 함수를 등록하면 메시지를 보낸 세션과 메시지를 인자로 받을 수 있습니다. 주의할 점은, 수신자가 사용한 트랜스포트 프로토콜(TCP, UDP, HTTP)과 관계 없이 메시지를 받는다는 것입니다.

또한 메시지 수신 함수 에서 메시지를 보낼 때 별도의 전송 프로토콜을 지정하지 않으면 자동으로 메시지를 수신한 트랜스포트 프로토콜로 보낸다는 점도 주의해야 합니다.

아래는 “hello” 라는 메시지를 받아 처리하는 OnHello 함수를 등록하는 예제입니다.

Note

메시지 타입을 기재하는 것은 아이펀팩토리 Github 계정 에 있는 클라이언트 플러그인을 쓸 경우 알아서 세팅되니 별도로 신경쓰지 않아도 됩니다.

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"];
    ...
  }

Protobuf 메시지 핸들러

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

// FunMessage 는 아이펀엔진의 Google Protocol Buffers 최상위 메시지입니다.
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 는 프로젝트를 만들 때 자동으로 생성됩니다.
  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 = 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));
    //
    // 또는 아래와 같이 정수형 메세지 타입을 지정할 수 있습니다.
    // 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;
    ...
  }
}

세션으로 메시지 보내기(송신)

특정 세션으로 메시지(패킷)을 보낼 때는 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

SendMessage ignored. no 'Tcp' transport 로그는 해당 전송 프로토콜에 대한 연결이 없는 경우이며(kTcp 로 보내려 했으나 TCP 연결이 끊긴 상태 등) 정상적인 상황에서도 흔하게 발생합니다.

Tip

Session::SendBackMessage() 함수를 이용하면 메시지 타입을 생략할 수 있으며 자동으로 마지막으로 수신된 메시지 타입으로 보내게 됩니다. 예를 들어 “login” 이란 메시지를 받는 핸들러에서 SendBackMessage() 를 호출한다면 SendMessage("login", ...) 와 같이 동작합니다.

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

이 예제에서는 아래 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);

Protobuf 메시지 전송

이 예제에서는 아래 HelloMessage 메시지를 “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);

// 명시적으로 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);

이 예제에서는 아래 HelloMessage 메시지를 “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);

// 명시적으로 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);

Important

트랜잭션 에 설명된 대로, 아이펀 엔진의 오브젝트 서브시스템은 잡고 있던 모든 락을 풀고 작업을 롤백하는 방식으로 데드락을 방지합니다. 작업을 롤백한다는 이야기는 패킷 핸들러 (메시지 핸들러) 가 여러번 중복 실행될 수 있다는 것을 의미합니다. 그런데 이런 중복 실행은 되돌릴 수 없는 작업을 수행하는 함수들에게는 문제가 됩니다. 그리고 SendMessage() 함수는 그런 되돌릴 수 없는 작업에 해당됩니다. 이를 피하기 위해서는 중복실행이 되서는 안되는 코드를 롤백 가능한 것들 뒤에 위치시키면 됩니다.

아이펀 엔진은 이런 함수들이 실수로 롤백 가능한 코드 앞에 위치해서 의도하지 않은 중복 실행이 되는 것을 방지하기 위하여 롤백이 발생하면 assertion 을 발생하게끔 되어있습니다. 개발 과정에서 이런 assertion 은 쉽게 발견될 수 있고, 코드 위치를 바꿔서 이를 피해주시면 됩니다.

서버에 연결된 모든 세션에게 메시지 보내기

로컬 서버에 연결된 모든 세션에 메시지를 전송하고 싶다면 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);
}
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);
}

Protobuf 메시지 전송

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

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);
  // TCP 로 접속한다고 가정하겠습니다.
  // 만약 UDP 로 접속한다면 kUdp 로 지정하세요.
  Session::BroadcastLocally("world", message);
  // 또는 다음과 같이 정수형 메세지 타입을 사용할 수 있습니다.
  // Session::BroadcastLocally(1000, message);
  // C++에서는 Protobuf 구조체를 메세지 타입으로 사용할 수 있습니다.
  // 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);

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

Important

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

세션 이벤트 타임아웃

위에서 등록한 세션 핸들러의 호출은 이벤트 로 동작합니다. 이벤트의 처리 시간이 길 경우 병목 현상을 유발할 수 있으며 그러한 이벤트를 감지하거나, 이벤트 실행을 취소할 수 있습니다. 각각 병목 이벤트 감지심각한 병목 이벤트 강제 취소 을 참고해 주세요.

세션 종료

세션은 아래 3 가지 원인으로 닫힐 수 있습니다.

  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 ();

세션 데이터

세션 태그

세션에 태그를 붙이고 태그로 세션을 검색할 수 있습니다.

  • 태그 추가: 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)

  • 특정 태그를 가진 세션 개수: static size_t Session::CountWithTag(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 (...);
  }
}

세션 콘텍스트

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

  • 콘텍스트 쓰기: 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() 반드시 session->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

아래 함수를 사용하면 세션 콘텍스트를 조금 더 편리하게 쓸 수 있습니다. thread-safe 하며 콘텍스트 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");
  }
}

세션 Ping(RTT)

아이펀 엔진은 클라이언트와의 round-trip time(RTT) 를 얻는 기능을 제공합니다. 또한, 일정 시간 동안 핑 요청에 응답하지 않은 클라이언트와의 연결을 끊는 기능도 제공합니다.

Note

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

설정하기

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

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

동작

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

값 얻기

  • 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;
}

예제

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

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

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

// 세션이 열리면 불리는 핸들러입니다.
void OnSessionOpened(const Ptr<Session> &session) {
  // 10 초 주기로 RTT 를 측정하며 5 초 이상 응답하지 않으면 연결을 닫습니다.
  // kTcp 또는 kWebSocket 을 사용할 수 있습니다.
  // 이 코드 대신 MANIFEST/SessionService 의 tcp_ping 또는 websocket_ping 값을
  // 설정해도 됩니다.
  session->SetPingSamplingInterval(10, kTcp);
  session->SetPingTimeout(5, kTcp);

  ...
}

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

// 임의의 사용자 함수
void OnXYZ(const Ptr<Session> &session, ...) {
  // 필요할 때 아래와 같이 RTT 값을 얻을 수 있습니다.
  // kTcp 또는 kWebSocket 을 사용할 수 있습니다.
  Session::Ping ping = session->GetPing(kTcp);
  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 초 이상 응답하지 않으면 연결을 닫습니다.
  // kTcp 또는 kWebSocket 을 사용할 수 있습니다.
  // 이 코드 대신 MANIFEST/SessionService 의 tcp_ping 또는 websocket_ping 값을
  // 설정해도 됩니다.
  session.SetPingSamplingInterval(10, kTcp);
  session.SetPingTimeout(5, kTcp);
}

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

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

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

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

세션 메시지 핸들러 후킹

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

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

Important

세션 메시지 핸들러 후킹 함수들은 예외적으로 이벤트 위에서 호출하지 않습니다. 후킹 함수에서 ORM 을 사용하려면 함수 안에서 Event::Invoke() 함수를 호출해야 합니다.

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

세션 메시지 전송 후킹

서버 성능 통계 목적으로 SendMessage() 가 불릴 때 불리는 함수를 등록할 수 있습니다. 이 함수와 마지막 메시지 수신 시간 를 이용하면 메시지를 수신하고 응답하는데 걸린 시간을 측정할 수 있습니다.

아래 함수로 훅 함수를 등록할 수 있습니다.

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# 예제는 추후 업데이트 됩니다.

세션의 메시지 전송 안정성

모바일 네트워크처럼 연결 끊김이 빈번한 환경에서 안정적인 게임 서비스를 제공하려면 충분한 대비가 필요합니다.

끊어진 TCP 연결을 다시 맺는 것은 어렵지 않지만 재연결 처리 중 유실된 메시지를 구분하거나, 유실 여부를 판단하기는 매우 어렵기 때문입니다. 만약 이 메시지들을 직접 구분하려면 클라이언트와 서버 코드 모두 복잡해질 것입니다.

유실된 메시지를 복원하지 못하면 게임을 진행하는 데 필요한 정보 전체 또는 일부가 사라지므로 게임을 진행할 수 없거나 예상하지 못한 문제가 발생할 가능성이 매우 높습니다. 그래서 사용자 연결을 강제로 끊거나, 게임을 재시작하게 해야합니다. 네트워크 환경이 불안정한 곳에서 게임을 플레이하는 사용자들은 매우 불편해할 것입니다.

아이펀 엔진은 연결 끊김이 빈번한 상황에서도 안정적으로 메시지를 처리하는 기능을 제공합니다. 개발자는 연결 끊김을 고려할 필요 없이 게임을 만들 수 있고, 사용자도 연결이 빈번하게 끊어지는 환경에서 안정적으로 게임을 즐길 수 있습니다.

아이펀 엔진은 아래의 configuration 과 클라이언트 플러그인의 session reliability 옵션을 활성화하면, 자동으로 재연결 후 유실된 패킷들을 찾아 순서에 맞게 전달합니다.

세션 편의 기능

세션 검색

아래 함수를 이용하여 세션 아이디로 세션을 얻을 수 있습니다.

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

세션 아이디는 아래 함수로 얻을 수 있습니다.

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

세션 주소

아래 함수를 이용하여 세션에 연결된 클라이언트의 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 연결이 끊긴 경우입니다.
  }
}

마지막 송수신 메시지 타입

아래 두 함수로 마지막 송수신된 메시지 타입을 얻을 수 있습니다.

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

마지막 메시지 수신 시간

아래 함수로 지정된 메시지 타입의 마지막 수신 시간을 얻을 수 있습니다. 이 기능은 세션 메시지 전송 후킹 에 설명된 훅 함수를 등록할 때만 작동합니다.

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

통계 목적으로 제공되는 기능이며 세션 메시지 전송 후킹 의 설명을 참고하시기 바랍니다.

세션 별 타임아웃 변경

OverrideSessionTimeout() 함수를 사용하면 세션 별로 서로 다른 타임아웃 시간을 설정할 수 있습니다. 이 함수는 1.0.0-4006 experimental 버전 이후부터 사용할 수 있습니다.

세션 타임아웃을 결정하는 기준은 다음과 같습니다.

  • OverrideSessionTimeout 로 설정한 값이 있으면 이 값을 타임아웃 기준으로 합니다.

  • OverrideSessionTimeout() 로 타임아웃을 설정하지 않은 세션들은 MANIFEST.json 의 session_timeout_in_second 값을 기준으로 합니다.

  • OverrideSessionTimeout() 또는 MANIFEST.json 에 설정된 타임 아웃 값이 0인 경우 타임 아웃 검사 자체를 하지 않습니다.

    Important

    세션 타임아웃을 비활성화할 경우 세션을 직접 종료하기 전까지 서버에 남아있으므로 각별히 주의해서 사용해야 합니다.

함수 사용 방법은 다음과 같습니다.

void OverrideSessionTimeout(
    const Ptr<Session> &session, const int64_t timeout_in_ms);
지원 예정입니다.