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

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

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

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

    Tip

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

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

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

27.1.1. MANIFEST 설정

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

Important

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

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

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

        ....,

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

    Note

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

27.1.2. 서버 코드

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

그러나 만일 멀티캐스팅이나 채팅 메시지를 후킹하고 싶은 경우 멀티캐스팅 메시지 전송 후킹 에서 언급된 방법을 쓸 수 있습니다.

27.1.3. 클라이언트 코드

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

Important

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

Tip

Unity3d 로 작성된 GitHub engine-plugin-unity3d 에서도 멀티캐스팅과 채팅 기능을 사용한 예제 소스를 볼 수 있습니다. GitHub FunapiMulticastClient 를 참고해주세요.

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

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

27.2.1. 메시지 확인하기

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

멀티캐스팅 메시지로 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);
}

27.2.1.1. 예제 - 메시지 변경하기

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

27.2.1.2. 예제 - 메시지 전송 실패 처리하기

다음은 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);
}

27.2.2. 메시지 전달받기

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

멀티캐스팅 메시지로 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);
}

27.2.3. 예제: 채널 내 메시지를 로그로 남기기

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