콘텐츠 지원 Part 3: 멀티캐스팅,채팅

멀티캐스팅과 채팅은 채널에 연결돼 있는 모든 유저들에게 메시지를 전송할 수 있는 기능입니다. 이 때 같은 채널에 속한 경우 다른 서버에 속한 유저들이라 하더라도 모두 메시지를 받게 됩니다.

멀티캐스팅과 채팅의 차이는 다음과 같습니다.

  • 멀티캐스팅은 전송하려는 메시지에 대한 제한이 없습니다. JSON 또는 Protobuf 를 자유롭게 보낼 수 있으며, 해당 메시지가 채널에 연결된 모든 유저들에게 그대로 전달됩니다.

    Tip

    임의로 메시지를 보내고 공유 받을 수 있다는 특성 덕분에 멀티캐스팅 기능은 그자체로 릴레이 서버로 이용될 수 있습니다.

  • 채팅은 유저들간에 채팅 메시지를 주고 받는 것에 특화되어 있습니다. 그래서 문자열만 입력할 수 있고 그 문자열만 전달 가능합니다. 채팅은 멀티캐스팅의 특수한 경우로 멀티캐스팅 기능을 이용하여 처리됩니다.

멀티캐스팅 이용을 위한 설정

MANIFEST 설정

아래와 같이 MANIFEST.json 파일을 설정합니다.

Important

멀티캐스팅과 채팅을 사용하기 위해서는 RpcService 역시 활성화 되어야 합니다. RpcService 의 설정에 대한 자세한 설명은 분산처리 기능관련한 설정들 를 참고하세요.

{
    "dependency": {
        .....,

        "MulticastServer": {
          "transport_for_multicast_channel": "tcp",
          "max_member_count_per_channel": 0
        },

        ....,

        "RpcService": {
          "rpc_enabled": true,
          ...
        }
    }
}
  • transport_for_multicast_channel: 멀티캐스트 시 사용할 Transport(연결)을 지정 합니다. tcp와 websocket 중 선택할 수 있습니다. (type=string, default=”tcp”)

  • max_member_count_per_channel: 채널 당 유저 정원을 지정합니다. 0 이면 무제한으로 지정됩니다. 0 이 아닌 경우 2 이상 부터 지정 가능합니다. (type=uint64, default=0)

    Note

    채널 입장 정원을 1명으로 설정할 수는 없습니다.

서버 코드

여러분이 멀티캐스팅 기능을 사용하기 위해서 서버에서 해야 할 일은 앞에서 설명한 MANIFEST 설정 뿐입니다. 그런 다음 클라이언트 플러그인에 구현되어 있는 멀티캐스팅과 채팅 기능을 사용하면 플러그인과 아이펀 엔진이 알아서 처리하도록 되어 있습니다.

추가로 메시지 후킹처럼 서버에서 이용 가능한 멀티캐스팅 기능은 서버에서 멀티캐스팅 다루기 에서 설명합니다.

클라이언트 코드

클라이언트의 멀티캐스팅과 채팅 구현은 클라이언트 플러그인에 이미 구현되어있습니다. 자세한 설명은 클라이언트 플러그인 문서의 멀티캐스팅과 채팅 을 참고하세요.

Important

클라이언트에서 멀티캐스팅과 채팅을 동시에 사용할 수 없습니다. 멀티캐스팅(FunapiMulticastClient) 만 사용하거나 채팅(FunapiChatClient) 만 사용해야 합니다.

서버에서 멀티캐스팅 다루기

서버에서 MANIFEST 를 설정하고 나면 클라이언트 기능만으로 기본적인 멀티캐스팅 기능을 사용할 수 있지만 서버를 통해서 다음과 같은 부가 기능들을 제공합니다.

멀티캐스팅 채널 생성하기

멀티캐스팅 기능에서 채널 생성은 클라이언트가 입장하고자 하는 채널이 존재하지 않으면 생성과 동시에 입장합니다. 그러나, 다음과 같이 서버 쪽에서 미리 생성해 둘 수도 있습니다.

void CreateMulticastChannel(const string &channel, const string &token);
public static class MulticastServer
{
  public static void CreateMulticastChannel(string channel, string token);
}

멀티캐스팅 채널의 토큰 가져오기

CreateMulticastChannel 인터페이스를 통해 생성한 서버 측 채널의 토큰 정보를 가져오는 인터페이스들 입니다. 하나의 채널 이름을 전달하여 해당 채널의 토큰 값을 받아오거나 서버 측 채널들의 토큰 목록을 가져올 수 있습니다.

typedef boost::unordered_map<string /*channel*/,
                         string /*token*/> MulticastChannelTokenMap;

bool GetMulticastChannel(const string &channel, string *out_token);

size_t GetMulticastChannelTokenMap(MulticastChannelTokenMap *out);
using MulticastChannelTokenMap = Dictionary<string, string>;

public static class MulticastServer
{
  public static bool GetMulticastChannel(string channel, out string token);

  public static long GetMulticastChannelTokenMap(out MulticastChannelTokenMap out_token);
}

GetMulticastChannel 인터페이스는 전달받은 이름의 채널이 존재하지 않는 경우 false 를 리턴합니다.

없는 채널의 입장 방지

클라이언트가 존재하지 않는 채널을 생성하면서 입장하는 것을 방지하는 인터페이스입니다. 이 인터페이스를 호출하면 클라이언트에서는 채널 목록에 존재하지 않는 채널에 입장할 수 없습니다.

void DisallowToCreateClientsideMulticastChannel();
public static class MulticastServer
{
  public static void DisallowToCreateClientsideMulticastChannel();
}

전체 채널 목록 가져오기

현재 생성된 모든 채널들의 목록을 반환하는 인터페이스입니다. 채널들의 이름과 참여 인원 수를 확인할 수 있습니다. 클라이언트에서 생성한 채널과 RPC 로 연결된 다른 서버에서 생성한 채널들의 목록을 포함합니다.

struct MulticastChannelInfo {
  MulticastChannelInfo(const string &name_in, size_t member_count_in) {
    name = name_in;
    member_count = member_count_in;
  }

  string name;
  size_t member_count;
};

typedef std::vector<MulticastChannelInfo> MulticastChannelInfoVector;

void GetGlobalMulticastChannels(MulticastChannelInfoVector *out);
using MulticastChannelInfoVector = List<MulticastChannelInfo>;

public struct MulticastChannelInfo
{
  public MulticastChannelInfo (string name_in, long member_count_in)
  {
    name = name_in;
    member_count = member_count_in;
  }

  public string name;
  public long member_count;
}

public static class MulticastServer
{
  public static void GetGlobalMulticastChannels(out MulticastChannelInfoVector out_info);
}

Note

CreateMulticastChannel 인터페이스로 생성한 서버 측 채널의 경우 아직 참여한 인원이 없다면 반환되는 목록에 포함되지 않습니다.

멀티캐스팅 메시지 전송 후킹

아이펀 엔진에서는 전송되는 멀티캐스팅 메시지를 후킹할 수 있는 기능을 제공하고 있습니다. 메시지를 수정하거나 전송 여부를 선택할 수 있으며, 전송 완료된 메시지를 전달받을 수도 있습니다.

메시지 확인하기

채널 안에서 전송되는 메시지를 확인할 때 사용합니다. 메시지를 수정하거나 전송 여부를 선택할 수 있습니다. 인자로 넘어오는 세션에게 별도의 게임 메시지를 보낼 수도 있습니다.

멀티캐스팅 메시지로 JSON 을 이용할 경우에는 MulticastServer::InstallJsonMessageChecker() 함수를 사용합니다. Protobuf 인 경우에는 MulticastServer::InstallProtobufMessageChecker() 함수를 사용합니다.

class MulticastServer {
public:
  typedef function<
      bool(const string & /*channel*/,
          const string & /*sender*/,
          const Ptr<Session> & /*session*/,
          Json * /*message*/)> JsonMessageChecker;
  typedef function<
      bool(const string & /*channel*/,
          const string & /*sender*/,
          const Ptr<Session> & /*session*/,
          const Ptr<FunMessage> & /*message*/)> ProtobufMessageChecker;

  static void InstallJsonMessageChecker(
      const JsonMessageChecker &json_message_checker);

  static void InstallProtobufMessageChecker(
      const ProtobufMessageChecker &protobuf_message_checker);
};
public static class MulticastServer
{
  public delegate bool JsonMessageChecker(
    string channel, string sender, Session session, ref JObject message);

  public delegate bool ProtobufMessageChecker(
    string channel, string sender, Session session, ref FunMessage message);

  public static void InstallJsonMessageChecker (JsonMessageChecker chekcer);

  public static void InstallProtobufMessageChecker (ProtobufMessageChecker chekcer);
}
예제 - 메시지 변경하기

아래 예제에서는 JSON 을 이용한 채팅 메시지의 내용을 변경해보겠습니다. 전달되는 JSON 메시지는 다음과 같다고 가정하겠습니다.

{
  "message": "Hello World"
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
bool CheckJsonMessage(const string &channel, const string &sender,
                      const Ptr<Session> &session, Json *message) {
  LOG_ASSERT(message != NULL);
  LOG_ASSERT(message->HasAttribute("message", Json::kString));

  // 메시지를 변경합니다.
  (*message)["message"] = "Hello iFun";

  // true 를 반환하여 변경된 message 를 전송합니다.
  return true;
}

void Install() {
  MulticastServer::InstallJsonMessageChecker(CheckJsonMessage);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public static bool CheckJsonMessage(
  string channel, string sender, Session session, ref JObject message)
{
  if (message["message"] != null) {
    // 메시지를 변경합니다.
    message["message"] = "Hello iFun";
  }

  return true;
}

public static bool Install(ArgumentMap arguments)
{
  MulticastServer.InstallJsonMessageChecker(CheckJsonMessage);
}
예제 - 메시지 전송 실패 처리하기

다음은 1초안에 2회 이상 채팅 메시지를 전송할 경우 실패 처리하는 예제입니다. 실패하면 예제에서는 sc_chat_error 라는 가상의 메시지 타입으로 메시지를 보냅니다. 엔진에서는 메시지 타입명과 내용을 제한하지 않기 때문에 게임에 맞게 메시지를 만들어서 보낼 수 있습니다.

 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
bool CheckJsonMessage(const string &channel, const string &sender,
                      const Ptr<Session> &session, Json *message) {
  LOG_ASSERT(message != NULL);
  LOG_ASSERT(message->HasAttribute("message", Json::kString));

  // 마지막 채팅 시간을 검사합니다.
  int64_t last_chat_time = 0;
  if (session->GetFromContext("last_chat_time", &last_chat_time)) {
    int64_t elapsed_time = WallClock::GetTimestampInSec() - last_chat_time;
    if (elapsed_time < 1) {
      // 1초안에 2회 이상 채팅은 금지되어 있습니다.
      Json response;
      response["result"] = false;
      response["message"] = "your error message.";
      session->SendMessage("sc_chat_error", response, kDefaultEncryption, kTcp);
      return false;
    }
  }

  // 만약 다른 조건이 있다면 여기서 처리합니다.

  last_chat_time = WallClock::GetTimestampInSec();
  session->AddToContext("last_chat_time", last_chat_time);

  (*message)["message"] = "Hello iFun";
  return true;
}

void Install() {
  MulticastServer::InstallJsonMessageChecker(CheckJsonMessage);
}
 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
public static bool JsonChecker(
    string channel, string sender, Session session, ref JObject message)
{
    long last_chat_time = 0;

    if (session.GetFromContext("last_chat_time", out last_chat_time))
    {
      long elapsed_time = WallClock.GetTimestampInSec() - last_chat_time;
      if (elapsed_time < 1)
      {
        // 1초안에 2회 이상 채팅은 금지되어 있습니다.
        JObject response = new JObject();
        response["result"] = false;
        response["message"] = "your error message.";
        session.SendMessage("sc_chat_error", response,
            Session.Encryption.kDefault, Session.Transport.kTcp);

        return false;
      }
    }

    last_chat_time = WallClock.GetTimestampInSec();
    session.AddToContext("last_chat_time", last_chat_time);

    if (message["message"] != null) {
      // 메시지를 변경합니다.
      message["message"] = "Hello iFun";
    }

    return true;
}

public static bool Install(ArgumentMap arguments)
{
    MulticastServer.InstallJsonMessageChecker(CheckJsonMessage);
}

메시지 전달받기

전송 완료된 메시지를 전달받고 싶을 때 사용합니다. 전송 완료된 메시지를 로그로 기록하고 싶다면 이 기능을 사용하면 됩니다.

멀티캐스팅 메시지로 JSON 을 이용할 경우에는 MulticastServer::InstallJsonMessageHook() 함수를 사용합니다. Protobuf 인 경우에는 MulticastServer::InstallProtobufMessageHook() 함수를 사용합니다.

class MulticastServer {
public:
  typedef function<
      void(const string & /*channel*/,
          const string & /*sender*/,
          const SessionId & /*session_id*/,
          const Json & /*message*/)> JsonMessageHook;
  typedef function<
      void(const string & /*channel*/,
          const string & /*sender*/,
          const SessionId & /*session_id*/,
          const Ptr<const FunMessage> & /*message*/)> ProtobufMessageHook;

  static void InstallJsonMessageHook(const JsonMessageHook &hook);
  static void InstallProtobufMessageHook(const ProtobufMessageHook &hook);
};
public static class MulticastServer
{
  public delegate void JsonMessageHook (string channel, string sender,
                                        Guid session_id, JObject message);
  public delegate void ProtobufMessageHook (string channel, string sender,
                                            Guid session_id, FunMessage message);

  public static void InstallJsonMessageHook (JsonMessageHook hook);
  public static void InstallProtobufMessageHook (ProtobufMessageHook hook);
}
예제: 채널 내 메시지를 로그로 남기기
 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
void OnJsonHook(const string &channel,
                const string &sender,
                const SessionId &session_id,
                const Json &message) {
  // message 에는 hello 가 있다고 가정하겠습니다.
  const string msg = message["hello"].GetString();

  LOG(INFO) << "OnJsonHook: "
            << "channel=" << channel
            << ", sender=" << sender
            << ", session_id=" << session_id
            << ", message=" << msg
            << ", json_message=" << message.ToString();
}


void OnProtobufHook(const string &channel,
                    const string &sender,
                    const SessionId &session_id,
                    const Ptr<const FunMessage> &message) {
  // FunMulticastMessage 를 확장하여 만든 PbufHelloMessage 를
  // 전송했다고 가정하겠습니다.
  const FunMulticastMessage &multicast_msg = message->GetExtension(multicast);
  const PbufHelloMessage &pbuf_hello_msg = multicast_msg.GetExtension(pbuf_hello);
  const string &msg = pbuf_hello_msg.message();

  LOG(INFO) << "OnProtobufHook: "
            << "channel=" << channel
            << ", sender=" << sender
            << ", session_id=" << session_id
            << ", message=" << msg
            << ", pbuf_message=" << message->ShortDebugString();
}

void Install() {
  MulticastServer::InstallJsonMessageHook(OnJsonHook);
  MulticastServer::InstallProtobufMessageHook(OnProtobufHook);
}
 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
static string kJsonHookMsgFormat = String.Join(
    "", new string[]{
        "OnJsonHook: channel= {0},",
        "sender= {1},",
        "session_id= {2},",
        "message={3},",
        "json_message = {4}"});

static string kPbufHookMsgFormat = String.Join(
    "", new string[]{
        "OnProtobufHook: channel= {0},",
        "sender= {1},",
        "session_id= {2},",
        "message={3},",
        "json_message = {4}"});

public static void OnJsonHook(string channel, string sender,
                              Guid session_id, JObject message)
{
  // message 에는 hello 가 있다고 가정하겠습니다.
  string msg = (string) message ["hello"];

  Log.Info (kJsonHookMsgFormat, channel, sender,
            session_id.ToString(), msg, message.ToString());
}

public static void OnProtobufHook(string channel, string sender,
                                  Guid session_id, FunMessage message)
{
  FunMulticastMessage multicast_msg;
  if (!message.TryGetExtension_multicast (out multicast_msg)) {
    return;
  }

  // FunMulticastMessage 를 확장하여 만든 PbufHelloMessage 를
  // 전송했다고 가정하겠습니다.
  PbufHelloMessage hello_msg;
  if (!multicast_msg.TryGetExtension_pbuf_hello (out hello_msg)){
    return;
  }

  Log.Info (kPbufHookMsgFormat, channel, sender, session_id.ToString (),
            hello_msg, message.ToString ());
}

public static void Install(ArgumentMap arguments)
{
  MulticastServer.InstallJsonMessageHook (OnJsonHook);
  MulticastServer.InstallProtobufMessageHook (OnProtobufHook);
}