48. Cookbook 1: 방 기반 MO 게임 제작

iFun Engine을 이용해서 MO Game을 만들 때, 엔진의 이벤트 서브시스템을 이용해서 실시간/다중유저가 플레이하는 게임 세션을 만들 수 있습니다.

이 문서에서는 간단한 MO Room 을 만들어서 이 개념을 설명합니다:

  • 비동기 이벤트 처리
  • 락 없는 동기화
  • 비동기 함수 구현 (C++ 11, 14, 17 이용)

48.1. Room 프로젝트 준비

다음과 같은 명령으로 room 프로젝트를 생성합니다:

$ funapi_initiator room
$ room-source/setup_build_environment --type=makefile

다음 파일을 수정합니다:

  • CMakeLists.txt
  • src/event_handlers.cc
  • src/object_model/example.json

C++ 최신 표준을 사용하게 수정

source tree 상 root 에 존재하는 CMakeLists.txt 파일에서 set(WANT_CXX11 false) 를 true 로 변경합니다. 이미 true 로 되어 있는 경우 무시합니다.

# 중략

# Needs C++1x features? (Requires modern C++ compiler)
set(WANT_CXX11 true)
$ funapi_initiator room --csharp
$ room-source/setup_build_environment --type=makefile

다음 파일을 수정합니다:

  • CMakeLists.txt
  • mono/server.cs
  • src/object_model/example.json

48.2. MO 세션 구현

48.2.1. Room 구현

먼저 간단하게 ORM 을 등록하겠습니다.

src/object_model/example.json

{
  "User": {
    "Name": "String KEY",
    "Level": "Integer",
    "CharacterId": "Integer"
  }
}

다음은 간단한 Room 구현 내용입니다.

Note

결과 메시지를 전송할 때 미리 정의한 ErrorCode 를 이용할 수 있으나, 여기서는 간단한 예제 설명을 위해 성공, 실패로 단순하게 처리하였습니다.

src/event_handlers.cc

  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
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
#include <funapi.h>

void SendMessage(const Ptr<Session> &session, const string &msg_type,
                 const bool result) {
  Json message;
  message["result"] = result;
  session->SendMessage(msg_type, message);
  return;
}


// 하나의 Room 을 관리하기 위한 클래스입니다.
class Room : public boost::enable_shared_from_this<Room> {
 public:
  DECLARE_CLASS_PTR(Room);

  typedef boost::unordered_map<Uuid, Ptr<Room> > RoomMap;

  // 유저를 관리하기 위한 struct 입니다.
  // 유저를 위한 데이터를 모두 DB 에 저장하지 않을 뿐더러,
  // DB 에 저장되지 않는 데이터 중에 방 관련 데이터를 기록해야되기 때문에,
  // 이 클래스는 ORM 의 User 와는 별개입니다.
  struct User {
    User(const Ptr<Session> &_session, const string &_name,
         const int64_t _character_id, const int64_t _level)
      : session(_session), name(_name),
        character_id(_character_id), level(_level) {
    }

    // 유저의 Session 입니다.
    const Ptr<Session> session;

    // 유저의 이름입니다.
    string name;

    // 유저의 캐릭터 id 입니다.
    int64_t character_id;

    // 유저의 레벨입니다.
    int64_t level;
  };

  static Ptr<Room> Create(const string &name,
                          const int64_t joinable_min_level,
                          const string &master_user_name) {
    Uuid id = RandomGenerator::GenerateUuid();
    Ptr<Room> room(new Room(id, name, joinable_min_level, master_user_name));

    boost::mutex::scoped_lock lock(the_mutex);
    the_rooms.insert(std::make_pair(id, room));

    return room;
  }


  static Ptr<Room> Find(const Uuid &id) {
    boost::mutex::scoped_lock lock(the_mutex);

    RoomMap::iterator it = the_rooms.find(id);
    if (it == the_rooms.end()) {
      return Room::kNullPtr;
    }

    return it->second;
  }


  static Json GetRooms() {
    Json rooms;
    {
      boost::mutex::scoped_lock lock(the_mutex);

      BOOST_FOREACH(const RoomMap::value_type &pair, the_rooms) {
        string room_id = boost::lexical_cast<string>(pair.first);
        rooms[room_id] = pair.second->GetInfo();
      }
    }

    return rooms;
  }


  ~Room() {
    sessions_.clear();
  }


  const Uuid &id() const { return id_; }
  const string &name() const { return name_; }
  int64_t joinable_min_level() const { return joinable_min_level_; }
  const string &master_user_name() const { return master_user_name_; }
  void SetMasterUserName(const string &master_user_name) {
    master_user_name_ = master_user_name;
  }


  // 유저가 Room 에 입장하는 것을 처리합니다.
  // 클라이언트로부터 "client_join" 이라는 메시지가 오면 호출됩니다.
  void HandleJoin(const Ptr<Session> &session, const string &name,
                  const int64_t character_id, const int64_t level) {
    // 유저가 Room 에 입장할 수 있는지 조건을 검사합니다.

    // 이미 입장했었는지 확인합니다.
    if (sessions_.count(session->id()) > 0) {
      SendMessage(session, "client_join", false);
      return;
    }

    // 이름이나 캐릭터 id 가 정상인지 확인합니다.
    if (name.empty() || character_id < 0) {
      SendMessage(session, "client_join", false);
      return;
    }

    // 입장 가능한 최소 레벨을 만족하는지 검사합니다.
    if (joinable_min_level() < level) {
      SendMessage(session, "client_join", false);
      return;
    }

    // 모든 조건에 만족하였습니다. 이제 Room 에 입장시키도록 하겠습니다.
    Ptr<User> user(new User(session, name, character_id, level));
    bool has_not_joined =
        sessions_.insert(std::make_pair(session->id(), user)).second;

    // 입장한 적이 없어야 합니다.
    BOOST_ASSERT(has_not_joined);

    // 성공적으로 입장했습니다.
    // session context 에 room id 를 저장하여
    // 메시지 핸들러에서 클라이언트가 room id 를 보내지 않아도
    // session context 로부터 room id 를 가져와 사용하도록 하겠습니다.
    session->AddToContext("room_id", boost::lexical_cast<string>(id()));

    // 입장하는 유저를 포함한 Room 내 유저들에게
    // 입장 메시지를 broadcasting 합니다.
    Json message;

    // 입장한 유저의 정보를 입력합니다.
    message["name"] = name;
    message["character_id"] = character_id;
    message["level"] = level;
    for (const auto &v: sessions_) {
      // server_user_join 이라는 msg type 으로 메시지를 전송합니다.
      // session 에 transport 가 연결되어 있을 경우에만 메시지를 전송.
      if (v.second->session->IsTransportAttached()) {
        v.second->session->SendMessage("server_user_join", message);
      }
    }
  }


  // Room 나가기를 처리합니다.
  void HandleLeave(const Ptr<Session> &session) {
    string user_name;
    {
      auto user = sessions_.find(session->id());

      // Room 에 존재하지 않는 session(유저) 입니다.
      if (user == sessions_.cend()) {
        SendMessage(session, "client_leave", false);
        return;
      }

      user_name = user->second->name;

      // Room 에서 유저를 삭제합니다.
      sessions_.erase(user);

      // 모든 유저가 Room 에서 나갔으므로 방을 빈방으로 처리하겠습니다.
      // 여기서는 간단하게 name 만 지우면 빈방이 되겠다고 하겠습니다.
      if (sessions_.size() == 0) {
        name_ = "";
      }

      // 떠나는 유저에게 메시지를 전송합니다.
      SendMessage(session, "client_leave", true);
    }

    // 떠나는 유저를 제외한 Room 내 유저들에게 메시지를 전송합니다.
    Json message;
    message["name"] = user_name;
    for (const auto &v: sessions_) {
      // session 에 transport 가 연결되어 있을 경우에만 메시지를 전송.
      if (v.second->session->IsTransportAttached()) {
        v.second->session->SendMessage("server_user_leave", message);
      }
    }
  }


  // Room 내 유저들과 채팅하는 것을 처리합니다.
  void HandleChat(const Ptr<Session> &session, const string &msg) {
    string user_name;
    {
      auto user = sessions_.find(session->id());

      // Room 에 존재하지 않는 session(유저) 입니다.
      if (user == sessions_.cend()) {
        SendMessage(session, "client_chat", false);
        return;
      }

      user_name = user->second->name;
    }

    // 유저 이름이 비어 있으면 안됩니다.
    BOOST_ASSERT(not user_name.empty());

    // Room 내 유저들에게 채팅 메시지를 전송합니다.
    Json message;
    message["name"] = user_name;
    message["msg"] = msg;
    for (const auto &v: sessions_) {
      // session 에 transport 가 연결되어 있을 경우에만 메시지를 전송.
      if (v.second->session->IsTransportAttached()) {
        v.second->session->SendMessage("server_user_chat", message);
      }
    }
  }


  // Room 에 모인 유저들과의 대전을 시작합니다.
  // 방장이 Start 버튼을 누르면 이 함수가 호출된다고 가정하겠습니다.
  void HandleStartMatch(const Ptr<Session> &session,
                        const string &user_name) {
    // 방장이 아니면 이상한 클라이언트라고 가정하겠습니다.
    if (user_name != master_user_name()) {
      session->Close();
      return;
    }

    Json message;

    // 대전을 진행할 맵을 선택합니다. 여기서는 간단하게
    // 100 ~ 200 사이의 값을 랜덤하게 선택하겠습니다.
    int64_t map_id = RandomGenerator::GenerateNumber(100, 200);

    // 대전 시간을 입력합니다. 여기서는 180 초라고 가정하겠습니다.
    message["match_time"] = 180;
    message["match_map_id"] = map_id;

    // 대전 정보를 전송합니다.
    for (const auto &v: sessions_) {
      // session 에 transport 가 연결되어 있을 경우에만 메시지를 전송.
      if (v.second->session->IsTransportAttached()) {
        v.second->session->SendMessage("server_start_match", message);
      }
    }

    // 여기서는 단순히 180 초가 지나면 대전이 끝난다고 가정하겠습니다.
    // 만약 중간에라도 대전이 끝날 수 있다면 match_timer_id_ 를
    // Timer::Cancel(match_timer_id) 함수를 호출하여
    // 아래에서 등록하는 timer 를 취소시킬 수 있습니다.

    // 180 초 이후에 대전을 종료시키는 timer 를 등록합니다.
    match_timer_id_ =
        Timer::ExpireAfter(
            WallClock::FromMsec(180 * 1000),
            bind(&Room::EndMatch, shared_from_this()));
  }


  // 대전을 종료합니다.
  void EndMatch() {
    // 승자와 패자 정보를 전송합니다.
    Json message;

    // ...

    for (const auto &v: sessions_) {
      // session 에 transport 가 연결되어 있을 경우에만 메시지를 전송.
      if (v.second->session->IsTransportAttached()) {
        v.second->session->SendMessage("server_end_match", message);
      }
    }
  }


 private:
  Room(const Uuid &id, const string &name, const int64_t joinable_min_level,
       const string &master_user_name)
    : id_(id), name_(name), joinable_min_level_(joinable_min_level),
      master_user_name_(master_user_name) {
  }


  // Room list 를 전달할 때 사용합니다. Json 으로 정보를 전달합니다.
  Json GetInfo() const {
    Json info;
    info["name"] = name();
    int64_t user_count = sessions_.size();
    info["user_count"] = user_count;
    info["joinable_min_level"] = joinable_min_level();
    info["master_user_name"] = master_user_name();

    return info;
  }


  // Room 의 고유 id 입니다.
  const Uuid id_;

  // Room 의 이름입니다. 이름은 변경 가능하다고 가정했습니다.
  string name_;

  // 방에 입장가능한 최소 레벨입니다. 최소 레벨은 변경 가능하다고 가정했습니다.
  int64_t joinable_min_level_;

  Timer::Id match_timer_id_;

  // 방장 이름입니다.
  string master_user_name_;

  typedef boost::unordered_map<
      SessionId, Ptr<User>, boost::hash<Uuid>> SessionMap;
  SessionMap sessions_;

  static boost::mutex the_mutex;
  static RoomMap the_rooms;
};


DEFINE_CLASS_PTR(Room);
boost::mutex Room::the_mutex;
Room::RoomMap Room::the_rooms;

여기서 기능 구현을 위해 필요한 부분은,

  • class Roomid() 멤버 함수 (동기화 할 단위)
  • class RoomHandleJoin(), HandleLeave(), HandleChat(), HandleStartMatch() 멤버 함수(이벤트 실행)

두 가지입니다. Lock 없이 동기화 하기 위해서는 Handle... 함수를 호출하는 방식을 아래에서 다루는 것처럼 제한합니다.

  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
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
using funapi;

...

public class Room
{
  public static Dictionary<System.Guid, Room> TheRooms =
      new Dictionary<System.Guid, Room>();

  struct User
  {
    public User(Session session, string name, ulong character_id, ulong level)
    {
      Session = session;
      Name = name;
      CharacterId = character_id;
      Level = level;
    }

    public Session Session
    {
      get;set;
    }

    public string Name
    {
      get;set;
    }

    public ulong CharacterId
    {
      get;set;
    }

    public ulong Level
    {
      get;set;
    }
  }

  void SendMessage(Session session, string msg_type, bool result)
  {
    JObject message = new JObject ();
    message ["result"] = result;
    session.SendMessage (msg_type, message);
    return;
  }

  public static Room Create(
      string name, ulong joinable_min_level, string master_user_name)
  {
    System.Guid id = RandomGenerator.GenerateUuid ();

    Room room = new Room (id, name, joinable_min_level, master_user_name);

    lock (TheRooms)
    {
      TheRooms.Add (id, room);
    }

    return room;
  }

  public static bool Find(System.Guid id, out Room room)
  {
    lock (TheRooms)
    {
      return TheRooms.TryGetValue (id, out room);
    }
  }

  static JObject GetRooms()
  {
    JObject rooms = new JObject ();

    lock (TheRooms)
    {
      foreach (var room in TheRooms)
      {
        string room_id = room.Key.ToString ();
        rooms [room_id] = room.Value.GetInfo ();
      }
    }

    return rooms;
  }

  public void HandleJoin(Session session, string name,
                         ulong character_id, ulong level)
  {
    // 유저가 Room 에 입장할 수 있는지 조건을 검사합니다.

    // 이미 입장했었는지 확인합니다.
    if (Sessions.ContainsKey (session.Id))
    {
      SendMessage (session, "client_join", false);
      return;
    }

    // 이름이나 캐릭터 id 가 정상인지 확인합니다.
    if (name == String.Empty || character_id < 0) {
      SendMessage (session, "client_join", false);
      return;
    }

    // 입장 가능한 최소 레벨을 만족하는지 검사합니다.
    if (JoinableMinLevel < level) {
      SendMessage (session, "client_join", false);
      return;
    }

    // 모든 조건에 만족하였습니다. 이제 Room 에 입장시키도록 하겠습니다.
    User user = new User (session, name, character_id, level);
    Sessions.Add (session.Id, user);

    // 성공적으로 입장했습니다.
    // session context 에 room id 를 저장하여
    // 메시지 핸들러에서 클라이언트가 room id 를 보내지 않아도
    // session context 로부터 room id 를 가져와 사용하도록 하겠습니다.
    session.AddToContext ("room_id", Id.ToString ());

    foreach (var it in Sessions)
    {
      if (it.Value.Session.IsTransportAttached ())
      {
        it.Value.Session.SendMessage ("server_user_join", message, funapi.Session.Encryption.kDefault, funapi.Session.Transport.kTcp);
      }
    }
  }

  public void HandleLeave(Session session)
  {
    string user_name;

    User user;
    if (!Sessions.TryGetValue (session.Id, out user))
    {
      SendMessage (session, "client_leave", false);
      return;
    }

    user_name = user.Name;

    // Room 에서 유저를 삭제합니다.
    Sessions.Remove (session.Id);

    // 모든 유저가 Room 에서 나갔으므로 방을 빈방으로 처리하겠습니다.
    // 여기서는 간단하게 name 만 지우면 빈방이 되겠다고 하겠습니다.
    if (Sessions.Count == 0)
    {
      Name = "";
    }

    SendMessage (session, "client_leave", true);

    // 떠나는 유저를 제외한 Room 내 유저들에게 메시지를 전송합니다.
    JObject message = new JObject ();
    message ["name"] = user_name;
    foreach (var it in Sessions)
    {
      // session 에 transport 가 연결되어 있을 경우에만 메시지를 전송.
      if (it.Value.Session.IsTransportAttached ())
      {
        it.Value.Session.SendMessage ("server_user_leave", message);
      }
    }
  }

  // Room 내 유저들과 채팅하는 것을 처리합니다.
  public void HandleChat(Session session, string msg)
  {
    string user_name;

    User user;

    // Room 에 존재하지 않는 session(유저) 입니다.
    if (!Sessions.TryGetValue (session.Id, out user))
    {
      SendMessage (session, "client_chat", false);
      return;
    }

    user_name = user.Name;

    // 유저 이름이 비어 있으면 안됩니다.
    Log.Assert (user_name != String.Empty);

    // Room 내 유저들에게 채팅 메시지를 전송합니다.
    JObject message = new JObject ();
    message ["name"] = user_name;
    message ["msg"] = msg;
    foreach (var it in Sessions)
    {
      // session 에 transport 가 연결되어 있을 경우에만 메시지를 전송.
      if (it.Value.Session.IsTransportAttached ()) {
        it.Value.Session.SendMessage ("server_user_chat", message);
      }
    }
  }


  // Room 에 모인 유저들과의 대전을 시작합니다.
  // 방장이 Start 버튼을 누르면 이 함수가 호출된다고 가정하겠습니다.
  public void HandleStartMatch(Session session, string user_name)
  {
    // 방장이 아니면 이상한 클라이언트라고 가정하겠습니다.
    if (user_name != MasterUserName) {
      session.Close();
      return;
    }

    JObject message = new JObject();

    // 대전을 진행할 맵을 선택합니다. 여기서는 간단하게
    // 100 ~ 200 사이의 값을 랜덤하게 선택하겠습니다.
    ulong map_id = (ulong) RandomGenerator.GenerateNumber(100, 200);

    // 대전 시간을 입력합니다. 여기서는 180 초라고 가정하겠습니다.
    message ["match_time"] = 180;
    message ["match_map_id"] = map_id;

    // 대전 정보를 전송합니다.
    foreach (var it in Sessions)
    {
      // session 에 transport 가 연결되어 있을 경우에만 메시지를 전송.
      if (it.Value.Session.IsTransportAttached ()) {
        it.Value.Session.SendMessage ("server_start_match", message);
      }
    }

    // 여기서는 단순히 180 초가 지나면 대전이 끝난다고 가정하겠습니다.
    // 만약 중간에라도 대전이 끝날 수 있다면 match_timer_id_ 를
    // Timer::Cancel(match_timer_id) 함수를 호출하여
    // 아래에서 등록하는 timer 를 취소시킬 수 있습니다.

    // 180 초 이후에 대전을 종료시키는 timer 를 등록합니다.
    MatchTimerId =
        Timer.ExpireAfter(
            WallClock.FromMsec(180 * 1000), EndMatch);
  }


  // 대전을 종료합니다.
  public void EndMatch(UInt64 tid, DateTime value)
  {
    // 승자와 패자 정보를 전송합니다.
    JObject message = new JObject();

    // ...

    foreach (var it in Sessions)
    {
      // session 에 transport 가 연결되어 있을 경우에만 메시지를 전송.
      if (it.Value.Session.IsTransportAttached ()) {
        it.Value.Session.SendMessage ("server_end_match", message);
      }
    }
  }

  public ulong JoinableMinLevel
  {
    get;set;
  }

  public string MasterUserName
  {
    get;set;
  }

  public string Name
  {
    get;set;
  }

  public System.Guid Id
  {
    get;
  }

  Room(System.Guid id, string name,
       ulong joinable_min_level, string master_user_name)
  {
    Id = id;
    Name = name;
    JoinableMinLevel = joinable_min_level;
    MasterUserName = master_user_name;
  }

  JObject GetInfo()
  {
    JObject info = new JObject ();
    info ["name"] = Name;
    info ["user_count"] = Sessions.Count;
    info ["joinable_min_level"] = JoinableMinLevel;
    info ["master_user_name"] = MasterUserName;
    info ["id"] = Id.ToString ();
    return info;
  }

  Dictionary<System.Guid, User> Sessions =
      new Dictionary<System.Guid, User>();

  UInt64 MatchTimerId = 0;
}

48.2.2. 이벤트 직렬화

각각의 메시지 핸들러에서 Room 에서 처리할 이벤트를 직렬화 합니다.

src/event_handlers.cc

  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
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
void OnSessionOpened(const Ptr<Session> &session) {
}


// session 이 닫히면 호출됩니다.
// 이 예제에서는 session 이 닫힐 때만 Room 에서 나가도록 처리하겠습니다.
// tcp 연결이 끊겼을 때 호출되는 OnTcpTransportDetached() 함수에서는
// 방에서 나가는 처리를 하지 않겠습니다.
void OnSessionClosed(const Ptr<Session> &session, SessionCloseReason reason) {
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager::FindLocalAccount(session);
  if (user_name.empty()) {
    return;
  }

  string room_id_str;
  // session context 에서 Room id 를 가져옵니다.
  if (not session->GetFromContext("room_id", &room_id_str)) {
    // 방에 접속하지 않은 세션입니다.
    return;
  }

  Uuid room_id = boost::lexical_cast<Uuid>(room_id_str);
  Ptr<Room> room = Room::Find(room_id);

  // 반드시 Room 이 존재한다고 가정했습니다.
  BOOST_ASSERT(room);

  // 유저를 로그아웃 처리합니다.
  bool is_user_logged_out = AccountManager::SetLoggedOut(user_name);

  // 반드시 로그아웃 처리된다고 가정하겠습니다.
  BOOST_ASSERT(is_user_logged_out);

  // Room id 로 이벤트를 직렬화합니다.
  Event::Invoke([room, session]() {
        room->HandleLeave(session);
      }, room_id);
}


// tcp 연결이 끊어지면 이 함수가 호출됩니다.
// session 이 닫히는 것과는 다릅니다.
// tcp 연결이 끊겼어도 session 은 session timeout 이 지나지 않으면
// 닫히지 않습니다.
void OnTcpTransportDetached(const Ptr<Session> &session) {
  // 여기서는 tcp 연결이 끊겨도 Room 에서 나가지 않도록 처리하겠습니다.
  // session timeout 으로 session 이 닫히기 전에
  // 재연결하게 될 경우 다시 게임을 진행할 수도 있기 때문입니다.
}


// 클라이언트 로그인을 처리합니다.
void OnClientLogin(const Ptr<Session> &session, const Json &message) {
  // 유저 이름이 메시지에 포함되어 있는지 확인합니다.
  if (not message.HasAttribute("user_name", Json::kString)) {
    SendMessage(session, "client_login", false);
    return;
  }

  // 유저 이름을 가져옵니다.
  string user_name = message["user_name"].GetString();

  // Object subsystem 을 이용하여
  // user_name 에 해당하는 object 를 가져올 수 있습니다.
  Ptr<User> user = User::FetchByName(user_name);

  // 만약 유저가 존재하지 않으면 생성하겠습니다.
  if (not user) {
    user = User::Create(user_name);

    // 이미 user_name 에 해당하는 User 가 생성되었다면 nullptr 를 반환합니다.
    if (not user) {
      SendMessage(session, "client_login", false);
      return;
    }

    // 기본 레벨 1로 설정
    user->SetLevel(1);

    // 기본 캐릭터 아이디 1로 설정
    user->SetCharacterId(1);
  }

  // user 가 반드시 존재한다고 가정했습니다.
  BOOST_ASSERT(user);

  // level 과 character id 를 가져옵니다.
  int64_t user_level = user->GetLevel();
  int64_t character_id = user->GetCharacterId();

  // 필요하다면 다른 정보도 가져옵니다.
  // ...

  // 이제 AccountManager 를 이용하여 로그인 처리를 합니다.
  // session 을 user_name 으로 mapping 할 수 있습니다.
  // 이미 로그인 처리되어 있다면 false 가 리턴됩니다.
  if (not AccountManager::CheckAndSetLoggedIn(user_name, session)) {
    SendMessage(session, "client_login", false);
    return;
  }

  // 유저에게 자신의 정보를 전달합니다.
  Json response;
  response["result"] = true;
  response["user_level"] = user_level;
  response["character_id"] = character_id;
  session->SendMessage("client_login", response);
}


void OnClientLogout(const Ptr<Session> &session, const Json &message) {
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager::FindLocalAccount(session);
  if (user_name.empty()) {
    SendMessage(session, "client_logout", false);
    return;
  }

  // Room 에 입장할 때 session context 에 저장한 room id 를 가져옵니다.
  string room_id_str;
  if (not session->GetFromContext("room_id", &room_id_str)) {
    // 만약 Room 에 입장하지 않았다면 room_id 가 없을 수 있습니다.
    // 이 경우에는 단순히 로그아웃되었다고 처리합니다.
    SendMessage(session, "client_logout", true);
    return;
  }

  // 반드시 room id 가 저장되었다고 가정했습니다.
  BOOST_ASSERT(not room_id_str.empty());

  // 입장한 Room 에서 유저를 나가게 합니다.
  Uuid room_id = boost::lexical_cast<Uuid>(room_id_str);
  Ptr<Room> room = Room::Find(room_id);

  // 반드시 Room 이 존재한다고 가정했습니다.
  BOOST_ASSERT(room);

  // 유저를 로그아웃 처리합니다.
  bool is_user_logged_out = AccountManager::SetLoggedOut(user_name);

  // 반드시 로그아웃 처리된다고 가정하겠습니다.
  BOOST_ASSERT(is_user_logged_out);

  // Room id 로 이벤트를 직렬화합니다.
  Event::Invoke([room, session]() {
        room->HandleLeave(session);
      }, room_id);
}


void OnClientJoinRoom(const Ptr<Session> &session, const Json &message) {
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager::FindLocalAccount(session);
  if (user_name.empty()) {
    // 로그인한 유저가 아니면 실패 처리합니다.
    SendMessage(session, "client_join_room", false);
    return;
  }

  Ptr<User> user = User::FetchByName(user_name);
  // user object 가 존재한다고 가정하겠습니다.
  BOOST_ASSERT(user);

  // Room 입장 시 필요한 유저 정보를 가져옵니다.
  int64_t character_id = user->GetCharacterId();
  int64_t user_level = user->GetLevel();

  // 만약 Room 을 새로 만드는 것이라면 room_id 는 메시지에 없습니다.
  Ptr<Room> room;
  if (not message.HasAttribute("room_id", Json::kString)) {
    // 이 경우 Room 을 만들어야 하므로 room_name, joinable_min_level 이
    // 메시지에 있어야 합니다.
    if (not message.HasAttribute("room_name", Json::kString) ||
        not message.HasAttribute("joinable_min_level", Json::kInteger)) {
      // 필요한 값이 메시지에 없습니다.
      // 이상한 클라이언트라 판단하고 세션을 닫겠습니다.
      session->Close();
      return;
    }

    // Room 생성에 필요한 값을 가져오고 Room 을 생성합니다.
    const string room_name = message["room_name"].GetString();
    const int64_t joinable_min_level = message["joinable_min_level"].GetInteger();
    room = Room::Create(room_name, joinable_min_level, user_name);
    BOOST_ASSERT(room);
  } else {
    // 기존에 존재하는 Room 에 입장하는 것이면 room_id 는 메시지에 있어야 합니다.
    string room_id_str = message["room_id"].GetString();

    // room_id 값이 비어있지 않다고 가정했습니다.
    BOOST_ASSERT(not room_id_str.empty());

    Uuid room_id = boost::lexical_cast<Uuid>(room_id_str);
    room = Room::Find(room_id);

    // Room 은 반드시 존재해야 합니다.
    BOOST_ASSERT(room);
  }

  // Room 을 생성하거나 찾았습니다. 이제 Room 에 입장시키겠습니다.
  // Room id 로 이벤트를 직렬화합니다.
  Event::Invoke([room, session, user_name, character_id, user_level]() {
        room->HandleJoin(session, user_name, character_id, user_level);
      }, room->id());
}


void OnClientLeaveRoom(const Ptr<Session> &session, const Json &message) {
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager::FindLocalAccount(session);
  if (user_name.empty()) {
    // 로그인한 유저가 아니면 실패 처리합니다.
    SendMessage(session, "client_leave_room", false);
    return;
  }

  // Room 에 입장할 때 session context 에 저장해둔 room id 를 가져오겠습니다.
  string room_id_str;
  if (not session->GetFromContext("room_id", &room_id_str)) {
    // 방에 입장하지 않은 세션입니다.
    // 이상한 클라이언트라고 가정하여 세션을 닫겠습니다.
    session->Close();
    return;
  }

  Uuid room_id = boost::lexical_cast<Uuid>(room_id_str);
  Ptr<Room> room = Room::Find(room_id);

  // 반드시 Room 이 존재한다고 가정하겠습니다.
  BOOST_ASSERT(room);

  // Room 에서 유저를 나가게 합니다.
  // Room id 로 이벤트를 직렬화합니다.
  Event::Invoke([room, session]() {
        room->HandleLeave(session);
      }, room_id);
}


void OnClientChat(const Ptr<Session> &session, const Json &message) {
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager::FindLocalAccount(session);
  if (user_name.empty()) {
    // 로그인한 유저가 아니면 실패 처리합니다.
    SendMessage(session, "client_chat_room", false);
    return;
  }

  // chat msg 가 메시지에 없으면 이상한 클라이언트라고 가정하겠습니다.
  if (not message.HasAttribute("chat_msg", Json::kString)) {
    session->Close();
    return;
  }

  string chat_msg = message["chat_msg"].GetString();
  if (chat_msg.empty()) {
    // 비어있는 chat msg 도 이상한 클라이언트라고 가정하겠습니다.
    session->Close();
    return;
  }

  // Room 에 입장할 때 session context 에 저장해둔 room id 를 가져오겠습니다.
  string room_id_str;
  if (not session->GetFromContext("room_id", &room_id_str)) {
    // 방에 입장하지 않은 세션입니다.
    // 이상한 클라이언트라고 가정하여 세션을 닫겠습니다.
    session->Close();
    return;
  }

  Uuid room_id = boost::lexical_cast<Uuid>(room_id_str);
  Ptr<Room> room = Room::Find(room_id);

  // 반드시 Room 이 존재한다고 가정하겠습니다.
  BOOST_ASSERT(room);

  // Room 내 유저들에게 chat msg 를 전달합니다.
  // Room id 로 이벤트를 직렬화합니다.
  Event::Invoke([room, session, chat_msg]() {
        room->HandleChat(session, chat_msg);
      }, room_id);
}


// 대전을 시작합니다. 방장만이 이 메시지를 보낼 수 있습니다.
void OnClientStartMatch(const Ptr<Session> &session, const Json &message) {
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager::FindLocalAccount(session);
  if (user_name.empty()) {
    // 로그인한 유저가 아니면 실패 처리합니다.
    SendMessage(session, "client_start_match", false);
    return;
  }

  // Room 에 입장할 때 session context 에 저장해둔 room id 를 가져오겠습니다.
  string room_id_str;
  if (not session->GetFromContext("room_id", &room_id_str)) {
    // 방에 입장하지 않은 세션입니다.
    // 이상한 클라이언트라고 가정하여 세션을 닫겠습니다.
    session->Close();
    return;
  }

  Uuid room_id = boost::lexical_cast<Uuid>(room_id_str);
  Ptr<Room> room = Room::Find(room_id);

  // 반드시 Room 이 존재한다고 가정하겠습니다.
  BOOST_ASSERT(room);

  // 대전 처리를 합니다.
  // Room id 로 이벤트를 직렬화합니다.
  Event::Invoke([room, session, user_name]() {
        // HandleStartMatch() 함수에서 방장 여부를 검사합니다.
        room->HandleStartMatch(session, user_name);
      }, room_id);
}


// 모든 Room 정보를 보냅니다.
void OnClientGetRoomList(const Ptr<Session> &session, const Json &message) {
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager::FindLocalAccount(session);
  if (user_name.empty()) {
    // 로그인한 유저가 아니면 실패 처리합니다.
    SendMessage(session, "client_get_room_list", false);
    return;
  }

  // 모든 Room 들의 정보를 Json 으로 가져와서 메시지를 전송합니다.
  Json rooms = Room::GetRooms();
  session->SendMessage("client_get_room_list", rooms);
}


// 다음과 같이 RegisterEventHandlers() 함수에서 메시지 핸들러를 등록합니다.
void RegisterEventHandlers() {
  // OnSessionClosed() 함수가 호출되었을 때(즉 세션이 닫혔을 때)
  // Room 에서 나가도록 작업하였습니다.
  HandlerRegistry::Install2(OnSessionOpened, OnSessionClosed);

  HandlerRegistry::Register("client_login", OnClientLogin);
  HandlerRegistry::Register("client_logout", OnClientLogout);
  HandlerRegistry::Register("client_join_room", OnClientJoinRoom);
  HandlerRegistry::Register("client_leave_room", OnClientLeaveRoom);
  HandlerRegistry::Register("client_chat", OnClientChat);
  HandlerRegistry::Register("client_start_match", OnClientStartMatch);
  HandlerRegistry::Register("client_get_room_list", OnClientGetRoomList);

  // tcp 연결이 끊어지면 OnTcpTransportDetached 함수가 호출됩니다.
  // session 이 닫히는 것과는 다릅니다.
  // tcp 연결이 끊겼어도 session 은 session timeout 이 지나지 않으면
  // 닫히지 않습니다.
  // OnTcpTransportDetached() 함수가 호출되었을 땐 Room 에서 나가도록
  // 작업하지 않았습니다.
  HandlerRegistry::RegisterTcpTransportDetachedHandler(OnTcpTransportDetached);
}

OnClient... 메시지 핸들러에서 Event::Invoke 를 호출하는데, 여기에 C++ 11 lambda 를 콜백으로 넘깁니다. Room 에서 실행되는 모든 이벤트는 Event::Invoke 를 통해서 전달합니다. 여기서 동기화는 다음과 같이 이루어집니다:

  • 동기화할 단위: room id 가 같은 이벤트끼리 Event::Invoke 를 호출한 순으로 (나중에) 실행하게 됩니다.
  • Room 에 실행할 이벤트를 모두 Event::Invoke 를 통해서 실행하면, Room 안의 변수를 바꿀 때는 lock을 잡지 않아도 됩니다.
  • room id 가 다른 경우 (즉 다른 Room) 은 모두 concurrent하게 실행됩니다.
  • 콜백으로 C++11 lambda를 이용합니다. Lambda capture 구문에 필요한 변수들을 입력하여 (즉 room, session 등을 입력하여) 나중에 Invoke 안에 있는 코드에서 접근합니다. (필요하다면 Lambda capture 구문 ([=]) 을 써도 됩니다.)

Note

여기서는 C++11 lambda를 이용했지만 std::function 혹은 boost::function 을 이용해도 됩니다. 다만 C++11 lambda를 쓸 수 있는 환경인 경우 비동기로 실행할 코드에서 참조하는 외부 변수를 관리하기가 편합니다. 복사할지/참조할지 여부를 capture 구문 안에서 지정하면 버그를 예방하기 쉽습니다.

자세한 사항은 MSDN의 C++11 lambda 페이지 , cppreference.com lambda 페이지 를 참조해주세요.

  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
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
public static void SendMessage(Session session, string msg_type, bool result)
{
  JObject message = new JObject ();
  message ["result"] = result;
  session.SendMessage (msg_type, message);
  return;
}

public static void OnSessionOpened(Session session)
{

}

// session 이 닫히면 호출됩니다.
// 이 예제에서는 session 이 닫힐 때만 Room 에서 나가도록 처리하겠습니다.
// tcp 연결이 끊겼을 때 호출되는 OnTcpTransportDetached() 함수에서는
// 방에서 나가는 처리를 하지 않겠습니다.
public static void OnSessionClosed(
    Session session, Session.CloseReason reason)
{
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager.FindLocalAccount (session);
  if (user_name == String.Empty)
  {
    return;
  }

  string room_id_str;

  // session context 에서 Room id 를 가져옵니다.
  if (!session.GetFromContext ("room_id", out room_id_str))
  {
    // 방에 접속하지 않은 세션입니다.
    return;
  }

  System.Guid room_id = new System.Guid (room_id_str);
  Room room;
  Room.Find (room_id, out room);

  // 반드시 Room 이 존재한다고 가정했습니다.
  Log.Assert (room != null);

  bool is_user_logged_out = AccountManager.SetLoggedOut (user_name);

  // 반드시 로그아웃 처리된다고 가정하겠습니다.
  Log.Assert (is_user_logged_out);

  // Room id 로 이벤트를 직렬화합니다.
  Event.Invoke (() => {
    room.HandleLeave(session);
  }, room_id);
}

// tcp 연결이 끊어지면 이 함수가 호출됩니다.
// session 이 닫히는 것과는 다릅니다.
// tcp 연결이 끊겼어도 session 은 session timeout 이 지나지 않으면
// 닫히지 않습니다.
public static void OnTcpTransportDetached(
    Session session)
{
  // 여기서는 tcp 연결이 끊겨도 Room 에서 나가지 않도록 처리하겠습니다.
  // session timeout 으로 session 이 닫히기 전에
  // 재연결하게 될 경우 다시 게임을 진행할 수도 있기 때문입니다.
}

public static void OnClientLogin(Session session, JObject message)
{
  if (message ["user_name"] == null)
  {
    SendMessage(session, "client_login", false);
    return;
  }

  string user_name = (string) message ["user_name"];
  User user = User.FetchByName (user_name);

  // 만약 유저가 존재하지 않으면 생성하겠습니다.
  if (user == null)
  {
    user = User.Create(user_name);

    // 이미 user_name 에 해당하는 User 가 생성되었다면 null 을 반환합니다.
    if (user == null)
    {
      SendMessage(session, "client_login", false);
      return;
    }

    // 기본 레벨 1로 설정
    user.SetLevel (1);

    // 기본 캐릭터 아이디 1로 설정
    user.SetCharacterId (1);

    // user 가 반드시 존재한다고 가정했습니다.
    Log.Assert (user != null);
  }

  // level 과 character id 를 가져옵니다.
  Int64 user_level = user.GetLevel ();
  Int64 character_id = user.GetCharacterId ();

  // 필요하다면 다른 정보도 가져옵니다.
  // ...

  // 이제 AccountManager 를 이용하여 로그인 처리를 합니다.
  // session 을 user_name 으로 mapping 할 수 있습니다.
  // 이미 로그인 처리되어 있다면 false 가 리턴됩니다.
  if (!AccountManager.CheckAndSetLoggedIn (user_name, session))
  {
    SendMessage (session, "client_login", false);
    return;
  }

  // 유저에게 자신의 정보를 전달합니다.
  JObject response = new JObject();
  response ["result"] = true;
  response ["user_level"] = user_level;
  response ["character_id"] = character_id;
  session.SendMessage ("client_login", response);
}

public static void OnClientLogout(Session session, JObject message)
{
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager.FindLocalAccount (session);
  if (user_name == String.Empty)
  {
    // 로그인한 유저가 아니면 실패 처리합니다.
    SendMessage (session, "client_logout", false);
    return;
  }

  // Room 에 입장할 때 session context 에 저장한 room id 를 가져옵니다.
  string room_id_str;
  if (!session.GetFromContext ("room_id", out room_id_str))
  {
    // 만약 Room 에 입장하지 않았다면 room_id 가 없을 수 있습니다.
    // 이 경우에는 단순히 로그아웃되었다고 처리합니다.
    SendMessage (session, "client_logout", true);
    return;
  }

  Log.Assert (room_id_str != String.Empty);

  System.Guid room_id = new System.Guid (room_id_str);

  // 반드시 Room 이 존재한다고 가정했습니다.
  Room room = null;
  Log.Assert (Room.Find (room_id, out room));
  Log.Assert (room != null);

  // 유저를 로그아웃 처리합니다.
  bool is_user_logged_out = AccountManager.SetLoggedOut (user_name);

  // 반드시 로그아웃 처리된다고 가정하겠습니다.
  Log.Assert (is_user_logged_out);

  // Room id 로 이벤트를 직렬화합니다
  Event.Invoke (() => {
    room.HandleLeave (session);
  }, room_id);
}

public static void OnClientJoinRoom(Session session, JObject message)
{
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager.FindLocalAccount (session);
  if (user_name == String.Empty)
  {
    // 로그인한 유저가 아니면 실패 처리합니다.
    SendMessage (session, "client_join_room", false);
    return;
  }

  User user = User.FetchByName(user_name);

  // user object 가 존재한다고 가정하겠습니다.
  Log.Assert (user != null);

  // Room 입장 시 필요한 유저 정보를 가져옵니다.
  ulong user_level = (ulong) user.GetLevel ();
  ulong character_id = (ulong) user.GetCharacterId ();

  // 만약 Room 을 새로 만드는 것이라면 room_id 는 메시지에 없습니다.
  Room room;
  if (message ["room_id"] == null)
  {
    // 이 경우 Room 을 만들어야 하므로 room_name, joinable_min_level 이
    // 메시지에 있어야 합니다.
    if (message ["room_name"] == null ||
        message ["joinable_min_level"] == null)
    {
      // 필요한 값이 메시지에 없습니다.
      // 이상한 클라이언트라 판단하고 세션을 닫겠습니다.
      session.Close();
      return;
    }

    // Room 생성에 필요한 값을 가져오고 Room 을 생성합니다.
    string room_name = (string) message ["room_name"];
    ulong joinable_min_level = (ulong) message ["joinable_min_level"];
    room = Room.Create(room_name, joinable_min_level, user_name);
    Log.Assert (room != null);
  }
  else
  {
    // 기존에 존재하는 Room 에 입장하는 것이면 room_id 는 메시지에 있어야 합니다.
    string room_id_str = (string) message ["room_id"];

    // room_id 값이 비어있지 않다고 가정했습니다.
    Log.Assert(room_id_str != String.Empty);

    System.Guid room_id = new System.Guid (room_id_str);

    room = null;

    // Room 은 반드시 존재해야 합니다.
    Log.Assert (Room.Find (room_id, out room));
    Log.Assert (room != null);
  }

   // Room 을 생성하거나 찾았습니다. 이제 Room 에 입장시키겠습니다.
   // Room id 로 이벤트를 직렬화합니다.
  Event.Invoke (() => {
      room.HandleJoin (session, user_name, character_id, user_level);
  }, room.Id);
}

public static void OnClientLeaveRoom(Session session, JObject message)
{
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager.FindLocalAccount (session);
  if (user_name == String.Empty)
  {
    // 로그인한 유저가 아니면 실패 처리합니다.
    SendMessage (session, "client_leave_room", false);
    return;
  }

  // Room 에 입장할 때 session context 에 저장해둔 room id 를 가져오겠습니다.
  string room_id_str;
  if (!session.GetFromContext ("room_id", out room_id_str))
  {
    // 방에 입장하지 않은 세션입니다.
    // 이상한 클라이언트라고 가정하여 세션을 닫겠습니다.
    session.Close();
    return;
  }

  // room_id 값이 비어있지 않다고 가정했습니다.
  Log.Assert(room_id_str != String.Empty);

  System.Guid room_id = new System.Guid (room_id_str);
  Room room = null;

  // Room 은 반드시 존재해야 합니다.
  Log.Assert (Room.Find (room_id, out room));
  Log.Assert (room != null);

  // Room 에서 유저를 나가게 합니다.
  // Room id 로 이벤트를 직렬화합니다.
  Event.Invoke (() => {
    room.HandleLeave(session);
  }, room.Id);
}

public static void OnClientChat(Session session, JObject message)
{
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager.FindLocalAccount (session);
  if (user_name == String.Empty)
  {
    // 로그인한 유저가 아니면 실패 처리합니다.
    SendMessage (session, "client_leave_room", false);
    return;
  }

  // chat msg 가 메시지에 없으면 이상한 클라이언트라고 가정하겠습니다.
  if (message ["chat_msg"] == null)
  {
    session.Close();
    return;
  }

  string chat_msg = (string) message ["chat_msg"];
  if (chat_msg == String.Empty)
  {
    // 비어있는 chat msg 도 이상한 클라이언트라고 가정하겠습니다.
    session.Close ();
    return;
  }

  // Room 에 입장할 때 session context 에 저장해둔 room id 를 가져오겠습니다.
  string room_id_str;
  if (!session.GetFromContext ("room_id", out room_id_str))
  {
    // 방에 입장하지 않은 세션입니다.
    // 이상한 클라이언트라고 가정하여 세션을 닫겠습니다.
    session.Close ();
    return;
  }

  Log.Assert (room_id_str != String.Empty);

  System.Guid room_id = new System.Guid (room_id_str);
  Room room = null;

  // Room 은 반드시 존재해야 합니다.
  Log.Assert (Room.Find (room_id, out room));
  Log.Assert (room != null);

  // Room 내 유저들에게 chat msg 를 전달합니다.
  // Room id 로 이벤트를 직렬화합니다.
  Event.Invoke (() => {
        room.HandleChat (session, chat_msg);
  }, room_id);
}

// 대전을 시작합니다. 방장만이 이 메시지를 보낼 수 있습니다.
public static void OnClientStartMatch(Session session, JObject message)
{
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager.FindLocalAccount(session);
  if (user_name == String.Empty) {
    // 로그인한 유저가 아니면 실패 처리합니다.
    SendMessage(session, "client_start_match", false);
    return;
  }

  // Room 에 입장할 때 session context 에 저장해둔 room id 를 가져오겠습니다.
  string room_id_str;
  if (!session.GetFromContext("room_id", out room_id_str)) {
    // 방에 입장하지 않은 세션입니다.
    // 이상한 클라이언트라고 가정하여 세션을 닫겠습니다.
    session.Close();
    return;
  }

  System.Guid room_id = new System.Guid(room_id_str);

  Room room = null;

  // 반드시 Room 이 존재한다고 가정하겠습니다.
  Log.Assert (Room.Find(room_id, out room));
  Log.Assert (room != null);

  // 대전 처리를 합니다.
  // Room id 로 이벤트를 직렬화합니다.
  Event.Invoke (() => {
    // HandleStartMatch() 함수에서 방장 여부를 검사합니다.
    room.HandleStartMatch(session, user_name);
  }, room_id);
}


// 모든 Room 정보를 보냅니다.
public static void OnClientGetRoomList(Session session, JObject message)
{
  // 이 서버에 로그인한 유저인지 확인합니다.
  string user_name = AccountManager.FindLocalAccount(session);
  if (user_name == String.Empty) {
    // 로그인한 유저가 아니면 실패 처리합니다.
    SendMessage(session, "client_start_match", false);
    return;
  }

  // 모든 Room 들의 정보를 Json 으로 가져와서 메시지를 전송합니다.
  JObject rooms = Room.GetRooms();
  session.SendMessage("client_get_room_list", rooms);
}

public static void Install(ArgumentMap arguments)
{
  ...

  NetworkHandlerRegistry.RegisterMessageHandler ("client_login",
      new NetworkHandlerRegistry.JsonMessageHandler (OnClientLogin));

  NetworkHandlerRegistry.RegisterMessageHandler ("client_logout",
      new NetworkHandlerRegistry.JsonMessageHandler (OnClientLogout));

  NetworkHandlerRegistry.RegisterMessageHandler ("client_chat_room",
      new NetworkHandlerRegistry.JsonMessageHandler (OnClientChat));

  NetworkHandlerRegistry.RegisterMessageHandler ("client_join_room",
      new NetworkHandlerRegistry.JsonMessageHandler (OnClientJoinRoom));

  NetworkHandlerRegistry.RegisterMessageHandler ("client_leave_room",
      new NetworkHandlerRegistry.JsonMessageHandler (OnClientLeaveRoom));

  NetworkHandlerRegistry.RegisterMessageHandler ("client_start_match",
      new NetworkHandlerRegistry.JsonMessageHandler (OnClientStartMatch));

  NetworkHandlerRegistry.RegisterMessageHandler ("client_get_room_list",
      new NetworkHandlerRegistry.JsonMessageHandler (OnClientGetRoomList));

Warning

여기서는 TCP 연결이 끊겼을 때의 Room 에서 나가기 처리는 하지 않았습니다. 경우에 따라서는 TCP 연결이 끊겼어도 session 은 유지하고 있다가 다시 재연결되었을 때 대전을 유지시킬 수 있기 때문입니다. (session 이 닫혔을 때 호출되는 OnSessionClosed() 함수에서만 Room 에서 나가도록 처리하였습니다.)

만약 TCP 연결이 끊겼을 때도 Room 에서 나가게 처리하거나 AI 등으로 해당 유저를 대체하여 처리해야 할 경우에는 HandlerRegistry::RegisterTcpTransportDetachedHandler() 함수로 TCP 연결이 끊기는 경우를 처리하는 핸들러를 추가하여 처리하시면 됩니다.