클라이언트-서버 통신

클라이언트가 서버에 연결하면 우선 TCP로 통신이 이루어집니다.

그 동안 백그라운드로 서버와의 UDP 홀펀칭을 성공하게 되면 UDP 통신도 가능하지만 그 전까진 reliable, Unreliable 메시징 모두 TCP로 대체됩니다. 하지만 UDP 홀펀칭 성공 이후부터 Reliable 메시징 은 UDP로 대체됩니다.

클라이언트는 각각의 TCP port, UDP port를, 서버는 1개의 TCP listening port, 1개 이상의 UDP port를 가지고 통신하는데, 클라이언트는 서버와 TCP 연결을 유지하면서 서버 측의 UDP port 중 한 개를 선정합니다. 즉, 서버 측은 여러 개의 UDP port를 접속한 모든 클라이언트들에게 고루 공유합니다.

예를 들어, 4만 개의 클라이언트가 2만 개의 UDP port를 연 서버와 통신한다면, 서버 측의 각 UDP port 당 2개의 클라이언트가 통신하게 됩니다.

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

서버와 클라이언트에 통신을 추가해 보겠습니다. 편리성을 위해 서버와 클라이언트 모두 사용하는 Common(공용) 프로젝트를 생성 후 PIDL파일을 준비합니다. 준비된 PIDL파일에 서버에서 클라이언트로 통신을 보내기 위한 프로토콜을 정의 합니다.

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

위의 PIDL파일을 컴파일하면 ProxyStub객체가 생성됩니다.

Proxy 객체 사용 방법

먼저 사용하실 곳에 Header를 포함시킵니다.

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

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

// 객체를 생성합니다.
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와 마찬가지로 stub에도 사용할 곳에 Header를 포함시킵니다.

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

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

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

Stub Class에 명시된 Define문 중 DEFRMI_NameSpace_함수이름은 상속 받은 객체의 Header 파일에, 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 하는 것은 처리가 되었다라는 의미입니다. 'False'를 Return하게 되면 사용자가 프로토콜에 대한 처리를 하지 않은 것으로 판단하여 OnNoRmiProcessed Event가 콜백 됩니다. DEFRMI_S2C_Chat로 콜백된 함수 인자 중 remote는 RMI를 호출한 상대편 HostID 값 입니다. 이 ID값을 사용하여 Proxy를 호출하면 원하는 상대에게 통신을 보낼 수 있습니다.

이제 생성한 Stub객체를 Client객체에 등록시켜 보겠습니다.

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

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

개발 이후 반드시 트래픽을 확인하셔야 합니다. 트래픽은 각 클라이언트, (Super peer기능을 사용한다면 Super Peer에서도) 서버 등에서 각각 통신 양을 체크하고 불필요한 트래픽을 제거해주는 작업이 필요합니다. 트래픽 체크를 위해 아래의 방법들을 참고해 주세요.

  • NetLimiter와 같은 툴을 사용합니다.

  • 한 컴퓨터에 사용할 프로세스 개수만큼 정확히 실행 한 뒤, 작업 관리자의 간격당 주고받은 바이트 수를 확인하는 방법이 있습니다.

  • ProudNet의 내부 함수인 CNetServer::GetStats(CNetServerStats &outVal); 를 사용하여 초당 주고받은 트래픽, 보내거나 받은 수, 등을 실시간으로 얻을 수 있습니다. 한 클라이언트의 총 트래픽이 대략 20~30KB 이상 발생 된다면, 해외 서비스에서 문제가 발생할 수 있습니다.

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

이벤트

- 클라이언트와 서버 공통

파라미터 errorInfoerrorInfo -> ToString(); 을 사용하면 쉽게 문제에 대한 정보를 얻으실 수 있습니다.

- 서버

성능 테스트 등에 이용 가능합니다.

Last updated