코코스(Cocos2d-x) 플러그인

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

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

  • 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

3.1x

o

o

o

o

4.x

o

o

o

o

  • Android 의 경우 arm 만 지원합니다.

시작하기

코코스 command line tool 로 생성한 프로젝트에 코코스 플러그인을 적용 하는 방법을 설명합니다. 이후 코코스 command line tool 로 생성한 C++ 프로젝트는 새 프로젝트 로 부르겠습니다.

새 프로젝트에 코코스 플러그인을 적용하기 위해 여기 에서 예제 프로젝트를 다운로드 받습니다.

Note

코코스 플러그인은 여기 에서 예제 프로젝트에 포함시켜 배포하고 있습니다.

다운 받은 예제 프로젝트의 <Project Root> 폴더를 보면 아래와 같은 폴더가 있습니다.

|- Classes/funapi
|- proto
|- proj.win32/libsodium-1.0.10
|- proj.win32/protobuf-2.6.1
|- proj.win32/zstd-1.3.3
|- proj.ios_mac/mac/libprotobuf
|- proj.ios_mac/mac/libsodium
|- proj.ios_mac/mac/libzstd
|- proj.ios_mac/ios/libprotobuf
|- proj.ios_mac/ios/libsodium
|- proj.ios_mac/ios/libzstd
|- proj.android/app/jni/libprotobuf
|- proj.android/app/jni/libsodium
|- proj.android/app/jni/libzstd
  • Classes/funapi : 아이펀 엔진 서버와 연동에 사용되는 코코스 플러그인 소스코드.

  • proto : Protobuf 파일이 담겨있는 디렉토리.

  • proj.win32/protobuf-2.6.1 : Windows 에서 사용되는 Protobuf 프로젝트

  • proj.win32/libsodium-1.0.10 : Windows 에서 사용되는sodium 프로젝트

  • proj.win32/zstd-1.3.3 : Windows 에서 사용되는 zstd 프로젝트

  • <Target platform>/libprotobuf : Protobuf 라이브러리

  • <Target platform>/libsodium : sodium 라이브러리

  • <Target platform>/libzstd : zstd 라이브러리

위 폴더들을 새 프로젝트의 동일한 위치로 복사해 줍니다.

4.x 버전 프로젝트에 코코스 플러그인 적용하기

4.x 버전에서는 CMake 를 통해 새 프로젝트에 코코스 플러그인을 적용합니다.

새 프로젝트의 <Project Root> 디렉토리를 보시면 CMakeList.txt 파일이 있습니다. CMakeList.txt 에 다음의 내용을 추가해 코코스 플러그인 소스코드를 프로젝트에 포함시킵니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
set(GAME_RES_FOLDER
    "${CMAKE_CURRENT_SOURCE_DIR}/Resources"
    )
if(APPLE OR WINDOWS)
    cocos_mark_multi_resources(common_res_files RES_TO "Resources" FOLDERS ${GAME_RES_FOLDER})
endif()

# add cross-platforms source files and header files
list(APPEND GAME_SOURCE
     Classes/AppDelegate.cpp
     Classes/HelloWorldScene.cpp
     Classes/funapi/funapi_announcement.cpp
     Classes/funapi/funapi_compression.cpp
     Classes/funapi/funapi_downloader.cpp
     Classes/funapi/funapi_encryption.cpp
     Classes/funapi/funapi_http.cpp
     Classes/funapi/funapi_multicasting.cpp
     Classes/funapi/funapi_option.cpp
     Classes/funapi/funapi_rpc.cpp
     Classes/funapi/funapi_send_flag_manager.cpp
     Classes/funapi/funapi_session.cpp
     Classes/funapi/funapi_socket.cpp
     Classes/funapi/funapi_tasks.cpp
     Classes/funapi/funapi_utils.cpp
     Classes/funapi/funapi_websocket.cpp
     Classes/funapi/distribution/fun_dedicated_server_rpc_message.pb.cc
     Classes/funapi/management/maintenance_message.pb.cc
     Classes/funapi/network/fun_message.pb.cc
     Classes/funapi/network/ping_message.pb.cc
     Classes/funapi/service/multicast_message.pb.cc
     Classes/funapi/service/redirect_message.pb.cc
    )

list(APPEND GAME_HEADER
     Classes/AppDelegate.h
     Classes/HelloWorldScene.h
     Classes/funapi/funapi_announcement.h
     Classes/funapi/funapi_build_config.h
     Classes/funapi/funapi_compression.h
     Classes/funapi/funapi_downloader.h
     Classes/funapi/funapi_encryption.h
     Classes/funapi/funapi_encryption_legacy.h
     Classes/funapi/funapi_http.h
     Classes/funapi/funapi_multicasting.h
     Classes/funapi/funapi_option.h
     Classes/funapi/funapi_plugin.h
     Classes/funapi/funapi_rpc.h
     Classes/funapi/funapi_send_flag_manager.h
     Classes/funapi/funapi_session.h
     Classes/funapi/funapi_socket.h
     Classes/funapi/funapi_std_allocator.h
     Classes/funapi/funapi_tasks.h
     Classes/funapi/funapi_utils.h
     Classes/funapi/funapi_version.h
     Classes/funapi/funapi_websocket.h
     Classes/funapi/distribution/fun_dedicated_server_rpc_message.pb.h
     Classes/funapi/management/maintenance_message.pb.h
     Classes/funapi/network/fun_message.pb.h
     Classes/funapi/network/ping_message.pb.h
     Classes/funapi/service/multicast_message.pb.h
     Classes/funapi/service/redirect_message.pb.h
     )

코코스 플러그인에서 사용하는 외부 라이브러리도 새 프로젝트에 포함시켜 줍니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# mark app resources
setup_cocos_app_config(${APP_NAME})
if(APPLE)
    set_target_properties(${APP_NAME} PROPERTIES RESOURCE "${APP_UI_RES}")
    set_xcode_property(${APP_NAME} INSTALL_PATH "\$(LOCAL_APPS_DIR)")

    if(MACOSX)
        set_target_properties(${APP_NAME} PROPERTIES MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/proj.ios_mac/mac/Info.plist")
    elseif(IOS)
        set_target_properties(${APP_NAME} PROPERTIES MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/proj.ios_mac/ios/Info.plist")
        set_xcode_property(${APP_NAME} ASSETCATALOG_COMPILER_APPICON_NAME "AppIcon")
    endif()

# For code-signing, set the DEVELOPMENT_TEAM:
#set_xcode_property(${APP_NAME} DEVELOPMENT_TEAM "GRLXXXX2K9")
elseif(WINDOWS)
    cocos_copy_target_dll(${APP_NAME})
endif()

if(LINUX OR WINDOWS)
    cocos_get_resource_path(APP_RES_DIR ${APP_NAME})
    cocos_copy_target_res(${APP_NAME} LINK_TO ${APP_RES_DIR} FOLDERS ${GAME_RES_FOLDER})
endif()

if (WINDOWS)
  include_external_msproject(libprotobuf "${CMAKE_CURRENT_SOURCE_DIR}/proj.win32/protobuf-2.6.1/vsprojects/libprotobuf.vcxproj")
  include_external_msproject(libzstd "${CMAKE_CURRENT_SOURCE_DIR}/proj.win32/zstd-1.3.3/build/VS2017/libzstd/libzstd.vcxproj")
  include_external_msproject(libsodium "${CMAKE_CURRENT_SOURCE_DIR}/proj.win32/libsodium-1.0.10/libsodium.vcxproj")

  add_dependencies(${APP_NAME} libprotobuf)
  add_dependencies(${APP_NAME} libzstd)
  add_dependencies(${APP_NAME} libsodium)

  target_include_directories(${APP_NAME}
      PRIVATE  "${CMAKE_CURRENT_SOURCE_DIR}/proj.win32/protobuf-2.6.1/vsprojects/include/"
      PRIVATE  "${CMAKE_CURRENT_SOURCE_DIR}/proj.win32/zstd-1.3.3/lib/"
      PRIVATE  "${CMAKE_CURRENT_SOURCE_DIR}/proj.win32/libsodium-1.0.10/src/libsodium/include/"
  )

  target_link_libraries(${APP_NAME}
      debug "${CMAKE_CURRENT_SOURCE_DIR}/proj.win32/protobuf-2.6.1/vsprojects/Debug/libprotobuf.lib"
      debug "${CMAKE_CURRENT_SOURCE_DIR}/proj.win32/zstd-1.3.3/build/VS2017/bin/Win32_Debug/libzstd_static.lib"
      debug "${CMAKE_CURRENT_BINARY_DIR}/Debug.win32/libsodium.lib"
      optimized "${CMAKE_CURRENT_SOURCE_DIR}/proj.win32/protobuf-2.6.1/vsprojects/Release/libprotobuf.lib"
      optimized "${CMAKE_CURRENT_SOURCE_DIR}/proj.win32/zstd-1.3.3/build/VS2017/bin/Win32_Release/libzstd_static.lib"
      optimized "${CMAKE_CURRENT_BINARY_DIR}/Release.win32/libsodium.lib"
  )
elseif(APPLE)
   if(MACOSX)
      target_include_directories(${APP_NAME}
          PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/proj.ios_mac/mac/libprotobuf/include/"
          PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/proj.ios_mac/mac/libsodium/include/"
          PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/proj.ios_mac/mac/libzstd/include/"
      )

      target_link_libraries(${APP_NAME}
          "${CMAKE_CURRENT_SOURCE_DIR}/proj.ios_mac/mac/libprotobuf/lib/libprotobuf.a"
          "${CMAKE_CURRENT_SOURCE_DIR}/proj.ios_mac/mac/libsodium/lib/libsodium.a"
          "${CMAKE_CURRENT_SOURCE_DIR}/proj.ios_mac/mac/libzstd/lib/libzstd.a"
      )
  elseif(IOS)
      target_include_directories(${APP_NAME}
          PRIVATE  "${CMAKE_CURRENT_SOURCE_DIR}/proj.ios_mac/ios/libprotobuf/include/"
          PRIVATE  "${CMAKE_CURRENT_SOURCE_DIR}/proj.ios_mac/ios/libsodium/include/"
          PRIVATE  "${CMAKE_CURRENT_SOURCE_DIR}/proj.ios_mac/ios/libzstd/include/"
      )

      target_link_libraries(${APP_NAME}
          "${CMAKE_CURRENT_SOURCE_DIR}/proj.ios_mac/ios/libprotobuf/lib/libprotobuf.a"
          "${CMAKE_CURRENT_SOURCE_DIR}/proj.ios_mac/ios/libsodium/lib/libsodium.a"
          "${CMAKE_CURRENT_SOURCE_DIR}/proj.ios_mac/ios/libzstd/lib/libzstd.a"
      )
  endif()
elseif(ANDROID)
  target_include_directories(${APP_NAME}
      PRIVATE  "${CMAKE_CURRENT_SOURCE_DIR}/proj.android/app/jni/libprotobuf/include/ARMv7/"
      PRIVATE  "${CMAKE_CURRENT_SOURCE_DIR}/proj.android/app/jni/libsodium/include/"
      PRIVATE  "${CMAKE_CURRENT_SOURCE_DIR}/proj.android/app/jni/libzstd/include/ARMv7/"
  )

  target_link_libraries(${APP_NAME}
      "${CMAKE_CURRENT_SOURCE_DIR}/proj.android/app/jni/libprotobuf/lib/ARMv7/libprotobuf.a"
      "${CMAKE_CURRENT_SOURCE_DIR}/proj.android/app/jni/libsodium/lib/libsodium.a"
      "${CMAKE_CURRENT_SOURCE_DIR}/proj.android/app/jni/libzstd/lib/ARMv7/libzstd.a"
  )

  add_library(curl STATIC IMPORTED GLOBAL)
  set_target_properties(curl
      PROPERTIES IMPORTED_LOCATION "${COCOS2DX_ROOT_PATH}/external/curl/prebuilt/android/armeabi-v7a/libcurl.a"
  )
  target_link_libraries(${APP_NAME} curl)
endif()

Note

코코스 플러그인은 c++_static 이름의 안드로이드 C++ 라이브러리를 지원하지 않기 때문에 안드로이드 플랫폼 개발시 proj.android/build.gradle 파일의 -DANDROID_STL=c++_static-DANDROID_STL=gnustl_shared 로 수정해주세요.

이후 cmd 를 통해 개발 하고자하는 플랫폼에 따라 워크스페이스를 생성해 주시면 됩니다. 아래 코드는 Windows 플랫폼의 Visual studio 워크스페이스를 생성하는 예입니다.

cd <Project root>
mkdir win32-build && cd win32-build
cmake .. -G"Visual Studio 15 2017" -Tv141

플랫폼별 자세한 워크스페이스 생성방법은 Cocos2d CMake guid 를 참고해 주세요.

3.1x 버전 프로젝트에 코코스 플러그인 적용하기

3.1x 버전은 4.x 버전과 다르게 프로젝트 생성단계에서 플랫폼별 워크스페이스를 생성해줍니다. 이 생성된 워크스페이스에 코코스 플러그인을 적용하는 방법을 설명하겠습니다.

Windows 설정

<Project root>/proj.win32/<Project Name>.sln 파일을 Visual Studio 프로그램으로 열어서 설정을 진행합니다.

1. 외부 라이브러리 빌드 설정

외부 라이브러리 들은 프로젝트과 함께 제공되므로 빌드시 함께 빌드되도록 설정 해 줘야 합니다.

  1. 다음 외부 라이브러리의 프로젝트 파일들을 Visual Studio 의 솔루션 파일에 추가합니다.

  • proj.win32/libsodium-1.0.10/libsodium.vcxproj

  • proj.win32/protobuf-2.6.1/vsprojects/libprotobuf.vcxproj

  • proj.win32/zstd-1.3.3/build/VS2017/libzstd/libzstd.vcxproj

  1. 추가한 외부 라이브러리 프로젝트에 대해서 의존성 설정하기.

  • 솔루션 속성의 Project Dependencies 에서 새 프로젝트 를 선택 후 libcocos2d, libprotobuf, libsodium, libSpine, libzstd 체크박스를 활성화 합니다.

2. 새 프로젝트에 외부 라이브러리 설정

  1. 프로젝트 속성의 Include Directories 에 아래 목록들을 추가합니다.

  • $(EngineRoot)external/win32-specific/zlib/include

  • $(EngineRoot)external/uv/include

  • $(EngineRoot)external/websockets/include/win32

  • $(EngineRoot)external/curl/include/win32

  • $(ProjectDir)zstd-1.3.3/lib

  • $(ProjectDir)protobuf-2.6.1/vsprojects/include

  • $(ProjectDir)libsodium-1.0.10/src/libsodium/include

  1. 프로젝트 속성의 Libarary Directories 에 아래 목록들을 추가합니다.

  • $(ProjectDir)zstd-1.3.3/build/VS2017/bin/Win32_Debug

  • $(ProjectDir)protobuf-2.6.1/vsprojects/Debug

  1. 프로젝트 속성의 Additional Dependencies 에 아래 목록들을 추가합니다.

  • libzstd_static.lib

  • websockets.lib

  • libcrypto.lib

  • libssl.lib

  • libprotobuf.lib

  • libsodium.lib

3. 새 프로젝트에 플러그인 소스코드들과 md5.c 소스코드 추가

  • 코코스 플러그인 소스코드들을 새 프로젝트 빌드에 포함하기 위해 Visual Studio Solution Explorer<Project Name>/src 폴더에 Classes/funapi 소스코드들을 추가합니다.

  • 다른 플랫폼과 다르게 Win32 플랫폼은 MD5 의존성이 없기 때문에 <Project Name>/srccocos2d/external/md5/md5.c 소스코드를 추가합니다.

iOS 설정

<Project root>/proj.ios_mac/<Project Name>.xcodeproj 파일을 Xcode 프로그램으로 열어서 설정을 진행합니다.

iOS, macOS 는 같은 Xcode 워크스페이스를 사용하고 Scheme 으로 분리 되어 있기 때문에 Schememobile 로 변경 합니다.

1. 새 프로젝트에 외부 라이브러리 설정

iOS 환경에서 사용되는 외부 라이브러리 들은 미리 컴파일된 라이브러리 파일로 제공되기 때문에 Xcode라이브러리 설정 에 추가하면 컴파일하지 않아도 사용할 수 있습니다.

  1. 프로젝트 속성의 Header Search Path 에 아래 목록들을 추가합니다.

  • $(PROJECT_DIR)/../cocos2d/external/openssl/include/ios

  • $(PROJECT_DIR)/../cocos2d/external/uv/include

  • $(PROJECT_DIR)/../cocos2d/external/websockets/include/ios

  • $(PROJECT_DIR)/../cocos2d/external/curl/include/ios

  • $(PROJECT_DIR)/ios/libzstd/include

  • $(PROJECT_DIR)/ios/libsodium/include

  • $(PROJECT_DIR)/ios/libprotobuf/include

  • $(PROJECT_DIR)/ios/libprotobuf/include

  • $(PROJECT_DIR)/../Classes

  1. 프로젝트 속성의 Library Search Path 에 아래 목록들을 추가합니다.

  • $(PROJECT_DIR)/../cocos2d/external/curl/prebuilt/ios

  • $(PROJECT_DIR)/ios/libzstd/lib

  • $(PROJECT_DIR)/ios/libsodium/lib

  • $(PROJECT_DIR)/ios/libprotobuf/lib

  1. 프로젝트 속성의 Framework, Library, and Embedded Content 에 아래 파일들을 추가합니다.

  • proj.ios_mac/ios/libzstd/lib/libzstd.a

  • proj.ios_mac/ios/libsodium/lib/libsodium.a

  • proj.ios_mac/ios/libprotobuf/lib/libprotobuf.a

  • cocos2d/external/curl/prebuilt/ios/libcurl.a

2. 새 프로젝트에 코코스 플러그인 소스코드 추가

코코스 플러그인 소스코드들을 새 프로젝트 빌드에 포함하기 위해 Xcode Project Navigator<Project Name>/Classes 폴더에 Classes/funapi 폴더를 복사합니다.

macOS 설정

<Project root>\proj.ios_mac\<Project Name>.xcodeproj 파일을 Xcode 프로그램으로 열어서 설정을 진행합니다.

iOS, macOS 는 같은 Xcode 위크스페이스 사용하고 Scheme 으로 분리 되어 있기 때문에 Schemedesktop 로 변경 합니다.

1. 새 프로젝트에 외부 라이브러리 설정

macOS 환경에서 사용되는 플러그인 외부 라이브러리 들은 미리 컴파일된 라이브러리 파일로 제공되기 때문에 Xcode라이브러리 설정 에 추가하면 컴파일하지 않아도 사용할 수 있습니다.

  1. 프로젝트 속성의 Header Search Path 에 아래 목록들을 추가합니다.

  • $(PROJECT_DIR)/../cocos2d/external/openssl/include/mac

  • $(PROJECT_DIR)/../cocos2d/external/uv/include

  • $(PROJECT_DIR)/../cocos2d/external/websockets/include/mac

  • $(PROJECT_DIR)/../cocos2d/external/curl/include/mac

  • $(PROJECT_DIR)/mac/libzstd/include

  • $(PROJECT_DIR)/mac/libsodium/include

  • $(PROJECT_DIR)/mac/libprotobuf/include

  • $(PROJECT_DIR)/mac/libprotobuf/include

  • $(PROJECT_DIR)/../Classes

  1. 프로젝트 속성의 Library Search Path 에 아래 목록들을 추가합니다.

  • $(PROJECT_DIR)/../cocos2d/external/curl/prebuilt/mac

  • $(PROJECT_DIR)/mac/libzstd/lib

  • $(PROJECT_DIR)/mac/libsodium/lib

  • $(PROJECT_DIR)/mac/libprotobuf/lib

  1. 프로젝트 속성의 Framework, Library, and Embedded Content 에 아래 파일들을 추가합니다.

  • proj.ios_mac/mac/libzstd/lib/libzstd.a

  • proj.ios_mac/mac/libsodium/lib/libsodium.a

  • proj.ios_mac/mac/libprotobuf/lib/libprotobuf.a

  • cocos2d/external/curl/prebuilt/mac/libcurl.a

2. 새 프로젝트에 코코스 플러그인 소스코드 추가

코코스 플러그인 소스코드들을 새 프로젝트 빌드에 포함하기 위해 Xcode Project Navigator<Project Name>/Classes 폴더에 Classes/funapi 폴더를 복사합니다.

Android 설정

<Project root>\proj.android 폴더를 Android Studio 프로그램으로 열어서 설정을 진행합니다.

1. 새 프로젝트에 외부 라이브러리 설정

Android 환경에서 사용되는 플러그인 외부 라이브러리 들은 미리 컴파일된 라이브러리 파일로 제공이 되며 Android.mk 빌드 스크립트에서 모듈 로 등록해서 사용됩니다.

  • 외부 라이브러리 들을 로컬 라이브러리 모듈 로 만들기 위해 proj.android/app/jni/Android.mk 파일에 아래 코드를 추가합니다.

include $(CLEAR_VARS)
LOCAL_MODULE := libsodium
LOCAL_SRC_FILES := libsodium/lib/libsodium.a
include $(PREBUILT_STATIC_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := libprotobuf
LOCAL_SRC_FILES := libprotobuf/lib/ARMv7/libprotobuf.a
include $(PREBUILT_STATIC_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := libzstd
LOCAL_SRC_FILES := libzstd/lib/ARMv7/libzstd.a
include $(PREBUILT_STATIC_LIBRARY)
  • 아이펀 엔진 플러그인에서 cocos2d-x 에 의존하는 모듈들을 import 하기위해 proj.android/app/jni/Android.mk 파일에 아래 내용을 추가합니다.

$(call import-module, cocos)
$(call import-module, cocos/external)
$(call import-module, curl/prebuilt/android)
  • proj.android/app/jni/Android.mk 파일에 있는 LOCAL_STATIC_LIBRARIES 변수에 플러그인에서 사용하는 모듈들을 등록 합니다.

LOCAL_STATIC_LIBRARIES := cc_static libsodium libprotobuf libzstd ext_curl ext_crypto

2. 플러그인 소스코드들을 프로젝트에 추가

플러그인 소스코드들을 새 프로젝트 빌드에 포함하기 위해 proj.android/app/jni/Android.mk 파일의 LOCAL_SRC_FILES, LOCAL_C_INCLUDES 변수에 아래와 같이 플러그인 소스를 추가해 줍니다.

LOCAL_SRC_FILES := $(LOCAL_PATH)/hellocpp/main.cpp \
                   $(LOCAL_PATH)/../../../Classes/AppDelegate.cpp \
                   $(LOCAL_PATH)/../../../Classes/FunapiTestScene.cpp \
                   ../../../Classes/funapi/funapi_option.cpp \
                   ../../../Classes/funapi/funapi_multicasting.cpp \
                   ../../../Classes/funapi/funapi_encryption.cpp \
                   ../../../Classes/funapi/funapi_compression.cpp \
                   ../../../Classes/funapi/funapi_downloader.cpp \
                   ../../../Classes/funapi/funapi_session.cpp \
                   ../../../Classes/funapi/funapi_tasks.cpp \
                   ../../../Classes/funapi/funapi_announcement.cpp \
                   ../../../Classes/funapi/funapi_http.cpp \
                   ../../../Classes/funapi/funapi_utils.cpp \
                   ../../../Classes/funapi/funapi_socket.cpp \
                   ../../../Classes/funapi/funapi_websocket.cpp \
                   ../../../Classes/funapi/funapi_rpc.cpp \
                   ../../../Classes/funapi/management/maintenance_message.pb.cc \
                   ../../../Classes/funapi/network/fun_message.pb.cc \
                   ../../../Classes/funapi/network/ping_message.pb.cc \
                   ../../../Classes/funapi/service/multicast_message.pb.cc \
                   ../../../Classes/funapi/service/redirect_message.pb.cc \
                   ../../../Classes/funapi/distribution/fun_dedicated_server_rpc_message.pb.cc \
                   ../../../Classes/test_dedicated_server_rpc_messages.pb.cc \
                   ../../../Classes/test_messages.pb.cc

LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../../Classes \
                    $(LOCAL_PATH)/../../../Classes/funapi \
                    $(LOCAL_PATH)/../../../Classes/funapi/management \
                    $(LOCAL_PATH)/../../../Classes/funapi/network \
                    $(LOCAL_PATH)/../../../Classes/funapi/service \
                    $(LOCAL_PATH)/../../../Classes/funapi/distribution \
                    $(LOCAL_PATH)/libsodium/include \
                    $(LOCAL_PATH)/libprotobuf/include/ARMv7 \
                    $(LOCAL_PATH)/libzstd/include/ARMv7

3. Gradle 빌드 설정

플러그인은 ndk-build 를 이용해 빌드 및 테스트가 완료 되었고 ndk-build 빌드 방법을 안내합니다.

cmake 를 사용해서 빌드하는 경우 cocos2d-x 홈페이지를 참고해 주세요.

  • proj.android/app/jni/Application.mk 파일의 APP_STL 변수를 c++_static 에서 gnustl_shared 으로 변경합니다.

  • proj.android/gradle.properties 파일의 PROP_BUILD_TYPE 변수를 cmake 에서 ndk-build 로 변경합니다.

플러그인 초기화

플러그인을 사용하기 앞서 플러그인을 초기화 해야합니다.

아래 코드는 새 프로젝트를 만들면 자동으로 생성되는 HelloWorld 클래스의 createScene() 함수에서 플러그인을 초기화 하는 방법입니다.

#include "funapi_session.h"

..

Scene* HelloWorld::createScene()
{
  // funapi plugin's manager init
  fun::FunapiSendFlagManager::Init();

  return HelloWorld::create();
}

서버에 연결하기

세션이란?

서버와 클라이언트의 네트워크 연결은 세션으로 관리합니다. 한 번 연결된 세션은 서버에서 Close, 클라이언트에서 세션 CloseRequest, 설정한 시간 이상 네트워크 통신이 이루어지지 않아 timeout 이 발생하지 않는한 유지됩니다. 세션을 연결할 때 먼저 서버로부터 세션 ID 를 발급 받게 되고 이 후 서버와 주고 받는 모든 메시지에는 이 세션 ID 가 포함됩니다. 즉, 네트워크 연결이 끊기더라도 서버에서는 세션을 유지하며, 재연결시 동일한 ID의 세션이 있다면 연결이 끊어지지 않은 것처럼 계속 메시지를 주고 받을 수 있습니다.

하나의 세션은 TCP, UDP, HTTP, WebSocket 전송 프로토콜을 각각 또는 동시에 사용할 수 있습니다. 다만 동일한 전송 프로토콜로 포트만 다르게 같은 서버에 접속하려면 여러 세션을 연결해야 합니다.

Tip

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

FunapiSession 클래스

하나의 세션을 관리하는 클래스입니다. 전송 프로토콜를 관리하고 메시지를 송수신하는 등 하나의 세션으로 할 수 모든 기능을 담고 있습니다.

하나의 세션으로 연결할 수 있는 서버는 하나입니다. 처음 세션을 생성할 때 서버 주소가 정해지면 이 후 서버 주소는 변경할 수 없습니다.

Session Option

FunapiSession 객체를 생성할 때 SessionOption 을 전달하는데 아래는 옵션에 대한 설명입니다. 세션 옵션을 전달하지 않을 경우 기본 값을 사용하게 됩니다.

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

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

  // 메시지 순서를 보장합니다. 재연결시에도 이전 메시지와의 순서를 동기화합니다.
  // 기본값 : false
  void SetSessionReliability(const bool reliability);
  bool GetSessionReliability();

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

  // 기본적으로 모든 메시지에 session 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 입니다.
// 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 객체는 업데이트를 위해 코코스 엔진의 Cocos Thread 에서 Update() 함수를 호출해야 합니다. 아래 코드는 FunapiSession 를 멤버변수로 소유하고있는 HelloWorld 클래스에서 Update() 함수를 호출해주는 코드입니다.

void HelloWorld::update(float DeltaTime)
{
  if(session_ != nullptr)
  {
    session_->Update();
  }

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

Note

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

Transport 연결

위에서 만든 FunapiSession 객체로 TCP 연결을 시작합니다.

// AutoReconnect 옵션을 사용하기 위해 옵션 객체를 생성합니다.
// 특정 옵션을 사용하려는 것이 아니라면 Connect의 옵션 파라미터는 생략할 수 있습니다.
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 에는 세션과 Transport 의 상태 변화를 알려주는 콜백이 있습니다.

Session Event

AddSessionEventCallback() 함수을 통해 세션 이벤트 콜백 함수를 등록하면 세션의 상태가 변경될 때마다 아래와 같은 세션관련 이벤트 알림을 받을 수 있습니다.

enum class FUNAPI_API SessionEventType : int
{
  kOpened,             // 세션이 처음 연결되면 호출됩니다. 같은 세션으로 재연결시에는 호출되지 않습니다.
  kClosed,             // 세션이 닫히면 호출됩니다. Transport 의 연결이 끊겼다고 세션이 닫히는 것은 아닙니다.
  kChanged,            // deprecated. 세션 ID 가 변경되면 호출됩니다.

  kRedirectStarted,    // Redirect 관련 이벤트는 아래 '서버간 이동' 항목을 참고해주세요.
  kRedirectSucceeded,
  kRedirectFailed,
};

FunapiSession 객체의 AddSessionEventCallback() 함수를 통해 세션 이벤트 콜백 함수를 등록할 수 있습니다.

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

Transport와 관련된 이벤트는 아래와 같습니다.

enum class FUNAPI_API TransportEventType : int
{
  kStarted,                // 서버 연결이 완료되면 호출됩니다.
  kStopped,                // 서버와 연결이 종료되거나 연결에 실패하면 호출됩니다.
  kReconnecting,           // 재연결을 시작할 때 호출됩니다.
  kConnectionFailed,       // 서버와 연결이 실패하면 호출됩니다.
  kConnectionTimedOut,     // 서버에 timeout 으로 연결이 실패하면 호출됩니다.
  kDisconnected,           // 서버와연결이 단절되면 호출됩니다.
};

FunapiSession 객체의 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::kStarted)
      {
        if (transport_protocol == fun::TransportProtocol::kTcp)
        {
          // TCP 프로토콜을 사용할 준비가 되었습니다.
          fun::DebugUtils::Log("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() 함수를 호출할 때 파라미터로 Transport 의 옵션을 전달할 수 있습니다. 이 값이 null 인 경우에는 기본 값을 사용하게 됩니다. 사용할 프로토콜 (TCP, UDP, HTTP, WebSocket)에 따라 그에 맞는 TransportOption 클래스를 생성하여 지정해야 합니다. 예를 들어 UDP 의 경우, FunapiUdpTransportOption 입니다.

아래는 TransportOption 클래스가 정의된 코드입니다.

FunapiTcpTransportOption

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 까지 반복해서 시도하며
  // AutoReconnectTimeout 시간을 초과한경우 재연결 시도를 중단하고
  // TransportEventType::kStopped 이벤트를 TransportEventCallback 으로 전달합니다.
  // 기본값 : false
  void SetAutoReconnect(const bool use_auto_reconnect);
  bool GetAutoReconnect();

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

  // 클라이언트 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();

  // 메시지에 sequence number 를 붙여서 메시지의 유효성을 보장해주는 옵션입니다.
  // Session reliability 옵션을 사용하지 않고 메시지의 유효성만 보장하고 싶을 때 사용할 수 있습니다.
  // Session reliability 옵션을 사용하면 이 옵션은 무시됩니다.
  // 기본값 : false
  void SetSequenceNumberValidation(const bool validation);
  bool GetSequenceNumberValidation();

  // 서버와 연결할 때 Timeout 될 시간을 지정합니다.
  // Timeout에 설정한 시간을 초과하면 연결 시도를 중단하고, 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 옵션 클래스입니다.

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

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

  // 메시지에 sequence number 를 붙여서 메시지의 유효성을 보장해주는 옵션입니다.
  // Session reliability 옵션을 사용하지 않고 메시지의 유효성만 보장하고 싶을 때 사용할 수 있습니다.
  // Session reliability 옵션을 사용하면 이 옵션은 무시됩니다.
  // 기본값 : false
  void SetSequenceNumberValidation(const bool validation);
  bool GetSequenceNumberValidation();

  // 서버와 연결할 때 Timeout 될 시간을 지정합니다.
  // Timeout에 설정한 시간을 초과하면 연결 시도를 중단하고, 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 옵션 클래스입니다.

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 은 암호화 타입 파라미터입니다. 암호화를 사용하는데 타입을 지정하지 않을 경우 기본 타입으로 암호화를 하게 되고 암호화를 사용하지 않을 경우에 이 값을 입력하면 오류가 발생합니다. 서버에서 하나의 프로토콜에 여러 종류의 암호화를 허용할 경우 메시지를 보낼 때 암호화 타입을 선택해서 보낼 수 있습니다.

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

// 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";
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>/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)
      {
        fun::DebugUtils::Log("msg '%s' arrived.", msg_type.c_str());
        fun::DebugUtils::Log("json string: %s", 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)
      {
         fun::DebugUtils::Log("msg '%s' arrived.", fun_message.msgtype().c_str());

        if (fun_message.HasExtension(pbuf_echo))
        {
          PbufEchoMessage echo = fun_message.GetExtension(pbuf_echo);
          fun::DebugUtils::Log("proto: %s", echo.msg().c_str());
        }
      }
    }
);

응답 메시지 시간 제한

서버로부터 기다리는 메시지에 대해 타임아웃을 정하고 싶다면 FunapiSession 클래스에 있는 SetRecvTimeout() 함수를 사용해 원하는 메시지 타입에 타임아웃 시간을 지정하면 됩니다.

아래는 RecvTimeout 관련 함수들입니다.

// 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’ 의 이름을 가지는 MessageType 메시지를 10초 동안 기다리는 예제입니다. ‘sc_login’ 메시지를 10초 내에 받지 못할 경우 AddRecvTimeoutCallback() 함수로 등록한 콜백 함수가 호출됩니다.

// timeout 콜백 함수를 추가합니다.
session_->AddRecvTimeoutCallback(
    [](const std::shared_ptr<fun::FunapiSession> &session,
       const fun::string& msg_type)
    {
      fun::DebugUtils::Log("message timeout, msg_tpye : %s", 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)를 사용하고 있습니다. 이 두 암호화 타입을 사용하려면 공개 키 를 설정해야 합니다.

Tip

IFunEngine1, ChaCha20, Aes128 암호화 방식은 TCP 프로토콜에서만 사용이 가능합니다.

암호화 설정을 사용하는 상태에서 특정 메시지에 대해서 암호화를 적용하지 않고 그대로 보내고 싶다면 SendMessage() 함수의 암호화 타입으로 kDummyEncryption 을 전달하면 됩니다. 이 기능을 사용하려면 서버 쪽 암호화 설정에 dummy 가 포함되어 있어야 합니다.

암호화 사용

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

std::shared_ptr<fun::FunapiTcpTransportOption> tcp_option =
    fun::FunapiTcpTransportOption::Create();
tcp_option->SetEncryptionType(fun::EncryptionType::kIFunEngine1Encryption);

// 이 키값은 예제를 위한 키값입니다.
// 아이펀 엔진 서버의 MANIFEST.json 에 정의된 public_key 를 사용해주세요.
fun::string public_key = "7cf7672bc648c3a4b04f7e2a2427c08a2a355a38821e93df4e98c1decefa8200";
tcp_option->SetEncryptionType(fun::EncryptionType::kChacha20Encryption,
                              public_key);

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,
              // 설정한 암호화 타입중 Enumerator values 가 낮은
              // kIFunEngine1Encryption 암호화가 설정됩니다.
              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,
              fun::EncryptionType::kChacha20Encryption);
        }
      }
    }
);

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

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

Important

아이펀 엔진에서 제공하는 암호화 타입 중 UDP 와 HTTP 가 사용할 수 있는 암호화 타입은 IFunEngine2Encryption 밖에 없습니다.

Note

GitHub Cocos2d-x 에 배포된 클라이언트 플러그인은 IFunEngine1EncryptionIFunEngine2Encryption 타입의 암호화 기능이 포함되지 않은 무료 버전입니다. ChaCha20Aes128 타입은 사용 가능합니다.

유료 고객 의 경우 슬랙이나 iFun Engine support 로 요청 메일을 보내주시면 암호화 타입이 모두 포함된 플러그인 소스를 보내드립니다.

메시지 압축 기능

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

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

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

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

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

enum class FUNAPI_API CompressionType : int
{
#if FUNAPI_HAVE_ZSTD
  kZstd,     // Zstdandard. 실시간 전송에 적합한 압축 알고리즘입니다.
#endif
#if FUNAPI_HAVE_ZLIB
  kDeflate,  // Deflate. 지연 시간이 크고, 더 큰 메시지에 적당한 압축 알고리즘입니다.
#endif
  kDefault,
};

압축 관련 옵션은 서버와 클라이언트에서 같은 값을 사용해야 합니다. 같은 세션이라 해도 프로토콜 별로 압축 타입을 다르게 사용할 수 있습니다.

메시지 압축 예제

아래 코드는 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);
        return session_opt;
      }

      // 서버 이동전 옵션을 그대로 사용합니다.
      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);
        return tcp_option;
      }

      // 서버 이동전 옵션을 그대로 사용합니다.
      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() 함수를 호출해서 버릴 메시지를 선택할 수 있습니다.

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

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 객체를 생성하며 이 함수로 생성된 FunapiMulticast 객체는
// 멀티캐스트 서버와 연결을 해야 합니다.
// 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 객체를 생성합니다.
// 내부적으로 FunapiSession 객체를 생성하며 생성된 FunapiSession 에
// 추가적인 옵션을 설정 할 수 있습니다.
// 이 함수로 생성된 FunapiMulticast 객체는 멀티캐스트 서버에 연결을 해야 합니다.
// 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 객체를 활용해 멀티캐스트 객체를 생성합니다.
// 다음과 같은 상황에서 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 객체를 생성한 경우, 서버에 연결하는 동작을 추가하기 위한 인터페이스들입니다.

// 멀티캐스트 서버에 연결합니다.
// 별도의 FunapiSession 객체 없이 FunapiMulticast 객체를 생성한 경우 사용됩니다.
void Connect();

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

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

콜백 등록 인터페이스

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

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

// 세션 이벤트가 발생하면 호출되는 콜백 함수를 추가합니다.
// 서버와 연결된 FunapiSession 객체를 활용해 멀티캐스트 객체를 생성한 경우
// FunapiSession 객체의 AddSessionEventCallback() 함수를 사용해주세요.
void AddSessionEventCallback(const FunapiMulticast::SessionEventHandler &handler);

// 트랜스포트 이벤트가 발생하면 호출되는 콜백 함수를 추가합니다.
// 서버와 연결된 FunapiSession 객체를 활용해 멀티캐스트 객체를 생성한 경우
// FunapiSession 객체의 AddTransportEventCallback() 함수를 사용해주세요.
void AddTransportEventCallback(const FunapiMulticast::TransportEventHandler &handler);

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

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

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

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

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

채널 관련 인터페이스

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

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

// channel_id 가 입장한 채널인지 확인하는 property 입니다.
// 입장한 채널이면 true를 반환합니다.
public bool IsInChannel(string channel_id);

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

// 채널에 입장할 때 사용하는 함수입니다.
// channel_id 는 입장할 채널 이름입니다. 존재하지 않는 채널이면 서버에서 생성합니다.
// handler 는 메시지를 전달받으면 호출되는 콜백 함수입니다.
// token 은 채널 입장에 사용되는 인증 token 이며 체날의 token 과 인자로 사용된 token 이
// 일치하지 않다면 채널 입장에 실패합니다.
// token 인증을 사용하지 않는다면 빈 문자열 혹은 디폴트 인자를 사용해주세요.
// 다음과 같은 상황에서 false 를 반환합니다.
// 이미 channel_id 에 해당하는 채널에 입장, 서버와 연결되어 있지 않음, Protobuf 인코딩을 사용하지 않음.
bool JoinChannel(const fun::string &channel_id,
                 const ProtobufChannelMessageHandler &handler,
                 const fun::string &token);

// 채널을 나갈 때 사용하는 함수입니다.
// channel_id 는 나갈 채널 이름입니다.
// 입장한 채널이 없거나 서버와 연결되어 있지 않으면 false 를 반환합니다.
public bool LeaveChannel(string channel_id);

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

메세지 전송 인터페이스

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

// 채널에 JSON 메시지를 전송할 때 사용하는 함수입니다.
// channel_id 는 메세지를 전송할 채널 이름입니다.
// json_string 은 전송할 JSON 메세지 입니다.
// bounce 는 내가 전송한 메세지를 받는 옵션으로 디폴트는 true 입니다.
// 만약 내가 전송한 메세지를 받지 않으려면 false 로 비활성화 해주세요.
bool SendToChannel(const fun::string &channel_id,
                   fun::string &json_string,
                   const bool bounce);  // 내가 전송한 메세지를 받는 옵션.

// 채널에 메시지를 전송할 때 사용하는 함수입니다.
// 이 함수는 Protobuf 메시지를 전송할 때 사용합니다.
// channel_id 는 메세지를 전송할 채널 이름입니다.
// msg 은 전송할 Protobuf 메세지 입니다.
// bounce 는 내가 전송한 메세지를 받는 옵션으로 디폴트는 true 입니다.
// 만약 내가 전송한 메세지를 받지 않으려면 false 로 비활성화 해주세요.
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)
      {
        fun::DebugUtils::Log("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)
      {
        fun::DebugUtils::Log("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) */)
    {
      // 채널에 입장한 유저 를 알 수 있습니다.
      fun::DebugUtils::Log("Channel ID : %s, Joined user ID : %s",
          channel_id.c_str(), 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) */)
    {
      // 채널에서 퇴장한 유저 를 알 수 있습니다.
      fun::DebugUtils::Log("Channel ID : %s, Left user ID : %s",
          channel_id.c_str(), 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)
      {
        // 채널 이름과 해당 채널에 접속한 유저 수를 알 수 있습니다.
        fun::DebugUtils::Log("Channel ID : %s, Number of users on channel : %d",
            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() 함수를 호출합니다. 만약 존재하지 않는 채널이면 서버에서 생성합니다. 해당 채널로부터 메시지가 전송되면 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();
         // 여기서는 간단하게 로그만 출력하겠습니다.
         fun::DebugUtils::Log("%s", text.c_str());
       }
     }
   };

// 채널에 입장하기 위해 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 클래스로 서버에 공지사항 목록을 요청할 수 있습니다.

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

Note

한 페이지 당 공지사항 개수는 max_count 와 같습니다. 예를 들어 RequestList(3, 0) 을 호출할 경우 최근 공지사항(첫 페이지)을 최대 3개 가져옵니다. RequestList(3, 1) 을 호출할 경우 최근 공지사항 3개 이후의 공지사항(다음 페이지)부터 최대 3개를 가져옵니다.

아래는 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 << "}";
            fun::DebugUtils::Log("%s", 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);

공지사항에 들어가는 항목들은 정해져 있지 않습니다. 서버 관리자가 필요한 항목들을 정의해서 사용하면 됩니다. 단, 기존에 사용중인 필드들을 변경할 경우 플러그인 코드도 함께 수정해야 할 수도 있으므로 기존 필드를 변경해야 할 경우엔 iFun Engine support 로 문의해주시기 바랍니다.

공지사항 Update

FunapiAnnouncement 객체는 업데이트를 위해 코코스 엔진의 Cocos Thread 에서 Update() 함수를 호출해야 합니다. 아래 코드는 FunapiAnnouncement 를 멤버변수로 소유하고있는 HelloWorld 클래스에서 Update() 함수를 호출해주는 코드입니다.

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

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

Note

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

서버 점검 메시지

서버가 점검 중이라면 클라이언트에서 보낸 메시지는 무시되고 서버는 클라이언트로부터 메시지를 받을 때마다 점검 안내 메시지를 보냅니다. 서버 연결 후 클라이언트가 아무런 메시지도 보내지 않는다면 서버도 점검 안내 메시지를 보내지 않습니다.

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

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)
      {
        fun::DebugUtils::Log("Maintenance message : %s",
            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();
          fun::DebugUtils::Log("Maintenance message:\nstart: %s\nend: %s\nmessage: %s",
              date_start.c_str(), date_end.c_str(), 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);

// 각 파일에 대한 tineout 을 설정합니다.
// 기본값 : 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;
        fun::DebugUtils::Log("%s", 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;
      fun::DebugUtils::Log("%s", 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)
          {
              fun::DebugUtils::Log("file_path=%s", 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 객체는 업데이트를 위해 코코스 엔진의 Cocos Thread 에서 Update() 함수를 호출해야 합니다. 아래 코드는 FunapiHttpDownloader 를 멤버변수로 소유하고있는 HelloWorld 클래스에서 Update() 함수를 호출해주는 코드입니다.

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

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

Note

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