언리얼(Unreal) 플러그인

아이펀 엔진을 서버로 사용하는 언리얼 게임 클라이언트 개발을 돕는 플러그인 입니다.

언리얼 플러그인에서는 아래와 같은 기능들을 제공합니다.

  • TCP, UDP, HTTP, WebSocket 프로토콜 지원.

  • JSON, Protocol Buffers 형식의 메시지 타입 지원.

  • ChaCha20, AES-128 을 포함한 4종류의 암호화 방식 지원.

  • 멀티캐스트, 게임내 리소스 다운로드 등 다양한 기능 지원.

Important

stable, experimental 버전 모두 암호화 기능 중 ife1, ife2 암호화 방식은 사용할 수 없는 상태로 배포됩니다. ife1, ife2 암호화 방식을 사용하기 희망하시는 분은 iFun Engine support 로 요청해 주시기 바랍니다.

지원하는 언리얼 엔진 버전

아이펀 엔진의 언리얼 엔진 클라이언트 플러그인은 다음에 명시된 버전에서 동작하는 것을 확인했으며, 그 외의 버전에서는 오류가 발생할 수 있습니다.

언리얼 버전

Windows

macOS

Android

iOS

4.19

o

o

o

4.20

o

o

o

o

4.21

o

o

o

o

4.22

o

o

o

o

4.23

o

o

o

o

4.24

o

o

o

o

4.25

o

o

o

o

4.26

o

o

o

o

  • Android 의 경우 ARM 아키텍처용 ABI 만 지원합니다.

  • 4.19 버전 : Windows 개발 환경에서 패키징에 실패하는 문제가 있습니다.

플러그인 적용하기

가장 먼저 언리얼 런처로 생성한 C++ 프로젝트에 언리얼 플러그인을 적용 하는 방법을 알아보겠습니다.

Note

이후 언리얼 런처로 생성한 프로젝트는 새 프로젝트 로 플러그인 코드를 배포하기 위한 프로젝트는 플러그인 프로젝트 라고 부르겠습니다.

  1. 새 프로젝트에 언리얼 플러그인을 적용하기 위해 링크 에서 플러그인 프로젝트를 다운로드 받습니다.

  2. 다운 받은 플러그인 프로젝트의 <Project Root>/Plugins/ 폴더에는 용도별로 플러그인과 관련한 소스코드를 포함하는 하위 폴더들이 있습니다.

    |- Funapi
    |- FunapiDedicatedServer
    
    • Funapi : 아이펀 엔진 서버와 기본적인 통신을 하기 위한 기능을 제공합니다.

    • FunapiDedicatedServer : 아이펀 엔진의 DedicatedServerManager 콤포넌트, Unreal Dedicated Server 와 통신하기 위한 기능을 제공합니다.

    새 프로젝트에 <Project Root>/Plugins/ 폴더를 생성하고 그 안에 Funapi 플러그인과 FunapiDedicatedServer 폴더를 복사합니다.

    Note

    DedicatedServer 폴더는 Unreal 엔진의 데디케이티드 서버 기능을 사용하는 경우에만 복사하면 됩니다.

    DedicatedServer 지원에 대한 자세한 내용은 데디케이티드 서버 지원 를 참고해주세요.

  3. 이제 언리얼 에디터에서 새 프로젝트을 열어주세요.

    만약, 정상적으로 플러그인 폴더를 복사했다면 에디터 플러그인 UI 에서 Project/Funapi Plugin 을 확인하실 수 있습니다.

    아래와 같이 Funapi PluginEnabled 옵션을 체크하고 에디터를 재시작하면 새 프로젝트에 플러그인이 적용됩니다.

    _images/apply-ue4-plugin.png

서버에 연결하기

세션 개요 에서 설명한 것처럼 클라이언트과 서버 사이의 연결은 세션으로 관리합니다. 한 번 연결된 세션은 다음 세 경우에 해당하지 않는 한 유지됩니다.

  • 서버에서 특정 세션에 대해서 Session->Close() 함수를 호출.

  • 클라이언트에서 Session->CloseRequest() 함수를 호출.

  • 클라이언트와 서버 사이에 설정한 시간 이상 메시지 송수신이 없어서 timeout 이 발생하는 경우.

세션을 연결할 때 먼저 서버로부터 세션 ID 를 발급 받게 되고 이 후 서버와 주고 받는 모든 메시지에는 이 세션 ID 가 포함됩니다. 즉, 네트워크 연결이 끊기더라도 서버에서는 세션을 유지하며, 재연결시 동일한 ID의 세션이 있다면 연결이 끊어지지 않은 것처럼 계속 메시지를 주고 받을 수 있습니다.

하나의 FunapiSession 객체는 TCP, UDP, HTTP, WebSocket 전송 프로토콜을 하나 이상 사용할 수 있으며, 동일한 전송 프로토콜을 중복해서 사용할 수 없습니다.

IP 주소나 포트 번호가 다른 대상에 접속하려면 FunapiSession 객체를 따로 생성해야 합니다.

Tip

세션의 내부 구현 방식과 관련된 자세한 내용은 (고급) 아이펀 엔진의 네트워크 스택 에서 설명하고 있습니다.

FunapiSession 클래스

하나의 세션을 관리하는 클래스이며, 소유한 트랜스포트에 설정된 프로토콜을 사용해서 메시지를 송수신하는 등 하나의 세션으로 할 수 모든 기능을 담고 있습니다.

하나의 FunapiSession 객체는 서버의 IP 를 결정하며, 처음 세션을 생성할 때 결정된 후에는 변경할 수 없습니다.

Note

포트 번호는 사용하는 프로토콜마다 설정해야 하기 때문에 트랜트포트 연결 시 프로토콜 과 함께 설정합니다.

세션 옵션

FunapiSession 객체는 메시지 송수신과 관련한 몇가지 옵션들을 FunapiSessionOption 객체를 통해서 설정할 수 있으며, 내용은 다음과 같습니다.

Note

세션 옵션을 전달하지 않을 경우 기본 값을 사용하게 됩니다.

class FUNAPI_API FunapiSessionOption :
    public std::enable_shared_from_this<FunapiSessionOption>
{
 public:
  FunapiSessionOption();
  virtual ~FunapiSessionOption() = default;

  static std::shared_ptr<FunapiSessionOption> Create();

  // 네트워크 연결이 끊겼을 때에도 메시지 전송을 보장하는 기능입니다.
  // 메시지를 전송하고 ack 메시지를 확인하는 것으로 메시지가 전달됐음을
  // 확인하고, 그렇지 않은 경우 재연결시에 전달되지 못한 메시지를 다시 송신합니다.
  // 기본값 : false
  void SetSessionReliability(const bool reliability);
  bool GetSessionReliability();

  // SessionReliability 옵션을 사용할 때 서버와 주고받는 ack 메시지를 매번
  // 보내지 않고 설정한 시간마다 보냄으로써 네트워크 트래픽 양을 줄입니다.
  // (Piggybacking) 설정값이 0 보다 크면 동작합니다.
  // 기본값 : 0
#if FUNAPI_HAVE_DELAYED_ACK
  void SetDelayedAckIntervalMillisecond(const int millisecond);
#endif
  int GetDelayedAckIntervalMillisecond();

  // 기본적으로 모든 메시지에는 세션 ID 를 포함해서 전송하는데,
  // 이 옵션을 true 로 변경할 경우 처음 연결시에만 세션 ID 를 보내고 그 이후
  // 메시지에는 세션 ID 를 생략합니다.
  // 기본값: false
  void SetSendSessionIdOnlyOnce(const bool once);
  bool GetSendSessionIdOnlyOnce();

  // 서버이동 기능을 사용하면 '이동중' 에 보낸 메시지들은 기본적으로 버려집니다.
  // 이 옵션을 true 로 설정하면 '이동중' 에 보낸 메시지들을 이동한 서버로 전송합니다.
  // SetRedirectQueueCallback() 함수로 콜백함수를 등록하면 메시지를 전송하기 전
  // 메시지 목록을 확인할 수 있습니다.
  // 기본값: false
  void SetUseRedirectQueue(const bool use);
  bool GetUseRedirectQueue();

  ...
};

FunapiSession 객체 생성

FunapiSession 객체를 생성하고 콜백 함수를 등록하는 방법입니다.

// 메시지 순서를 보장하는 세션 신뢰성 기능을 활성화 합니다.
std::shared_ptr<fun::FunapiSessionOption> session_option =
    fun::FunapiSessionOption::Create();
session_option->SetSessionReliability(true);

// FunapiSession 객체를 생성합니다.
// 전달하는 파라미터는 서버 주소와 위에서 생성한 SessionOption 객체입니다.
// session_option 매개변수가 없으면 기본 값을 사용합니다.
std::shared_ptr<fun::FunapiSession> session_ =
    fun::FunapiSession::Create(address, session_option);

// 세션 이벤트가 발생하면 호출되는 함수입니다.
// 이 함수를 통해서 세션 이벤트를 처리할 수 있습니다.
session_->AddSessionEventCallback(
    [](const std::shared_ptr<fun::FunapiSession> &session,
       const fun::TransportProtocol transport_protocol,
       const fun::SessionEventType type,
       const fun::string &session_id,
       const std::shared_ptr<fun::FunapiError> &error)
    {
      // 이벤트 처리는 이후 이벤트 콜백 함수 항목에서 설명하겠습니다.
    }
);

// 트랜스포트 이벤트가 발생하면 호출되는 함수입니다.
// 이 함수를 통해서 트랜스포트 이벤트를 처리할 수 있습니다.
session_->AddTransportEventCallback(
    [](const std::shared_ptr<fun::FunapiSession> &session,
       const fun::TransportProtocol transport_protocol,
       const fun::TransportEventType type,
       const std::shared_ptr<fun::FunapiError> &error)
    {
      // 이벤트 처리는 이후 이벤트 콜백 함수 항목에서 설명하겠습니다.
    }
)

FunapiSession Update

FunapiSession 객체는 주기적으로 Update() 함수를 호출하는 것으로 메시지 송/수신을 비롯한 동작을 수행합니다.

언리얼 엔진의 경우 GameThread 로 실행하는 Actor Tick 내에서 세션 객체의 Update() 함수를 호출해야 합니다.

Note

사용자 쓰레드를 생성해서 세션 객체의 Update() 함수를 호출할 수도 있지만, 이런 경우에는 콜백함수에서 UObject 의 생성, 변경, 삭제가 불가능합니다.

아래 코드는 FunapiSession 객체를 멤버변수로 소유하고있는 MyActor 클래스에서 Update() 함수를 호출해주는 코드입니다.

void AMyActor::Tick(float DeltaTime)
{
  if(session_ != nullptr)
  {
    // 특정 세션 객체의 Update 를 수행하거나,
    session_->Update();
  }

  // 모든 FunapiSession 객체의 Update 를 수행합니다.
  fun::FunapiSession::UpdateAll();
}

Transport 연결

FunapiSession 객체로 서버에 연결하기 위해서는 FunapiTransport 객체가 필요합니다.

연결에 사용할 프로토콜에 맞는 트랜스포트 객체를 생성하고, FunapiSession 객체의 Connect() 함수에 전달해서 사용할 수 있습니다.

FunapiSession 객체는 하나 이상의 프로토콜을 사용할 수 있습니다.

Note

프로토콜에 따라서 실제 연결을 맺지 않기도 하지만, FunapiSession 은 가상의 연결을 다루기 때문에 연결이라고 표현합니다.

// FunapiSessionOption 과 비슷하게 TransportOption 객체를 생성하고,
// AutoReconnect 옵션을 사용하도록 설정합니다.
std::shared_ptr<fun::FunapiTcpTransportOption> tcp_transport_option =
    fun::FunapiTcpTransportOption::Create();
tcp_transport_option->SetAutoReconnect(true);

// 세션 연결을 수행하기 위해서는 프로토콜 타입, 인코딩 타입, 포트 번호가 필요합니다.
// 트랜스포트 옵션은 지정하지 않으면 기본 값을 사용합니다.
session_->Connect(fun::TransportProtocol::kTcp,
                  8012 /*port*/,
                  fun::FunEncoding::kJson,
                  tcp_transport_option);

Connect() 함수는 지정한 파라미터를 사용하여 Transport 를 생성하고 연결을 시도합니다. 기존 연결로 재연결하는 경우에는 파라미터로 프로토콜만 받는 Connect() 함수를 사용하는 것이 좋습니다.

프로토콜과 인코딩 타입

아래는 Connect() 함수에서 사용할 수 있는 protocolencoding 의 종류를 선언한 코드입니다.

// 프로토콜은 Tcp, Udp, Http, Websocket 4가지 타입을 지원합니다.
// Funapi transport protocol
enum class FUNAPI_API TransportProtocol : int
{
  kTcp = 0,
  kUdp,
  kHttp,
#if FUNAPI_HAVE_WEBSOCKET
  kWebsocket,
#endif
  kDefault,
};

// 메시지 인코딩 방식은 JSON 과 Protocol Buffers 2가지 방법이 있습니다.
enum class FUNAPI_API FunEncoding
{
  kNone,
  kJson,
  kProtobuf
};

이벤트 콜백 함수

FunapiSession 에는 세션과 트랜스포트의 상태 변화를 확인할 수 있는 이벤트를 다루기 위한 콜백 함수를 등록할 수 있습니다.

Session Event

FunapiSession 객체의 AddSessionEventCallback() 함수을 통해 세션 이벤트 콜백 함수를 등록할 수 있으며, 콜백 함수를 통해서 전달 받을 수 있는 이벤트 종류는 아래와 같습니다.

enum class FUNAPI_API SessionEventType : int
{
  kOpened,
  kClosed,
  kChanged,

  kRedirectStarted,
  kRedirectSucceeded,
  kRedirectFailed,
};
  • kOpened: 세션이 처음 연결되면 발생합니다. 세션 ID 가 없는 상태에서 트랜스포트가 처음 연결되어 세션ID 가 생성되면 발생합니다. 이후에는 세션 ID 는 바뀔 수 없기 때문에 더이상 발생하지 않습니다.

  • kClosed: 세션이 닫히면 발생하는 이벤트입니다. 트랜스포트의 연결이 닫히는 것과는 다른 상황이며 서버 쪽에서 세션 ID 를 삭제했거나 찾을 수 없을 때 발생합니다. 더 이상 세션 객체를 사용할 수 없기 때문에 새로운 세션 객체를 생성해야 합니다.

  • kChanged: 더 이상 사용하지 않습니다.

Note

kRedirectStarted, kRedirectSucceeded, kRedirectFailed 이벤트는 서버 이동 에서 자세히 설명합니다.

session_->AddSessionEventCallback(
    [this](const std::shared_ptr<fun::FunapiSession> &session,
           const fun::TransportProtocol transport_protocol,
           const fun::SessionEventType type,
           const fun::string &session_id,
           const std::shared_ptr<fun::FunapiError> &error)
    {
      if (type == fun::SessionEventType::kOpened)
      {
        // 세션이 연결되었습니다.
        // 여기서는 서버로 메시지를 전송합니다.
        session_->SendMessage("test" /*message type*/, json_string)
      }
      else if (type == fun::SessionEventType::kClosed) {
        // 서버에서 명시적으로 세션을 닫았습니다.
        // 클라이언트도 세션을 정리합니다.
        session_ = nullptr;
      }
    }
);

Transport Event

FunapiSession 객체의 AddTransportEventCallback() 함수를 통해 트랜스포트 이벤트 콜백 함수를 등록할 수 있으며, 콜백 함수를 통해서 전달 받을 수 있는 이벤트 종류는 아래와 같습니다.

enum class FUNAPI_API TransportEventType : int
{
  kStarted,
  kStopped,
  kReconnecting,
  kConnectionFailed,
  kConnectionTimedOut,
  kDisconnected,
};
  • kStarted: 트랜스포트가 서버와 연결되면 발생합니다. 프로토콜상 연결이 없는 경우는 트랜스포트 초기화가 완료됐음을 의미합니다.

  • kStopped: 서버와 연결이 종료되거나 연결에 실패하면 발생합니다.

  • kReconnecting: 재연결을 시작할 때 발생합니다.

  • kConnectionFailed: 서버와 연결에 실패하면 발생합니다.

  • kConnectionTimedOut: 서버로 연결 시도가 타임아웃에 의해서 실패하는 경우 발생합니다.

  • kDisconnected: 서버와 연결에 오류가 생겨서 끊어지면 발생합니다.

session_->AddTransportEventCallback(
    [](const std::shared_ptr<fun::FunapiSession> &session,
       const fun::TransportProtocol transport_protocol,
       const fun::TransportEventType type,
       const std::shared_ptr<fun::FunapiError> &error)
    {
      if (type == fun::TransportEventType::kStarted)
      {
        if (transport_protocol == fun::TransportProtocol::kTcp)
        {
          // TCP 프로토콜을 사용할 준비가 되었습니다.
          UE_LOG(LogFunapiExample, Log, TEXT("Tcp transport started"));
        }
      }
      else if (type == fun::TransportEventType::kReconnecting)
      {
        // 재연결 시도 중입니다.
        // AutoReconnect 옵션 항목을 참고해주세요
      }
      else if (type == fun::TransportEventType::kStopped ||
               type == fun::TransportEventType::kConnectionFailed ||
               type == fun::TransportEventType::kConnectionTimedOut)
      {
         // 서버와 연결이 종료되거나 연결에 실패했습니다.
         // 명시적으로 서버 연결을 끊지 않았다면 네트워크 상태를 확인해주세요.
      }
      else if (type == fun::TransportEventType::kDisconnected)
      {
        // 서버와 연결이 단절되었습니다.
        // 재연결을 시도합니다.
        session->Connect(transport_protocol, port, fun::FunEncoding::kProtobuf);
      }
    }
);

Transport 옵션

FunapiSession 객체의 Connect() 함수를 호출할 때 TransportOpion 객체를 통해서 옵션을 전달할 수 있습니다. 이 값이 null 인 경우에는 기본 값을 사용하게 됩니다.

사용할 프로토콜 (TCP, UDP, HTTP, WebSocket)에 따라 그에 맞는 TransportOption 객체를 생성하여 지정해야 합니다. 예를 들어 UDP 트랜스포트를 사용한다면, FunapiUdpTransportOption 객체를 전달합니다.

각 프로토콜 별 TransportOption 클래스의 정의를 알아보겠습니다.

FunapiTcpTransportOption

TCP 트랜스포트의 옵션을 설정할 수 있으며, 다음과 같은 기능을 제공합니다.

  • 네이글 알고리즘 비활성: TCP 프로토콜을 사용해서 작은 크기의 메시지를 빈번하게 보내는 경우 TCP 헤더 크기로 인해서 효율성이 떨어지기 때문에 작은 크기의 메시지들 모아서 전송하는 네이글 알고리즘을 기본적으로 사용하고 있습니다. 그러나 이 기능이 게임에서는 메시지 응답성을 떨어뜨릴 수 있기 때문에 사용하지 않는 편이 좋을 수 있습니다.

  • 자동 재연결: TCP 연결이 끊어지면 플러그인 수준에서 자동으로 다시 연결을 시도하는 기능입니다.

  • Ping: 주기적으로 확인 메시지를 보내서 실제로 메시지를 보낼 수 없는 상태라면 TCP 연결을 끊는 기능입니다.

  • 메시지 중복 확인: 구현상의 이유나 해킹 등의 이슈로 인해서 메시지가 중복 전송될 수 있는데, 이런 현상을 막을 수 있습니다.

  • 연결 제한 시간: TCP 연결을 완료하기까지 제한 시간을 설정합니다. 이 시간 내에 TCP 연결이 완료되지 않으면 연결에 실패한 것으로 간주합니다.

  • 메시지 암호화: 클라이언트와 서버 사이에 주고 받는 메시지를 암호화합니다. 클라이언트와 서버에 설정한 암호화 방식이 같아야 합니다.

  • 메시지 압축: 클라이언트와 서버 사이에 주고 받는 메시지를 압축합니다. 클라이언트와 서버에 설정한 압축 방식이 같아야 합니다.

class FUNAPI_API FunapiTcpTransportOption : public FunapiTransportOption {
 public:
  FunapiTcpTransportOption();
  virtual ~FunapiTcpTransportOption() = default;

  static std::shared_ptr<FunapiTcpTransportOption> Create();

  // 네이글 알고리즘을 사용하지 않도록 설정합니다.
  // 기본값 : false
  void SetDisableNagle(const bool disable_nagle);
  bool GetDisableNagle();

  // AutoReconnectTimeout 설정값까지 재연결을 시도합니다.
  // 만약, 전체 경과 시간이 설정값을 초과하면
  // 재연결 시도를 중단하고 TransportEventType::kStopped 이벤트를
  // TransportEventCallback 으로 전달합니다.
  // 기본값 : false
  void SetAutoReconnect(const bool use_auto_reconnect);
  bool GetAutoReconnect();

  // 재연결 시도의 전체 timeout 값을 초단위로 설정합니다.
  // 기본값 : 10
  void SetAutoReconnectTimeout(const int seconds);
  int GetAutoReconnectTimeout();

  // 클라이언트 Ping 기능의 사용 여부에 대한 옵션입니다.
  // 서버의 Ping 기능 사용 여부와 상관없이 따로 동작합니다.
  // 기본값 : flase
  void SetEnablePing(const bool enable_ping);
  bool GetEnablePing();

  // 서버로부터 Ping 에 대한 응답을 기다리는 최대 시간을 초단위로 설정합니다.
  // 이 시간 내에 Ping 응답이 오지 않는다면 서버와의 연결이 끊긴 것으로 보고 서버와 연결을 끊습니다.
  // 기본값 : 20
  void SetPingTimeout(const int seconds);
  int GetPingTimeout();

  // 클라이언트가 Ping 요청을 보내는 간격을 초단위로 설정합니다.
  // 기본값 : 3
  void SetPingInterval(const int seconds);
  int GetPingInterval();

  // 메시지 중복 전송 여부를 확인하도록 설정합니다.
  // Session reliability 옵션을 사용하지 않고 메시지가 순서대로 도착했는지만
  // 보장하고 싶을 때 사용할 수 있습니다.
  // Session reliability 옵션을 사용하면 이 옵션은 무시됩니다.
  // 기본값 : false
  void SetSequenceNumberValidation(const bool validation);
  bool GetSequenceNumberValidation();

  // 트랜스포트가 서버에 연결되기까지 제한 시간을 초단위로 설정합니다.
  // 이 설정을 초과할 때까지 서버와 연결되지 않으면 연결 시도를 중단하고,
  // kStopped 이벤트를 발생시킵니다.
  // 기본값 : 10
  void SetConnectTimeout(const int seconds);
  int GetConnectTimeout();

  // 서버와 주고 받는 메시지의 암호화 기능 중 공개키가 필요하지 않은 암호화
  // 방식 (kDummyEncryption, kIFunEngine1Encryption, kIFunEngine2Encryption) 을
  // 설정하기 위해 사용합니다.
  // 이 옵션을 설정하지 않으면 암호화 기능을 사용하지 않으며, 사용할 경우
  // 서버와 동일한 암호화 타입이 설정되어있어야 합니다.
  void SetEncryptionType(const EncryptionType type);
  fun::vector<EncryptionType> GetEncryptionTypes();

  // 암호화 방식 중 (kChacha20Encryption, kAes128Encryption) 설정하기 위해 사용합니다.
  // 이 옵션을 설정하지 않으면 암호화 기능을 사용하지 않으며, 사용할 경우
  // 서버와 동일한 암호화 타입이 설정되어있어야 합니다.
  void SetEncryptionType(const EncryptionType type, const fun::string &public_key);
  fun::string GetPublicKey(const EncryptionType type);

  // 서버와 주고 받는 메시지를 압축하도록 설정합니다.
  // 값을 지정하지 않을 경우 압축 기능을 사용하지 않습니다.
  // 압축 타입은 서버와 같은 값을 입력해야 합니다.
  void SetCompressionType(const CompressionType type);
  fun::vector<CompressionType> GetCompressionTypes();

  // TLS 사용 여부에 대한 옵션입니다.
  // 기본값 : false
#if FUNAPI_HAVE_TCP_TLS
  void SetUseTLS(const bool use_tls);
#endif
  bool GetUseTLS();

// 서버 인증에 사용되는 CACertificate 경로를 설정합니다.
// 기본값 : empty string
#ifdef FUNAPI_UE4_PLATFORM_PS4
  void SetCACert(const fun::string &cert);
#else
  void SetCACertFilePath(const fun::string &path);
#endif
  const fun::string& GetCACertFilePath();

  ...
};

FunapiUDPTransportOption

UDP 트랜스포트의 옵션을 설정할 수 있으며 암호화, 압축 기능만 제공합니다.

class FUNAPI_API FunapiUdpTransportOption : public FunapiTransportOption {
 public:
  FunapiUdpTransportOption();
  virtual ~FunapiUdpTransportOption() = default;

  static std::shared_ptr<FunapiUdpTransportOption> Create();

  // 서버와 주고 받는 메시지의 암호화 기능 중 공개키가 필요하지 않은 암호화
  // 방식 (kDummyEncryption, kIFunEngine2Encryption) 을
  // 설정하기 위해 사용합니다.
  // 이 옵션을 설정하지 않으면 암호화 기능을 사용하지 않으며, 사용할 경우
  // 서버와 동일한 암호화 타입이 설정되어있어야 합니다.
  void SetEncryptionType(const EncryptionType type);
  EncryptionType GetEncryptionType();

  // 서버와 주고 받는 메시지를 압축하도록 설정합니다.
  // 값을 지정하지 않을 경우 압축 기능을 사용하지 않습니다.
  // 압축 타입은 서버와 같은 값을 입력해야 합니다.
  void SetCompressionType(const CompressionType type);
  fun::vector<CompressionType> GetCompressionTypes();

  ...
};

FunapiHttpTransportOption

HTTP 트랜스포트의 옵션을 설정할 수 있으며, 다음과 같은 기능을 제공합니다.

  • 메시지 중복 확인: 구현상의 이유나 해킹 등의 이슈로 인해서 메시지가 중복 전송될 수 있는데, 이런 현상을 막을 수 있습니다.

  • 연결 제한 시간: HTTP 메시지를 전송하기 위해서는 먼저 TCP 연결을 맺어야 하는데 TCP 연결을 맺는데 제한 시간을 설정합니다.

  • 메시지 암호화: 클라이언트와 서버 사이에 주고 받는 메시지를 암호화합니다. 클라이언트와 서버에 설정한 암호화 방식이 같아야 합니다.

  • 메시지 압축: 클라이언트와 서버 사이에 주고 받는 메시지를 압축합니다. 클라이언트와 서버에 설정한 압축 방식이 같아야 합니다.

  • HTTPS 프로토콜 사용: 서버와 연결에 HTTPS 프로토콜을 사용합니다.

class FUNAPI_API FunapiHttpTransportOption : public FunapiTransportOption {
 public:
  FunapiHttpTransportOption();
  virtual ~FunapiHttpTransportOption() = default;

  static std::shared_ptr<FunapiHttpTransportOption> Create();

  // 메시지 중복 전송 여부를 확인하도록 설정합니다.
  // Session reliability 옵션을 사용하지 않고 메시지가 순서대로 도착했는지만
  // 보장하고 싶을 때 사용할 수 있습니다.
  // Session reliability 옵션을 사용하면 이 옵션은 무시됩니다.
  // 기본값 : false
  void SetSequenceNumberValidation(const bool validation);
  bool GetSequenceNumberValidation();

  // 트랜스포트가 서버에 연결되기까지 제한 시간을 초단위로 설정합니다.
  // 이 설정을 초과할 때까지 서버와 연결되지 않으면 연결 시도를 중단하고,
  // kStopped 이벤트를 발생시킵니다.
  // 기본값 : 10
  void SetConnectTimeout(const time_t seconds);
  time_t GetConnectTimeout();

  // HTTPS 프로토콜 사용 여부에 대한 옵션입니다.
  // 서버가 HTTPS 프로토콜만 지원하는 경우 이 값을 true로 입력해야 합니다.
  void SetUseHttps(const bool https);
  bool GetUseHttps();

  // 암호화 방식 중 (kDummyEncryption, kIFunEngine2Encryption) 설정하기 위해 사용합니다.
  // 이 옵션을 설정하지 않으면 암호화 기능을 사용하지 않으며, 사용할 경우
  // 서버와 동일한 암호화 타입이 설정되어있어야 합니다.
  void SetEncryptionType(const EncryptionType type);
  EncryptionType GetEncryptionType();

  // 서버와 주고 받는 메시지를 압축하도록 설정합니다.
  // 값을 지정하지 않을 경우 압축 기능을 사용하지 않습니다.
  // 압축 타입은 서버와 같은 값을 입력해야 합니다.
  void SetCompressionType(const CompressionType type);
  fun::vector<CompressionType> GetCompressionTypes();

  // 서버 인증에 사용되는 CACertificate 경로를 설정합니다.
  // 기본값 : empty string
#ifdef FUNAPI_UE4_PLATFORM_PS4
  void SetCACert(const fun::string &cert);
#else
  void SetCACertFilePath(const fun::string &path);
#endif
  const fun::string& GetCACertFilePath();

  ...
};

FunapiWebsocketTransportOption

WebSocket 트랜스포트의 옵션을 설정할 수 있으며, 다음과 같은 기능을 제공합니다.

  • Ping: 주기적으로 확인 메시지를 보내서 실제로 메시지를 보낼 수 없는 상태라면 WebSocket 연결을 끊는 기능입니다.

  • 메시지 압축: 클라이언트와 서버 사이에 주고 받는 메시지를 압축합니다. 클라이언트와 서버에 설정한 압축 방식이 같아야 합니다.

class FUNAPI_API FunapiWebsocketTransportOption : public FunapiTransportOption {
 public:
  FunapiWebsocketTransportOption();
  virtual ~FunapiWebsocketTransportOption() = default;

  static std::shared_ptr<FunapiWebsocketTransportOption> Create();

  // 클라이언트 Ping 사용 옵션입니다.
  // 기본값 : flase
  void SetEnablePing(const bool enable_ping);
  bool GetEnablePing();

  // 서버로부터 Ping 에 대한 응답을 기다리는 최대 시간 값입니다.
  // 이 시간 내에 Ping 응답이 오지 않는다면 서버와의 연결이 끊긴 것으로 보고 Disconnect 처리됩니다.
  // 기본값 : 20
  void SetPingTimeout(const int seconds);
  int GetPingTimeout();

  // Ping 메시지를 보내는 간격에 대한 시간 값입니다.
  // 기본값 : 3
  void SetPingInterval(const int seconds);
  int GetPingInterval();

  // 서버와 주고 받는 메시지를 압축 할 수 있습니다.
  // 값을 지정하지 않을 경우 압축 기능을 사용하지 않습니다.
  // 압축 타입은 서버와 같은 값을 입력해야 합니다.
  void SetCompressionType(const CompressionType type);
  fun::vector<CompressionType> GetCompressionTypes();

  ...
};

메시지 다루기

클라이언트 플러그인은 아이펀 엔진에서 지원하는 JSONProtocol Buffers (Protobuf) 의 두 가지 메시지 인코딩 방식을 지원합니다.

이번 챕터에서는 각각의 메시지 인코딩 방식을 사용해서 메시지를 보내고 받는 방법과 메시지를 암호화하고 압축하는 방법에 대해서 설명하겠습니다.

JSON 메시지 보내기

서버로 메시지를 보내고 싶다면 FunapiSession 클래스의 SendMessage() 함수를 호출하면 됩니다.

// JSON 용 인터페이스
void SendMessage(const fun::string &msg_type,
                 const fun::string &json_string,
                 const TransportProtocol protocol = TransportProtocol::kDefault,
                 const EncryptionType encryption_type = EncryptionType::kDefaultEncryption);
  • msg_type: 사용자가 정의한 메시지 타입 중 하나를 string 타입으로 지정합니다.

  • json_string: 서버로 전송할 json 문자열입니다.

  • protocol: 지정하지 않을 경우 FunapiSession 에 처음 등록(Connect) 된 프로토콜로

메시지가 전송됩니다. Transport가 여러 개 등록되어 있고 특정 프로토콜로 메시지를 보내고 싶다면 protocol 값을 지정하면 됩니다.

  • encryption_type: 암호화 타입 파라미터입니다. 암호화를 사용하는데 타입을 지정하지 않을 경우 기본

타입으로 암호화를 하게 되고 암호화를 사용하지 않을 경우에 이 값을 입력하면 오류가 발생합니다. 서버에서 하나의 프로토콜에 여러 종류의 암호화를 허용할 경우 메시지를 보낼 때 암호화 타입을 선택해서 보낼 수 있습니다.

암호화에 대한 좀 더 자세한 설명은 메시지 암호화 를 참고해 주세요.

fun::string temp_string = "hellow world";
TSharedRef<FJsonObject> json_object = MakeShareable(new FJsonObject);
json_object->SetStringField(FString("message"), FString(UTF8_TO_TCHAR(temp_string.c_str())));

// Convert JSON document to fun::string
FString ouput_fstring;
TSharedRef<TJsonWriter<TCHAR>> writer = TJsonWriterFactory<TCHAR>::Create(&ouput_fstring);
FJsonSerializer::Serialize(json_object, writer);
fun::string json_stiring = TCHAR_TO_UTF8(*ouput_fstring);

session_->SendMessage("echo", json_stiring);

Protocol Buffers 메시지 보내기

Protobuf 메시지를 코드에서 사용하기 위해서는 먼저 메시지를 정의하는 .proto 파일로부터 .h 파일 및 .cc 파일을 생성하는 과정이 필요합니다.

  1. <Project Root>/Plugins/Funapi/ThirdParty/proto 디렉터리에 메시지를 정의하는 .proto 파일이 있는지 확인합니다.

    Note

    플러그인 프로젝트는 기본적으로 test_messages.proto 파일을 포함하고 있으며, 이 파일 내용을 수정하거나 파일 이름을 변경해서 사용해도 됩니다.

  2. 같은 디렉터리 안에서 윈도우 호스트라면 build-protobuf-win.bat 스크립트를 macOS 호스트라면 build-protobuf-mac.sh 스크립트를 실행하면, 클라이언트 플러그인에서 사용할 .proto 파일로 부터 .h 파일 및 .cc 파일을 생성해서 정해진 위치로 복사합니다.

그럼, 이제 본격적으로 Protobuf 메시지를 전송하는 방법을 알아보도록 하겠습니다.

다음은 메시지를 전송하는 SendMessage 함수의 원형입니다.

// protobuf 용 인터페이스
// 메시지 타입은 메시지에 설정됩니다.
void SendMessage(const FunMessage &message,
                 const TransportProtocol protocol = TransportProtocol::kDefault,
                 const EncryptionType encryption_type = EncryptionType::kDefaultEncryption);
  • message 는 서버로 전송할 protobuf 메시지입니다.

  • protocol 은 지정하지 않을 경우 FunapiSession에 처음 등록(Connect) 된 프로토콜로 메시지가 전송됩니다. Transport가 여러 개 등록되어 있고 특정 프로토콜로 메시지를 보내고 싶다면 protocol 값을 지정하면 됩니다.

  • encryption_type 은 암호화 타입을 정의하는 파라미터입니다. 암호화를 사용하는데 타입을 지정하지 않을 경우 기본 암호화 방식을 사용하고 암호화를 사용하지 않을 경우에 이 값을 입력하면 오류가 발생합니다.

    서버에서 하나의 프로토콜에 대해서 여러 종류의 암호화 방식을 허용하도록 설정돼 있는 경우 메시지를 보낼 때 암호화 타입을 선택해서 보낼 수도 있습니다.

    암호화에 대한 좀 더 자세한 설명은 메시지 암호화 를 참고해 주세요.

fun::string temp_string = "hellow world";
FunMessage msg;
msg.set_msgtype("pbuf_echo");
PbufEchoMessage *echo = msg.MutableExtension(pbuf_echo);
echo->set_msg(temp_string.c_str());

session_->SendMessage(msg);

Protobuf 메시지의 기본형은 FunMessage 입니다. 사용자 메시지는 extend 형태로 되어있습니다. extend 메시지 중 0 ~ 15번까지는 예약된 메시지입니다. 사용자 메시지는 16번 필드부터 사용이 가능합니다.

Delayed ack & Piggy back 기능

Session 신뢰성 기능을 사용할 경우 메시지 동기화를 위해 서버로부터 메시지를 받을 때마다 ack (서버가 보낸 메시지를 받았다는 확인 메시지)를 보내는데 메시지를 받을 때마다 ack 메시지를 보내는 것이 부담스러울 수 있습니다. 이런 부담을 줄이기 위해 이후에 보내는 메시지에 ack를 실어 보내거나 일정 간격을 두고 한 번 씩만 보내도록 할 수 있습니다.

class FUNAPI_API FunapiSessionOption :
    public std::enable_shared_from_this<FunapiSessionOption>
{
  ...

  // SessionReliability 옵션을 사용할 경우 서버와 ack 메시지를 주고 받는데
  // 이 옵션을 사용할 경우 piggybacking, delayed sending 등을 통해 네트워크 트래픽을 줄여줍니다.
  // 옵션 값은 ack를 보내는 간격에 대한 시간 값이며 초단위 값을 사용합니다.
  // 시간 값이 0 보다 클 경우 piggybacking 은 자동으로 이루어집니다.
  // 기본값 : 0
#if FUNAPI_HAVE_DELAYED_ACK
  void SetDelayedAckIntervalMillisecond(const int millisecond);
#endif
  int GetDelayedAckIntervalMillisecond();
}

서버의 설정과는 별개로 동작하며 서버에서는 MANIFEST 파일에서 delayed_ack_interval_in_ms 값을 0보다 큰 값으로 설정해주면 됩니다.

메시지 받기

서버로부터 메시지를 받기 위해서는 FunapiSession 클래스의 아래 함수들을 통해 메시지 콜백 함수를 등록해야 합니다.

JSON 메시지

void AddJsonRecvCallback(const JsonRecvHandler &handler);

아래는 메시지 핸들링에 대한 예제입니다.

// JSON 메시지에 대한 처리 함수입니다.
session_->AddJsonRecvCallback(
    [](const std::shared_ptr<fun::FunapiSession> &session,
       const fun::TransportProtocol transport_protocol,
       const fun::string &msg_type,
       const fun::string &json_string)
    {
      if (msg_type.compare("echo") == 0)
      {
        UE_LOG(LogFunapiExample, Log, TEXT("msg '%s' arrived."),
               *FString(msg_type.c_str()));
        UE_LOG(LogFunapiExample, Log, TEXT("json string: %s"),
               *FString(json_string.c_str()));
      }
    }
);

Protocol Buffers 메시지

void AddProtobufRecvCallback(const ProtobufRecvHandler &handler);

아래는 메시지 핸들링에 대한 예제입니다.

// Protobuf 메시지에 대한 처리 함수입니다.
session_->AddProtobufRecvCallback(
    [](const std::shared_ptr<fun::FunapiSession> &session,
       const fun::TransportProtocol transport_protocol,
       const FunMessage &fun_message)
    {
      if (fun_message.msgtype().compare("pbuf_echo") == 0)
      {
        UE_LOG(LogFunapiExample, Log, TEXT("msg '%s' arrived."),
               *FString(fun_message.msgtype().c_str()));

        if (fun_message.HasExtension(pbuf_echo))
        {
          PbufEchoMessage echo = fun_message.GetExtension(pbuf_echo);
          UE_LOG(LogFunapiExample, Log, TEXT("proto echo message: %s"),
                 *FString(echo.msg().c_str()));
        }
      }
    }
);

응답 메시지 시간 제한

특정 메시지를 서버에 보내고 응답을 받아야 할 때, FunapiSession 클래스에 있는 SetRecvTimeout() 함수를 사용해서 특정 메시지 타입에 대해서 수신 제한 시간을 설정할 수 있습니다.

아래는 관련 함수들의 원형과 설명입니다.

// MessageType 의 메시지에 대한 타임아웃을 등록, 삭제 합니다.
void SetRecvTimeout(const fun::string &msg_type, const int seconds);
void EraseRecvTimeout(const fun::string &msg_type);

// MessageType 의 메시지에 대한 timeout 콜백 함수를 추가합니다.
void AddRecvTimeoutCallback(const RecvTimeoutHandler &handler);

// int 타입의 메세제에 대한 타임아웃을 등록, 삭제 합니다.
void SetRecvTimeout(const int32_t msg_type, const int seconds);
void EraseRecvTimeout(const int32_t msg_type);

// int 타입의 메시지에 대한 timeout 콜백 함수를 추가합니다.
void AddRecvTimeoutCallback(const RecvTimeoutIntHandler &handler);

아래 코드는 ‘sc_login’ 메시지를 10초 동안 기다리는 예제입니다. ‘sc_login’ 메시지를 10초 내에 받지 못할 경우 AddRecvTimeoutCallback() 함수로 등록한 콜백 함수가 호출됩니다.

// timeout 콜백 함수를 추가합니다.
session_->AddRecvTimeoutCallback(
    [](const std::shared_ptr<fun::FunapiSession> &session,
       const fun::string& msg_type)
    {
      UE_LOG(LogFunapiExample, Log, TEXT("message timeout, msg_tpye : %s"),
             *FString(msg_type.c_str()));
    }
);

// 기다리는 메시지 타입과 시간을 지정합니다.
// 등록되는 순간부터 시간이 흘러갑니다.
session->SetRecvTimeout("sc_login", 10 /* seconds */);

Note

한 번 등록한 RecvTimeout 은 서버로부터 응답을 받거나 타임아웃 이벤트가 발생하기 전까지 유지되며 타임아웃 이벤트가 만료된 후에는 다시 설정해야 합니다.

메시지 암호화

서버와 메시지를 주고 받을 때 메시지를 암호화할 수 있습니다.

암호화 타입

아이펀 엔진은 다음과 같은 종류의 암호화 타입을 지원합니다.

enum class FUNAPI_API EncryptionType : int
{
  // SendMessage() 함수의 파라미터로 사용되는 값으로
  // 설정한 암호화 타입이 있다면 그 중 하나의 암호화 타입을 선택해 메시지를 암호화 합니다.
  // 암호화 타입의 선택 기준은 가장 작은 Enumerator values 가지는 암호화 타입 입니다.
  kDefaultEncryption,

  // 암호화를 사용하지만 특정 메시지를 암호화하지 않은 상태로 보내고 싶을 때
  // SendMessage의 파라미터로 이 값을 전달하면 됩니다.
  kDummyEncryption,

  // iFun Engine에서 제공하는 암호화 타입입니다.
  // 메시지를 주고 받을 때마다 키 값이 변경되어 안정적인 암호화 방식입니다.
  // Tcp 프로토콜에서만 사용 가능합니다.
  kIFunEngine1Encryption,

  // iFun Engine에서 제공하는 암호화 타입입니다.
  // 고정된 암호화 키를 사용합니다. 프로토콜에 상관없이 사용 가능합니다.
  kIFunEngine2Encryption,

  // ChaCha20 암호화 타입입니다.
  // Tcp 프로토콜에서만 사용 가능합니다.
  kChacha20Encryption,

  // Aes 128 암호화 타입입니다.
  // Tcp 프로토콜에서만 사용 가능합니다.
  kAes128Encryption,
};

kIFunEngine1Encryption 타입은 메시지를 주고 받을 때마다 암호화 키가 변경됩니다. 동일한 메시지를 보내도 암호화 키 값이 매번 달라지므로 고정된 암호화 키를 사용하는 kIFunEngine2Encryption 보다 상대적으로 보안에 안정적인 암호화 타입입니다.

ChaCha20Aes128 은 외부 라이브러리(sodium)를 사용하고 있습니다. 이 두 암호화 타입을 사용하려면 공개 키 를 설정해야 합니다.

Warning

IFunEngine1, ChaCha20, Aes128 암호화 방식은 TCP 프로토콜에서만 사용이 가능합니다. 반대로 UDP 와 HTTP 프로토콜에 대해서 지원하는 암호화 타입은 IFunEngine2 밖에 없습니다.

암호화 설정을 사용하는 상태에서 특정 메시지에 대해서는 암호화를 적용하지 않고 그대로 보내고 싶다면 SendMessage() 함수의 암호화 타입 인자로 kDummyEncryption 을 지정하면 됩니다.

이 기능을 사용하려면 서버 쪽 암호화 설정에 dummy 가 포함되어 있어야 합니다.

암호화 사용

아래 코드는 의 kIFunEngine1Encryption, kChacha20Encryption 암호화를 사용해 메시지를 서버로 전송하는 예제 입니다.

std::shared_ptr<fun::FunapiTcpTransportOption> tcp_option =
    fun::FunapiTcpTransportOption::Create();
// iFunEngine1 암호화 방식을 사용하기 위해서 옵션을 설정합니다.
tcp_option->SetEncryptionType(fun::EncryptionType::kIFunEngine1Encryption);

// 이 키값은 예제를 위한 임시값입니다.
// 접속할 아이펀 엔진 서버의 MANIFEST.json 파일에 있는 public_key 설정과
// 같은 값을 사용해 주세요.
fun::string public_key = "7cf7672bc648c3a4b04f7e2a2427c08a2a355a38821e93df4e98c1decefa8200";
// Chacha20 암호화 방식을 추가로 설정합니다.
tcp_option->SetEncryptionType(fun::EncryptionType::kChacha20Encryption,
                              public_key);

// 이제 iFunEngine1, Chacha20 두가지 암호화 방식이 설정되었습니다.


// 세션 이벤트 콜백 함수를 등록합니다.
session_->AddSessionEventCallback(
    [](const std::shared_ptr<fun::FunapiSession> &session,
       const fun::TransportProtocol transport_protocol,
       const fun::SessionEventType type,
       const fun::string &session_id,
       const std::shared_ptr<fun::FunapiError> &error)
    {
      // 서버에 연결되어 세션이 생성되면 메시지를 보냅니다.
      if (type == fun::SessionEventType::kOpened)
      {
        fun::string temp_string = "hellow world";
        {
          FunMessage msg;
          msg.set_msgtype("pbuf_echo");
          PbufEchoMessage *echo = msg.MutableExtension(pbuf_echo);
          echo->set_msg(temp_string.c_str());

          session->SendMessage(
              msg,
              fun::TransportProtocol::kDefault,
              // 함수 인자로 메시지를 보내는 시점에 사용할 암호화 방식을
              // 선택할 수 있습니다. kDefaultEncryption 값은 설정한 암호화
              // 타입 중 Enumerator values 가 낮은 암호화 방식을 사용합니다.
              // 이번 예제에서는 IFunEngine1 암호화 방식을 사용합니다.
              fun::EncryptionType::kDefaultEncryption);
        }

        temp_string = "hellow server";
        {
          FunMessage msg;
          msg.set_msgtype("pbuf_echo");
          PbufEchoMessage *echo = msg.MutableExtension(pbuf_echo);
          echo->set_msg(temp_string.c_str());

          session->SendMessage(
              msg,
              fun::TransportProtocol::kDefault,
              // 명시적으로 Chacha20 암호화 방식을 사용하도록 설정합니다.
              fun::EncryptionType::kChacha20Encryption);
        }
      }
    }
);

// 서버에 연결을 시도합니다.
session_->Connect(fun::TransportProtocol::kTcp, port, fun::FunEncoding::kProtobuf, tcp_option);

만약 클라이언트의 암호화 타입이 서버에서 사용하지 않는 타입일 경우 오류가 발생하고 메시지를 주고 받을 수 없게 됩니다.

Note

GitHub Unreal 에 배포된 클라이언트 플러그인은 보안상의 이유로 Chacha20AES128 암호화 타입만 동작하는 상태로 배포됩니다. IFunEngine1IFunEngine2 방식을 사용하기 원하는 고객께서는 iFun Engine support 로 요청 메일을 보내주기 바랍니다.

메시지 압축 기능

서버와 주고 받는 메시지의 크기가 클 경우 메시지를 압축해서 전송하는 기능입니다.

서버와 클라이언트에서 같은 압축 방식과 압축할 메시기 크기를 설정해야 합니다.

아이펀엔진에서는 다음 압축 알고리즘들을 사용할 수 있습니다.

  • Zstd(Zstandard): 실시간 전송해야 하는 메시지에 적당한 알고리즘입니다.

  • Deflate: 큰 데이터를 지연 시간을 감수하고 전송할 경우 적당한 알고리즘입니다.

enum class FUNAPI_API CompressionType : int
{
#if FUNAPI_HAVE_ZSTD
  kZstd,
#endif
#if FUNAPI_HAVE_ZLIB
  kDeflate,
#endif
  kDefault,
};

Tip

압축 방식은 트랜스포트에 종속적이기 때문에 같은 세션일지라도 프로토콜 별로 다른 압축 방식을 사용할 수 있습니다.

메시지 압축 예제

아래 코드는 zstd 압축 방식을 사용해 메시지를 서버로 전송하는 예제 입니다.

std::shared_ptr<fun::FunapiTcpTransportOption> tcp_option =
    fun::FunapiTcpTransportOption::Create();
tcp_option->SetCompressionType(fun::EncryptionType::kZstd);

 session_->AddTransportEventCallback(
    [](const std::shared_ptr<fun::FunapiSession> &session,
       const fun::TransportProtocol transport_protocol,
       const fun::TransportEventType type,
       const std::shared_ptr<fun::FunapiError> &error
    {
      if (type == fun::TransportEventType::kStarted)
      {
        FunMessage msg;
        msg.set_msgtype("pbuf_echo");
        PbufEchoMessage *echo = msg.MutableExtension(pbuf_echo);
        echo->set_msg(temp_string.c_str());
        session_->SendMessage(msg);
      }
    }
);

session_->Connect(fun::TransportProtocol::kTcp, port,
                  fun::FunEncoding::kProtobuf, tcp_option);

연결 종료 및 재연결

서버와 연결이 끊기거나 세션이 닫혔을 때 클라이언트는 이를 인지하고 그에 맞는 동작을 할 필요가 있습니다.

이번 절에서는 서버와 연결 상태가 변하는 상황에 대한 내용을 설명하겠습니다.

연결 종료 알림

아이펀 엔진에서 서버와 직접적인 연결은 트랜스포트가 담당하고 있기 때문에 서버와 연결이 끊기면 트랜스포트 이벤트 가 발생하고, 서버 쪽에서 세션에 대해 Timeout 이 발생했다고 판단하거나 함수를 호출해서 명시적으로 세션을 닫으면 kClosed 세션 이벤트 가 발생합니다.

Note

세션 및 트랜스포트 이벤트 종류와 다루는 방법에 대한 자세한 내용은 Transport EventSession Event 를 참고해 주시기 바랍니다.

  • 트랜스포트 이벤트

    트랜스포트가 서버와 연결에 실패하거나 정상적인 종료과정을 거쳐서 끊기면 kStopped 트랜스포트 이벤트가 발생하고, 연결에 오류가 발생해 사용할 수 없어서 끊기면 kDisconnected 이벤트가 발생합니다.

세션 객체에서도 연결 종료를 의미하는 이벤트가 발생할 수 있는데, 소유하는 모든 트랜스포트가 끊겨 더 이상 서버에 연결할 수 없을 때 발생합니다.

  • 세션 이벤트

    세션 kClosed 이벤트는 트랜스포트의 연결이 끊겼다고 발생하는 것이 아니라 세션 Timeout 이 되거나 서버에서 명시적으로 세션을 닫았을 경우에 발생합니다. 서버로부터 세션이 닫혔다는 메시지를 받으면 세션의 모든 트랜스포트의 연결을 끊고 kClosed 이벤트를 발생시킵니다.

세션 닫기 요청

클라이언트와 서버가 공유하는 세션을 닫도록 서버에 요청하는 함수입니다. 세션의 생성과 삭제가 서버 주도로 이루어지기 때문에 서버에 요청하는 방식을 사용합니다.

서버는 클라이언트의 요청을 받으면 서버 쪽 세션을 정리하고 클라이언트로 세션이 닫혔다는 메시지를 전송합니다.

이메시지를 받은 클라이언트는 세션의 모든 트랜스포트의 연결을 끊고, kClosed 이벤트가 발생합니다.

void FunapiSession::CloseRequest()

연결 끊기

클라이언트는 서버의 세션 정보를 직접 삭제할 수는 없지만 트랜스포트를 정리하는 것은 가능합니다.

세션 객체가 제공하는 Stop() 함수는 세션의 전체 또는 특정 프로토콜을 사용하는 트랜스포트를 연결을 끊고 정리합니다.

세션 정보를 서보에 남아있기 때문에 트랜스포트를 다시 연결하면 세션은 계속해서 사용할 수도 있습니다.

// FunapiSession 의 모든 트랜스포트를 정리합니다.
void FunapiSession::Stop()


// 세션 객체가 소유하는 트랜스포트 중 인자에 해당하는 프로토콜을 사용하는
// 트랜스포트를 정리합니다.
void FunapiSession::Stop(const TransportProtocol protocol)

Note

Stop 함수는 서버와의 연결을 종료하지만 서버의 세션에는 영향을 미치지 않기 때문에 서버에는 세션이 남아있는 상태로 유지됩니다. 서버와 연결을 종료하면서 서버의 세션도 닫고 싶다면 CloseRequest() 함수를 사용해야 합니다.

재연결

서버와의 연결을 종료한 후 재연결을 할 때에는 세션 객체의 Connect() 함수에 연결할 트랜스포트의 프로토콜 타입을 인자로 전달하면 트랜스포트를 다시 생성하지 않고도 다시 연결할 수 있습니다.

session_->AddTransportEventCallback(
    [](const std::shared_ptr<fun::FunapiSession> &session,
       const fun::TransportProtocol transport_protocol,
       const fun::TransportEventType type,
       const std::shared_ptr<fun::FunapiError> &error
    {
      if (type == fun::TransportEventType::kStopped)
      {
        session->Connect(transport_protocol);
      }
    }
);

AutoReconnect 옵션

TCP 프로토콜을 사용하는 FunapiTcpTransportOption 객체는 연결이 끊겼을 때 자동으로 재연결을 시도하는 AutoReconnect 기능을 제공합니다.

SetAutoReconnect() 함수를 통해 설정할 수 있습니다.

재연결 기능과 관련하여 다음과 같은 트랜스포트 이벤트가 발생할 수 있습니다.

  • kReconnecting: 재연결 기능에 의해서 연결을 시도할 때마다 발생합니다.

  • kStopped: SetAutoReconnectTimeout() 함수로 설정한 제한시간 내에 재연결에 성공하지 못한 경우 발생합니다.

std::shared_ptr<fun::FunapiTcpTransportOption> tcp_option =
    fun::FunapiTcpTransportOption::Create();
tcp_option->SetAutoReconnect(true);
// 기본값 : 10 초.
tcp_option->SetAutoReconnectTimeout(10);

session_->AddTransportEventCallback(
    [](const std::shared_ptr<fun::FunapiSession> &session,
       const fun::TransportProtocol transport_protocol,
       const fun::TransportEventType type,
       const std::shared_ptr<fun::FunapiError> &error
    {
      if (type == fun::TransportEventType::kReconnecting)
      {
        // AutoReconnect 옵션으로 인해 재연결을 시도할 때 호출됩니다.
        // auto reconnect timeout 으로 지정된 시간까지 재시도 하며
        // 재연결을 시도할 때마다 kReconnecting 이벤트가 발생합니다.
      }
      else if (type == fun::TransportEventType::kStopped)
      {
        // 제한 시간내에 재연결에 성공하지 못하면 최종적으로 실패합니다.
        // 다시 서버에 연결하려면 새 FunapiSession 객체를 생성해 시도해 주세요.
      }
    }
);

session_->Connect(fun::TransportProtocol::kTcp, port,
                  fun::FunEncoding::kProtobuf, tcp_option);

연결 상태 확인(Ping)

TCP 프로토콜과 WebSocket 프로토콜처럼 연결 지향형(Connection Oriented) 프로토콜에서는 연결이 끊겼다는 신호를 수신하지 못한 채 실제로는 연결이 끊겼지만 인식하지 못하는 경우가 있습니다.

특히 단말이 이동하는 모바일 네트워크 환경에서는 이런 상황이 더욱 빈번하게 발생하는데, 이런 경우에 대비하기 위해 확인 메시지를 전송하고 일정시간 이상 응답을 받지 못하면 문제가 있는 것으로 간주하고 연결을 끊을 필요가 있습니다.

클라이언트 플러그인의 Ping 기능은 SetTimeInterval() 함수로 설정한 시간마다 서버로 Ping 메시지를 보내고, SetPingTimeout() 함수로 설정한 시간 내에 응답이 오지 않을 경우 서버와의 연결에 문제가 있다고 판단하면 연결을 끊고, kStopped 트랜스포트 이벤트를 발생시킵니다.

Note

이 기능은 TCP 프로토콜과 Websocket 프로토콜에서 사용 가능하며, 클라이언트와 서버 양쪽에서 독립적으로 동작합니다. 서버의 Ping 기능에 대해서는 세션 Ping(RTT) 을 참고하시기 바랍니다.

다음은 Ping 기능을 사용하도록 설정하는 예제 코드입니다.

std::shared_ptr<fun::FunapiTcpTransportOption> tcp_option =
    fun::FunapiTcpTransportOption::Create();
tcp_option->SetEnablePing(true);
// 서버로 부터 Ping 응답을 기다리는 최대 시간을 지정합니다.
// 기본값 : 30 초.
tcp_option->SetPingTimeout(30);
// Ping 메시지를 보내는 간격을 지정합니다.
// 기본값 : 3 초.
tcp_option->SetPingInterval(10);

std::shared_ptr<fun::FunapiWebsocketTransportOption> websocket_option =
    fun::FunapiWebsocketTransportOption::Create();
websocket_option->SetEnablePing(true);
// 서버로 부터 Ping 응답을 기다리는 최대 시간을 지정합니다.
// 기본값 : 30 초.
websocket_option->SetPingTimeout(30);
// Ping 메시지를 보내는 간격을 지정합니다.
// 기본값 : 3 초.
websocket_option->SetPingInterval(10);

session_->AddTransportEventCallback(
    [](const std::shared_ptr<fun::FunapiSession> &session,
       const fun::TransportProtocol transport_protocol,
       const fun::TransportEventType type,
       const std::shared_ptr<fun::FunapiError> &error
    {
      if (type == fun::TransportEventType::kStopped &&
          error->GetErrorType() ==  fun::ErrorType::kPing)
      {
        // ping timeout 으로 인해 transport 연결이 끊겼습니다.
        // 네트워크 연결을 확인해주세요.
      }
    }
);

session_->Connect(fun::TransportProtocol::kTcp, tcp_port,
                  fun::FunEncoding::kProtobuf, tcp_option);

session_->Connect(fun::TransportProtocol::kWebsocket, websocket_port,
                  fun::FunEncoding::kProtobuf, websocket_option);

서버 이동

역할 별로 여러 종류의 서버들에 접속해야 하는 경우 클라이언트는 현재 연결되어 있는 서버와의 연결을 끊고, 새로운 서버로 접속해야 하는 경우가 있을 수 있습니다.

아이펀 엔진은 서버 이동(Redirect) 기능을 통해서 서버의 요청을 받은 클라이언트가 기존 서버와 연결을 끊고, 다른 서버에 접속하는 기능을 제공합니다.

서버 이동 상태 확인

서버 이동이 시작되면 AddSessionEventCallback() 함수로 등록한 콜백을 통해서 서버 이동과 관련한 다음 세션 이벤트가 발생합니다.

  • kRedirectStarted: 클라이언트가 서버로부터 서버 이동 요청을 받아서 이동을 시작하면 발생합니다. 이 이벤트가 발생하고 나면 플러그인은 서버와 연결을 끊기 때문에 서버로 전송하는 메시지는 버려집니다.

    Tip

    단, 서버 이동 중 보내는 메시지의 전송 보장 를 사용하면 버려지는 메시지를

    처리할 수 있습니다.

  • kRedirectSucceeded: 서버 이동이 완료되어 새로운 서버와 세션이 생성됐습니다. 이 이벤트가 발생하면 새로운 서버로 메시지를 보낼 수 있습니다.

  • kRedirectFailed: 새로운 서버에 연결하지 못해 서버 이동에 실패했을 때 발생하는 이벤트입니다. 이 이벤트가 발생하면 세션 객체를 정리하고 새로운 세션 객체를 생성해서 서버에 접속해야 합니다.

enum class FUNAPI_API SessionEventType
{
    ...
    kRedirectStarted,       // 서버간 이동을 시작합니다.
    kRedirectSucceeded,     // 서버간 이동을 완료했습니다.
    kRedirectFailed         // 서버간 이동에 실패했습니다.
};

서버 이동 시 세션 및 트랜스포트 옵션 변경

클라이언트가 서버 이동을 할 때는 기본적으로 이동전 서버와의 연결에 사용했던 세션 및 트랜스포트 옵션을 그대로 사용합니다.

그러나, 이동 전 서버와 이동할 서버의 세션 또는 트랜스포트 설정이 다르다면 이동할 서버와 세션이 생성되더라도 정상적으로 메시지를 송수신할 수 없기 때문에 클라이언트가 서버를 이동할 때 세션과 트랜스포트의 옵션을 조정해야 할 필요가 있습니다.

세션 객체의 SetTransportOptionCallback 함수를 통해서 서버 이동 시에 적용할 세션과 트랜스포트 옵션을 변경할 수 있습니다.

클라이언트가 서버로부터 이동 메시지를 받으면 이동할 서버의 세션과 트랜스포트를 새로 만들기 전에 이 콜백 함수들을 호출하며, 인자로 전달되는 flavor 값과 protocol 을 사용해서 서버 종류 별로 세션 또는 트랜스포트 옵션을 다르게 적용할 수 있습니다.

Note

flavor 값은 각 서버의 MANIFEST 파일에서 설정한 값입니다.

session_->SetSessionOptionCallback(
    [](const fun::string &flavor) -> std::shared_ptr<fun::FunapiSessionOption>
    {
      if (flavor.compare("lobby") == 0)
      {
        std::shared_ptr<fun::FunapiSessionOption> session_opt =
            fun::FunapiSessionOption::Create();

        session_opt->SetSessionReliability(true);

        // 새로 적용하고자 하는 세션 옵션을 설정한
        // FunapiSessionOption 객체를 리턴합니다.
        return session_opt;
      }

      // nullptr 을 리턴하거나 콜백을 등록하지 않으면
      // 이동하기 전 서버에 사용했던 옵션을 그대로 사용합니다.
      return nullptr;
    }
);

session_->SetTransportOptionCallback(
    [](const fun::TransportProtocol protocol,
       const fun::string &flavor) -> std::shared_ptr<fun::FunapiTransportOption>
    {
      if (protocol == fun::TransportProtocol::kTcp &&
          flavor.compare("lobby") == 0)
      {
        std::shared_ptr<fun::FunapiTcpTransportOption> tcp_option =
            fun::FunapiTcpTransportOption::Create();
        tcp_option->SetEnablePing(true);

        // 새로 적용하고자 하는 트랜스포트 옵션을 설정한
        // FunapiTransportOption 객체를 리턴합니다.
        return tcp_option;
      }

      // nullptr 을 리턴하거나 콜백을 등록하지 않으면
      // 이동하기 전 서버에 사용했던 옵션을 그대로 사용합니다.
      return nullptr;
    }
);

Tip

옵션을 변경할 필요가 없는 경우에는 콜백 함수를 등록하지 않아도 되며, 이동하기 전 서버에 같은 프로토콜을 사용하는 트랜스포트가 없는 경우에는 기본값을 사용합니다.

서버 이동 중 보내는 메시지의 전송 보장

클라이언트 플러그인에서 kRedirectStared 이벤트가 발생하고, 서버 이동 중 상태가 되면 클라이언트 플러그인을 통해서 서버로 전송하는 메시지는 기본적으로 버려집니다.

만약, 서버 이동 중 상태일 때 보내는 메시지를 전송하기 원한다면 세션 객체의 SetUseRedirectQueue() 함수를 사용해서 전용 메시지 큐를 사용하도록 설정할 수 있습니다.

void SetUseRedirectQueue(const bool use);
bool GetUseRedirectQueue();

서버 이동 메시지 큐를 사용하면 클라이언트가 메시지를 보내려할 때 서버 이동 중 상태이면, 해당 메시지를 큐에 넣어두었다가 서버 이동이 완료되면 이동한 서버로 순차적으로 전송합니다.

만약, 큐에 있던 메시지를 전송하기 전에 확인하고 싶다면 SetRedirectQueueCallback() 함수로 등록한 콜백 함수를 활용할 수 있습니다. 클라이언트의 서버 이동 이 완료된 후 큐에 저장된 메시지를 전송하기 전에 이 콜백 함수가 호출되며 함수 인자는 다음과 같습니다.

  • protocol: 메시지를 전송하려고 했던 프로토콜입니다. 프로토콜 별로 콜백 함수가 실행됩니다.

  • current_tag: 이동하기 전 서버의 tag 목록입니다.

  • target_tag: 이동한 서버의 tag 목록입니다.

Note

tag 는 서버의 MANIFEST 파일에서 설정한 값이며, 기본적으로 flavor 값을 포함합니다.

  • message: 큐에 있던 메시지 목록입니다. SetDiscard() 함수를 호출해서 버릴 메시지를 선택할 수 있습니다.

다음은 큐에 저장되는 메시지의 멤버함수 원형과 사용 예제입니다.

class FunapiUnsentMessageImpl : public FunapiUnsentMessage
{
public:
    std::shared_ptr<FunapiMessage> GetMessage() const;

    const fun::string& GetMsgType();
    int32_t GetMsgType2();

    void SetDiscard(const bool discard);
    bool GetDiscard();
};

session_->SetRedirectQueueCallback(
    [](const fun::TransportProtocol protocol,
       const fun::vector<fun::string> &current_tags,
       const fun::vector<fun::string> &target_tags,
       const fun::deque<std::shared_ptr<FunapiUnsentMessage>>& message)
    {
      for (auto i = message.begin(); i != message.end(); ++i)
      {
        if (i->GetMessageType() == "echo")
        {
          // 해당 메시지는 전송하지 않고 버립니다
          i->SetDiscard(true);
        }
      }
    }
);

멀티캐스팅 기능

아이펀 엔진은 멀티캐스트 콤포넌트를 설정하는 것만으로 멀티캐스트 서버 기능을 제공합니다.

멀티캐스팅 기능은 아이펀 엔진의 멀티캐스트 서버 기능을 사용해서 채널에 참여한 모든 클라이언트에게 메시지를 전송하거나 다른 클라이언트가 보낸 메시지를 받을 수 있는 기능입니다.

Note

아이펀 엔진 서버의 멀티캐스트 기능을 설정해야 사용할 수 있습니다. 기능의 설명과 서버 쪽 설정에 대해서는 멀티캐스팅 기능 편을 참고하시기 바랍니다.

멀티캐스트 클래스 인터페이스

멀티캐스팅 기능은 FunapiMulticast 클래스를 통해서 제공합니다. 이번 장에서는 클래스가 제공하는 인터페이스에 대해서 설명합니다.

가장 먼저 FunapiMulticast 객체를 생성하기 위해서는 FunapiSession 객체가 필요한데,

  • 이미 생성돼 있는 객체를 사용하거나

  • FunapiMulticast 객체와 함께 생성하는

두가지 방식 중에 선택할 수 있습니다.

Caution

하나의 세션은 하나의 멀티캐스팅 객체만 생성할 수 있으며, TCP 또는 WebSocket 프로토콜을 지원합니다.

객체 생성 인터페이스

FunapiMulticast 객체의 생성은 이미 서버에 연결되어 있는 FunapiSession 를 사용하는지 여부에 따라서 두가지 방식을 사용할 수 있습니다.

FunapiMulticast 객체 생성 시에 FunapiSession 객체를 함께 생성한다면 이벤트 콜백 함수 등록과 서버로의 연결을 추가로 진행해야 합니다.

다음은 함수 매개변수에 대한 설명입니다.

  • sender: 멀티캐스트 채널에서 사용할 내 ID.

  • session: FunapiMulticast 객체를 생성할 때 사용할 세션 객체. 세션 객체는 서버에 연결되어 있는 상태여야 합니다.

  • protocol: FunapiMulticast 객체가 사용할 프로토콜. TCP 또는 WebSocket 프로토콜만 사용할 수 있으며, 세션 객체에서 사용하고 있어야 합니다.

  • hostname_or_ip: 연결할 멀티캐스트 서버 주소입니다. 세션 객체를 생성할 때 사용합니다.

  • port: 연결할 멀티캐스트 서버의 포트입니다. TCP 또는 WebSocket 프로토콜을 사용해야 합니다.

  • encoding: 멀티캐스트 메시지의 인코딩입니다. JSON 또는 Protobuf 를 사용할 수 있습니다.

  • reliability: 세션 객체를 생성하는 경우, 세션 신뢰성 기능을 사용하도록 설정합니다. 그 밖에 옵션은 기본값을 사용합니다.

  • session_opt: 세션 객체를 생성하는 경우 세션 옵션을 설정합니다.

  • transport_opt: 세션 객체를 생성하는 경우 트랜스포트 옵션을 설정합니다.

// FunapiSession 객체 없이 FunapiMulticast 객체를 생성합니다.
// 세션 신뢰성 기능 옵션 이외의 세션과 트랜스포트 옵션은 기본값을 사용합니다.
// 함수 인자를 토대로 내부적으로 FunapiSession 객체를 생성하지만
// 이벤트를 처리하는 콜백함수를 등록하고, connect() 함수를 호춣해서
// 서버에 연결해야 합니다.
// TCP, WebSocket 이외의 프로토콜이 인자로 사용된 경우 nullptr 을 반환합니다.
static std::shared_ptr<FunapiMulticast> Create(
    const char* sender,  // 내 ID(name)
    const char* hostname_or_ip,
    const uint16_t port,
    const FunEncoding encoding,
    const bool reliability,
    const TransportProtocol protocol);

// FunapiSession 객체 없이 FunapiMulticast 객체를 생성합니다.
// FunapiSessionOption 객체와 FunapiTransportOption 객체를 통해서 옵션을
// 함께 생성되는 세션 객체의 옵션을 설정할 수 있습니다.
// 함수 인자를 토대로 내부적으로 FunapiSession 객체를 생성하지만
// 이벤트를 처리하는 콜백함수를 등록하고, connect() 함수를 호춣해서
// 서버에 연결해야 합니다.
// TCP, WebSocket 이외의 프로토콜이 인자로 사용된 경우 nullptr 을 반환합니다.
static std::shared_ptr<FunapiMulticast> Create(
    const char* sender,  // 내 ID(name)
    const char* hostname_or_ip,
    const uint16_t port,
    const FunEncoding encoding,
    const TransportProtocol protocl,
    const std::shared_ptr<FunapiTransportOption> &transport_opt,
    const std::shared_ptr<FunapiSessionOption> &session_opt);

// 이미 서버에 연결되어 있는 FunapiSession 객체를 사용해서 FunapiMulticast
// 객체를 생성합니다.
// 다음과 같은 상황에서 nullptr 을 반환합니다.
// 1. FunapiSession 객체가 멀티캐스트 서버와 연결되어있지 않음.
// 2. FunapiSession 객체에 함수 인자와 일치하는 Protocol 이 없음.
// 3. TCP 또는 WebSocket 이외의 프로토콜이 인자로 사용된 경우.
static std::shared_ptr<FunapiMulticast> Create(
    const char* sender,  // 내 ID(name)
    const std::shared_ptr<FunapiSession> &session,  // FunapiMulticast 가 사용할 세션입니다.
    const TransportProtocol protocol);  // FunapiMulticast 가 사용할 프로토콜

연결 관련 인터페이스

세션 객체 없이 FunapiMulticast 객체를 생성한 경우, 서버에 연결하는 동작을 추가하기 위한 인터페이스들입니다.

// 멀티캐스트 서버에 연결합니다.
void Connect();

// 멀티캐스트 서버에 연결이 되어있는지 확인하는 함수입니다.
// 연결이 되어있다면 true 를 반환합니다.
bool IsConnected() const;

// 멀티캐스트 서버와 연결을 종료합니다.
void Close();

콜백 등록 인터페이스

멀티캐스트 기능을 사용할 때 필요한 콜백 함수들을 등록하기 위한 인터페이스들입니다.

세션 객체 없이 FunapiMulticast 객체를 생성한 경우에는 세션 객체의 콜백 함수도 등록 할 수 있습니다.

// 세션 이벤트가 발생하면 호출되는 콜백 함수를 추가합니다.
// 세션 객체를 따로 생성하는 경우에는 세션 객체의
// AddSessionEventCallback() 함수를 사용해 주세요.
void AddSessionEventCallback(const FunapiMulticast::SessionEventHandler &handler);

// 트랜스포트 이벤트가 발생하면 호출되는 콜백 함수를 추가합니다.
// 세션 객체를 따로 생성하는 경우에는 세션 객체의
// AddTransportEventCallback() 함수를 사용해 주세요.
void AddTransportEventCallback(const FunapiMulticast::TransportEventHandler &handler);

// 채널의 입/퇴장과 관련한 정보를 전달받기 위한 콜백 합수 타입입니다.
// channel_id: 입/퇴장 이벤트가 발생한 채널 이름.
// sender_id: 입/퇴장 주체의 아이디.
typedef std::function<void(const std::shared_ptr<FunapiMulticast>&,
                           const fun::string & channel_id,
                           const fun::string & sender_id)> ChannelNotify;

// 채널 입장을 알려주는 콜백 함수를 추가합니다.
void AddJoinedCallback(const ChannelNotify &handler);

// 채널 퇴장을 알려주는 콜백 함수를 추가합니다.
void AddLeftCallback(const ChannelNotify &handler);

// 채널에 오류가 발생했을 때, 실행되는 콜백 함수 타입입니다.
// error_code: 서버로부터 받은 오류 코드입니다. 자세한 오류 코드 목록은 예제를
// 참고해 주세요.
typedef std::function<void(const std::shared_ptr<FunapiMulticast>&,
                           const int error_code)> ErrorNotify;

// 서버로부터 에러 메시지를 받으면 호출되는 콜백 함수를 추가합니다.
void AddErrorCallback(const ErrorNotify &handler);


// 채널 목록을 받았을 때, 실행되는 콜백 함수 타입입니다.
// 채널 목록은 <채널명, 인원수> 의 맵 자료구조로 전달됩니다.
typedef std::function<void(const std::shared_ptr<FunapiMulticast>&,
                           const fun::map<fun::string, int> &)> ChannelListNotify;

// 서버로부터 채널 목록을 받으면 호출되는 콜백 함수를 추가합니다.
void AddChannelListCallback(const ChannelListNotify &handler);

채널 관련 인터페이스

채널을 사용하기 위한 일반적인 기능을 제공하는 함수들입니다.

// 서버에 전체 채널 목록을 요청합니다.
// 채널 목록과 함께 채널에 있는 유저 수도 함께 전달됩니다.
// 요청에 성공하면 AddChannelListCallback() 로 추가한 콜백 함수가 호출됩니다.
public void RequestChannelList();

// 지정한 채널에 입장한 상태인지 확인하는 함수입니다.
// 입장한 채널이면 true 를 반환합니다.
public bool IsInChannel(string channel_id);

// 지정한 채널에 입장하도록 서버에 요청합니다.
// channel_id: 입장할 채널 이름입니다. 존재하지 않는 채널일 경우 생성하고,
//             입장하는 것이 기본 동작입니다.
// handler: 채널에서 메시지를 받으면 호출되는 콜백 함수입니다.
// token: 채널에 입장할 때 사용하는 인증 token 이며 채널의 token 과 인자로
//        사용된 token 이 일치하지 않다면 채널 입장에 실패합니다.
//        token 인증을 사용하지 않는다면 빈 문자열 혹은 디폴트 인자를
//        사용해 주세요.
// 리턴값: 다음 상황에서는 false 를 반환합니다.
//       * 이미 해당 채널에 입장한 상태일 때
//       * 서버와 연결되어 있지 않을 때
//       * 메시지 인코딩이 다를 때
bool JoinChannel(const fun::string &channel_id,
                 const JsonChannelMessageHandler &handler,
                 const fun::string &token);

bool JoinChannel(const fun::string &channel_id,
                 const ProtobufChannelMessageHandler &handler,
                 const fun::string &token);

// 지정한 채널에서 나갑니다.
// 지정한 채널에 입장한 상태가 아니거나, 서버와 연결되어 있지 않으면 false 를
// 반환합니다.
public bool LeaveChannel(string channel_id);

// 입장해 있는 모든 채널에서 나갈 때 사용하는 함수입니다.
public void LeaveAllChannels();

메시지 전송 인터페이스

채널에 메시지를 보내는 기능을 담당하는 함수들입니다.

// 채널에 메시지를 전송할 때 사용하는 함수입니다.
// JSON 또는 Protobuf 메시지를 보내는 함수를 구분해서 사용해 주세요.
// channel_id: 메시지를 전송할 채널 이름입니다.
// msg: 전송할 Protobuf 메시지 입니다.
// json_string: 전송할 json 메시지입니다.
// bounce: 메시지를 전송한 자신도 메시지를 받는 옵션으로 기본값은 true 입니다.
bool SendToChannel(const fun::string &channel_id,
                   fun::string &json_string,
                   const bool bounce);  // 내가 전송한 메시지를 받는 옵션.

bool SendToChannel(const fun::string &channel_id,
                   FunMessage &msg,
                   const bool bounce);  // 내가 전송한 메시지를 받는 옵션.

멀티캐스팅 예제

FunapiMulticast 객체 생성

가장 먼저 멀티캐스팅 기능을 사용하기 위해 FunapiMulticast 객체를 생성하는 방법을 알아보겠습니다.

FunapiMulticast 객체를 생성하는 인터페이스는 총 3 가지가 있으며 여기서는 대표적인 2 가지 생성 방법에 대해서 설명하겠습니다.

1. FunapiSession 객체의 생성을 FunapiMulticast 에 맡기는 방법

FunapiMulticast 의 내부에서 FunapiSession 을 생성해 사용합니다. 내부에서 생성된 FunapiSessionFunapiMulticast 객체가 소멸할 때 같이 소멸됩니다.

// FunapiMulticast 객체가 FunapiSession 객체를 내부적으로 생성합니다.
multicast_ = fun::FunapiMulticast::Create("my name",  /* 내 ID(name) */
                                          "127.0.0.1",  /* hostname or ip */
                                          port,
                                          encoding,
                                          false,  /* session reliability */
                                          TransportProtocol::kTcp);

if (multicast_ == nullptr)
{
  // multicast 가 지원하는 프로토콜은 TCP, WebSocket 입니다
  // 프로토콜을 확인해 주세요.
}

// AddSessionEventCallback 메써드를 호출해 세션 이벤트를 확인하는 콜백 함수를 등록합니다.
multicast_->AddSessionEventCallback(
    [](const std::shared_ptr<fun::FunapiMulticast>& funapi_multicast,
       const fun::SessionEventType event_type,
       const fun::string &session_id,
       const std::shared_ptr<fun::FunapiError> &error)
    {
      if (event_type == fun::SessionEventType::kOpened)
      {
        // 연결에 성공했습니다. 채널에 접속을 시도합니다.
        // JoinChannel() 함수는 "채널 입장" 목차에서 설명하겠습니다.
        funapi_multicast->JoinChannel("test channel id",
                                      on_multicast_channel_received)
      }
      else if (event_type == fun::SessionEventType::kClosed)
      {
        // 명시적으로 서버가 연결을 끊었습니다.
        // 만약 재연결이 필요한 경우 아래 메써드를 통해 재연결을 시도합니다.
        // funapi_multicast->Connect();
      }
      ...
    }
);

// AddTransportEventCallback 메써드를 호출해 트랜스포트 이벤트를 확인하는 콜백 함수를 등록합니다.
multicast_->AddTransportEventCallback(
    [](const std::shared_ptr<FunapiMulticast> &funapi_multicast,
       const fun::TransportEventType type,
       const std::shared_ptr<fun::FunapiError> &error)
    {
      if (type == fun::TransportEventType::kConnectionFailed)
      {
         UE_LOG(LogTemp, Log, TEXT("Error code : %d, Error message : %s"),
                error->GetErrorCode(), error->GetErrorString().c_str());

         // 연결을 다시 시도합니다.
         funapi_multicast->Connect();
      }
      ...
    }
);

// 멀티캐스트 서버와 연결을 시도합니다.
multicast_->Connect("my name",  /* 내 ID(name) */
                    "127.0.0.1"  /* hostname or ip */);

// FunapiMulticast 객체를 소멸시키는 방법으로 내부에서 생성된 FunapiSession 객체도 소멸됩니다.
// multicast_ = nullptr;

2. 직접 생성한 FunapiSession 객체로 FunapiMulticast 객체를 생성하는 방법.

FunapiMuticast 에 사용하는 FunapiSession 객체를 직접 관리하기 위해 사용됩니다. 이미 생성되어 있던 FunapiSession 객체를 등록해서 사용할 수 있습니다.

session_ = FunapiSession::Create("127.0.0.1",  /* hostname or ip */
                                 false  /* session reliability */);

// AddSessionEventCallback() 메써드를 호출해 세션 이벤트를 확인하는 콜백 함수를 등록합니다.
// 이후 정상적으로 세션이 연결되면 FunapiMulticast 객체를 생성합니다.
session_->AddSessionEventCallback(
    [](const std::shared_ptr<fun::FunapiSession> &session,
       const fun::TransportProtocol transport_protocol,
       const fun::SessionEventType type,
       const fun::string &session_id,
       const std::shared_ptr<FunapiError> &error)
    {
      if (type == fun::SessionEventType::kOpened)
      {
        // 연결된 세션을 이용해 FunapiMulticast 객체를 생성합니다.
        // 이미 연결된 FunapiSession 객체를 활용하기 때문에 "FunapiSession 객체의 생성을 FunapiMulticast 에 맡기는 방법"
        // 예제와 다르게 별도의 connect 과정이 필요하지 않습니다.
        multicast_ = FunapiMulticast::Create("my name",  /* 내 ID(name) */,
                                             session_,
                                             fun::TransportProtocol::kTcp);

        if (multicast_ == nullptr)
        {
          // FunapiMulticast 객체 생성에 실패했습니다.
          // 실패하는 경우는 아래와 같습니다.
          // FunapiSession 객체가 서버에 연결되어있지 않다.
          // FunapiSession 객체에 함수 인자와 일치하는 Protocol 이 없다.
          // Tcp, WebSocket 이외의 프로토콜을 사용.
        }

        // 필요한 콜백 함수 추가 후 채널에 접속을 시도합니다.
        // 콜백 함수는 "콜백 함수 추가" 목차에서 설명하겠습니다.

        // 채널에 접속을 시도합니다.
        // JoinChannel() 함수는 "채널 입장" 목차에서 설명하겠습니다.
        auto on_multicast_channel_received =
            [](const std::shared_ptr<fun::FunapiMulticast> &funapi_multicast,
                const fun::string &channel_id,
                const fun::string &sender_string,  // User ID(name)
                const FunMessage& message)
            {
              // 메시지를 받았습니다.
              // 받은 메시지를 사용하는 방법은 "채널 입장" 목차에서 설명합니다.
            };

        multicast_->JoinChannel("test channel id",
                                on_multicast_channel_received)
      }
      if(event_type == fun::SessionEventType::kClosed)
      {
        // 명시적으로 서버가 연결을 끊었습니다.
        // 만약 재연결이 필요한 경우 아래 메써드를 통해 재연결을 시도합니다.
        // session->Connect(fun::TransportProtocol::kTcp, port, fun::FunEncoding::kJson);
      }
      ...
    }
);

// AddTransportEventCallback() 메써드를 호출해 트랜스포트 이벤트를 확인하는 콜백 함수를 등록합니다.
session_->AddTransportEventCallback(
    [](const std::shared_ptr<fun::FunapiSession> &session,
       const fun::TransportProtocol transport_protocol,
       const fun::TransportEventType type,
       const std::shared_ptr<fun::FunapiError> &error)
    {
      if (type == fun::TransportEventType::kConnectionFailed)
      {
         UE_LOG(LogTemp, Log, TEXT("Error code : %d, Error message : %s"),
                error->GetErrorCode(), error->GetErrorString().c_str());

        // 연결을 다시 시도합니다.
        // session_->Connect(...)
      }
      ...
    }
);

// 멀티캐스트 서버에 연결합니다.
session_->Connect(fun::TransportProtocol::kTcp, port, fun::FunEncoding::kJson);

콜백 함수 추가

FunapiMulticast 에는 부가적인 콜백 함수들이 있으며 이를 통해 채널 입/퇴장 정보, 채널의 목록과 구성원 수, 에러 정보를 받을 수 있습니다.

이 예제에서는 위에 나열된 콜백을 모두 추가하겠습니다.

1. 입장 콜백 함수 추가

유저가 채널에 입장하면 호출되는 콜백 함수로 나를 포함해서 채널에 입장하는 모든 유저를 알 수 있습니다.

// 유저가 채널에 입장하면 호출되는 콜백 함수를 추가합니다.
multicast_->AddJoinedCallback(
    [](const std::shared_ptr<fun::FunapiMulticast>& funapi_multicast,
       const fun::string &channel_id, const fun::string &multicast_sender /* User ID(name) */)
    {
      // 채널에 입장한 유저 를 알 수 있습니다.
      UE_LOG(LogTemp, Log, TEXT("Channel ID : %s, Joined user ID : %s"),
             *FString(channel_id.c_str()), *FString(multicast_sender.c_str()));
    }
);

2. 퇴장 콜백 함수 추가

유저가 채널에서 퇴장하면 호출되는 콜백 함수를 통해 나를 포함해서 채널에서 나가는 모든 유저를 알 수 있습니다.

// 유저가 채널에서 퇴장하면 호출되는 콜백 함수를 추가합니다.
multicast_->AddLeftCallback(
    [](const std::shared_ptr<fun::FunapiMulticast>& funapi_multicast,
       const fun::string &channel_id,
       const fun::string &multicast_sender /* User ID(name) */)
    {
      // 채널에서 퇴장한 유저 를 알 수 있습니다.
      UE_LOG(LogTemp, Log, TEXT("Channel ID : %s, Left user ID : %s"),
             *FString(channel_id.c_str()), *FString(multicast_sender.c_str()));
    }
);

3. 채널 목록 콜백 함수 추가

채널 목록 요청 함수 RequestChannelList() 에 대한 응답을 수신하면 불리우는 콜백 함수로 서버에 있는 채널 목록과 채널에 접속한 유저의 수를 알 수 있습니다.

// 서버로 부터 채널 목록을 받았을 때 호출되는 콜백 함수를 추가합니다.
// 채널 목록을 요청하려면 RequestChannelList() 함수를 호출해 주세요.
multicast_->AddChannelListCallback(
    [](const std::shared_ptr<FunapiMulticast>& funapi_multicast,
       const fun::map<fun::string, int> &channels)
    {
      for (auto channel : channels)
      {
        // 채널 이름과 해당 채널에 접속한 유저 수를 알 수 있습니다.
        UE_LOG(LogTemp, Log, TEXT("Channel ID : %s, Number of users on channel : %d"),
               *FString(channel.first.c_str()), channel.second);
      }

      // 만약 접속을 원하는 채널이 있거나 새 채널을 만드시려면 JoinChannel() 메써드를
      // 사용해 주세요.
      // JoinChannel() 함수는 "채널 입장" 목차에서 설명하겠습니다.
      funapi_multicast->JoinChannel("test channel id",
                                    on_multicast_channel_received)
    }
);

4. 에러 콜백 함수 추가

서버가 멀티캐스트 기능과 관련한 요청을 처리하는 중에 오류가 발생하면 응답 메시지에 오류코드를 함께 돌려주고, 에러 콜백 함수를 통해서 오류 코드를 전달 받을 수 있습니다.

// 에러가 발생했을 때 알림을 받는 콜백 함수를 추가합니다.
// 에러 종류는 multicast_message.pb.h 파일의 FunMulticastMessage_ErrorCode 를 참고해주세요.
multicast_.AddErrorCallback(
    [](const std::shared_ptr<fun::FunapiMulticast>& funapi_multicast,
       int error_code)
    {
      // enum FunMulticastMessage_ErrorCode {
      //   FunMulticastMessage_ErrorCode_EC_ALREADY_JOINED = 1,
      //   FunMulticastMessage_ErrorCode_EC_ALREADY_LEFT = 2,
      //   FunMulticastMessage_ErrorCode_EC_FULL_MEMBER = 3,
      //   FunMulticastMessage_ErrorCode_EC_CLOSED = 4,
      //   FunMulticastMessage_ErrorCode_EC_INVALID_TOKEN = 5,
      //   FunMulticastMessage_ErrorCode_EC_CANNOT_CREATE_CHANNEL = 6
      // }

      if (error_code == 1)
      {
        // 이미 채널에 입장 했습니다.
      }
      else if (error_code == 2)
      {
        // 이미 채널에서 퇴장 했습니다.
        // 다시 채널에 접속하려면 JoinChannel() 메써드를 사용해주세요.
        // JoinChannel() 함수는 "채널 입장" 목차에서 설명하겠습니다.
        funapi_multicast->JoinChannel("test channel id",
                                      on_multicast_channel_received)
      }
    }
);

채널 입장

채널에 입장하기 위해서 JoinChannel() 함수를 호출합니다. 존재하지 않는 채널에 입장하려고 시도하면 서버는 채널을 새로 생성하고 입장시킵니다.

Tip

만약, 존재하지 않는 채널을 임의로 생성하는 것을 막고 싶다면, 없는 채널의 입장 방지 를 참고 해 주세요.

해당 채널로부터 메시지가 전송되면 JoinChannel() 함수를 호출할 때 등록했던 콜백 함수가 호출됩니다.

동시에 여러 개의 채널에 입장하려면 입장을 원하는 채널마다 JoinChannel() 함수를 호출하면 됩니다.

이 예제에서는 채팅에 특화된 멀티캐스트 서버를 구축한 다음 아이펀 엔진에 미리 정의되어 있는 FunChatMessage 를 사용해서 메시지를 받는다고 가정하겠습니다.

FunChatMessage 클래스는 multicast_message.pb.h 에 정의되어 있으며 추가적인 .proto 파일 없이 사용 가능합니다.

// 입장할 채널 이름을 입력합니다.
// 채널이 존재하지 않으면 서버에 새 채널이 생성됩니다.
string channel_id = "protobuf_channel";

// 채널에서 전송된 메시지를 받을 콜백 함수입니다.
// 이 예제에서는 채팅에 특화된 멀티캐스팅 서버를 구축했고
// FunChatMessage 를 사용해 메시지를 받는다고 가정합니다.
auto on_multicast_channel_received =
   [](const std::shared_ptr<fun::FunapiMulticast> &funapi_multicast,
      const fun::string &channel_id,
      const fun::string &sender_string,  // User ID(name)
      const FunMessage& message)
   {
     if (message.HasExtension(multicast))
     {
       FunMulticastMessage mcast_msg = message.GetExtension(multicast);
       if (mcast_msg.HasExtension(chat))
       {
         FunChatMessage chat_msg = mcast_msg.GetExtension(chat);
         // 채팅 메시지를 가져옵니다.
         fun::string text = chat_msg.text();
         // 여기서는 간단하게 로그만 출력하겠습니다.
       }
     }
   };

// 채널에 입장하기 위해 JoinChannel() 메써드를 호출합니다.
// 해당 채널로부터 메시지가 전송되면 on_multicast_channel_received 콜백 함수가 호출됩니다.
multicast.JoinChannel(channel_id, on_multicast_channel_received);

메시지 전송

메시지를 전송하려면 SendToChannel() 함수를 호출합니다.

channel_id 에 해당하는 채널에 접속한 모든 유저에게 메시지를 전송할 수 있습니다.

// channel 이름을 입력합니다.
string channel_id = "test_channel";

if (multicast_->GetEncoding() == fun::FunEncoding.kJson)
{
  fun::string temp_messsage = "multicast test message";

  TSharedRef<FJsonObject> json_object = MakeShareable(new FJsonObject);
  json_object->SetStringField(FString("message"), FString(temp_messsage.c_str()));

  // Convert JSON document to fun::string
  FString ouput_fstring;
  TSharedRef<TJsonWriter<TCHAR>> writer = TJsonWriterFactory<TCHAR>::Create(&ouput_fstring);
  FJsonSerializer::Serialize(json_object, writer);
  fun::string json_string = TCHAR_TO_ANSI(*ouput_fstring);

  // 채널에 메시지를 전송합니다.
  multicast_->SendToChannel(channel_id, json_string);

  // 만약 내가 보낸 메시지를 받기 싫다면 아래와 같이 사용합니다.
  // multicast_->SendToChannel(channel_id, json_string, false /* bounce */);
}
else
{
  FunMessage msg;
  FunMulticastMessage* mcast_msg = msg.MutableExtension(multicast);
  FunChatMessage *chat_msg = mcast_msg->MutableExtension(chat);
  chat_msg->set_text("multicast test message");

  // 채널에 메시지를 전송합니다.
  multicast_->SendToChannel(channel_id, msg);

  // 만약 내가 보낸 메시지를 받기 싫다면 아래와 같이 사용합니다.
  // multicast_->SendToChannel(channel_id, msg, false /* bounce */);
}

채널 퇴장

채널에서 나갈 때는 LeaveChannel() 함수를 호출합니다.

// 채널 나가기
string channel_id = "test_channel";
multicast_->LeaveChannel(channel_id);

공지사항 확인하기

아이펀 엔진은 운영 편의성을 위해서 공지사항 서비스 를 엔진 패키지와 별도로 제공하고 있습니다.

클라이언트 플러그인의 FunapiAnnouncement 클래스는 공지사항 서비스로부터 공지 내용을 받아서 처리하는 기능을 제공하며 다음과 같은 기능들을 제공합니다.

  • 공지사항과 첨부 파일을 다운로드하는 기능

  • 공지사항을 페이지 단위로 받는 기능

  • 공지사항을 카테고리 별로 받는 기능

FunapiAnnouncement 클래스의 주요 인터페이스는 아래와 같습니다.

class FUNAPI_API FunapiAnnouncement :
    public std::enable_shared_from_this<FunapiAnnouncement>
{
  ...
  static std::shared_ptr<FunapiAnnouncement> Create(
      const fun::string &url,  // 공지사항 서버의 IP 와 Port 번호를 주소로 초기화 합니다. ex) "http://127.0.0.1:8080"
      const fun::string &path); // 다운로드 경로.

  // 다운로드가 끝나면 호출되는 콜백함수를 등록 합니다.
  void AddCompletionCallback(const CompletionHandler &handler);

  // 공지사항 목록을 요청합니다. 카테고리와 페이지를 지정할 수 있습니다.
  void RequestList(int max_count, int page = 0, const fun::string& category = "");
  ...
}

주요 매개 변수애한 설명은 다음과 같습니다.

  • url: 공지사항 서비스 호스트의 IP 와 Port 번호를 HTTP 도메인 형태로 설정합니다. ex) http://127.0.0.1:8000

  • path: 첨부파일을 클라이언트 쪽에 저장할 경로입니다.

  • max_count: 공지사항 목록을 페이지 단위로 나눠서 받을 때, 페이지 당 공지사항 수입니다. 생략하면 서버에서 기본값으로 5 를 적용합니다.

  • page: 공지사항 목록을 페이지 단위로 나눠서 받을 때, 받을 페이지 순서입니다. 기본값 첫번째 페이지(0) 입니다.

  • category: 공지사항을 등록할 때 설정한 카테고리 별로 공지사항들을 가져올 수 있습니다. 기본값은 카테고리 구분없이 모두 가져옵니다.

아래는 FunapiAnnouncement 클래스를 사용해서 서버에 공지사항을 요청하고 콜백 함수에서 공지사항 목록을 파싱하는 샘플 코드입니다. 메시지는 타입은 JSON 입니다.

  fun::stringstream ss_url;
  ss_url << "http://" << "127.0.0.1" << ":" << "8080";

  std::shared_ptr<fun::FunapiAnnouncement> announcement_ =
      fun::FunapiAnnouncement::Create(ss_url.str(),
                                      TCHAR_TO_UTF8(*(FPaths::ProjectSavedDir())));

  announcement_->AddCompletionCallback(
      [](const std::shared_ptr<fun::FunapiAnnouncement> &announcement,
         const fun::vector<std::shared_ptr<fun::FunapiAnnouncementInfo>> &info,
         const fun::FunapiAnnouncement::ResultCode result)
      {
        if (result == fun::FunapiAnnouncement::ResultCode::kSucceed)
        {
          for (auto &i : info)
          {
            fun::stringstream ss;
            ss << "FunapiAnnounce reponse : " << "data=" << i->GetDate() << " ";
            ss << "message=" << i->GetMessageText() << " ";
            ss << "subject=" << i->GetSubject() << " ";
            ss << "file_path=" << i->GetFilePath() << " ";
            ss << "kind=" << i->GetKind() << " ";
            ss << "extra_image_path={";

            auto extra_image_infos = i->GetExtraImageInfos();
            for (auto &extra_info : extra_image_infos)
            {
              ss << extra_info->GetFilePath() << " ";
            }

            ss << "}";
            UE_LOG(LogFunapiExample, Log, TEXT("%s"), *FString(UTF8_TO_TCHAR(ss.str().c_str())));
          }
        }
        else
        {
          if (result == fun::FunapiAnnouncement::ResultCode::kInvalidUrl)
          {
            // 서버의 주소가 올바른지 확인해주세요.
          }

          if (result == fun::FunapiAnnouncement::ResultCode::kListIsNullOrEmpty)
          {
            // 공지사항 목록이 존재하지 않습니다.
            // 공지사항 서버를 확인해 주세요.
          }

          if (result == fun::FunapiAnnouncement::ResultCode::kExceptionError)
          {
            // 다운로드 경로가 없거나 파일쓰기가 불가능한 경로 입니다.
          }
        }
      }
  );
}

announcement_->RequestList(3, 0);

Note

공지사항의 본문에 해당하는 문자열은 순수 텍스트가 될수도 있고, XML 또는 JSON 같은 포맷 문자열이 될 수도 있습니다.

JSON 같은 포맷을 사용하면 공지사항에 다양한 정보를 함께 제공할 수 있습니다.

공지사항 Update

FunapiAnnouncement 객체는 세션 객체와 마찬가지로 언리얼 엔진의 GameThread 에서 Update() 함수를 호출해야 합니다.

아래 코드는 FunapiAnnouncement 객체를 멤버변수로 소유하고있는 MyActor 클래스에서 Update() 함수를 호출해주는 코드입니다.

void AMyActor::Tick(float DeltaTime)
{
  if(announcement_ != nullptr)
  {
    announcement_->Update();
  }

  // 모든 FunapiAnnouncement 객체를 업데이트 하는 방법입니다.
  // fun::FunapiAnnouncement::UpdateAll();
}

Note

별도 쓰레드로 FunapiAnnouncement 객체의 Update() 함수 호출이 가능하지만 콜백함수에서 UObject 의 생성, 변경, 삭제가 불가능합니다.

서버 점검 메시지

아이펀 엔진 서버가 점검 모드 로 설정돼 있을 때 클라이언트가 메시지를 보내면 서버 쪽 메시지 핸들러는 무시되고 점검 안내 메시지로 응답합니다.

Note

만약, 클라이언트가 어떤 메시지도 보내지 않는다면 서버도 응답하지 않습니다.

점검 안내 메시지 처리는 메시지 수신 콜백으로 전달되며 메시지는 아래와 같은 내용을 담고 있습니다.

date_start

string

서버 점검 시작 일시

date_end

string

서버 점검 종료 일시

messages

string

메시지

session_->AddJsonRecvCallback(
    [](const std::shared_ptr<fun::FunapiSession> &session,
       const fun::TransportProtocol transport_protocol,
       const fun::string &msg_type,
       const fun::string &json_string)
    {
      if (msg_type.compare("_maintenance") == 0)
      {
        UE_LOG(LogFunapiExample, Log, TEXT("Maintenance message : %s"),
               *FString(json_string.c_str()));
      }
    }
);

session_->AddProtobufRecvCallback(
    [](const std::shared_ptr<fun::FunapiSession> &session,
       const fun::TransportProtocol transport_protocol,
       const FunMessage &fun_message)
    {
      if (fun_message.msgtype().compare("_maintenance") == 0)
      {
        if (fun_message.HasExtension(pbuf_maintenance))
        {
          MaintenanceMessage maintenance = fun_message.GetExtension(pbuf_maintenance);
          fun::string date_start = maintenance.date_start();
          fun::string date_end = maintenance.date_end();
          fun::string message_text = maintenance.messages();
          UE_LOG(LogFunapiExample, Log, TEXT("Maintenance message:\nstart: %s\nend: %s\nmessage: %s"),
              *FString(date_start.c_str()), *FString(date_end.c_str()), *FString(message_text.c_str()));
        }
      }
    }
);

클라이언트 리소스 파일 다운로드

아이펀 엔진은 클라이언트가 로드할 수 있는 파일의 업데이트 편의성을 제공하기 위해 클라이언트 리소스 서비스 를 제공하고 있습니다.

클라이언트 쪽에서는 FunapiHttpDownloader 클래스를 사용해서 서버가 제공하는 클라이언트 리소스 서비스를 사용할 수 있습니다.

FunapiHttpDownloader 클래스

FunapiHttpDownloader 클래스가 제공하는 함수들에 대해서 설명합니다.

static std::shared_ptr<FunapiHttpDownloader> Create(
    const fun::string &url,  //  다운로드 서버의 ip와 port 번호를 주소로 초기화 합니다. ex) "http://127.0.0.1:8080"
    const fun::string &path); // 다운로드 경로.

// 리소스 목록을 받고 파일 유효성 검사를 마친 후 다운로드 준비가 완료됬을 때
// 호출되는 콜백을 등록하는 함수입니다.
void AddReadyCallback(const ReadyHandler &handler);

// 다운로드 중인 파일의 진행상황을 알려줍니다.
// UI 업데이트용으로 사용하시면 좋습니다.
void AddProgressCallback(const ProgressHandler &handler);

// 다운로드가 완료되면 호출되는 콜백을 등록하는 함수입니다.
void AddCompletionCallback(const CompletionHandler &handler);

// 각 파일에 대한 timeout 을 설정합니다.
// 기본값 : 30 초.
void SetTimeoutPerFile(long timeout_in_seconds);

// 서버 인증에 사용되는 CACertificate 경로를 설정합니다.
void SetCACertFilePath(const fun::string &path);

// 다운로드를 시작합니다.
void Start();

// 아이펀 엔진 서버의 client_data 폴더 내에 특정 폴더만 다운로드 합니다.
void Start(const fun::string &inclusive_path);

Start() 함수를 호출해 다운로드를 시작하면 가장 먼저 서버에 다운로드할 파일들의 목록을 받고 이전에 받은 파일과의 유효성을 검증하고 새로 받아야 할 파일이 무엇인지 확인합니다.

파일 확인 작업이 끝나면 AddReadyCallback() 함수로 등록한 콜백이 호출됩니다. 등록한 콜백함수로 받아야할 총 파일의 개수와 데이터 크기를 확인할 수 있고 새로 다운 받을 파일이 없다면 등록한 콜백함수는 호출되지 않고 AddCompletionCallback() 에 등록한 콜백 함수만 호출됩니다.

다운로드 도중의 진행상황을 확인하고 싶다면 AddProgressCallback() 으로 콜백 함수를 등록하면 현재 다운로드 중인 파일의 정보를 주기적으로 알려줍니다. 해당 콜백으로 전달되는 정보는 다운로드 받을 파일들의 정보, 현재 다운로드 중인 파일 index, 다운로드 받을 파일의 수, 다운로드한 데이터 크기, 현재 다운받을 파일의 총 크기 정보입니다.

다운로드가 완료되거나 실패했을 경우 AddCompletionCallback() 에 등록한 콜백 함수가 호출됩니다.

FunapiHttpDownloader 예제

아래는 FunapiHttpDownloader 를 생성하고 콜백 함수들을 등록하는 예제 코드입니다.

fun::stringstream ss_download_url;
ss_download_url << "http://" << "127.0.0.1" << ":" << "8020";

std::shared_ptr<fun::FunapiHttpDownloader> downloader_ =
    fun::FunapiHttpDownloader::Create(ss_download_url.str(),
                                      TCHAR_TO_UTF8(*(FPaths::ProjectSavedDir())));

downloader_->AddReadyCallback(
    [](const std::shared_ptr<fun::FunapiHttpDownloader>&downloader,
       const fun::vector<std::shared_ptr<fun::FunapiDownloadFileInfo>>&info)
    {
      for (auto i : info)
      {
        fun::stringstream ss_temp;
        ss_temp << i->GetUrl() << std::endl;
        UE_LOG(LogFunapiExample, Log, TEXT("%s"), *FString(ss_temp.str().c_str()));
      }
    }
);

downloader_->AddProgressCallback(
    [](const std::shared_ptr<fun::FunapiHttpDownloader> &downloader,
       const fun::vector<std::shared_ptr<fun::FunapiDownloadFileInfo>>&info,
       const int index,
       const int max_index,
       const uint64_t received_bytes,
       const uint64_t expected_bytes)
    {
      auto i = info[index];

      fun::stringstream ss_temp;
      ss_temp << index << "/" << max_index << " " << received_bytes << "/" << expected_bytes << " ";
      ss_temp << i->GetUrl() << std::endl;
      UE_LOG(LogFunapiExample, Log, TEXT("%s"), *FString(ss_temp.str().c_str()));
    }
);

downloader_->AddCompletionCallback(
    [](const std::shared_ptr<fun::FunapiHttpDownloader>&downloader,
       const fun::vector<std::shared_ptr<fun::FunapiDownloadFileInfo>>&info,
       const fun::FunapiHttpDownloader::ResultCode result_code)
    {
      if (result_code == fun::FunapiHttpDownloader::ResultCode::kSucceed)
      {
          for (auto i : info)
          {
              UE_LOG(LogFunapiExample, Log, TEXT("file_path=%s"), *FString(i->GetPath().c_str()));
          }
      }

      if (result_code == fun::FunapiHttpDownloader::ResultCode::kFailed)
      {
        // 잘못된 url 입니다.
      }
    }
);

// 각 파일에 대한 timeout 시간을 지정합니다.
// 기본값 : 30초
downloader_->SetTimeoutPerFile(5);
downloader_->Start();
// 특정 폴더만 다운로드 하는 방법입니다.
// downloader_->Start("images");

FunapiHttpDownloader Update

FunapiHttpDownloader 객체는 세션 객체와 마찬가지로 언리얼 엔진의 GameThread 에서 Update() 함수를 호출해야 합니다.

아래 코드는 FunapiAnnouncement 객체를 멤버변수로 소유하고있는 MyActor 클래스에서 Update() 함수를 호출해주는 코드입니다.

void AMyActor::Tick(float DeltaTime)
{
  if(downloader_ != nullptr)
  {
    downloader_->Update();
  }

  // 모든 FunapiHttpDownloader 객체를 업데이트 하는 방법입니다.
  // fun::FunapiHttpDownloader::UpdateAll();
}

Note

별도 쓰레드로 FunapiHttpDownloader 객체의 Update() 함수 호출이 가능하지만 콜백함수에서 UObject 의 생성, 변경, 삭제가 불가능합니다.