용어집

📚 Burst Time

Burst time은 크게 CPU burst time Device burst time으로 나뉩니다.

- CPU burst time

특정 루틴을 실행하는데 CPU 만 연산하는 시간. CPU burst time 중인 스레드의 CPU 코어 점유율은 100 으로 볼 수 있습니다.

- Device burst time

특정 루틴을 실행하는 동안 CPU가 다른 처리의 완료를 기다리는 시간으로 Device burst time 중인 스레드의 CPU 코어 점유율은 0 입니다. CPU가 다른 처리의 완료를 기다리는 대표적인 경우는 파일 읽기/쓰기 중, DB 쿼리 실행 중, 다른 호스트로의 서비스 응답 대기 중이 있습니다.

📚 C++ Singleton

C++ 언어에서 Singleton과 전역 변수의 차이가 있습니다. 전역 변수는 WinMain() 이나 main() 의 실행 시 종료 직전에 파괴되지 않고, 하위 호출 함수들에서 파괴됩니다. 전역 변수들의 파괴 순서는 한 개의 C++ 파일의 컴파일 결과물에서나 보장되며 서로 다른 C++ 파일들의 컴파일 결과물 간의 파괴 순서는 보장되지 않습니다.

하지만 C++ singleton 은 WinMain() 이나 main() 이 리턴하기 직전에 호출됩니다. 게다가 처음으로 singleton을 접근하는 순간 인스턴스의 생성자가 호출되며, 파괴되는 순서도 생성자가 호출된 역순으로 호출됩니다. 따라서 전역 변수보다 더 안전한 생성/파괴 규칙을 보장합니다.

➡️ C++ singleton 구현 예시
class A
{
    A(){}
public:
    static A& Instance()
    {
        static A inst;
        return inst;
    }
    void Goo() {}
};
 
 
 
void Foo()
{
    A::Instance().Goo();
}

위 구현은 짧은 시간에 동시에 여러 스레드에서 singleton을 접근한다면 생성자가 2회 이상 호출이 있을 수 있다는 위험이 있습니다.

이러한 문제를 해결하면서도 singleton 접근에 대한 critical section 부하가 없는 클래스 Proud.CSingleton 사용을 권장합니다.

📚 DB Constraints

필드에 들어갈 값이 특정 조건을 만족하지 못하면 들어가지 못하게 하는 것을 지칭합니다.

Primary Key Unique Index 기타 등등 ( >, <, =, != ) Constraints 는 Unique Index, Trigger 등으로 구현됩니다.

📚 Fast Heap

ProudNet의 Fast heap은 Lookaside allocator 보다 약간 더 느리지만 OS 환경에서의 메모리 할당/해제 속도보다는 훨씬 빠르고 다양한 크기의 메모리 블럭을 할당/해제가 가능합니다.

ProudNet의 Fast heap의 구현 클래스는 Proud.CFastHeap 입니다. Proud.CFastHeap 또한 Lookaside allocator와 마찬가지로 모든 메모리 블럭이 파괴된 후에야 Proud.CFastHeap 객체를 제거할 수 있습니다.

Fast heap을 쓰는 방법은 다음과 같습니다.

  • 먼저 Proud.CFastHeap.New 메서드로 fast heap 객체를 생성합니다. 전역 객체로 생성해도 됩니다.

  • Proud.CFastHeap.Alloc 메서드로 메모리 블럭을 할당합니다.

  • 해제는 Proud.CFastHeap.Free 로 합니다. CFastHeap.Realloc 으로 메모리 블럭을 재할당할 수 있습니다.

  • 모든 메모리 블럭을 해제한 후 Proud.CFastHeap 객체를 파괴합니다.

📚 P2P 그룹

만약 두 클라이언트 A,B가 서로 P2P 통신을 하려면 A,B가 같은 최소한 1개의 P2P 그룹에 속해야 합니다. 여러 사람들이 1개의 채팅창에서 서로 채팅을 할 수 있으며, 여러 개의 채팅창을 만들어서 다중 채팅도 가능합니다. 다만 들어가지 않은 채팅창에서는 채팅할 수 없습니다.

ProudNet은 인터넷 메신저에서 각 채팅창은 P2P 그룹에 대응합니다.

다만, 채팅창을 만들거나 다른 채팅창에 들어가는 권한이 서버에만 있다는 차이가 있습니다. P2P 그룹 식별자 또한 Proud.HostID 타입입니다.

📚 PIDL

PIDL은 RMI를 위한 자체 제작된 컴파일러 입니다.

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

📚 Reliable 메시징

Reliable 메시징(또는 reliable send)은 송신 측에서 보내는 메시지 내용과 순서가 항상 수신 측에서 동일하게 받는 것을 말합니다. 예를 들어 메시지 A,B,C,D,E를 보내면 받는 측에서도 A,B,C,D,E 순으로 데이터 손실없이 받게 됩니다.

신뢰성(reliablility)이 있는 장점이 있지만, 가끔 수신되는 시간이 Unreliable 메시징 에 비해 느릴 수 있습니다.

📚 RMI

RMI(Remote Method Invocation, 원격 메서드 호출) 는 다른 네트워크 혹은 다른 프로세스에 있는 함수를 호출하는 것을 뜻하며, 송수신 루틴과 메시지(Message) 구조체를 정의하는 코딩 작업을 사람 대신 기계가 해줌으로써 개발자를 힘들게 하는 프로그래밍 작업 (메시지 구조체 정의, 송신 함수, 수신 함수 작성)을 함수 호출의 형태로 간소화하는 역할을 합니다.

메시지를 보낼 때 RMI 함수를 호출하는 쪽을 Proxy, 호출 받는 쪽을 Stub이라고 합니다.

RMI 개념
RMI 개념2

- RMI를 사용하지 않았을 때

// Message header ID definitions
#define Message_Knight_Move_ID 12
#define Message_Knight_Attack_ID 13
 
// Message format definitions
struct Message
{
    int m_msgID;
};
struct Message_Knight_Move:public Message
{
    int m_id;
    float m_x,m_y,m_z;
};
struct Message_Knight_Attack:public Message
{
    int m_id;
    int m_target;
    int m_damage;
};
 
// A function which send a formatted message
void Knight_Move(int id,float x,float y,float z)
{
    Message_Knight_Move msg;
    msg.m_msgID=Message_Knight_Move_ID;
 
    msg.m_id=id;
    msg.m_x=x;
    msg.m_y=y;
    msg.m_z=z;
 
    Send(msg);
}
 
// A function which send a formatted message
void Knight_Attack(int id,int target,int damage)
{
    Message_Knight_Attack msg;
    msg.m_msgID=Message_Knight_Attack_ID;
 
    msg.m_id=id;
    msg.m_target=target;
    msg.m_damage=damage;
 
    Send(msg);
}
 
// Identified a received message 
// and call an appropriate function for message handling
void DoReceivedMessage(Message* msg)
{
    switch(msg->m_msgID)
    {
    case Message_Knight_Move_ID:
    {
        Message_Knight_Move* msg2=
            (Message_Knight_Move*)msg;
 
        Do_Knight_Move(
            msg2->m_id,
            msg2->m_x,
            msg2->m_y,
            msg2->m_z);
    }
    break;
    // ... cases for other message types
    case Message_Knight_Attack_ID:
    {
        Message_Knight_Attack* msg2=
            (Message_Knight_Attack*)msg;
 
        Do_Knight_Attack(
            msg2->m_id,
            msg2->m_target,
            msg2->m_damage);
    }
    break;
    // ... cases for other message types
    }
}c

하지만 RMI를 쓰면 아래와 같이 짧은 코드로 정리할 수 있습니다.

Knight_Move([in] int id,[in] float x,[in] float y,[in] float z);
Knight_Attack([in] int id,[in] int target,[in] int damage);

위 형식은 IDL(Interface Description Language) 형식의 언어로서, 컴파일하면 C++ 또는 C# 소스가 만들어집니다. 만들어진 소스 파일은 메시지 구조체 선언, 송신 함수, 수신 처리 함수 등으로 이루어져 있어서 개발자가 직접 네트워크 처리 루틴을 만들지 않아도 PIDL 컴파일러가 자동으로 생성합니다.

만들어진 파일 중 함수 호출을 메시지로 바꾸어 네트워크로 보내는 모듈을 proxy, 네트워크를 통해 받은 메시지를 분석하여 유저 함수를 호출하는 모듈을 stub이라 부릅니다.

호스트 A에서 RMI 함수 X를 호출하면, 실제로 X의 proxy가 호출되며 proxy는 네트워크 메시지로 바꾸어 그것을 호스트 B로 보냅니다. 그리고 호스트 B는 메시지를 받아 stub에 보내며, stub은 분석한 후 유저가 작성한 함수 X를 호출합니다.

RMI 처리 순서

호스트 A에서 호스트 B에 있는 함수를 호출하는 것과 비슷한 모양을 가지는 것으로 보여서 Remote Method Invocation(원격 메서드 호출)이라는 의미를 갖게 된 것입니다.

📚 Stored Procedure

Stored Procedure 은 DBMS 자체에 넣을 수 있는 SQL 구문으로 만들어진 프로그램 함수 입니다. Database 를 접근할 때에는 쿼리 구문 문자열을 응용 프로그램에서 직접 만들어 던지지만 가능하다면 쿼리 구문 루틴을 Stored Procedure 를 미리 만들어 저장해두고, 응용 프로그램은 Stored Procedure 를 직접 호출하는 것이 성능과 안정성(데이터베이스 잠금 정책 등)에서 더 효과적입니다.

📚 Unreliable 메시징

Unreliable 메시징(혹은 unreliable send)은 송신 측에서 보내는 메시지 내용과 순서가 통신선의 길이와 상태에 따라 다르게 수신 받을 수 있습니다.

예를 들어 메시지 A,B,C,D,E를 보내면 받는 측에서는 A,B,C,D,E를 받기도 하지만 같은 메시지를 두 번 받거나(A,B,B,C,C,D,E) 메시지가 중간에 소실되거나(A,B,D) 순서가 다르게 메시지가 도착할 수 있습니다.(A,C,B,E,D)

그러나 메시지 내부의 데이터가 깨지는 일은 없습니다. Unreliable 메시징이 이러한 단점을 갖고 있지만 Reliable 메시징 보다 송달되는 시간이 빠릅니다.

📚 UUID 또는 GUID

Unique or global universal identifier (UUID 또는 GUID)는 16바이트 크기의 데이터 블록으로, 생성한 GUID 는 확률 상 전세계에서 유일합니다.

ProudNet DB 에서는 UUID 를 각 Gamer, Hero, WorldObject 마다 배정합니다. 비록 UUID 가 16bytes 이기 때문에 크기가 큰 편이라고 생각할 수 있지만, UUID 가 지구 상에서 중복되지 않는다는 장점이 있기 때문에 몇 가지 중요한 케이스에서 유용하게 이용됩니다. 대표적인 경우는 서버 통폐합, 게이머 계정 이동, 게이머 ID의 변경 등입니다.

📚 경쟁 상태 (Race Condition)

공학 분야에서 경쟁 상태(race condition)란 둘 이상의 입력 또는 조작이 동시에 이루어지는 상태를 일컫습니다. 이런 상태에서는 정상적인 결과가 나오지 않게 될 위험이 있는데 이를 경쟁 위험이라고 합니다.

전산학에서 경쟁 상태란 공유 자원에 대해 여러 개의 프로세스가 동시에 접근을 시도하는 상태를 의미하며, 동시 접근할 때 자료의 일관성을 해치는 결과가 나타날 수 있습니다. 이를 방지하기 위해서는 프로세스 협력 기법이 필요합니다.

📚 데이터 양자화

부동 소수점 단위의 큰 값을 네트워크로 주고 받을 때, 패킷의 양을 줄이는 트릭이 가능한 상황이 있습니다. 예를 들어 캐릭터 위치 x값은 100 ~ 200 사이에서만 결정되고 소수점 둘째자리 이하의 정밀도는 무시되어도 될 정도라면, 이 값은 100 * 100 = 10000 이하의 값으로 변환해서 주고받을 수 있으며 double(8바이트)를 word(2바이트)로 절약하게 됩니다.

이러한 트릭을 양자화(quantization)이라 지칭합니다.

이 클래스는 양자화 및 양자화의 반대 급부 기능을 제공합니다.

Proud::CQuantizer q(-10000,10000,65535); // -10000~10000 사이의 값을 65535 등분의 정밀도로 양자화하는 기능
double a = 3423.38274f;
int b = q.Quantize(a);      // 양자화
double c= q.Dequantize(b);  // 양자화된 값으로부터 실제값을 복원

📚 리스닝 포트 (Listening Port)

호스트 주소포트로 구성되어 있으며 서버가 클라이언트의 접속을 받기 위해서는 리스닝 포트가 있어야 합니다.

호스트 주소는 111.222.111.222 또는 mycomputer.mydomain.com 형태로 구성되어 있는 인터넷 상에서의 주소입니다. 포트는 1,000~65,500 사이의 값입니다. 이는2개 이상의 프로그램이 같은 리스닝 포트를 쓰지 않는 한 얼마든지 지정할 수 있습니다.

📚 마샬링 (Marshaling)

RMI 호출을 메시지로 변환하거나 메시지로부터 RMI를 호출하기 위한 값을 추출하는 것을 마샬링(marshaling) 이라고 칭합니다. ProudNet에서는 int나 float 등 기본적인 타입에 대한 마샬링 기능을 제공하고 있습니다.

📚 멀티캐스트

한 개의 메시지를 여러 호스트에게 한 번에 전달하는 것을 멀티캐스트, 메시지를 한 호스트에만 전달하는 것은 유니캐스트 라고 부릅니다.

📚 스레드 풀 (Thread Pool)

스레드 1개를 생성 후 제거하는 과정은 많은 처리량이 발생하기 때문에 실행 중인 스레드가 많다면 운영 체제에 과부하가 걸릴 수 있습니다.

따라서 최대한 적은 수의 스레드를 유지하며 스레드를 제거 및 생성하는 과정을 최소화해야 하는데, 이를 위해 일정 갯수로 구성된 1개의 스레드 집합을 두어 필요 시에만 가져다 쓰고, 필요 없을 땐 집합에 반환하는 과정이 필요합니다. 이 과정을 스레드 풀이라 부릅니다.

즉, 스레드 풀여러 개의 스레드미리 준비된 여러 개의 스레드로 구성된 한 개의 집합을 지칭합니다.

- 예시

클라이언트 A,B,C가 서버에 수용된 상태에서 각각 RMI 또는 이벤트로 인하여 Proud.CNetServer 내의 queue에서 대기 중인 상태입니다.

A1,A2,A3 -> 클라이언트 A에 대한 이벤트 또는 RMI B1,B2,B3 -> 클라이언트 B에 대한 이벤트 또는 RMI 스레드 풀의 스레드는 총 2개입니다. 이때 규칙에 따라 다음과 같이 실행됩니다.

  • A1,A2,A3가 동시에 실행되지는 않습니다.

  • B1,B2,B3 와 C1,C2,C3 도 마찬가지로 동시에 실행되지는 않습니다.

  • A1,A2,A3 중 하나와 B1,B2,B3 중 하나, C1,C2,C3 중 하나는 동시에 실행될 수 있습니다.

  • 스레드 풀의 스레드 갯수가 2개 뿐이므로 A,B,C 중 2개가 선별되어 콜백되지만, 콜백 루틴이 먼저 완료된 스레드가 선별되지 못한 클라이언트의 RMI 혹은 이벤트 콜백을 수행합니다.

스레드 풀 실행 예시

📚 추측 항법(dead reckoning)

ProudNet에서는 게임 캐릭터의 매끈한 위치 표현을 위해서 추측 항법(dead reckoning)위한 도구를 제공합니다.

추측 항법은 대략적으로 다음과 같은 방식으로 작동합니다.

  • 호스트 A에서 움직이는 캐릭터의 위치와 속도를 호스트 B로 전송합니다. 이때 전송 주기는 1초당 2~10회입니다.

  • 전송 주기는 동적인 것이 좋습니다. 캐릭터의 가속도가 큰 경우에만 전송 주기를 짧게하면 보다 정확한 움직임을 동기화합니다. 가속도가 큰 경우란, 캐릭터의 속도가 급격히 변할 때나 캐릭터가 다른 물체에 부딪히면서 이동 방향이 급격히 변할 때 등입니다. (아래 표 참고)

  • 호스트 B에서는 호스트 A로부터 메시지가 도착하는데 걸리는 시간(레이턴시)을 pinging을 통해 얻습니다.

  • 호스트 B에서 캐릭터의 위치,속도 정보를 수신하면, 다음과 같은 공식으로 호스트 A의 캐릭터의 실제 위치를 예측합니다. P: 예측한 위치, V: 받은 속도, T: 레이턴시, P0: 받은 위치 P = (V * T) + P0

여기까지 하면 캐릭터의 위치는 예측할 수 있지만, 계산된 위치 값을 렌더하면 캐릭터의 위치가 뚝뚝 끊기는 문제점이 발생합니다. 이 문제를 해결하기 위해 Proud.CPositionFollower 를 사용합니다.

Proud.CPositionFollower는 움직이는 타겟의 위치까지 제한 시간 안에 도착하도록 추적자(follower)를 이동시키는 역할을 합니다. 특히 움직이는 타겟 위치를 직선으로 추적할 수 있도록 만들어져 있어, 다른 호스트들의 캐릭터 위치가 튀는 현상이 줄어들게 됩니다.

추측 항법을 구현하는 나머지 단계를 설명하겠습니다.

  • 호스트 B에서 예측된 위치와 속도 값을 Proud.CPositionFollower 객체에 입력합니다. 이때 타겟의 위치, 속도로서 입력합니다.

  • 호스트 B에서는 호스트 A의 캐릭터의 위치를 렌더하기 위해 Proud.CPositionFollower 객체의 추적자 위치를 얻습니다.

- 추측 항법 예시

P(t=0,1,2)는 예측된 지점이고 붉은 선은 Proud.CPositionFollower 에 의해 보정된 캐릭터 위치입니다. 통상적으로, 호스트 A에서 캐릭터의 위치를 보내는 주기와 Proud.CPositionFollower 에서 추적자가 타겟의 위치에 도달하는 시간 제한을 동일하게 설정하는 것이 좋으며 캐릭터 위치를 보내는 주기는 상황마다 다릅니다.

권장 예시

상황보내는 데이터 종류평균 보내는 주기급격히 증가하는 가속도 예시

대규모전 RPG 게임(MMORPG)의 플레이어 캐릭터

위치(xyz),속도(xyz),바라보는 방향(z)

0.3

캐릭터의 이동 방향 전환, 스탠스, 도트, 버프

비행기 또는 차량

위치(xyz),속도(xyz),가속도(xyz), 바라보는 방향(xyz)

0.3

장애물 충돌, 급격한 선회

1인칭 슈팅 게임(FPS)의 플레이어 캐릭터

위치(xyz), 바라보는 방향(xyz)

0.03

캐릭터 이동 방향이 바뀌는 직후, 저격총 발사 순간

보내는 주기가 짧은 경우에는 송신량이 지나치게 커질 위험이 있으니 송신량 자동 조절 기능 (Throttling) 도 사용할 것을 권장합니다.

- 곡선형 추적자(Spline based follower)

Proud.CPositionFollower 는 직선 뿐만 아니라 곡선으로 타겟을 쫓아가도록 하는 follower도 제공합니다. 이는 3차 함수 형태의 곡선형 추적자(spline based follower) 입니다. 이것은 직선형 follower보다 더 매끈한 형태로 쫓아가는 모습을 보여줍니다. 하지만, 항상 매끈한 모습을 나타내진 않으므로 게임 플레이에서 테스트해보면서 선택하시는 것이 좋습니다.

곡선형 추적자를 얻기 위해 아래의 메서드를 이용하시기 바랍니다.

  • Proud.CPositionFollower.GetSplineFollowerPosition

  • Proud.CPositionFollower.GetSplineFollowerVelocity

각도에 대한 보정 처리는 아래의 메서드를 이용합니다.

  • Proud.CPositionFollower: 위치에 대한 보정 역할

  • Proud.CAngleFollower: 각도에 대한 보정 역할

📚 홀 펀칭 (Hole Punching)

공유기가 라우터의 특성도 함께 가지고 있어 Routing Table 을 작성하기 위한 P2P 통신을 목적으로, 사전에 상대방과 패킷을 주고받아 각자의 공유기에 Routing Table을 작성하는 것을 뜻합니다. 정확한 명칭은 STUN (Simple Traversal of User Datagram Protocol Through Network Address Translators) 입니다.

홀 펀칭 방식은 다음과 같습니다.

  • Full Cone NAT

  • Restricted Cone

  • Port Restricted Cone

  • Symmetric Cone

Last updated