JavaScript 플러그인 (Beta)

Warning

JavaScript 플러그인은 현재 베타 버전으로 제공되고 있습니다. 공개된 API 가 이후 변경될 수 있습니다. 사용 도중 발생한 문제에 대한 공식적인 기술적 지원이 제공되지 않습니다. 베타 버전 사용에 의해 발생한 문제에 대해 공식적인 책임도 없음을 밝힙니다.

iFun Engine 을 서버로 사용하는 웹 브라우저 용 HTML5 게임 클라이언트 개발을 돕는 플러그인입니다. JavaScript 플러그인을 사용하면 웹 브라우저에서 손쉽게 iFun Engine 으로 개발된 서버와 연동할 수 있습니다.

게임 서버와 WebSocket 이나 HTTP 로 손쉽게 메시지를 주고 받을 수 있으며 채팅과 공지사항 등의 기능도 편리하게 사용할 수 있습니다. JSON 과 Protocol Buffers 메시지 형식을 지원합니다.

시작하기

JavaScript 플러그인은 파일을 다운받아서 웹 페이지에 포함시키는 것만으로 손쉽게 HTML5 게임 개발을 시작하실 수 있습니다. 아래 패키지를 다운로드 받아 저장합니다.

https://www.ifunfactory.com/engine/plugin-js/beta/ifun-engine-plugin.bundle.js

게임 클라이언트로 개발할 HTML 파일과 동일한 디렉터리에 .js 파일을 위치시키고, 아래와 같이 로드합니다.

index.html
<!DOCTYPE html>
<html>
  <head>
    <title>Funapi Plugin Sample</title>
  </head>
  <body>
    <script type="module">
import { FunapiSession } from './ifun-engine-plugin.bundle.js';
    </script>
  </body>
</html>

이것만으로 iFun Engine 서버와 메시지를 주고 받을 수 있는 준비가 끝났습니다. 물론 HTML 파일에 직접 JavaScript 로직을 넣으면 구조적으로 좋지 않습니다. 아래처럼 별도의 JavaScript 소스 코드를 만들어 JavaScript 로직을 깨끗하게 분리할 수 있습니다.

main.js
import { FunapiSession } from './ifun-engine-plugin.bundle.js';
index.html
<script type="module" src="./main.js"></script>

Caution

ifun-engine-plugin.bundle.js 파일은 JavaScript module(ECMAScript6 module) 형식으로 되어 있습니다. 일반적으로 HTML 파일에 JavaScript 코드를 포함할 때 사용하는 방법인 <script type="text/javascript">...</script> 와 같이 사용하면 동작하지 않는 점에 주의해 주세요.

서버와 통신하기 위해서는 우선 세션을 연결해야 합니다. 세션에서 사용할 전송 프로토콜(플러그인에서 transport 라고 표현됩니다)과 메시지 타입을 지정해 세션을 연결하면 서버와 메시지를 주고 받을 수 있습니다.

전송 프로토콜은 WebSocket 과 HTTP 를 사용할 수 있습니다. HTTP 는 요청-응답 구조를 가지고 있어 실시간 동기성이 필요 없거나, 서버로부터 항상 응답을 기대하는 경우, 서버에서 클라이언트로 브로드캐스팅 등이 필요 없는 경우에 사용합니다. 반면 WebSocket 은 양방향 통신이 가능하고, 연결된 이후의 메시지 크기가 작은 편이라 실시간 동기성을 가진 빠른 응답 속도가 필요한 게임에 적합합니다.

메시지 타입은 웹에서 일반적으로 가장 많이 사용하는 JSON 과 Protocol Buffers 를 사용할 수 있습니다. JSON 은 추가적인 작업 없이 바로 사용하실 수 있어 초기에 편리하지만, Protocol Buffers 에 비해 텍스트 기반이므로 메시지 크기가 크고, 성능이 떨어집니다. Protocol Buffers 는 바이너리 기반으로 크기가 JSON 에 비해 경량이고, 처리하는데 드는 연산 비용이 적지만, 작동 특성 상 바로 사용할 수 없고 추가적인 작업이 별도로 필요 합니다.

시작하기 섹션에서는 보다 많은 기능이 지원되는 WebSocket 전송 프로토콜과 추가 작업 없이 사용할 수 있는 JSON 메시지 타입을 사용하겠습니다.

세션 객채를 만들어 생성합니다.

import { FunapiSession, SessionEventType, TransportProtocol, FunEncoding } from './ifun-engine-plugin.bundle.js';

let session = FunapiSession.create('127.0.0.1');

// 세션 관련 이벤트가 발생하면 실행될 콜백(또는 이벤트 핸들러)를 등록합니다.
session.sessionEventCallback.add((type) => {
    switch (type) {
        case SessionEventType.CONNECTED:
            // 세션이 연결 완료되어서 서버로 메시지를 보내는 것이 가능해 졌을 때 발생하는 이벤트입니다.
            // 여기서부터 서버와 메시지을 주고 받는 등의 로직을 시작할 수 있습니다.
            session.sendMessage("echo", { message: "hello" });
            break;
    }
});

// 서버로부터 메시지를 받았을 때 실행될 콜백을 등록합니다.
session.receivedMessageCallback.add((type, body, encoding) => {
    if (type === "echo") {
        console.log("hello from the server.")
    }
}));

FunapiSession 객체를 서버 주소로 생성하고, 이후 서버 통신이 일어날 때 호출될 콜백을 등록하였습니다. JavaScript 플러그인은 전체적으로 비동기 방식으로 이루어져 있습니다. 서버에 연결하거나, 메시지를 보내는 등 서버와 통신할 때 API를 호출하더라도 해당 작업이 끝날 때까지 기다리지 않습니다. 대신 이벤트가 일어날 때 실행될 콜백을 미리 등록하는 구조로 되어 있습니다.

위 예제에서는 세션 연결이 완료되면 “echo” 타입의 메시지를 서버로 전송하고, 서버로 부터 “echo” 타입의 메시지를 받았을 때 console 에 로그를 찍도록 하였습니다.

아직 FunapiSession 객체를 생성만 하였고 실제로 서버에 연결 요청을 보내지 않았습니다. 아래 코드는 서버에 접속해 세션 연결을 시작합니다.

session.connect(TransportProtocol.WEBSOCKET, FunEncoding.JSON, 8012);

전송 프로토콜로 WebSocket 을 지정하고, 메시지 타입으로 JSON 을 지정해서, 8012 포트로 세션 연결을 시작하였습니다. 실제 메시지를 주고 받을 수 있는 시점은 SessionEventType.CONNECTED 이벤트가 발생한 이후입니다. 따라서 서버 통신 관련 모든 로직은 위에서 등록하였던 콜백에서 부터 시작하여야 합니다. session.connect(...) 는 연결이 시작하면 반환합니다.

플러그인을 가져와 사용을 시작하는 방법을 소개하였습니다. 이제부터는 플러그인이 제공하는 각 기능들을 상세하게 설명합니다. 또한 사용자가 Protocol Buffers 메시지 타입을 추가한 경우 필요한 작업도 다룹니다.

서버에 연결하기

세션이란?

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

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

Tip

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

FunapiSession 클래스

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

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

Session Option

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

 let sessionOption = new SessionOption();

 // 기본적으로 모든 메시지에 session ID 값이 포함되며 이 옵션을 true 로 변경할 경우
 // 처음 연결시에만 세션 ID 를 보내고 그 이후 메시지에는 세션 ID 를 포함하지 않습니다.
 // 기본값: false
 sessionOption.sendSessionIdOnlyOnce = false;

 // 서버와 연결할 때 타임아웃 될 시간을 지정합니다.
 // 기본 값은 10초이며, 0을 입력할 경우 타임아웃 처리를 하지 않고 계속 연결을 시도합니다.
 // 이 경우, 서버로부터 응답이 오지 않으면 무한히 대기하는 상황이 발생할 수 있기 때문에
 // 디버깅 목적으로만 사용하기를 권장합니다.
 // 타임아웃에 설정한 시간을 초과하면 연결 시도를 중단하고, SessionEventType.STOPPED 이벤트를 발생시킵니다.
 // 기본값: 10
 sessionOption.connectionTimeoutInSeconds = 10;

 // 서버이동 기능을 사용할 경우 이동에 대한 타임아웃 시간 값입니다. 초단위 값을 사용합니다.
 // 기본 값은 10초이며 해당 시간 이내에 서버 이동이 완료되지 못할 경우 서버이동 실패로 처리합니다.
 // 기본값: 10
 sessionOption.redirectTimeoutInSeconds = 10;

  // 서버이동시에 이동 도중에 보낸 메시지들은 기본적으로 버려집니다.
  // 이 값을 true 로 변경할 경우 서버이동 도중 버려지는 메시지들에 대해 이동한 서버로의 메시지 전송을 보장합니다.
  // 메시지를 전달하기 전에 redirectQueueCallback 에 목록을 전달하므로 이 콜백에서 메시지를 확인할 수 있습니다.
  // 기본값: false
  sessionOption.useRedirectQueue = false;

  // 클라이언트 콘솔에 출력할 로그 레벨을 지정합니다.
  // 기본값: LogLevel.NONE
  sessionOption.logLevel = LogLevel.NONE;
}

FunapiSession 객체 생성

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

// Session 옵션
let option = new SessionOption();
option.sendSessionIdOnlyOnce = true;

// FunapiSession 객체를 생성합니다.
// 전달하는 파라미터는 서버 주소와 SessionOption 입니다.
// option 값을 전달하지 않으면 기본 값을 사용합니다.
session = FunapiSession.create(address, option);

// 이벤트 콜백을 등록합니다.
session.sessionEventCallback.add((sessionEventType) => { ... });
session.transportEventCallback.add((transportProtocol, transportEventType) => { ... });
session.transportErrorCallback.add((transportProtocol, transportError) => { ... });

// 받은 메시지를 처리하기 위한 콜백을 등록합니다.
session.receivedMessageCallback.add((type, body, funEncoding) => { ... });

전송 프로토콜 지정 연결

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

// autoReconnect 옵션을 사용하기 위해 옵션 객체를 생성합니다.
// 특정 옵션을 사용하려는 것이 아니라면 connect의 옵션 파라미터는 생략할 수 있습니다.
let option = new WebsocketOption();
option.autoReconnect = false;    // 기본값이 false 입니다.

// 서버에 연결하기 위해서는 프로토콜 타입, 인코딩 타입, 포트 번호가 필요합니다.
// 옵션 값은 필요할 경우 사용하면 되고 지정하지 않으면 기본 값을 사용합니다.
session.connect(TransportProtocol.WEBSOCKET, FunEncoding.JSON, 8012, option);

connect 함수는 지정한 파라미터를 사용하여 지정된 전송 프로토콜을 이용해 세션을 연결하거나, 이미 연결된 세션에 전송 프로토콜을 추가합니다.

같은 Transport 가 연결된 적이 있고 옵션 값이 같다면, Transport 를 다시 생성하지 않고 재연결합니다.

기존 연결로 재연결하는 경우에는 파라미터로 프로토콜만 지정해서 connect 함수를 호출하는 것이 좋습니다.

프로토콜과 인코딩 타입

connect 함수에서 사용할 수 있는 protocol 인자는 HTTP, WebSocket 2 가지 타입을 지원합니다

  • TransportProtocol.HTTP

  • TransportProtocol.WEBSOCKET

메시지 인코딩 방식은 JSON과 Protocol Buffers 2 가지 방법이 있습니다.

  • FunEncoding.JSON

  • FunEncoding.PROTOBUF

이벤트 콜백 함수

FunapiSession에는 Session의 상태 변화를 알려주는 콜백과 Transport의 상태 변화를 알려주는 콜백 함수, Transport의 에러에 대해서 알려주는 콜백 함수가 있습니다. Transport에서 오류가 발생했을 경우, 에러 콜백과 함께 Transport의 이벤트 콜백이 함께 호출될 수도 있습니다.

Session Event

sessionEventCallback 에 콜백 함수를 등록하면 세션의 상태가 변경될 때마다 호출됩니다. 아래와 같은 세션 이벤트 타입이 있습니다.

  • SessionEventType.OPENED: 새로운 세션이 생성되면 발생합니다.

  • SessionEventType.CONNECTED: 세션이 연결되어 메시지를 보낼 수 있게 될 때 발생합니다.

  • SessionEventType.STOPPED: 세션에 등록된 모든 Transport 가 종료되면 발생합니다.

  • SessionEventType.CLOSED: 세션이 닫히면 발생합니다. Transport 가 종료되었다고 세션이 닫히는 것은 아닙니다. 세션은 서버에서 명시적으로 close가 호출되거나 세션 타임아웃이 되었을 때 닫힙니다.

  • SessionEventType.REDIRECT_STARTED

  • SessionEventType.REDIRECT_SUCCEEDED

  • SessionEventType.REDIRECT_FAILED: Redirect 관련 이벤트는 아래 ‘서버간 이동’ 항목을 참고해주세요.

FunapiSession 세션 이벤트 콜백은 아래와 같이 등록합니다.

session.sessionEventCallback.add( (eventType, sessionId) => {
    switch (eventType) {
        case SessionEventType.OPENED:
            // ...
            break;
        case SessionEventType.CONNECTED:
            // ...
            break;
         // ...
});
  • eventType 인자는 위에서 언급한 SessionEventType 값 중 하나로 어떤 세션 이벤트가 발생했는지를 나타냅니다.

  • sessionId 인자는 세션 이벤트가 발생한 세션을 가르키는 고유 세션 ID 값입니다.

Transport Event

transportEventCallback 에 콜백 함수를 등록하면 Transport 의 상태가 변경될 때마다 호출됩니다. 아래와 같은 이벤트가 가 있습니다. STARTED 를 제외하고는 모두 연결에 실패하거나 연결이 끊겼을 때 발생하는 이벤트들입니다. STOPPED 는 정상적이든 오류로 인한 것이든 Tansport 가 종료되면 항상 발생하는 이벤트입니다.

  • TransportEventType.STARTED: 서버와 연결이 완료되어, 해당 Transport 로 메시지를 보낼 수 있으면 발생합니다.

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

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

자세한 종료 사유에 대해서는 FunapiSession.getLastError 로 확인할 수 있습니다.

Transport 이벤트 콜백은 FunapiSession.transportEventCallback 에 다음과 같이 등록합니다.

session.transportEventCallback.add( (protocol, eventType) => {
    switch (eventType) {
        case TransportEventType.STARTED:
            // ...
            break;
        case TransportEventType.STOPPED:
            // ...
            break;
         // ...
});
  • protocol 인자는 이벤트를 발생시킨 Transport 입니다. 위에서 언급한 TransportProtocol 값 중 하나입니다.

  • eventType 인자는 발생한 이벤트 타입입니다. 위에서 언급한``TransportEventType`` 값 중 하나입니다.

Transport Error

transportErrorCallback 에 콜백 함수를 등록하면 Transport 에서 에러가 발생할 때마다 호출됩니다. 아래와 같은 에러 타입이 있습니다.

  • TransportErrorType.STARTING_FAILED: Transport 초기화에 실패했을 때 발생합니다.

  • TransportErrorType.CONNECTION_TIMEOUT: 연결 제한시간을 초과했을 경우에 발생합니다.

  • TransportErrorType.SENDING_FAILED: 메시지를 보내는 과정에서 오류가 발생했을 때 발생합니다.

  • TransportErrorType.RECEIVING_FAILED: 메시지를 받는 과정에서 오류가 발생했을 때 발생합니다.

  • TransportErrorType.REQUEST_FAILED: HTTP 요청에 실패했을 때 발생합니다.

  • TransportErrorType.WEBSOCKET_ERROR: WebSocket에서 오류가 발생했을 때 발생합니다.

  • TransportErrorType.DISCONNECTD: 예기치 않게 서버와의 연결이 끊겼을 때 호출됩니다.

Transport error 콜백은 FunapiSession.transportErrorCallback 에 다음과 같이 등록합니다.

session.transportErrorCallback.add( (protocol, error) => {
    switch (error.type) {
        case TransportErrorType.STARTING_FAILED:
            // ...
            break;
        case TransportEventType.CONNECTION_TIMEOUT:
            // ...
            break;
         // ...
});
  • protocol 인자는 에러를 발생시킨 Transport 를 가르킵니다. 위에서 언급한 TransportProtocol 값 중 하나입니다.

  • error 인자는 발생한 에러입니다. 에러 타입과 함께 에러 메시지가 전달됩니다.

    • error.type 은 발생한 에러의 종류입니다. 위에서 언급한 TransportErrorType 값 중 하나입니다.

    • error.message 는 발생한 에러를 나타내는 메시지 문자열입니다.

Transport 에서 오류가 발생했을 때는 해당 Transport 가 중단됩니다.

Tip

Tranpsort 오류는 대부분 연결이 불안정하거나 망 변경 등의 이유로 재연결이 필요한 상태에서 발생합니다. Tranpsort 오류가 발생하면 FunapiSession.stop 을 호출해서 모든 연결을 끊고 SessionEventType.STOPPED 이벤트가 발생하면 FunapiSession.reconnect 로 재연결을 고려해볼 것을 권장합니다.

Transport 옵션

FunapiSession.connect 함수를 호출할 때 파라미터로 Transport의 옵션을 전달할 수 있습니다. 이 값을 지정하지 않는 경우 기본 값을 사용하게 됩니다. 따로 지정할 경우 사용할 Transport(HTTP, WebSockeet)에 따라 그에 맞는 TransportOption 클래스를 생성하여 지정해야 합니다. 예를 들어 WebSocket의 경우, WebsocketTransportOption 입니다.

HTTP 옵션

HTTP Transport의 옵션 클래스입니다.

let option = new HttpTransportOption();

// TLS(HTTPS) 사용 여부에 대한 옵션입니다.
// 서버가 TLS(HTTPS)일 경우 이 값을 true로 입력해야 합니다.
// 기본값: false
option.useTLS = false;

// 메시지에 sequence number를 붙여서 메시지의 유효성을 보장해주는 옵션입니다.
// 기본값: false
option.sequenceValidation = false;

// HTTP 요청 타임아웃 시간을 지정합니다.
// 기본값: 10
option.requestTimeoutInSeconds = 10;

Websocket 옵션

Websocket Transport의 옵션 클래스입니다.

let option = new WebsocketTransportOption();

// TLS(WSS, WebSocket Secure) 사용 여부에 대한 옵션입니다.
// 서버가 TLS을 사용하면, 이 값을 true로 입력해야 합니다.
// 기본값: false
option.useTLS = false;

// 이 값이 true일 경우 연결에 실패하거나 연결이 끊겼을 경우 재연결을 시도합니다.
// 기본값: false
option.autoReconnect = false;

// 메시지에 sequence number를 붙여서 메시지의 유효성을 보장해주는 옵션입니다.
// 기본값: false
option.sequenceValidation = false;

// 클라이언트 Ping 사용 옵션입니다.
// 기본값: false
option.enablePing = false;

// Ping 값을 로그로 표시할지 여부를 결정합니다.
// 기본값: false
option.enablePingLog = false;

// Ping 메시지를 보내는 간격에 대한 시간 값입니다.
// 기본값: 0
option.pingIntervalInSeconds = 0;

// 서버로부터 Ping에 대한 응답을 기다리는 최대 시간 값입니다.
// 이 시간 내에 Ping 응답이 오지 않는다면 서버와의 연결이 불안정한 것으로 보고 해당 Transport 를 중단합니다.
// 인터벌 내에 핑 응답을 받지 못하면 핑을 재전송 하지 않고 핑 응답을 받은 후에 핑을 전송합니다.
option.pingTimeoutInSeconds = 0;

메시지 전송 및 수신

메시지 전송

메시지를 보내고 싶다면 FunapiSession.sendMessage 함수를 호출하면 됩니다.

sendMessage (msgtype, message, protocol = TransportProtocol.DEFAULT)

msgtype 인자는 메시지 타입을 나타냅니다. JSON 은 string 타입, Protocol Buffers 는 string 또는 정수 값을 가진 number 타입입니다.

message 는 실제 데이터가 포함된 메시지로 object 타입입니다. JSON, Protobuf 메시지를 모두 보낼 수 있습니다.

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

JSON 메시지 보내기

평범한 JavaScript 객체를 메시지로 사용합니다.

let message = {};
message.message = "hello world";
session.sendMessage("echo", message);

Protocol Buffers 메시지 보내기

Caution

JavaScript 플러그인으로 사용자가 직접 정의한 Protocol Buffers 메시지를 송수신 하려면 별도의 작업이 필요합니다. 사용자 정의 Protcol Buffers 메시지 사용하기 설명을 참고해 주세요.

Protobuf 메시지의 기본형은 FunMessage 입니다. 사용자가 추가한 메시지는 extend 형태로 되어있습니다. 아래와 같은 방식으로 사용합니다.

let echo = new PbufEchoMessage();
echo.msg = "hello Protocol Buffers";
let msg = new FunMessage();
msg[".pbufEcho"] = echo;
session.sendMessage('pbuf_echo', msg, protocol)

extend 메시지 중 0 ~ 15번까지는 예약된 메시지입니다. 사용자 메시지는 16번 필드부터 사용이 가능합니다.

메시지 수신

서버로부터 메시지를 받기 위해서는 메시지 콜백 함수를 등록해야 합니다. FunapiSession.receivedMessageCallback 에 아래와 같은 방식으로 콜백 함수를 등록합니다.

// 받은 메시지를 처리할 콜백을 등록합니다.
session.receivedMessageCallback.add(
    (msgtype, body, encoding) => {
        if (encoding === FunEncoding.JSON) {
            // JSON 메시지에 대한 처리.
            if (msgtype === "echo") {
               // ...
            }
            // ...
        } else if (encoding === FunEncoding.PROTOBUF) {
            // Protobuf 메시지에 대한 처리 함수입니다.
            // iFun Engine에서 사용하는 protobuf 메시지는 FunMessage를 기본으로 하고
            // 사용자 메시지는 FunMessage 안에 extend 형태로 추가해서 사용합니다.
            if (msgtype === "pbuf_echo") {
                // extend 된 echo 메시지를 가져옵니다.
                let mainMsg = body['.pbufEcho'];
                // ...
            }
            // ...
        }
    });
  • msgtype 인자는 수신한 메시지 타입입니다.

  • body 인자는 메시지 본문입니다. JSON 메시지는 평범한 JavaScript object 이고, Protocol Buffers 메시지인 경우 FunMessage 타입입니다.

  • encoding 인자는 메시지 인코딩 타입입니다. 위에서 언급한 FunEncoding 값 중 하나입니다.

연결 종료 및 재연결

연결 종료

서버와의 연결을 종료하기 위해서는 FunapiSession.stop 함수를 호출하면 됩니다.

session.stop();

인자가 없는 stop 함수는 연결된 모든 Transport 의 연결을 끊습니다. 특정 Transport 의 연결을 끊고 싶다면 파라미터로 프로토콜 타입을 전달하면 됩니다.

session.stop(TransportProtocol.WEBSOCKET);

stop 함수는 Transport 종료를 시작하는 비동기 함수입니다. 종료가 시작되면 해당 메써드는 바로 반환됩니다. 각각의 Transport 가 실제로 반환된 이후에 처리할 로직은 FunapiSession.transportEventCallback 을 통해서 처리해 주세요. 세션에 등록된 모든 Transport 가 종료된 이후에 처리할 로직은 FunapiSession.sessionEventCallback 에 등록하실 수 있습니다.

연결 종료 알림

Transport Event

Transport 가 종료되면 Transport Event 로 TransportEventType.STOPPED 이벤트가 전달됩니다. 이 이벤트는 오류로 중단되었거나, 정상적으로 종료되었거나 상관없이 Transport 가 종료되면 항상 발생합니다.

Transport 이벤트는 FunapiSession.transportEventCallback 에 콜백을 등록해서 받을 수 있습니다.

Session Event

Session 에 등록된 모든 Transport 가 종료되면 Session Event 로 SessionEventType.STOPPED 이벤트가 전달됩니다.

Caution

SessionEventType.CLOSED 이벤트는 Transport 의 연결이 끊겼다고 발생하는 것이 아니라 Session Timeout 이 되거나 서버에서 명시적으로 Session 을 닫았을 경우에만 발생합니다. 서버로부터 Session 이 닫혔다는 메시지를 받으면 모든 Transport 의 연결을 끊고 CLOSED 이벤트를 전달하게 됩니다.

Session 이벤트는 FunapiSession.sessionEventCallback 에 콜백을 등록해서 받을 수 있습니다.

재연결

서버와의 연결을 종료한 후 재연결을 할 때에는 FunapiSession.connect 함수를 사용합니다. 연결할 Transport 의 프로토콜 타입만 파라미터로 전달하면 됩니다.

connect (protocol)
  • protocol 인자는 재연결할 Transport 를 지정합니다. 위에서 언급한 TransportProtocol 값 중 하나입니다.

TransportOption 파라미터가 포함 된 connect 함수를 사용해서 재연결을 할 수도 있지만 한 번 등록된 Transport 의 옵션은 변경할 수 없으며 새로운 TransportOption 을 전달할 경우에는 기존의 Transport 를 삭제하고 새로운 Transport 를 생성해서 연결하게 됩니다. 동일한 프로토콜에 같은 옵션으로 재연결시에는 connect 함수를 protocol 인자만 지정해서 호출하는 것이 좋습니다.

Auto reconnect 옵션

WebSocket 의 경우 연결이 끊겼을 때 자동으로 지속해서 재연결을 시도하게 할 수 있는데 WebsocketTransportOption.autoReconnect 옵션 값을 true로 입력하면 됩니다. autoReconnect 옵션이 true일 경우 연결이 종료된 것을 감지했을 때 항상 재연결을 시도합니다.

재연결을 시작할 때 Transport 의 이벤트인 TransportEventType.RECONNECTING 이벤트가 호출되며 연결에 실패할 경우 TransportEventType.STOPPED 이벤트가 발생합니다.

 // ...
 session.transportEventCallback.add(
     (protocol, type) => {
         if (type == TransportEventType.RECONNECTING) {
             // 재연결 시작시 호출됩니다.
             // 연결이 끊겨서 직접 connect를 호출할 때에는 발생하지 않습니다.
             // AutoReconnect 옵션으로 인해 재연결을 시도할 때에만 호출됩니다.
    }
}

Ping 사용하기

네트워크의 연결 상태를 확인하기 위해 터미널에서 사용하는 ping 이라는 명령어가 있습니다. 플러그인에도 이와 비슷하게 동작하는 ping 기능이 있습니다. 서버와의 연결이 유지되고 있는지 확인하는 용도로 사용할 수 있습니다.

ping 기능을 사용할 경우 주기적으로 서버와의 연결을 체크해서 일정 시간 이상 서버에서 응답이 없을 경우 연결이 끊기고 STOPPED 이벤트가 발생합니다.

예측하기 힘든 모바일 네트워크 환경에서 ping을 사용하여 지속적으로 연결 상태를 검사할 수 있습니다. 이를 통하여 연결이 끊겼는지를 좀 더 빠르게 판단하고 대응할 수 있습니다.

Tip

이 기능은 WebSocket 프로토콜에서만 사용 가능합니다.

Ping 설정

Ping은 FunapiSession.connect 함수를 호출하기 전에 Transport 옵션으로 설정할 수 있습니다.

WebsocketTransportOption option = new WebsocketTransportOption();

// 이 값을 true로 주면 Ping 기능을 사용할 수 있습니다.
// EnablePing의 기본 값은 false 입니다.
option.enablePing = true;
// Ping 값을 로그로 보고 싶다면 이 값을 true로 주면 됩니다.
option.enablePingLog = true;
// Ping 메시지를 보내는 간격을 지정합니다. (초단위)
option.pingIntervalSeconds = 3;
// 서버로부터 Ping 응답을 기다리는 최대 시간을 지정합니다. (초단위)
// 이 시간 이내에 응답이 없을 경우 연결을 끊고 종료 처리를 하게 됩니다.
option.PingTimeoutSeconds = 20;

ping 타임아웃 시간 내에 서버로부터 응답이 하나도 없을 경우 연결을 끊고 종료 처리를 하게 됩니다. 종료가 완료되면 FunapiSession.transportEventCallback 으로 STOPPED 가 전달됩니다.

서버에서 ping 기능을 활성화 하는 법은 세션 Ping(RTT) 에서 확인할 수 있습니다.

서버간 이동

서버의 요청에따라 접속 중인 서버와의 세션을 끊고 새로운 서버로 접속해야 하는 경우가 있을 수 있습니다. 이 과정은 플러그인 내부에서 이루어지며 서버 쪽의 요청으로 이루어집니다.

서버간 이동 상태 확인

서버간 이동이 시작되면 sessionEventCallback 을 통해서 서버간 이동에 관한 진행 상태가 전달됩니다. 아래는 콜백을 통해서 받게 되는 이벤트 타입의 종류입니다.

  • SessionEventType.REDIRECT_STARTED: 서버간 이동이 시작되었을 때(서버로 부터 이동을 요청 받았을 떄) 발생합니다.

  • SessionEventType.REDIRECT_SUCCEEDED: 서버간 이동이 성공적으로 완료되었을 때 발생합니다.

  • SessionEventType.REDIRECT_FAILED: 서버간 이동에 실패하였을 때 발생합니다.

서버간 이동이 시작되면 REDIRECT_STARTED 이벤트를 전달한 후 현재 연결된 서버와의 접속을 종료하고 새로운 서버로 연결을 시작합니다. 서버간 이동이 완료될 때까지는 redirect 관련 이벤트를 제외한 session 이나 Transport 관련 이벤트는 발생되지 않습니다. 연결 도중 오류가 발생해도 어떠한 이벤트도 발생되지 않습니다. 서버간 이동이 성공 또는 실패로 끝나면 REDIRECT_SUCCEEDEDREDIRECT_FAILED 이벤트가 전달됩니다.

서버 이동 후 사용할 Transport 의 옵션

서버를 이동할 때 접속 중인 서버와 이동하는 서버 간의 설정이 다를 수 있습니다. 이럴 경우 클라이언트에서 Session / Transport 의 옵션을 직접 설정해야 합니다. 옵션 값을 설정하지 않은 경우 이동 전 Session 과 Transport 의 옵션을 그대로 사용하게 됩니다. 옵션 값을 전달하는 콜백 함수의 인터페이스는 다음과 같습니다.

// ...

session.sessionOptionHandler.add((flavor) => { /* ... */ });
session.transportOptionHandler.add((flavor, protocol) => { /* ... */ });

flavor 는 서버의 종류를 나타냅니다. 이 타입은 서버에서 정해주는 문자열 값입니다. 서버로부터 이동 메시지를 받으면 이동할 서버의 Transport를 새로 만들기 전에 이 콜백 함수들을 호출합니다. 해당 서버의 종류와 프로토콜 타입에 따라 아래와 같이 옵션을 구성해서 반환하면 됩니다.

Note

이동 전 서버에 동일한 프로토콜의 Transport 가 없을 경우에는 기본 TransportOption 을 사용합니다.

session.sessionOptionCallback.add(
     (flavor) => {
         if (flavor === "lobby") {
             option = new SessionOption();
             option.sendSessionIdOnlyOnce = true;
             return option;
         }
         // 서버 이동전 옵션을 그대로 사용합니다.
         return null;
     });
session.transportOptionCallback.add(
    (flavor, protocol) => {
        if (flavor === "lobby" && protocol === TransportProtocol.TCP) {
            websocketOption = new WebsocketTransportOption();
            websocketOption.sequenceValidation = true;
            return websocketOption;
        }
        // 서버 이동전 옵션을 그대로 사용합니다.
        return null;
    }
);

이미 사용중이던 옵션과 동일한 옵션을 사용하거나 기본 옵션으로 연결해도 되는 경우에는 굳이 이 이벤트 콜백을 등록할 필요는 없습니다.

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

서버 이동 중에 보내는 메시지는 기본적으로 무시됩니다. 이동 중 보내는 메시지의 전송을 보장받고 싶다면 SessionOption.useRedirectQueue 값을 true 로 설정하면 됩니다.

useRedirectQueue 값을 true 로 설정하면 서버 이동 중 보내는 메시지를 큐에 넣어두었다가 이동이 완료된 후 순차적으로 메시지를 전송합니다.

저장된 메시지를 확인하고 싶다면 FunapiSession.redirectQueueCallback 에 콜백 함수를 등록하면 됩니다. 이 콜백을 등록해두면 서버 이동이 완료된 후 저장된 메시지를 전송하기 전에 이 콜백이 호출됩니다.

session.redirectQueueCallback.add( (protocol, queue, currentTags, targetTags) => { /* ... */ });

저장된 메시지의 프로토콜 별로 콜백 함수가 호출됩니다. queue 인자는 이동을 준비하면서 보내지 못한 메시지들의 목록입니다. currentTags 는 이전에 접속중이던 서버의 태그 배열이며 targetTags 는 이동한 서버의 태그 배열입니다. 이 목록에 들어 있는 값으로 서버의 종류를 구분할 수 있습니다.

콜백이 호출되었을 때 큐잉된 메시지 중 보내고 싶지 않은 메시지를 선별할 수 있습니다. discard 값을 true 로 설정해두면 해당 메시지는 전송하지 않고 버려집니다.

for (let msg of queue) {
    if (<조건>) {
        msg.discard = true;
    }
}

서버 이동 중에 전송을 보장할 메시지를 구분해야할 경우 이 콜백을 등록해주세요.

멀티캐스팅과 채팅

플러그인의 멀티캐스팅 기능을 사용하면 원하는 채널에 접속해서 채널의 모든 유저들과 메시지를 주고 받을 수 있습니다. FunapiSession 객체를 이용해 멀티캐스팅 역할을 하는 서버와 연결을 하고 메시지를 전송합니다. FunapiMulticastClient 객체를 만들면서 FunapiSession 객체를 전달하여 멀티캐스팅 기능을 사용할 수 있습니다.

FunapiMulticastClient 는 같은 공간(채널)에 있는 모든 유저에게 메시지를 보내고자 할 때 사용할 수 있습니다. _bounce 옵션을 true 로 주면 보내는 주체인 ‘나’도 그 메시지를 받을 수 있습니다.

채팅에 특화된 클래스인 FunapiChatClientFunapiMulticastClient 를 상속받아 만들어진 파생 클래스입니다. FunapiChatClient 는 텍스트만 주고 받는 클래스로 플레이어간 채팅에 사용하기 적합합니다.

멀티캐스팅은 WebSocket 을 사용 하며, Transport 를 따로 설정하지 않을 경우 디폴트 값을 사용합니다. Encoding 은 연결된 FunapiSession에서 사용하는 방식을 따릅니다. JSON과 Protobuf 를 사용할 수 있습니다.

Caution

멀티캐스팅은 하나의 session 마다 하나의 객체만 연결할 수 있습니다. FunapiMulticastClient 클래스와 FunapiChatClient 클래스도 역시 동시에 두 개를 같은 Session으로 연결할 수 없습니다.

멀티캐스팅 인터페이스

FunapiMulticastClient 에는 다음과 같은 메써드가 존재합니다.

class FunapiMulticastClient {

    // 생성자입니다.
    // FunapiSession 와 주고 받을 메시지의 Encoding을 입력받습니다.
    constructor(session, encoding, protocol = TransportProtocol.DEFAULT) { /* ... */ }

    // FunapiMulticastClient 에서 사용하던 리소스를 반환합니다.
    destroy() { /* ... */ }

    // channel_id 가 입장한 채널의 id 인지 확인하는 property 입니다.
    // 입장한 채널이면 true를 반환합니다.
    inChannel(channel_id) { /* ... */ }

    // 채널 목록을 요청하는 함수입니다.
    // 채널 목록과 함께 채널에 있는 유저 수도 함께 전달됩니다.
    // 요청에 성공하면 ChannelListCallback에 등록한 함수가 호출됩니다.
    requestChannelList() { /* ... */ }

    // 채널에 입장할 때 사용하는 함수입니다.
    // channelId 는 입장할 channel ID 입니다. 존재하지 않는 채널이면 서버에서 생성합니다.
    // handler는 메시지를 전달받으면 호출되는 함수입니다.
    // 이미 channel_id에 해당하는 채널에 입장했거나 서버와 연결되어 있지 않으면 false 를 반환합니다.
    joinChannel(channel_id, handler, token) { /* ... */ }

    // 채널을 나갈 때 사용하는 함수입니다.
    // channel_id는 나갈 channel ID 입니다.
    // 입장한 채널이 없거나 서버와 연결되어 있지 않으면 false 를 반환합니다.
    leaveChannel(channel_id) { /* ... */ }

    // 입장해 있는 모든 채널에서 나갈 때 사용하는 함수입니다.
    leaveAllChannel() { /* ... */ }

    // 채널에 메시지를 전송할 때 사용하는 함수입니다.
    sendToChannel(message) { /* ... */ }
}

아래와 같은 속성들을 사용하실 수 있습니다.

// ...
let multicast = new FunapiMulticastClient(session, TransportProtocol.WEBSOCKET, FunEncoding.JSON);

// 내 ID(name)입니다.
// sender를 지정하면 채널 입/퇴장이나 메시지를 보낼 때 sender를 함께 전송합니다.
// 나를 포함한 채널의 모든 유저가 해당 메시지를 보낸 이를 확인할 수 있습니다.
multicast.sender = "johndoh";

// 서버와 연결되어 있는지 확인하기 위한 property 입니다.
// 서버와 연결이 되어 있다면 true를 반환합니다.
multicast.isConnected;

// 사용 중인 인코딩 타입입니다.
multicast.encoding;

아래와 같이 콜백들을 등록합니다.

// 채널 목록을 받으면 호출되는 콜백을 등록합니다.
multicast.channelListCallback.add((channelList) => { /* ... */ });

// 채널에 입/퇴장하면 호출되는 콜백을 등록합니다.
// 채널의 입/퇴장 알림을 받고 싶다면 JoinedCallback이나 LeftCallback을 등록하면 됩니다.
multicast.joinedCallback.add((channelId, sender) => { /* ... */ });
multicast.leftCallback.add((channelId, sender) => { /* ... */ });

// 에러가 발생했을 때 호출되는 콜백을 등록합니다
// errorCode 인자는 FunapiMulticastMessage.ErrorCode 값 중 하나입니다.
multicast.errorCallback.add((errorCode) => { /* ... */ });

멀티캐스팅 예제

FunapiMulticastClient 로 멀티캐스팅을 처리하기 위한 사전 준비로써, 먼저 FunapiSession 을 만들고 WebSocket 으로 connect 합니다.

const serverIp = "127.0.0.1";

// 우선 FunapiSession 을 만듭니다.
let session = FunapiSession.create(serverIp);

// Transport의 이벤트를 받기 위한 콜백함수를 등록합니다.
session.transportEventCallback.add(onTransportEvent);

// 서버에 접속합니다.
session.connect(TransportProtocol.WEBSOCKET, FunEncoding.JSON, 8012);

이제 메시지를 전송하기 위해 FunapiMulticastClient 를 만들겠습니다.

채널 입/퇴장을 알려주는 콜백 함수를 등록할 수 있습니다. 나를 포함해서 채널에 들어오고 나가는 모든 유저들의 입/퇴장을 알려줍니다.

joinedCallback

유저가 채널에 입장하면 호출되는 함수

leftCallback

유저가 채널에서 나가면 호출되는 함수

errorCallback

오류가 발생하면 에러코드를 전달해주는 함수

// FunapiMulticastClient를 만듭니다.
// 위에서 만든 session과 session과 같은 encoding 타입을 전달합니다.
let multicast = new FunapiMulticastClient(session, FunEncoding.JSON);

// 내 아이디를 입력합니다.
multicast.sender = "my name";

// 채널 목록을 받았을 때 호출되는 콜백입니다.
multicast.channelListCallback.add((channelList) => {
    onMulticastChannelList(encoding, channel_list);
});

// Player가 채널에 입장하면 호출되는 콜백입니다.
multicast.joinedCallback.add((channelId, sender) => {
    console.log("joinedCallback is called. player:%s", sender);
});

// Player가 채널에서 퇴장하면 호출되는 콜백입니다.
multicast.leftCallback.add((channelId, sender) => {
    console.log("leftCallback called. player:%s", sender);
});

// 에러가 발생했을 때 알림을 받는 콜백입니다.
// 에러 종류는 enum FunMulticastMessage.ErrorCode 타입을 참고해주세요.
multicast.errorCallback.add((errorCode) => {
    // error
});;

채널에 입장하기 위해서 joinChannel() 함수를 호출합니다. 해당 채널로부터 메시지가 전송되면 onMulticastChannelReceived 함수가 호출됩니다.

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

// 입장할 채널 id 를 입력합니다.
// 채널이 존재하지 않으면 JoinChannel() 함수를 호출할 때 채널이 생성됩니다.
const channelId = "test_channel";

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

// 채널에 메시지 전송
let helloMsg = new PbufHelloMessage();
hello_msg.message = "multicasting message";

PbufHelloMessage 는 서버의 {project}_messages.proto 파일에 존재합니다. funapi_initiator 로 처음 프로젝트를 생성하면 기본적으로 추가되어 있습니다. PbufHelloMessage는 예제를 위해 기본 생성된 것이며 MulticastServer 에서는 proto message 의 이름이나 field 를 제한하지 않습니다. 따라서 필요한 이름으로 proto message 를 생성하고 사용할 field 들을 자유롭게 정의하시면 아이펀 엔진에서는 이를 알아서 클라이언트에게 전송합니다. 이것은 JSON 도 마찬가지입니다.

아래는 서버의 {project}_messages.proto 파일에 있는 기본 내용입니다.

message PbufEchoMessage {
  required string msg = 1;
}

message PbufAnotherMessage {
  optional string msg = 1;
}

extend FunMessage {
  optional PbufEchoMessage pbuf_echo = 16;
  optional PbufAnotherMessage pbuf_another = 17;
}


import "funapi/service/multicast_message.proto";

message PbufHelloMessage
{
  optional string message = 1;
}
extend FunMulticastMessage
{
  optional PbufHelloMessage pbuf_hello = 16;
}

서버를 빌드할 때 사용자가 기본 생성된 proto 파일을 수정해서 빌드할 수 있습니다. 이런 경우 JavaScript 플러그인은 해당 proto 파일을 적용시켜주는 별도의 과정이 필요합니다. 이에 대한 자세한 설명은 사용자 정의 Protcol Buffers 메시지 사용하기 를 참고해 주세요. 사용자의 편의를 위해 기본 배포되는 JavaScript 플러그인은 PbufHelloMessage Protocol Buffers 메시지를 포함하고 있습니다. 만약 해당 메시지를 제거하거나 수정한 proto 파일을 JavaScript 플러그인에 적용하였다면, 이 예제는 정상 동작하지 않습니다.

function onMulticastChannelReceived (channelId, sender, body)
{
    if (multicast.encoding === FunEncoding.JSON) {
        // 채널이 맞는지 확인합니다.
        let channel = body["_channel"];

        let message = body["_message"];

        // 여기서는 간단하게 로그를 출력하겠습니다.
        console.log("Received a multicast message from the '%s' channel. Message: %s",
                    channel_id, message);
    } else {
        // 수신한 메시지를 Protobuf인 FunMulticastMessage로 처리합니다.
        // MulticastMessageType 은 서버를 빌드할 때 proto 파일도 함께 빌드하면 자동 생성됩니다.
        // 이 예제에서는 PbufHelloMessage를 사용하므로 이것의 field 이름인 pbuf_hello를 사용합니다.
        const helloMsg = body[".pubfHello"];
        if (helloMsg == null)
            return;

        // 메시지를 전송할 때 PbufHelloMessage의 message field에 메시지를 저장합니다.
        // 따라서 helloMsg.message 에 전달된 메시지가 담겨 있습니다.

        console.log("Received a multicast message from the '%s' channel. Message: %s",
                     channelId, helloMsg.message);
    }
}

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

// channel id를 입력합니다.
const channelId = "test_channel";

if (multicast.encoding == FunEncoding.JSON) {
    // JSON으로 메시지를 전송합니다.
    let mcastMsg = {};

    // _channel 필드에 channel ID를 입력합니다.
    mcastMsg["_channel"] = channelId;

    // 보낸 메시지를 나도 받고 싶다면 _bounce 필드값을 true로 설정합니다.
    mcastMsg["_bounce"] = true;

    mcastMsg["_message"] = "multicast test message";

    // 멀티캐스팅 역할을 하는 서버에 메시지를 전송합니다.
    multicast.sendToChannel(mcastMsg);
} else {
    // protobuf로 메시지를 전송합니다.
    // PbufHelloMessage 파일은 서버의 {project}_messages.proto 파일에 정의 되어 있습니다.

    let helloMsg = new PbufHelloMessage();

    // message field에 전송할 메시지를 담겠습니다.
    helloMsg.message = "multicast test message";

    // 멀티캐스팅 메시지를 전송하기 위해서 FunMulticastMessage 를 생성합니다.
    // 이 부분은 멀티캐스팅 역할을 하는 서버에서 필요한 proto message 이므로 반드시 필요한 부분입니다.
    let mcastMsg = new FunMulticastMessage();

    // channel field 에 channel ID 를 입력합니다.
    mcastMsg.channel = channelId;

    // 보낸 메시지를 나도 받고 싶다면 bounce 필드 값을 true로 설정합니다.
    mcastMsg.bounce = true;

    // 멀티캐스팅 역할을 하는 서버에 메시지를 전송합니다.
    multicast.sendToChannel(mcastMsg);
}

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

multicast.leaveChannel("test_channel"));

채팅 인터페이스

FunapiChatClient 클래스를 사용합니다. FunapiMulticastClient 와의 차이점은 string 타입으로 메시지를 보낼 수 있는 sendText 메써드가 추가되었다는 것입니다.

// 채널에 메시지를 전송할 때 사용하는 함수입니다.
// chat_channel은 메시지를 전송할 channel 의 id 입니다.
// text는 전송할 메시지입니다.
class FunapiMulticastClient {
    // ...
    public sendText (channel_id, text) { /* ... */ }
}

채팅 예제

FunapiChatClient 는 멀티캐스팅 기능을 활용하여 만든 클래스로써 사용법은 FunapiMulticastClient 와 매우 유사합니다. FunapiChatClient 를 사용하는 간단한 예제입니다.

예제에서는 encoding 을 Json 으로 하겠습니다.

// FunapiSession을 생성합니다.
let session = FunapiSession.create(serverIp);
session.transportEventCallback.add(onTransportEvent);

// 서버에 연결합니다.
session.connect(TransportProtocol.WEBSOCKET, FunEncoding.JSON, 8012);

// ...

// FunapiChatClient 객체를 생성합니다.
let chat = new FunapiChatClient(session, FunEncoding.JSON);

// 입장하는 플레이어의 이름을 입력합니다.
chat.sender = "my name";

// 채널 입장
const channelId = "test_channel";
chat.joinChannel(channelId, onMulticastChannelReceived);

메시지를 전달받으면 onMulticastChannelReceived 함수가 호출됩니다.

// 메시지를 전달받으면 호출됩니다.
// FunapiMulticastClient 와는 달리 encoding에 따라 message를 처리하지 않아도 됩니다.
// 메시지 보낸 사람인 sender와 전송된 메시지인 text를 처리하시면 됩니다.
function onMulticastChannelReceived (chatChannel, sender, text) {
    // 여기서는 간단하게 로그를 출력하겠습니다.
    console.log("Received a chat channel message. Channel=%s, sender=%s, text=%s",
                chatChannel, sender, text);
}

sendText() 함수를 이용하여 채널에 메시지를 전송합니다.

// 채널에 메시지 전송
const channelId = "test_channel";
const message = "Hello World";
chat.sendText(channelId, message);

채널에서 나갈 때는 leaveChannel() 함수를 사용합니다.

// 채널 나가기
chat.LeaveChannel(channelId);

공지사항 확인하기

엔진에서 제공하는 공지서버를 사용하고 있다면 클라이언트 플러그인을 통해 공지서버로부터 공지사항 목록을 받을 수 있고 언제든지 원하는 시점에 공지사항을 업데이트할 수 있습니다.

FunapiAnnouncement 클래스로 서버에 공지사항 목록을 요청할 수 있습니다.

class FunapiAnnouncement {
    constructor() { /* ... */ }

    // 공지사항 서버의 ip와 port 번호를 주소로 초기화 합니다. ex)"http://127.0.0.1:8080"
    init(url) { /* ... */ }

    // 공지사항 목록을 가져옵니다.
    updateList(count_max) { /* ... */ }

    // 각 공지사항의 정보를 가져옵니다.
    getAnnouncement(index) { /* ... */ }
}

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

function onAnnouncementResult (result) {
    // 결과가 Success가 아닐경우
    if (result !== AnnounceResult.SUCCEEDED)
        return;

    if (announcement.listCount > 0) {
        for (let i = 0; i < announcement.listCount; ++i) {
             // 각각의 목록에 대해 정보를 가져올 수 있습니다.
             let item = announcement.getAnnouncement(i);

             console.log(
                 "Announcement #%d. subject:%s, date:%s, message:%s, link URL:%s, image URL:%s",
                 item.subject,
                 item.date,
                 item.message,
                 item.link_url,
                 item.image_url);
        }
    }
 }


 let announcement = new FunapiAnnouncement();

 // 공지사항은 웹 서비스입니다.
 // 서버에서 공지사항 용도로 열어 둔 ip와 port를 주소로 요청합니다.
 announcement.init("http://127.0.0.1:8080");
 announcement.resultCallback.add(onAnnouncementResult);

 // 이 함수를 호출하면 목록을 가져오거나 이미 있을경우 갱신해줍니다.
 // 가져올 개수를 인자로 넘깁니다.
 announcement.updateList(5);

공지사항에 들어가는 항목들은 정해져 있지 않습니다. 서버 관리자가 필요한 항목들을 정의해서 사용하면 됩니다.

서버 점검 메시지

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

점검 안내 메시지 처리는 메시지 수신 콜백이 아닌, FunapiSession.maintenanceCallback 함수로 전달됩니다. 서버 점검 메시지를 처리하고 싶다면 maintenanceCallback 함수를 등록하고 콜백 함수에서 필요한 작업을 수행하면 됩니다.

서버 점검 메시지를 받으면 아래와 같은 방식으로 파싱해서 사용하면 됩니다. 메시지 내용은 JSON 타입으로 점검 시작 일시, 종료 일시, 메시지 로 이루어져 있습니다.

date_start

string

서버 점검 시작 일시

date_end

string

서버 점검 종료 일시

messages

string

메시지

// ...

// 서버로부터 점검 안내 메시지를 받았을 때 이 콜백함수가 호출됩니다.
session.maintenanceCallback.add(
    (body, encoding) => {
        if (encoding === FunEncoding.JSON) {
            console.log("Maintenance message start: %s, end: %s, message: %s",
                        body.date_start,
                        body.date_end,
                        body.messages);
        } else if (encoding === FunEncoding.PROTOBUF) {
          let maintenance = body[".pbufMaintenance"];
          if (!maintenance)
            return;

          console.log("Maintenance message start: %s, end: %s, message: ",
                       maintenance.date_start, maintenance.date_end, maintenance.messages);
        }
    });

사용자 정의 Protcol Buffers 메시지 사용하기

사용자가 직접 정의한 Protocol Buffers 메시지를 사용할 수 있습니다. 서버 프로젝트 최초 생성 시 자동으로 생성되는 {project}_messages.proto 파일을 수정하였다면, 추가 작업이 필요합니다.

공식적으로 배포하는 ifun-engine-plugin.bundle.js 에는, iFun Engine 이 기본적으로 사용하는 메시지만 사용할 수 있도록 구성되어 있습니다. 수정된 proto 파일을 사용하기 위해서는 아래 과정을 거쳐, 수정된 proto 파일이 적용된 플러그인 파일을 새롭게 빌드해야 합니다.

NPM 설치 확인 및 설치

JavaScript 패키지 매니저로 널리 알려진 NPM 을 사용합니다. NPM 이 이미 설치되어 있다면 이 단계는 무시하셔도 됩니다.

NPM 설치 여부가 불확실 하면 아래 명령어로 확인합니다.

$ npm -v

만약 해당 명령어 실행이 실패하거나, 6.12 미만이라면 설치 또는 업그레이드가 필요합니다.

https://nodejs.org/ 에서 다운로드 받아서 설치하거나, 다른 방법으로 NPM 을 설치해 주세요. npm -v 명령을 실행하였을 때, 버전이 6.12 이상으로 나오면 됩니다.

NPM (더미) 프로젝트 생성

수정한 proto 파일을 적용하기 위해, 임시로 더미 NPM 프로젝트를 생성합니다. 적당한 디렉토리로 이동해 아래 명령어를 실행합니다. ifun-engine-js-proto 디렉터리를 생성하고, 진입한 후에 NPM 프로젝트를 생성합니다.

$ mkdir ifun-engine-js-proto
$ cd ifun-engine-js-proto
$ npm init -y

@ifunfactory/engine-plugin-js 패키지 다운로드

아래 명령어로 @ifunfactory/engine-plugin-js 패키지를 다운로드 합니다.

$ npm install @ifunfactory/engine-plugin-js@beta

Note

@beta 는 현재 JavaScript 플러그인이 베타 상태이기 때문에 필요합니다. 정식 릴리즈 이후에는 제거해도 설치가능해질 예정입니다.

node_modules/@ifunfactory/engine-plugin-js 디렉터리가 생성됩니다.

proto 파일 적용

사용자가 수정한 proto 파일을 플러그인에 적용해야 합니다. 수정된 proto 파일을 아래 명령어를 이용하거나, 다른 방법을 이용해서 node_modules/@ifunfactory/engine-plugin-js/proto/customized.proto 에 복사합니다.

$ cp <사용자 proto 파일> node_modules/@ifunfactory/engine-plugin-js/proto/customized.proto

사용자 정의 플러그인 빌드

아래 명령어를 실행해서 node_modules/@ifunfactory/engine-plugin-js 디렉터리에 진입 후, 사용자 정의 proto 파일이 적용된 JavaScript 플러그인을 다시 생성합니다.

$ cd node_modules/@ifunfactory/engine-plugin-js
$ npm install
$ npm run build

위 명령어가 성공적으로 실행되면 dist 디렉터리에 ifun-engine-plugin-customized.bundle.js 파일이 생성됩니다.

플러그인 JS 파일 교체

공식 배포되는 ifun-engine-plugin.bundle.js 대신, 위에서 생성한 파일인 ifun-engine-plugin-customized.bundle.js 파일을 사용합니다.

import { FunapiSession } from "./ifun-engine-plugin.bundle.js";

위와 유사하게 작성한 부분을, 아래와 같이 수정해, 직접 정의한 Protocol Buffers 메시지를 import 하여 사용하실 수 있습니다.

import { FunapiSession, CustomizedMessage } from "./ifun-engine-plugin-customized.bundle.js";

JavaScript Bundler (Rollup, NPM) 단일 파일로 묶기

JavaScript 를 이용해 웹 애플리케이션을 만들 때, 외부 라이브러리를 많이 사용하는 등의 이유로 webpack, Rollup 같은 bundler 를 사용하는 경우가 빈번합니다. Bundler 를 사용하지 않아도 개발 및 서비스는 가능하지만, 외부 라이브러리를 다수 사용하고, 복잡도를 관리하기 위해 JS 파일을 분리하기 시작하면 모듈을 import 할 때마다 HTTP 요청이 발생해서 로딩 성능이 저하됩니다. JS 라이브러리가 모두 ECMAScript6 module 형식인 것도 아니기 때문에, 모듈 형식을 적절히 자동으로 변환해야 하는 경우도 있습니다. 그 밖에도 브라우저 지원 범위를 넓히거나, JS 파일 크기 줄이기, 코드 난독화 등의 목적을 위해 변환하는 경우도 있습니다.

JavaScript 클라이언트 플러그인은 bundler 를 통해, JS 파일을 단일 파일로 묶어서 사용하는 시나리오를 지원합니다. 이 섹션에서는 RollupNPM 을 이용해 JS 파일을 단일 파일로 묶어서 사용하는 법을 설명합니다.

이 섹션에서는 NPM 이 설치되어 있다고 가정합니다. NPM 설치에 관한 내용은 NPM 설치 확인 및 설치 섹션을 참고해 주세요.

NPM 패키지 디렉토리 생성

아직 게임 클라이언트를 위한 NPM 패키지 디렉토리가 없다면 아래 명령어로 생성해 주세요. 패키지 이름을 지정해야 하는데 ifun-js-game 이라고 가정하겠습니다. 이미 사용중인 패키지 디렉토리가 있다면 본 절차는 무시하셔도 됩니다.

$ mkdir ifun-js-game
$ cd ifun-js-game
$ npm init

npm init 명령어 이후 나오는 질문들에 적절히 답해 주세요. 추후 변경 가능하므로, 우선 진행하는데는 기본값으로도 충분합니다.

플러그인 패키지 설치 및 의존성 설정

생성된 패키지 디렉토리에 플러그인에 대한 의존성을 설정합니다. 아래 명령어를 실행하면 플러그인에 대한 의존성이 pakcage.json 에 설정되고, 플러그인 패키지 및 의존하는 패키지들을 다운로드 받습니다.

$ npm install @ifunfactory/engine-plugin-js@beta

Note

@beta 는 현재 JavaScript 플러그인이 베타 상태이기 때문에 필요합니다. 정식 릴리즈 이후에는 제거해도 설치가능해질 예정입니다.

Rollup 설정

게임 클라이언트가 사용할 모듈의 main JS 파일을 생성 및 설정합니다. 아래 명령어로 소스 및 산출물 디렉토리를 생성합니다.

$ mkdir -p src dist

src/index.js 파일을 아래 내용으로 생성합니다.

src/index.js
// 플러그인이 export 하는 모든 심볼을 ifun 으로 접근할 수 있도록 합니다.
import * as ifun from "@ifunfactory/engine-plugin-js";

// ifun 을 통해 플러그인 기능을 사용합니다.
let session = ifun.FunapiSession.create("localhost");

위 예제는 플러그인 기능을 정상적으로 사용할 수 있는지 확인하기 위한 간단한 테스트 코드만 넣었습니다.

Rollup 을 이용해 빌드할 수 있도록, 설정 파일을 생성합니다. rollup.config.js 파일을 아래와 같이 생성합니다.

rollup.config.js
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';

export default [{
        input: 'src/index.js',
        output: [{
            file: "dist/bundle.js",
            format: 'iife'
        }],
        plugins: [
            resolve(),
            commonjs({
                exclude: [
                    "node_modules/lodash-es/**.js"
                ],
                namedExports: {
                    "protobufjs/minimal": ["Reader", "Writer", "util", "roots"]
               }
            })]
}];

npm 커맨드를 이용해 빌드를 쉽게 할 수 있도록 package.json 파일에 아래처럼 라인을 추가합니다.

package.json
{
  "name": "ifun-js-game",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "rollup -c"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@ifunfactory/engine-plugin-js": "^1.0.0-1",
    "rollup": "^2.8.1"
  }
}

빌드

아래처럼 명령어로 빌드를 실행해 단일한 JS 파일을 생성합니다.

$ npm run build

생성된 JS 파일은 dist/bundle.js 파일에 생성됩니다. 해당 파일은 브라우저에 바로 적용해서 사용하실 수 있습니다.

적용

게임 클라이언트에서 사용할 HTML 파일에 아래와 같이 JS 파일을 적용합니다. HTML 파일과 JS 파일이 동일한 디렉터리에 있다고 가정합니다.

<!-- ... --->
<body>
    <!-- ... -->
    <script src="./bundle.js">
</body>
<!-- ... -->

사용자 정의 Protocol Buffers 메시지 사용

아이펀 엔진 서버 프로젝트에서 기본생성한 proto 파일을 수정한 경우, 사용자 정의 Protcol Buffers 메시지 사용하기 섹션에서 언급한 추가 작업이 필요합니다.

우선 위에서 언급한 과정을 플러그인 패키지 설치 및 의존성 설정 까지 진행 하신 후, proto 파일 적용 단계와 사용자 정의 플러그인 빌드 단계를 순서대로 진행합니다.

이후 개발 작업에서 아래같은 방식으로 Protocol Buffers 메시지를 import 해서 사용하실 수 있습니다.

import * as ifun from "@ifunfactory/engine-plugin-js";

ifun.CustomizedMessage

또는 아래처럼 사용합니다.

import { CustomizedMessage } from "@ifunfactory/engine-plugin-js";

Note

만일 proto 파일이 변경된다면, 이 섹션에 기술한 과정을 다시 반복해주세요.

TypeScript 사용하기

TypeScript 를 사용할 수 있습니다. TypeScript 를 사용하기 위해서는 NPMRollup 같은 bundler 가 필요합니다.

본 섹션에서는 TypeScript 로 코드를 작성하고, NPMRollup 을 사용해 빌드하는 방법을 기술합니다.

아래 단계는 이미 진행되어 있다고 가정합니다. 아래 단계를 먼저 진행해 주세요.

TypeScript 설치

아래 명령어로 TypeScript 를 설치하고, 의존성을 지정합니다.

$ npm install typescript

TypeScript 설정 파일 추가

TypeScript 빌드 시 사용하게 되는 설정파일을 tsconfig.json 아래와 같이 추가합니다.

tsconfig.json
{
    "compilerOptions": {
        "moduleResolution": "node",
        "outDir": "dist",
        "target": "ES2015",
        "module": "ES2015"
    }
}
  • "moduleResolution" 은 모듈을 찾는 방식을 지정합니다. "node" 로 지정하지 않으면, 빌드가 실패하므로 변경하지 않아야 합니다.

  • "outdir" 은 빌드된 JS 파일이 저장될 디렉토리를 지정합니다. 이 예제에서는 흔히 사용하는 dist 디렉토리로 지정하였습니다.

  • "target" 은 빌드해서 생성할 JS 파일의 JavaScript 의 버전을 지정합니다. 이 예제에서는 ES2015(=ES6) 형식으로 생성하도록 하였습니다. https://www.typescriptlang.org/v2/tsconfig#target 을 참고해서 legacy 형식이나, 더 최근의 표준으로 변경할 수 있습니다.

  • "module" 은 빌드해서 생성할 JS 파일이 따를 모듈 형식을 지정합니다. 이 예제에서는 ES2015(=ES6) 모듈 형식으로 빌드하도록 하였습니다. 만약 다른 모듈 형식(e.g. AMD)이 필요하다면, https://www.typescriptlang.org/v2/tsconfig#module 을 참고해서 변경할 수 있습니다.

NPM build 명령어 추가

npm 명령어를 통해 빌드할 수 있도록 아래와 같이 package.jsonbuild 커맨드를 추가합니다.

package.json
{
  "name": "ifun-js-game",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "tsc"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@ifunfactory/engine-plugin-js": "^1.0.0-3",
    "typescript": "^3.8.3"
  }
}

이제 npm run build 커맨드를 통해 빌드할 수 있습니다. dist 디렉토리에 각 .ts 파일에 해당하는, .js 파일이 생성됩니다.

Rollup 이용해서 단일 파일로 묶기

빌드해서 생성된 JS 파일은 웹 브라우저에서 바로 사용할 수 없습니다. JS 파일이 의존하는 라이브러리들을 같이 묶어야 합니다. Rollup 을 이용해, JS 파일들을 의존하는 라이브러리를 포함시켜 하나의 JS 파일을 생성합니다.

Rollup 을 이용해 빌드할 수 있도록, 설정 파일을 생성합니다. rollup.config.js 파일을 아래와 같이 생성합니다.

rollup.config.js
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';

export default [{
        input: 'dist/index.js',
        output: [{
            file: "dist/bundle.js",
            format: 'iife'
        }],
        plugins: [
            resolve(),
            exclude: [
                "node_modules/lodash-es/**.js"
            ],
            commonjs({
                namedExports: {
                    "protobufjs/minimal": ["Reader", "Writer", "util", "roots"]
               }
            })]
}];

'dist/index.js' 는 TypeScript 로 작성한 파일 중, main 함수가 포함되어 있는 파일이 컴파일되어 변환된 JS 파일 경로를 지정하시면 됩니다.

npm 커맨드를 이용해 빌드를 쉽게 할 수 있도록 package.json 파일의 build 커맨드를 아래처럼 수정합니다.

package.json
{
   "name": "ifun-js-game",
   "version": "1.0.0",
   "description": "",
   "main": "index.js",
   "scripts": {
     "test": "echo \"Error: no test specified\" && exit 1",
     "build": "tsc && rollup -c"
   },
   "keywords": [],
   "author": "",
   "license": "ISC",
   "dependencies": {
     "@ifunfactory/engine-plugin-js": "^1.0.0-3",
     "typescript": "^3.8.3"
   }
}

이제 npm run build 커맨드를 실행하면 웹 브라우저에서 사용가능한 dist/bundle.js 파일이 생성됩니다.

Note

TypeScript 파일을 빌드하고 하나의 파일로 묶는 방법은 이 섹션에서 기술한 방법 이외에도 매우 다양합니다. @ifunfactory/engine-plugin-js 패키지를 import 해서 사용할 수 있는 설정 하에서라면, 어떠한 bundler 를 사용해서 빌드 및 bundling 을 해도 무방합니다. 예를 들어 TypeScript 에서 직접 제공하는 명령어 인 tsc 명령어 대신, Rollup TypeScript plug-in(@rollup/plugin-typescript) 을 활용해 Rollup 으로 한 번에 빌드하는 방법이나, webpack 과 같은 다른 bundler 를 사용하는 것도 가능합니다.

이러한 방식을 모두 기술하는 것은 이 문서의 범위를 벗어나므로, 각 bundler 에서 TypeScript 를 사용하는 방법을 기술한 문서를 참조해 주세요.