로그인 예제

이번 챕터에서는 아이펀 엔진을 이용하여 유저의 로그인 요청을 처리하는 과정을 보여줍니다.

해당 예제는 로그인 요청 처리 방법 중 하나의 예시를 보여주는 것이며 똑같이 개발하는 것이 정답은 아닙니다. 아이펀 엔진을 사용하는 데에 아직 익숙하지 않은 사용자분들을 위한 아주 간단한 예제이며, 개발하는 방식에 따라 로그인 시 수행되는 로직이 변경되거나 필요한 데이터가 추가되는 등 다양한 방식으로 구현될 수 있습니다.

로그인 서버 구현

먼저 로그인 요청을 처리하는 서버를 구현하도록 하겠습니다.

유저의 uuid 에 따라 처음 접속하는 유저는 선언해놓은 ORM 오브젝트를 생성하여 초기값을 설정해 주는 부분과 기존에 접속한 이력이 있는 유저는 저장되어 있는 데이터를 가져와 적용하고 유저에게 응답을 보내주는 예제입니다.

예제는 크게 로그인 요청 / 유저 데이터 요청 2가지로 나누어져 있으며 정상적으로 로그인 요청이 처리되면 다음으로 유저 데이터 요청을 처리하는 방식입니다.

Note

해당 예제는 아이펀 엔진을 한 번도 사용해보지 않은 사용자 입장에서는 이해하기 어려울 수 있는 설명 및 코드가 존재합니다. 예제를 진행하기 이전에 아래의 내용들을 먼저 확인해보시면 예제를 이해하는데 훨씬 수월합니다.

예제 중간에 설명이 필요한 부분에도 별도로 링크를 걸어놓았으니 예제를 순서대로 차근차근 진행해보셔도 괜찮습니다.

ORM 오브젝트 정의하기

해당 예제에서는 데이터베이스 관련하여 아이펀 엔진이 제공하는 ORM 기능을 사용하고 있습니다. ORM 기능을 사용하기 위해서는 MySQL 또는 MariaDB 가 설치되어 있어야 하며 MANIFEST.json 을 통한 설정이 진행되어 있어야 합니다.

예제를 시작하기 전에 해당 부분이 진행되지 않았다면 4.2 DB 기능 활성화하기 를 참조하여 ORM 사용에 필요한 기본적인 설정을 완료하시기 바랍니다.

ORM 정의에 대한 상세한 설명은 ORM Part 2: 오브젝트 정의하기 를 참조하시기 바랍니다.

사용할 유저의 ORM 모델은 아래와 같습니다. 로그인 요청에 사용될 데이터(Login)와 유저 데이터 (Status, Pvp)를 아래와 같이 정의하였습니다.

src/object_model/example.json

{
  "User": {
    "UniqueDeviceId": "String(50) Key",
    "LoginData": "Login",
    "UserStatus": "Status Foreign",
    "PvpData": "Pvp Foreign"
  },

  "Login": {
    "Active": "String(1)",
    "Locale": "String(3)",
    "LoginTime": "Datetime"
  },

  "Status": {
    "Level": "Integer",
    "Exp": "Integer",
    "Coin": "Integer",
    "Energy": "Integer"
  },

  "Pvp": {
    "Point": "Integer",
    "WinCount": "Integer",
    "LoseCount": "Integer"
  }
}

Note

User 오브젝트의 정의를 보면 오브젝트 내에 다른 오브젝트를 어트리뷰트로 정의하였습니다. 이런 경우 User 오브젝트를 읽어올 때 하위 오브젝트 데이터도 함께 읽어오기 때문에 편리한 점도 있지만, 하위 오브젝트가 많다면 불필요한 데이터를 읽느라 부하가 발생할 수 있습니다.

위 예제처럼 어트리뷰트가 오브젝트이거나 오브젝트 형식의 Array 또는 Map 인 경우 Foreign 키워드를 사용할 수 있는데, 이 키워드는 오브젝트를 한 번에 읽어오는 것을 방지하여 부하를 줄일 수 있습니다.

Foreign 의 사용법의 다른 예제는 Foreign 을 이용한 오브젝트 fetch 최소화 링크를 참고하세요.

Protobuf 메시지 선언하기

다음으로 클라이언트와 서버 간 요청 및 응답에 사용할 메시지를 정의하겠습니다. 예제에서 사용할 메시지 포맷은 protobuf 이며 메시지 정의는 아래와 같습니다.

Protobuf에 대한 자세한 설명은 Protobuf 메시지 링크를 참고해주세요.

src/{ProjectName}_messages.proto

// 로그인 요청에 사용할 메시지입니다.
message PbufLoginRequest {
  required string uuid = 1;
  required string locale = 2;
  //  필요에 따라 데이터를 추가하여 사용 할 수 있습니다.
  // ex) 인증키, 접속 환경 등등
}

// 로그인 요청에 대한 응답에 사용할 메시지 입니다.
message PbufLoginResponse {
  required bool result = 1;
  optional string uuid = 2;
}

// 유저 데이터 요청에 사용할 메시지 입니다.
message PbufGetUserInfoRequest {
  required string uuid = 1;
}

// 유저 데이터 요청에 대한 응답에 사용할 메시지 입니다.
message PbufGetUserInfoResponse {
  required int32 level = 1;
  required int32 exp = 2;
  required int32 coin = 3;
  required int32 energy = 4;
  required int32 pvp_point = 5;
  required int32 pvp_win_count = 6;
  required int32 pvp_lose_count = 7;
}

extend FunMessage {
  optional PbufLoginRequest pbuf_login_request = 16;
  optional PbufLoginResponse pbuf_login_response = 17;
  optional PbufGetUserInfoRequest pbuf_get_user_info_request = 18;
  optional PbufGetUserInfoResponse pbuf_get_user_info_response = 19;
}

메시지 핸들러 등록하기

다음으로 클라이언트로부터 전달받을 protobuf 메시지의 핸들러를 등록합니다. 아래의 RegisterEventHandlers() 함수는 프로젝트 생성 시 자동으로 생성되는 함수입니다.

RegisterEventHandlers() 함수 내부에 선언한 메시지와 핸들러를 등록합니다. 해당 서버로 특정 메시지 요청이 오는 경우 지정한 핸들러를 수행하도록 등록하는 과정입니다.

메시지 핸들러 외에 추가로 Remote Logout 핸들러도 같이 등록합니다. 로그아웃 처리를 위한 핸들러이며 상세한 설명은 아래 예제를 진행하면서 설명합니다.

src/event_handlers.cc

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void RegisterEventHandlers() {
  // 생략...

  // pbuf_login_request 메시지를 수신하면 OnLoginRequest 함수를 호출하도록 등록합니다.
  HandlerRegistry::Register2("pbuf_login_request", OnLoginRequest);
  // pbuf_get_user_info_request 메시지를 수신하면
  // OnGetUserInfoRequest 함수를 호출하도록 등록합니다.
  HandlerRegistry::Register2("pbuf_get_user_info_request", OnGetUserInfoRequest);

  // 다른 서버에서 로그인 시 호출되는 Remote logout 핸들러도 같이 등록합니다.
  AccountManager::RegisterRemoteLogoutHandler(OnRemoteLogout);

  // 생략...
}

로그인 및 로그아웃 관련 콜백함수 구현하기

이번에는 로그인 및 로그아웃과 관련된 처리를 위한 콜백함수를 구현하겠습니다.

구현할 함수는 총 3가지로 로그인 처리를 진행하는 AccountManager::CheckAndSetLoggedInAsync() 함수에서 사용할 로그인, 로그아웃 콜백함수 하나씩 그리고 위에 Remote logout을 위해 등록한 OnRemoteLogout() 핸들러입니다.

다른 코드보다 OnLoginCallback() 함수에서 진행되는 일들이 많습니다. 로그인 요청 핸들러에서는 AccountManager::CheckAndSetLoggedInAsync() 함수로 로그인 시도만 진행하고 실제 로그인 관련 유저 데이터 생성 및 로드는 로그인 콜백 함수에서 진행하기 때문입니다.

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
// CheckAndSetLoggedInAsync 함수에서 로그인 시도시에 불리는 콜백함수 입니다.
//
// id: 로그인을 시도한 Uuid
// session: 로그인을 시도한 Session
// result: 로그인 시도 결과
// locale: 콜백함수에서 제공하는 값이 아닌, 로그인 처리에 필요한 데이터를 bind 로 전달.
void OnLoginCallback(const string& id, const Ptr<Session>& session, bool result,
  string locale) {
  // 로그인에 실패하는 경우를 처리합니다.
  if (not result) {
    LOG(INFO) << "Login Fail. id : " << id;
    return;
  }

  // 로그인에 성공했을 경우 ORM 데이터를 생성하거나 가져옵니다.

  // 클라이언트에게 보낼 FunMessage 응답을 생성합니다.
  Ptr<FunMessage> response(new FunMessage);
  PbufLoginResponse* response_message =
    response->MutableExtension(pbuf_login_response);

  // uuid 를 통해 User 오브젝트를 Fetch 합니다.
  Ptr<User> user = User::FetchByUuid(id);
  if (not user) {
    // Fetch 에 실패했다면 해당 오브젝트가 없는 경우가 일반적입니다.
    // uuid 로 User 오브젝트를 생성합니다.
    user = User::Create(id);
    // Fetch로 NULL이 반환되는 경우 Create(Key) 함수는 무조건 성공합니다.
    if (not user) {
      // 포인터에 대한 NULL 체크는 코드의 안정성을 위해 진행합니다.
      LOG(ERROR) << "pbuf_login_request. User create false.";
      response_message->set_result(false);
      session->SendMessage("pbuf_login_response", response, kDefaultEncryption, kTcp);
      return;
    }
  }

  LOG(INFO) << "Login Success. id : " << id;

  // User의 Login 오브젝트를 가져옵니다.
  Ptr<Login> user_login_data = user->GetLoginData();

  // login 데이터가 없는 경우
  if (not user_login_data) {
    user_login_data = Login::Create();

  // 생성된 login 오브젝트를 SetLoginData 함수를 통해 User 오브젝트에 적용합니다.
    user->SetLoginData(user_login_data);

    // login data의 초기 데이터를 적용합니다.
    user_login_data->SetActive("Y");
  }

  // 초기 데이터가 아닌 로그인시마다 변경되는 데이터를 적용합니다.
  user_login_data->SetLoginTime(WallClock::Now());
  user_login_data->SetLocale(locale);

  // 클라이언트에게 전송할 데이터를 적용합니다.
  response_message->set_result(true);
  response_message->set_uuid(id);

  // 클라이언트에게 응답 메시지를 전달합니다.
  session->SendMessage("pbuf_login_response", response, kDefaultEncryption, kTcp);
}

// CheckAndSetLoggedInAsync 함수에서 불리는 로그아웃 콜백함수 입니다.
//
// id: 로그아웃 된 Uuid
// session: 로그아웃 된 Session
// result : 로그아웃 시도 결과.
void OnLogoutCallback(const string& id, const Ptr<Session>& session, bool result) {
  // CheckAndSetLoggedInAsync 함수에서 불리는 로그아웃 콜백함수 입니다.
  LOG(INFO) << "Logout from another login. id : " << id;

  if (result) {
    if (session != nullptr) {
      // session이 null이 아닌 경우 같은 서버에 session이 존재함을 의미합니다.
      // session 종료 이전에 수행할 동작을 구현할 수 있습니다.
      session->Close();
    }
    else {
      // session이 없는 경우 다른 서버에 session이 존재함을 의미합니다.
      // 해당 session에 대한 처리는 OnRemoteLogout 함수에서 처리합니다.
    }
  }
}

// CheckAndSetLoggedInAsync 함수로 로그인 시 다른 서버에 해당 id로 이미 로그인되있는 경우
// 기존에 로그인된 서버에서 호출되는 로그아웃 핸들러 입니다.
//
// id: 로그아웃 된 Uuid
// session: 로그아웃 된 Session
void OnRemoteLogout(const string& id, const Ptr<Session>& session) {
  // Remote Logout으로 인해 불리는 핸들러 입니다.
  // 로그아웃시에 해야하는 동작을 여기서 진행할 수 있습니다.
  LOG(INFO) << "OnRemoteLogout. Logout from other server login";
  if (session) {
    session->Close();
  }
}

로그인 요청 핸들러 구현하기

클라이언트가 로그인을 요청 메시지를 보냈을 때 실행될 메시지 핸들러를 구현합니다.

클라이언트 요청 메시지는 uuid 를 포함해야 하며, 서버는 전달받은 메시지의 uuidAccountManager::CheckAndSetLoggedInAsync() 함수에 넘겨서 로그인을 시도합니다.

src/event_handler.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
// pbuf_login_requset 메시지 요청이 올 때 호출되는 핸들러입니다.
void OnLoginRequest(const Ptr<Session>& session, const Ptr<FunMessage>& message) {
  // 클라이언트에게 전송할 FunMessage를 생성합니다.
  Ptr<FunMessage> response(new FunMessage);
  PbufLoginResponse* response_message =
    response->MutableExtension(pbuf_login_response);

  // 전달된 메시지에 처리하려는 메시지 타입이 있는지 검사합니다.
  // 메시지 핸들러를 잘못 등록하면 발생할 수 있습니다.
  if (not message->HasExtension(pbuf_login_request)) {
    LOG(ERROR) << "pbuf_login_request extension error.";
    // 클라이언트에게 로그인 실패를 전달합니다.
    response_message->set_result(false);
    session->SendMessage("pbuf_login_response", response);
    return;
  }

  // 실제로 처리할 FunMessage 하위의 서브 메시지를 추출해서 필요한 데이터를 초기화합니다.
  const PbufLoginRequest& pbuf_data = message->GetExtension(pbuf_login_request);
  string uuid = pbuf_data.uuid();

  // CheckAndSetLoggedInAsync 함수를 사용하여 로그인을 시도합니다.
  // 세번째 인자값으로 최대 접속시도 횟수를 설정할 수 있으며,
  // 로그인 실패시 횟수만큼 자동으로 로그인을 재시도합니다.
  // 로그인 시도마다 OnLoginCallback 함수가 호출되며
  // 기존에 연결된 id가 로그아웃되는 경우 OnLogoutCallback 함수가 호출됩니다.
  AccountManager::CheckAndSetLoggedInAsync(uuid, session, 10,
    bind(&OnLoginCallback, _1, _2, _3, pbuf_data.locale()),
    bind(&OnLogoutCallback, _1, _2, _3));
}

서버에 처음 접속을 요청하는 uuid 의 경우 핸들러와 콜백함수가 정상적으로 수행되었다면 UserLogin 오브젝트가 DB 에 저장되어 있는 것을 확인할 수 있습니다. ORM 에서 자동으로 생성되는 테이블은 tb_Object_{ObjectName} 의 형식이며 아래 쿼리문으로 해당 예제에서 설정한 초기값을 확인할 수 있습니다.

1
2
mysql> SELECT * FROM tb_Object_User;
mysql> SELECT * FROM tb_Object_Login;

Note

CheckAndSetLoggedInAsync() 함수를 사용하여 반드시 로그인을 해야하는 것은 아닙니다. 로그인 기능을 직접 구현해서 사용하거나, 싱글플레이 게임의 경우 해당 기능이 필요하지 않을 수 있습니다.

다만 CheckAndSetLoggedInAsync() 함수를 통해 로그인을 진행하면 게임 개발에 편의성을 제공하는 AccountManager 의 기능들을 사용할 수 있습니다. AccountManager 가 제공하는 편의 기능들을 분산 환경에서 클라이언트 관리 항목에서 확인할 수 있습니다.

유저정보 요청 핸들러 구현하기

다음으로 클라이언트는 pbuf_login_response 응답에 이어 pbuf_get_user_info_request 메시지를 생성하여 서버에 유저 정보를 요청한다고 가정합니다.

UserLogin 오브젝트는 위의 예제를 거치면서 이미 생성되어 있어야 하며 없는 경우 에러로 처리합니다.

src/event_handler.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
// 게임 진행에 필요한 데이터를 가져와 클라이언트에게 전달하는 핸들러입니다.
// 최초로 접속한 유저는 ORM 오브젝트를 생성 및 초기화를 진행합니다.
void OnGetUserInfoRequest(const Ptr<Session>& session, const Ptr<FunMessage>& message) {
  if (not message->HasExtension(pbuf_get_user_info_request)) {
    LOG(ERROR) << "pbuf_get_user_info_request extension error.";
    return;
  }

  const PbufGetUserInfoRequest& pbuf_data =
    message->GetExtension(pbuf_get_user_info_request);

  Ptr<FunMessage> response(new FunMessage);
  PbufGetUserInfoResponse* response_message =
    response->MutableExtension(pbuf_get_user_info_response);

  // User 오브젝트는 로그인 요청에서 이미 생성되어 있어야 합니다.
  Ptr<User> user = User::FetchByUuid(pbuf_data.uuid());
  if (not user) {
    // User 오브젝트가 없는 경우 비정상 요청으로 처리합니다.
    LOG(ERROR) << "OnGetUserInfoRequest user is null. uuid : " << pbuf_data.uuid();
    return;
  }

  // Login 오브젝트 역시 User 오브젝트와 마찬가지로 이미 생성되어 있어야 합니다.
  Ptr<Login> login_data = user->GetLoginData();
  if (not login_data) {
    LOG(ERROR) << "OnGetUserInfoRequest login_data is null. uuid : " << pbuf_data.uuid();
    return;
  }

  Ptr<Status> user_status = Status::Fetch(user->GetUserStatus());
  Ptr<Pvp> pvp_data = Pvp::Fetch(user->GetPvpData());

  // Status 오브젝트가 없는 경우
  if (not user_status) {
    user_status = Status::Create();
    // 생성된 오브젝트의 id를 SetUserStatus 함수를 통해 User 오브젝트에 적용합니다.
    user->SetUserStatus(user_status->Id());
    // 초기값을 적용합니다.
    user_status->SetLevel(1);
    user_status->SetExp(0);
    user_status->SetCoin(0);
    user_status->SetEnergy(60);
  }

  // Pvp 오브젝트가 없는 경우
  if (not pvp_data) {
    pvp_data = Pvp::Create();
    // 생성된 오브젝트의 id를 SetPvpData 함수를 통해 User 오브젝트에 적용합니다.
    user->SetPvpData(pvp_data->Id());
    // 초기값을 적용합니다.
    pvp_data->SetPoint(1000);
    pvp_data->SetWinCount(0);
    pvp_data->SetLoseCount(0);
  }

  // 클라이언트에게 보낼 정보들을 메시지에 설정합니다.
  response_message->set_level(user_status->GetLevel());
  response_message->set_exp(user_status->GetExp());
  response_message->set_coin(user_status->GetCoin());
  response_message->set_energy(user_status->GetEnergy());
  response_message->set_pvp_point(pvp_data->GetPoint());
  response_message->set_pvp_win_count(pvp_data->GetWinCount());
  response_message->set_pvp_lose_count(pvp_data->GetLoseCount());

  // 클라이언트에게 응답 메시지를 전달합니다.
  session->SendMessage("pbuf_get_user_info_response", response);
}

게임서버에 처음 접속하는 유저의 경우 Status / Pvp 오브젝트가 생성된 것을 아래 쿼리문을 통해 확인할 수 있습니다.

1
2
mysql> SELECT * FROM tb_Object_Status;
mysql> SELECT * FROM tb_Object_Pvp;

또한 클라이언트에서 받은 응답을 확인해보면 서버에서 보낸 데이터들을 확인할 수 있습니다.

Important

OnGetUserInfo() 핸들러에서 수행되는 Fetch() 함수는 Rollback 을 발생시켜 이벤트 루틴을 처음부터 다시 수행할 수 있기 때문에 함수 앞쪽에서 한 번에 Fetch() 를 진행하는 것이 좋습니다.

Rollback 으로 인해 발생할 수 있는 문제와 문제를 회피하기 위한 방법은 트랜잭션 에서 확인하실 수 있습니다.

테스트 클라이언트로 메시지 주고받기

간단한 로그인 처리를 위한 서버코드를 작성했으니 테스트 클라이언트를 통해서 실제로 요청을 보내고 응답을 받아보도록 하겠습니다.

메시지를 요청하는 클라이언트는 다양한 방법으로 만들 수 있습니다. Unity 또는 Unreal 게임엔진을 이용하여 클라이언트를 만들 수도 있고, 아이펀 엔진에서 제공하는 기능을 사용하거나, 플러그인으로 제공되는 샘플 프로젝트를 사용할 수도 있습니다.

본 예제에서는 두가지 방법으로 테스트 클라이언트를 구현해 보겠습니다. 먼저, 아이펀 엔진에서 제공하는 funtest 객체를 활용하는 방법에 대해서 알아보고 난 후, Unity 클라이언트 플러그인에 함께 제공되는 샘플 프로젝트를 활용하는 방법에 대해서 알아보겠습니다.

funtest 를 이용한 메시지 주고받기

이번 예제에서는 아이펀 엔진에서 제공하는 funtest::Network, funtest::Session 객체를 이용하여 메시지를 주고받아 보겠습니다.

funtest 는 실제로 클라이언트 코드를 작성하는것이 아닌 서버코드 내에서 클라이언트의 동작을 수행하는 funtest 세션을 생성하여 서버와 메시지를 주고받을 수 있는 콤포넌트 입니다.

예제에 사용되는 funtest 에 대한 상세한 내용은 방법1: 아이펀 엔진의 Bot 콤포넌트 이용 을 참고하시기 바랍니다.

funtest 세션 생성 및 콜백함수 구현하기

먼저 서버와 연결할 세션을 생성하겠습니다. 서버코드의 Install() 함수에서 세션이 열리고 닫힐 때 호출되는 콜백함수를 등록하고 Start() 함수에서 funtest 세션을 생성하도록 하겠습니다.

src/{ProjectName}_server.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
static bool Install(const ArgumentMap &argument) {
  // 생략...

  // funtest 세션이 열리고 닫힐 떄 사용할 콜백함수를 등록합니다.
  funtest::Network::Install(OnsessionOpened, OnSessionClosed, 4);

  return true;
}

static bool Start() {
  // 생략...

  // funtest 세션을 생성하여 서버와 Tcp 연결을 맺습니다.
  // IP는 localhost, 포트는 MANIFEST.json에서 지정한 TCP + Protobuf의 포트인 8022로 설정합니다.
  Ptr<funtest::Session> session = funtest::Session::Create();
  session->ConnectTcp("127.0.0.1", 8022, kProtobufEncoding);

  return true;
}

// funtest 세션이 열릴 때 불리는 콜백함수 입니다.
// funtest 세션이 열리면 자동으로 서버에게 pbuf_login_request 요청을 보냅니다.
static void OnSessionOpened(const Ptr<funtest::Session> &session) {
  LOG(INFO) << "[test_client] session created: sid = " << session->id();

  // user를 구별할 때 사용할 uuid를 생성하고 string으로 변환하여 사용합니다.
  string uuid = boost::lexical_cast<string>(RandomGenerator::GenerateUuid());

  // 서버에게 로그인 요청 메시지를 생성하여 전송합니다.
  Ptr<FunMessage> msg(new FunMessage);
  PbufLoginRequest *request = msg->MutableExtension(pbuf_login_request);
  request->set_uuid(uuid);
  request->set_locale("KR");
  session->SendMessage("pbuf_login_request", msg, kTcp);
}

// funtest 세션이 닫힐 때 불리는 콜백함수 입니다.
static void OnSessionClosed(const Ptr<funtest::Session>& session,
  SessionCloseReason reason) {
  LOG(INFO) << "[test_client] session closed: sid = " << session->id();
}

메시지 핸들러 등록 및 구현하기

다음으로 서버가 응답하는 메시지에 대한 핸들러를 Install() 함수에 등록하고 구현하도록 하겠습니다.

src/{ProjectName}_server.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
static bool Install(const ArgumentMap &argument) {
  // 생략...

  // 서버로부터 응답받을 protobuf 메시지의 핸들러를 등록합니다.
  funtest::Network::Register2("pbuf_login_response", OnLoginResponse);
  funtest::Network::Register2("pbuf_get_user_info_response", OnGetUserInfoResponse);

  return true;
}

// 서버로부터 받은 pbuf_login_response 메시지의 핸들러 함수입니다.
// 로그인에 성공했다는 응답을 받으면, PbufGetUserInfoRequest 요청을 보냅니다.
static void OnLoginResponse(const Ptr<funtest::Session>& session,
  const Ptr<FunMessage>& msg) {
  if (not msg->HasExtension(pbuf_login_response)) {
    LOG(ERROR) << "pbuf_login_response extension error";
    return;
  }

  const PbufLoginResponse& response = msg->GetExtension(pbuf_login_response);

  // 로그인 실패 응답을 받았을 때 처리할 코드를 추가합니다.
  if (response.result() == false) {
    LOG(ERROR) << "Login false.";
    return;
  }

  const string uuid = response.uuid();

  // 유저 정보를 요청할 메시지를 생성하여 전송합니다.
  Ptr<FunMessage> request_msg(new FunMessage);
  PbufGetUserInfoRequest* request =
    request_msg->MutableExtension(pbuf_get_user_info_request);
  request->set_uuid(uuid);
  session->SendMessage("pbuf_get_user_info_request", request_msg, kTcp);
}

// 서버로부터 받은 pbuf_get_user_info_response 응답의 핸들러 함수 입니다.
static void OnGetUserInfoResponse(const Ptr<funtest::Session>& session,
  const Ptr<FunMessage>& msg) {
  if (not msg->HasExtension(pbuf_get_user_info_response)) {
    LOG(ERROR) << "pbuf_get_user_info_response extension error";
    return;
  }

  // 응답으로 온 데이터를 Log로 출력합니다.
  // 실제 클라이언트에서는 데이터를 가져와 필요한 곳에 사용할 수 있습니다.
  const PbufGetUserInfoResponse& response =
    msg->GetExtension(pbuf_get_user_info_response);
  LOG(INFO) << "OnGetUserInfoResponse. Level : " << response.level() << ", exp : " <<
    response.exp() << ", coin : " << response.coin() << ", energy : " <<
    response.energy() << ", pvp_point : " << response.pvp_point() <<
    ", pvp_win_count : " << response.pvp_win_count() <<
    ", pvp_lose_count : " << response.pvp_lose_count();
}

funtest 예제 코드까지 추가하여 서버를 실행하면 OnGetUserInfoResponse 핸들러에서 서버로부터 응답받은 데이터가 Log로 찍히는 것을 확인할 수 있습니다.

샘플 프로젝트를 이용한 메시지 주고받기

다음으로 Unity 플러그인에서 제공하는 샘플 프로젝트를 통해 메시지를 주고받아 보겠습니다. 실제로 작성되어있는 클라이언트 코드를 다루기 때문에 funtest 예제보다는 전반적으로 클라이언트 코드를 작성할 때 어떤 것을 준비해야 하는지, 코드는 어떻게 작성해야 하는지 확인할 수 있는 예제입니다.

Important

본 예제를 원활하게 이해하고 진행하기 위해 유니티 플러그인 샘플 프로젝트 이용 문서를 먼저 보시는 것을 권장드립니다.

샘플 프로젝트 코드 수정하기

샘플 프로젝트에서 수정할 코드 경로는 engine-plugin-unity3d/csharp-sample/src 이며 client.csmain.cs 모두 수정하도록 하겠습니다.

이미 구현되어 수정할 필요가 없는 부분은 수정하지 않고 메시지를 다루는 부분 위주로 수정하였으며 함수 이름만 변경한 부분이 있으니 코드내의 주석을 참고하시기 바랍니다.

Note

샘플 프로젝트에서 이미 사용하는 dll 이 아닌 예제 서버에서 빌드하여 생성된 dll 을 사용해야 예제에서 선언한 Protobuf 메시지를 클라이언트 코드에서 사용할 수 있습니다.

client.cs

  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
// 변경된 protobuf dll 파일을 사용하기 위해 using 관련 코드를 수정합니다.
using Fun;
// 생략...

//protobuf
// 기존 코드인 using plugin_messages;를 아래로 대체합니다.
// {ProjectName}_messages 형식이며 ProjectName은 서버 프로젝트 이름입니다.
using login_example_messages;
using funapi.network.fun_message;

namespace Tester
{
  class Tester
  {
    // 생략...

    // 코드 가독성을 위해 함수명을 변경합니다.
    // SendEchoMessageWithCount에서 SendLoginMessageWithCount 로 변경합니다.
    public void SendEchoMessageWithCount (TransportProtocol protocol, int count)
    {
      sending_count_ = count;

      for (int i = 0; i < count; ++i)
      {
        // 호출하는 함수명도 변경합니다.
        // sendEchoMessage 에서 sendLoginMessage로 변경합니다.
        sendLoginMessage(protocol);
      }
    }

    // 코드 가독성을 위해 함수명을 변경합니다.
    // SendEchoMessage에서 SendLoginMessage로 변경합니다.
    void sendLoginMessage (TransportProtocol protocol)
    {
      // session이 protocol에 맞는 transport 객체를 가지고 있는지 확인합니다.
      // 예제에서는 Tcp를 사용합니다.
      FunapiSession.Transport transport = session_.GetTransport(protocol);
      if (transport == null)
      {
        FunDebug.LogWarning("sendLoginMessage - transport is null.");
        return;
      }

      // transport 객체의 encoding이 protobuf인지 확인합니다.
      if (transport.encoding != FunEncoding.kProtobuf)
      {
        FunDebug.LogWarning("sendLoginMessage - encoding is not protobuf.");
        return;
      }

      // protobuf 메시지 객체를 생성하여 요청할 데이터를 적용합니다.
      PbufLoginRequest request = new PbufLoginRequest();
      Guid guid = Guid.NewGuid();
      request.uuid = guid.ToString();
      request.locale = "KR";

      // FunMessage를 생성하여 서버에게 요청 메시지를 보냅니다.
      FunMessage message =
      FunapiMessage.CreateFunMessage(request, MessageType.pbuf_login_request);
      session_.SendMessage("pbuf_login_request", message, protocol);
    }

    // 서버로부터 메시지를 수신하는 경우 불리는 콜백함수 입니다.
    void onReceivedMessage (string type, object message)
    {
      if (type == "pbuf_login_response" || type == "pbuf_get_user_info_response")
      {
        --sending_count_;
        if (sending_count_ <= 0)
          is_done_ = true;
      }

      // 받은 메시지의 타입을 통해 어떤 동작을 수행할지 결정합니다.
      if (type == "pbuf_login_response")
      {
        FunMessage msg = message as FunMessage;
        object obj = FunapiMessage.GetMessage(msg, MessageType.pbuf_login_response);
        if (obj == null)
          return;

        PbufLoginResponse response = obj as PbufLoginResponse;

        // 서버로부터 로그인 실패 응답을 받은 경우
        if (response.result == false)
        {
          FunDebug.Log("Login fail.");
          return;
        }

        // 유저 정보를 요청하는 메시지를 생성하여 전송합니다.
        PbufGetUserInfoRequest request = new PbufGetUserInfoRequest();
        request.uuid = response.uuid;
        FunMessage request_message =
          FunapiMessage.CreateFunMessage(request, MessageType.pbuf_get_user_info_request);
        session_.SendMessage("pbuf_get_user_info_request",
        request_message, TransportProtocol.kTcp);
      }
      else if (type == "pbuf_get_user_info_response")
      {
        FunMessage msg = message as FunMessage;
        object obj = FunapiMessage.GetMessage(msg, MessageType.pbuf_get_user_info_response);
        if (obj == null)
          return;

        // 응답으로 받은 유저 정보를 Log로 확인합니다.
        PbufGetUserInfoResponse response = obj as PbufGetUserInfoResponse;
        FunDebug.Log("level: {0}, exp: {1}, coin: {2}, energy: {3}, pvp_point: {4}," +
          pvp_win_count: {5}, pvp_lose_count: {6}", response.level, response.exp,
          response.coin, response.energy, response.pvp_point, response.pvp_win_count,
          response.pvp_lose_count);
      }
    }

    // 생략...

    // getPort 함수는 사용하는 프로토콜, 인코딩에 따라 설정한 값을 사용하도록 변경합니다.
    ushort getPort (TransportProtocol protocol, FunEncoding encoding)
    {
      ushort port = 0;
      if (protocol == TransportProtocol.kTcp)
        port = (ushort)(encoding == FunEncoding.kJson ? 8012 : 8022);
      else if (protocol == TransportProtocol.kUdp)
        port = (ushort)(encoding == FunEncoding.kJson ? 8013 : 8023);
      else if (protocol == TransportProtocol.kHttp)
        port = (ushort)(encoding == FunEncoding.kJson ? 8018 : 8028);
      else if (protocol == TransportProtocol.kWebsocket)
        port = (ushort)(encoding == FunEncoding.kJson ? 8019 : 8029);

      return port;
    }

    // 생략...
  }
}

main.cs

 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
// 생략...

namespace Tester
{
  class TesterMain
  {
    // 생략...

    void onTest()
    {
      // 생략...

      // client.cs 에서 변경한 함수이름을 여기에도 수정합니다.
      // SendEchoMessageWithCount를 SendLoginMessageWithCount로 변경합니다.
      // 추가로 간단한 예제 확인을 위해 함수에 넘겨주는 인자값을 1로 수정합니다.
      client.SendLoginMessageWithCount(TransportProtocol.kTcp, 1);
        while (!client.IsDone)
          Thread.Sleep(10);

      // 생략...
    }

    // 생략...
  }
}

코드까지 수정이 완료되었으면 예제 프로젝트를 실행하여 메시지를 잘 주고 받는지 확인해봐야 합니다. 먼저 미리 만들어놓은 예제 서버를 실행합니다. 그리고 샘플 프로젝트를 빌드하기 위해 engine-plugin-unity3d/csharp-samples 경로에서 아래 명령을 실행하면 빌드 후 샘플 클라이언트가 실행되어 서버와 메시지를 주고받는 것을 확인할 수 있습니다.

$ make
$ mono tester.exe

Note

샘플 클라이언트와 서버가 메시지를 주고받지 못하거나 연결을 맺는데 문제가 있다면 ip와 port를 한번 더 확인해보세요. 샘플 클라이언트가 연결하려는 서버의 ip는 localhost(127.0.0.1) port는 8022(Tcp + Protobuf)를 사용하고 있습니다.

여기까지 간단한 예제를 통해 ORM 오브젝트와 Protobuf 메시지를 선언하고, 메시지에 대응하는 핸들러를 등록하고, 로그인 요청을 받아서 유저의 초기 데이터를 생성하거나 기존 데이터를 가져와 클라이언트에게 전달하는 코드를 살펴보았습니다.

여러분이 구현하고자 하는 내용에 따라 예제와는 전혀 다른 로그인 코드가 작성될 수 있습니다. 로그인 시 다른 인증 과정을 추가할 수도 있고, 기간이 지난 우편이나 아이템을 삭제한다던가, 페이스북, 구글, 카카오 아이디를 이용한 로그인을 수행할 수도 있습니다.

실제 게임 서버 개발을 진행하시다 보면 예제보다 훨씬 많은 데이터와 규모가 큰 오브젝트를 다루게 될 것입니다.

기본적인 로그인을 구현하셨다면 본격적으로 원하는 게임 콘텐츠 개발을 진행하시면 됩니다. 아이펀 엔진은 게임 개발에 도움이 되는 다양한 기능들을 지원합니다. 매칭 시스템을 위한 매치메이킹, 랭킹 시스템을 위한 리더보드, 멀티캐스팅 및 채팅 등 해당 예제 외에 아이펀 엔진에서 제공하는 다양한 기능들을 레퍼런스 페이지에서 확인하실 수 있습니다.