Tomson
Deep Learner
Tomson
전체 방문자
오늘
어제
  • 분류 전체보기 (4)
    • Network (2)
    • IOCP (1)
    • 기타 (1)

블로그 메뉴

  • 홈
  • 태그
  • 방명록
  • GitHub
  • 관리자

태그

  • gafferongames
hELLO · Designed By 정상우.
Tomson

Deep Learner

Network

[Gaffer On Games] Reading and Writing Packets 정리

2022. 6. 23. 14:58

Reading and Writing Packets | Gaffer On Games

 

Reading and Writing Packets | Gaffer On Games

Introduction Hi, I’m Glenn Fiedler and welcome to Building a Game Network Protocol. In this article we’re going to explore how AAA multiplayer games like first person shooters read and write packets. We’ll start with text based formats then move into

gafferongames.com

※ 번역이 아니라 정리입니다!

 

Background

게임과 같은 프로그램에서는 반응속도가 아주 중요하다. 따라서 게임의 상태를 짧은 주기마다 보내는데, 데이터의 크기가 꽤 크다. 오브젝트의 상태, 플레이어의 상태들이 담긴 정보는 그 크기가 아주 클 수 밖에 없다. 그래서 압축 기술이나 비트 패킹 기술들을 활용해 그 크기를 줄이려고 노력하는 것이 일반적이다.

 

패킷 데이터가 의미하는 정보, 즉 스키마는 실제로 쓰이는 값보다 더 큰 경우도 존재한다. 즉, 값을 의미하는 정보를 같이 패킷에 담아서 보내는 것은 아주 큰 낭비이다. 그래서 클라이언트가 연결되면 딱 한번 데이터의 스키마들을 한번에 보내어 추후에 이 구조를 패킷 분석에 사용하는 방법을 사용하는 것이 더 효과적이다.


Binary Format

내가 사용한 방식은 클라이언트와 서버 모두 미리 정의한 프로토콜을 알고 있고, 구조체 단위로 패킷을 생성하여 보내기 때문에 스키마 때문에 패킷 크기가 커지지는 않는다. 구조체 단위로 패킷을 구분하고, 패킷 내부에 미리 데이터를 정의하면 쉽게 패킷 타입만으로 어떤 패킷인지 구분할 수 있다.

enum class PckType
{
	PCK_0,
    PCK_1,
    PCK_2
};

struct packet_header
{
	int id;
	char pck_type;
}

struct Packet0 : packet_header
{
	int x;
    int y;
    int z;
}

struct Packet1 : packet_header
{
	int elem;
    int elems[MaxElems];
}

struct Packet2 : packet_header
{
	bool b;
    short s;
    int i;
}

이와 같은 방식은 초보자가 쉽게 접근할 수 있는 기본적인 구조이다. 하지만 대부분의 고급 게임들은 이런 구조를 사용하지 않는다.

 

그 이유는...

첫번째로 컴파일러나 플랫폼에 따라 packing 구조가 다르기 때문이다. 그래서 #pragma pack을 넣어서 packing이 1바이트로 정렬되도록 확실히 해야 하는 수고를 들여야 한다. 개인적으로 그렇게 큰 수고는 아니다..

두번째는 Endian을 신경쓰지 않는다는 점이다. 구조체를 그대로 버퍼에 복사해서 사용하면 서로 다른 Endian 시스템을 가진 호스트끼리는 통신할 수가 없다. 따라서 버퍼에 넣기 전에 항상 variable의 바이트 정렬을 네트워크 기준으로 변경해서 넣고, 버퍼에서 읽을 때도 Endian 기준으로 바꿔서 읽어야 한다.

세번째는 패킷 구조체의 크기가 정해져 있다는 점이다. 예를들어 Packet0의 크기는 4 * 3 = 12 바이트이다. 하지만 각 integer가 항상 4바이트씩 차지할 필요가 없다. 대부분의 경우 패킷이 비어있는 상태이며 이것은 큰 낭비이다.

네번째는 보안 위험성이 크다는 것이다. 구조체를 통째로 버퍼에 복사해서 전송하고 또 그것을 그대로 구조체로 변환하여 사용하면 누군가 패킷의 정보를 변환시켰더라고 알아차리기 어렵다.


Read and Write Method

패킷 데이터를 읽을 때, 구조체를 그대로 복사하는 것이 아니라 패킷 구조체 내부에 Write / Read 함수를 넣어서 각각의 필드 별로 버퍼에 넣는 방식을 사용한다. 이제 각 함수마다 Endian의 차이를 처리하고 encoding 기능까지 넣으면 간편하고 효과적으로 패킷을 쓰고 읽을 수 있다.

struct Packet0
{
	int x;
    int y;
    int z;
    
    void Write(Buffer& buffer)
    {
    	WriteInt(buffer, x);
        WriteInt(buffer, y);
        WriteInt(buffer, z);
    }
    
    void Read(Buffer& buffer)
    {
    	ReadInt(buffer, x);
        ReadInt(buffer, y);
        ReadInt(buffer, z);
    }
 }

Bitpacker

Integer의 값이 겨우 1000을 넘기지 않는 경우에는 이 필드는 최대 10비트 밖에 필요로 하지 않는다. 따라서 이를 극복하기 위해 bitfield를 사용해서 수동으로 각 필드의 범위를 제한하기도 한다. 하지만 이렇게 하는 건 너무 번거롭고, 에러가 발생하기 쉬우며(범위가 예상치 못하게 커지면 에러), 서로 다른 컴파일러에서 똑같이 비트를 할당한다는 보장이 없다.

 

Bitpacker를 만들기 전, 주의해야 할 점은 버퍼에 데이터를 넣을 때, 바이트 단위가 아닌 워드 단위로 넣어야 한다는 점이다. 왜냐하면 요즘의 CPU는 워드 단위로 메모리를 읽고 쓰기 때문이다. 따라서 버퍼의 크기도 워드 크기의 배수로 설정하는 것이 좋다.

 

※ Word: 컴퓨터 설계 시 정해지는 메모리의 기본 단위. 32비트 컴퓨터에서 1워드는 4바이트, 64비트에서는 8바이트이다.

 

Bitpacker에서 bit를 쓰기 위해서는 비트를 담아둘 64비트짜리 변수가 필요하다. uint64_t 타입을 써서 32비트를 넘어가는 경우도 처리할 수 있도록 한다. 32비트를 넘었을 시에는 32비트만큼 버퍼에 넣는다. 마지막으로 남은 비트들을 버퍼에 넣는다. 읽는 것도 마찬가지로 한다. 

 

Bitpacker는 소스코드 예제를 보고, 직접 구현해보면서 시행착오를 겪어야 이해가 될 듯 싶습니다.

예제에는 uint32까지만 넣을 수 있는데, uint64의 범위는 어떻게 해야할지가 나와있지 않습니다.

이것도 직접 구현해보면서 공부해야 할 것 같습니다.


networkedphysics-gdc2015/BitPacker.h at master · gafferongames/networkedphysics-gdc2015 (github.com)

 

GitHub - gafferongames/networkedphysics-gdc2015: Networked Physics Demo (GDC 2015)

Networked Physics Demo (GDC 2015). Contribute to gafferongames/networkedphysics-gdc2015 development by creating an account on GitHub.

github.com

 

'Network' 카테고리의 다른 글

[Gaffer On Games] Client Server Connection 정리  (0) 2022.06.23
    'Network' 카테고리의 다른 글
    • [Gaffer On Games] Client Server Connection 정리
    Tomson
    Tomson

    티스토리툴바