18.Server & Client 통신

Edit

18.1ProudNet 사용하기

ProudNet을 사용하기 위해서는 Lib파일이 프로젝트에 링크되어야 합니다. Lib파일은 ProudNet을 인스톨하신 위치의 Lib폴더에 있습니다. 사용하는 Visual Studio 버전과 컴파일 버전 (debug, release 등)에 맞는 버전을 링크하시면 됩니다. 만약 버전이 맞지 않는다면 정상적인 작동이 되지 않습니다. ProudNet의 모듈을 사용하기 위해서 ‘./include’폴더의 ‘ProudNet.h’를 include해야 합니다. ProudNet의 모든 Source는 ‘namespace Proud’로 묶여 있기 때문에 ‘using namespace Proud’를 사용하신다면 보다 편리하게 사용하실 수 있습니다. 앞으로 제시될 예제들은 ProudNet의 Sample폴더안에 있는 Simple Sample과 도움말을 참고하시면 보다 쉽게 이해하실 수 있습니다.

18.2Server의 구동

먼저 Server를 만들어 보겠습니다.

필요한 객체와 Header

// ProudNet을 include 합니다.
#include “include\ProudNetServer.h”
  
// ProudNet은 모든 객체가 
// Proud라는 namespace로 
// 묶여 있습니다.
using namespace Proud;
  
// port 정의
int g_ServerPort = 33334;

Δ 준비

CNetServer* srv = 
         ProudNet::CreateServer();
srv->SetEventSink(
         &g_eventSink);
  
CStartServerParameter p1;
 
// Client 의 Connection을 받을 Port
p1.m_tcpPort = 33334;  
  
srv->Start(p1);

Δ 서버 시작하기

먼저 Server객체를 생성한 후 SetEventSink라는 함수를 호출합니다. Server에서 일어나는 Event를 Callback받기 위한 객체를 등록하는 과정입니다. INetServerEvent 객체를 상속받아 생성한 객체의 포인터를 넘겨주게 되면 Server는 이 객체를 통하여 Event를 Callback해줍니다.

Class CServerEventSink 
         : public INetServerEvent 
{
       // Client의 접속이 완료되면 
       // Callback됩니다.
       Virtual void OnClientJoin(
           CNetClientInfo *info) 
           OVERRIDE
       {
           // Client의 정보를 받아 
           // 처리합니다.
       }
       // Client의 접속이 해제되면 
       // Callback됩니다.
           Virtual void OnClientLeave(
               CNetClientInfo *info) 
               OVERRIDE
       {
               // Client의 정보를 받아 
               // 처리합니다.
       }
       // 나머지 Event는 생략
}

Δ CNetServer로부터 Event 를 받을 객체

Callback받은 Event에서 CNetClientInfo 객체를 인자로 받습니다. CNetClientInfo객체에는 접속된 Client의 정보가 들어 있으며, CNetClientInfo의 멤버 m_HostID는 각 Host를 구분할 수 있는 ID값이 됩니다.

연결 해제

Proud::HostID

HostID는 모든 각 host들을 구분할 수 있는 유일한 ID값 입니다. 각각의 Server에서 발급하는 ID값이며, 한 Server 에서 발급되는 ID 는 중복되지 않습니다.

HostID_None = 0
HostID_Server = 1
HostID_Last = 2
  
3번부터 접속되거나 생성되는 순으로 
HostID값이 부여됩니다.
(HostID 재사용 Option off 시)

18.3Client의 구동

Server에 접속을 할 Client를 만듭니다.필요한 객체와 Header

// ProudNet을 include 합니다.
#include “include\ProudNetClient.h”
  
// ProudNet은 모든 객체가 
// Proud라는 namespace로 
// 묶여 있습니다.
Using Namespace Proud;
  
// port 정의
int g_ServerPort = 33334;

Δ 준비

간단한 소스 작성만으로도 Server에 접속이 되어, 통신이 가능해 집니다.

CNetClient *client 
= ProudNet::CreateClient();
  
// Server와 마찬 가지로 Event를 
// Callback받을 객체를 등록합니다.
client ->SetEventSink(&g_eventSink);
// 서버 연결을 위한 프록시 설정
client->AttachProxy(&g_c2sProxy); 
// 데이터 통신을 위한 스텁 설정
client->AttachStub(&g_c2sStub);   
// 클라이언트 연결을 위한 프록시 설정
client->AttachProxy(&g_c2cProxy); 
// 데이터 통신을 위한 스텁 설정
client->AttachStub(&g_c2cStub);   
  
CNetConnectionParam cp;
// Server의 주소. 
// 필요하신 IP Address 주소를 
// 기입하시면 됩니다.
// localhost의 기입은 같은 기기에 
// Server와 Client가 있음을 
// 의미합니다. 
cp.m_serverIP = L"localhost";   
// Server의 포트
cp.m_serverPort = g_ServerPort; 
  
client->Connect(cp);
While(1)
{
// Client는 매 프레임 
// FrameMove를 호출 해주어야 합니다.
Client->FrameMove();
Sleep(100); 
}
  
delete client;

Δ Server에 접속 시작

위의 방식으로 네 객체를 연결 후 Connect & FrameMove를 호출 하면 통신이 이루어지며, 예제에서 Frame마다 Client의 FrameMove를 호출하고 있습니다. Server와 달리 Client에서 ‘CNetClient::FrameMove()’를 호출하는 이유는 Threading Model 차이 때문입니다. 아래는 Threading Model의 차이를 보여줍니다.

표 18-1Threading Model

Client 객체 (CNetClient)

Server 객체 (CNetServer)

Single Thread Model

Multi Thread Model

Polling Model

▶ CNetClient::FrameMove()

(FrameMove에서 Event를 Callback)

Thread Pool Event Callback

(Event를 Callback하는 Thread가 별도로 존재)

게임 Client는 일반적으로 빠르게 도는 루프를 갖고 있습니다. 이때 Polling Model을 사용하게 되면 메인 루프에서 특정 함수(FrameMove)를 호출했을 때, 호출한 thread에서만 Event를 Callback되는 구조를 갖게 됩니다. 이로 인하여 Client개발자는 의도하지 않은 (복잡도 높은) Thread Programing의 부담을 줄일 수 있습니다.

class CClientEventSink
         : public INetClientEvent
{
       // Server에 접속이 완료되면 
       // callback됩니다.
       virtual void OnJoinServerComplete(
           ErrorInfo *info, 
           const ByteArray & replyFromServer) 
       {
           if(info->m_errorType != 
                    ErrorType_Ok)
           {
                // 이 경우 접속이 
                // 실패한 경우입니다.
                // 왜 실패 하였는지 
                // 로그를 남기세요
           }
       }
  
       // Server와의 접속이 끊겼을 때 
       // Callback됩니다.
       Virtual void OnLeaveServer(
           ErrorInfo *errorInfo) { }
       // 기타 Event 생략
}
CClientEventSink g_eventSink;

Δ CNetClient로부터 Event 를 받기 위한 객체

연결 해제

18.4Event

아래의 Event는 Client와 Server에서 공통으로 사용되는 Event입니다. Parameter인 errorInfo의 errorInfo -> ToString(); 을 사용하면 쉽게 문제에 대한 정보를 얻으실 수 있습니다.

아래의 Event는 Server Event입니다. 성능 Test 등에 이용할 수 있습니다.

18.5Server & Client 간 통신

ProudNet에서는 Server & Client간의 통신에 있어 RMI를 사용합니다.

RMI

RMI이란?

RMI(Remote Method Invocation)란 다른 네트워크 혹은 다른 프로세스에 있는 함수를 호출하는 것을 말합니다. RMI를 쓰면 송수신 루틴과 메시지(Message) 구조체를 정의하는 코딩 작업을 사람 대신 기계가 해줌으로써 단순 반복적인 작업을 없애주고 실수를 방지할 수 있습니다. 메시지를 보내는 쪽에서 RMI 함수를 호출하는 쪽을 Proxy라 하고, RMI 함수 호출을 받는 쪽을 Stub이라고 합니다.

PIDL이란?

PIDL은 RMI를 위한 자체 제작된 컴파일러 입니다. 특정 파일에 Protocol을 정의 후 설정하면 자동으로 객체가 생성된 파일을 만들어 줍니다. 이때 생성된 객체는 Server와 Client에서 같이 사용되기 때문에 Common(공용) Project를 생성하여 관리하면 편리합니다.

PIDL파일 생성하기 & Setting

파일을 생성하는 방법은 간단합니다. Visual Studio에서 txt파일로 파일을 생성하시고, 확장자를 PIDL로 바꾸어 준 뒤 컴파일을 하기 위한 세팅을 해주시면 됩니다.확장자가 PIDL인 파일들은 Custom Build 설정이 되어 있어야 합니다. [ Visual Studio 솔루션 Viewer ▶ 생성한 PIDL파일 우 클릭 ▶ 속성 ▶ General ▶ Item Type: Custom Build Tool ] 자세한 PIDL Custom Build 설정 방법은 도움말 혹은 Sample을 참고하세요. [ ProudNet 사용법 도움말 ▶ Remote Method Invocation 사용 방법 ▶ PIDL 파일을 컴파일 하기 ]

PIDL 문법

PIDL은 아래와 같은 구조로 이루어져 있습니다.

global (namespace) 
         (메시지의 ID 첫 시작 값) 
{ 
     함수 선언([in] 함수 Parameter, …) 
}

컴파일을 하게 되면 namespace를 생성 후, Stub과 Proxy Class는 이 namespace 안에 들어가게 됩니다. 모든 RMI함수들은 고유ID를 갖게 되는데 이 값은 ‘메시지의 ID 첫 시작 값’ 으로부터 +1씩 추가되어 ID가 부여됩니다. 단, 0~1300사이의 ID와 63000이 후의 ID는 ProudNet 내부 메시지로 사용하기 때문에 이 외의 번호를 사용해야 합니다.

예제)
global S2C 1000 
{
     // Protocol을 정의합니다.
    Chat([in] Proud::String txt);
}

생성된 Proxy & Stub 파일 사용법

PIDL을 실행하시면 아래와 같은 6개의 파일이 생성됩니다.

PIDL파일명_Common .Cpp & .h

PIDL파일명_proxy .Cpp & .h

PIDL파일명_stub .Cpp & .h

Common 파일을 제외한 각각 h파일은 header에, cpp파일은 cpp파일에 #include 해주면 편리합니다. (.h파일 .cpp 파일은 프로젝트에 포함시켜도 되지만, Custom Build 사용으로 인하여, 파일에 변경이 자주 일어납니다.) 생성된 Rmi 함수에는 정의한 변수 외에 두 가지 Parameter가 자동으로 추가됩니다.

Proud::HostID – 통신하고자 하는 상대 Host의 ID값

roud::RmiContext - 보내거나 받기 위한 옵션

Proud::RmiContext

RmiContext는 통신을 하기 위한 Option Class이며, 내부에 Static으로 선언 되어 있는 변수들을 사용하시면 편리합니다.

ReliableSend : Reliable통신

FastEncryptedReliableSend : 빠른 암호화 방식(보안성이 낮은)을 사용하는 Reliable통신

SecureReliableSend : 암호화된 Reliable통신

UnreliableSend : Unreliable 통신

FastEncryptedUnreliableSend : 빠른 암호화 방식(보안성이 낮은)을 사용하는 Unreliable 통신

SecureUnreliableSend : 암호화된 Unreliable 통신

많이 사용하는 Option 은 Static으로 미리 만들어져 있습니다. 사용자 편의에 따라 직접 생성하여 원하시는 Option을 정할 수 있습니다.

m_reliability : Reliable & Unreliable 통신방식 선택

m_encryptMode : 암호화 여부(속도와 보안성에 따라서 세가지 Option이 선택 가능합니다.)

m_compressMode : 압축 여부

다른 자세한 기능에 대한 Option들은 도움말을 참고하십시오.

Reliable & Unreliable

두 통신 방식의 차이는 다음과 같습니다.

Reliable

Unreliable

Packet을 받는 순서가 보장

Packet을 받는 순서가 보장되지 않음

손실이 없음을 보장

Packet의 손실이 있을 수 있음

TCP & UDP 사용

(Hole-Punching성공시 내부적으로 Reliable UDP 사용)

TCP & UDP를 둘 다 사용하나UDP가 가능한 상태면 UDP 사용

Reliable UDP는 UDP를 Packet의 순서를 보장하고 손실이 없도록 사용하기 위한 방법으로써 일반적인 Unreliable UDP보다는 Traffic이 더 발생할 수 있습니다.

Proxy & Stub 통신객체에 등록 & 사용

Server와 Client에 통신을 추가해 보겠습니다. Common(공용) 프로젝트를 생성 후 PIDL파일을 준비합니다. 생성된 파일은 Server와 Client 모두에서 사용되기 때문에 편리성을 위하여 Common 프로젝트를 생성하여 관리하고 있다는 가정하에 진행합니다. 준비된 PIDL파일에 Server에서 Client로 통신을 보내기 위한 Protocol을 정의 합니다.

Global S2C 3000
{
   Chat(Proud::StringA txt);
}

Δ Server ▶ Client

위의 PIDL파일을 컴파일하면 Proxy와 Stub객체가 생성됩니다. Proxy 객체를 사용하는 방법을 다루어 보겠습니다. 먼저 사용하실 곳에 Header를 포함시킵니다.

// Server: Server ▶ Client임으로 
// Server에서는 호출을 하기 위하여 
// Proxy 객체를 포함합니다.
// header file에 선언
// Common 프로젝트를 만드는 것을 
// 가정 하였기 때문에Common 프로젝트의 
// 폴더로부터 생성된 파일을 포함시킵니다.
#include "../Common/S2C_proxy.h"
  
// cpp 파일에 선언
#include "../Common/S2C_proxy.cpp"

Δ Proxy

Proxy를 생성하고 Server객체에 등록시켜 보겠습니다.

// 객체를 생성합니다.
S2C::Proxy g_S2CProxy;
  
void main()
{
    // Server 설명에서 생성하였던 
    // Server 객체 입니다.
    CNetServer* srv = 
         ProudNet::CreateServer();
    Svr->AttachProxy(&g_S2CProxy);
  
    // 이하 생략
}

AttachProxy라는 함수를 이용하여 생성된 Proxy객체의 포인터를 넘겨주는 방식으로 등록시켰습니다. AttachProxy는 내부에서 배열로 관리 하기 때문에 여러 종류의 PIDL을 등록 시킬 수 있습니다. 등록을 시키셨다면 Proxy 객체의 함수를 사용하여 통신을 할 수 있습니다.

// HostID와 RmiContext가 
// 자동으로 추가 됩니다.
// hostID로 보내고자 하는 
// Client의 HostID값을 넣습니다.
g_S2CProxy.Chat(
         hostID, 
         RmiContext::ReliableSend, 
         “Send Message”);

Proxy와 마찬가지로 사용하실 곳에 Header를 포함시킵니다.

Client:
 Server ▶ Client임으로 Client에서는 
 호출을 받기 위한 Stub객체를 포함합니다.
// header file에 선언
// Common 프로젝트를 만드는 것을 
// 가정 하였기 때문에Common폴더에 
// 생성된 파일을 포함시킵니다.
#include "../Common/S2C_stub.h"
  
// cpp 파일에 선언
#include "../Common/S2C_stub.cpp"

Δ Stub

Stub객체의 경우 받을 Protocol의 정의 함수들이기 때문에 상속받은 객체를 생성하여 사용해야 합니다. AttachStub 함수를 사용하여 등록하면, 해당 호출이 왔을 시 Callback 됩니다. 생성된 Stub 객체 안에는 정의(Define)가 만들어집니다. 이 정의를 사용하면 Protocol을 변경해도 cpp파일과 h파일을 따로 수정할 필요가 없습니다.

#define DECRMI_C2S_Chat bool Chat(
        Proud::HostID remote,
        Proud::RmiContext &rmiContext,
        const Proud::StringA txt)

Stub Class에 명시된 Define문 중 ‘DEFRMI_NameSpace_함수이름’은 상속 받은 객체의 Header File에 선언해줍니다. ‘DECRMI_NameSpace_함수이름 (Class_Name)’ 은 cpp에 선언줍니다.

class CS2CStub
          : public S2C::Stub
{
public:
  
// Protocol은 변경되어도 사용자가 
// class를 수정할 필요 없도록 stub안에 
// define문으로 처리되어 있습니다.
// ‘DEFRMI_NameSpace_함수이름’ 으로 
// 되어 있으면 header에 선언합니다.
    DECRMI_S2C_Chat;
};
CS2CStub g_S2CStub;
  
// ‘DEFRMI_Protocol분류명_protocol명(
//               상속받은 class name)’
// 으로 되어 있으면 cpp에 선언합니다.
DEFRMI_S2C_Chat(CS2CStub)
{
    printf( 
         "[Client] HostID:%d, text: %s”, 
         remote, 
         txt);
  
         // 반드시 true를 return해야 합니다
    return true;
}

위를 보시면 True를 Return하고 있습니다. True를 Return 하는 것은 처리가 되었다라는 의미입니다.False를 Return하게 되면 사용자가 Protocol에 대한 처리를 하지 않은 것으로 판단하여 OnNoRmiProcessed Event가 Callback 됩니다. DEFRMI_S2C_Chat로 Callback된 함수의 인자 중 remote는 RMI를 호출한 상대편 Host의 ID값입니다. 이 ID값을 사용하여 Proxy를 호출하면 원하는 상대에게 통신을 보낼 수 있습니다. 이제 생성한 Stub객체를 Client객체에 등록시켜 보겠습니다.

CNetClient *client 
         = ProudNet:CreateClient();
client->AttachStub(&g_S2CStub);
// 이하 생략

AttachStub함수도 내부에서 배열로 관리되고 있으며, 포인터를 넘겨주는 방식으로 등록됩니다.

항상 traffic에 유의하세요.

개발 이후 반드시 확인을 해보셔야 하는 부분이 traffic입니다. traffic은 각 Client, (Super peer기능을 사용한다면 Super Peer에서도) Server등에서 각각 통신 양이 많지 않은지 체크 하고 불필요한 Traffic을 제거해주는 작업이 필요합니다. traffic을 체크하는 방법으로는 다음과 같은 방법이 있습니다. ‘NetLimiter’와 같은 Tool을 사용하는 방법이 있습니다. 한 컴퓨터에 사용할 프로세스 개수만큼만 정확히 실행 한 뒤 작업 관리자의 간격당 받은 & 보낸 바이트 수를 확인하는 방법이 있습니다. ProudNet의 내부 함수인 CNetServer::GetStats(CNetServerStats &outVal);를 사용하여 초당 보내고 받은 Traffic, 보내거나 받은 수, 등을 실시간으로 얻을 수 있습니다. 한 Client의 총 Traffic이 대략 20~30KB이상 발생 된다면, 해외 서비스에서 문제가 발생할 수 있습니다.

***주의사항***

NetLimiter와 같은 Tool은 사용 후 삭제하길 권장합니다. Kernel Hooking 기능으로 인하여 통신 device를 잡고 있는 코어에 본래 속도의 20배 이상의 부담을 줍니다.