Serializing Buffer

2021. 11. 23. 00:19게임서버/namespace univ_dev

320x100
안녕하세요 대학생 개발자입니다.

 

이번 글에서는 직렬화 버퍼에 대해서 얘기를 해보려고합니다.

지금까지 제가 TCP 프로토콜을 이용한 간단한 스트리트 파이터 같은 게임을 만들어봤는데요...

사실 이정도면 나름 완성이 되어있는 상태라고 봐도 무방합니다.

 

왜냐하면 사실 플레이어가 서버에게 보낼 수 있는 프로토콜은 전부 5바이트로 확정되어있기 때문이고 (이때문에 사실 링버퍼의 의미도 크게 없긴함) 5바이트로 되어있기 때문에 그냥 버퍼를 5바이트 잡고(char [5]같은걸로요) 그걸 패킷이라고 전달해줘도 되기 때문입니다. 하지만 큰 게임들은 가변 길이 버퍼를 많이 사용하게 되는데요... 가변길이 버퍼를 사용하게 되면 좋은점은 당연하게도 너무 큰 버퍼를 잡지 않아도 된다는 장점이 분명 있습니다.

 

우선 당장은 직렬화 버퍼의 필요성에 대해서 설명을 드리기가 그렇습니다...

왜냐면 지금 당장은 쓰는데 크게 지장이 없을 뿐더러 사용으로 얻을 수 있는 이득보다 실이 더 큰 경우거든요...

 

잠시 다른길로 세어보겠습니다.

우리가 아이템 목록이라는 정보를 서버에서 클라이언트에게 보내려고 합니다.

그러면 저도 그렇고 대부분 사람들은 이런 방식을 생각할 거에요

서버에서 클라이언트의 아이템 목록을 드르르르륵 긁으면서 다보내주고 클라이언트는 그걸 받아서 처리한다.

 

상당히 그럴싸 해 보이고 맞는 말인것 같죠?

하지만 데이터의 앞뒤로 어떠한 상황이라는 것을 알리는 특정 메시지가 도착한다는 것만으로도 우리에겐 추가 로직이 필요하게 될겁니다.

1. 아이템이 왔고 내 인벤토리에 왔다는 플래그를 true로 만든다.

2. 그뒤로 끝났다는 메시지가 올때마다 거기다가 넣는다.

3. 그동안 다른동작의 여부는 어떻게 해야될지에 대한 고민 -> 여기서 비동기가 되겠죠...

4. 메시지의 종류는 몇개로 할건지..(시작, 데이터, 끝) 등과같이

이런 고민거리를 안고가야됩니다.

 

서버에서는 클라이언트를 뷰어로 만드는 것이 제일 좋습니다.

클라이언트가 입력, 출력밖에 안되고 중간의 모든 로직이 서버에 있다면 당연히 핵 방어에도 유용할겁니다.

하지만 위의 케이스와 같은 경우라면 우리가 메시지를 보냄으로써 클라이언트가 스스로 해야될 일이 생기게 되는꼴이죠...

그리고 사실 저렇게 구현하면 더 클라이언트도 만들기 어려워지고요...(만약 이 메시지라면 ...해라 라는 로직이 추가로 들어가기 때문입니다)

그런 로직을 넣지 않기 위해서는 모든 메시지에 이 메시지는 아이템창 3열2행에 저장하면 된다 라고 보내는 겁니다.

그렇게 되면 클라이언트는 그냥 받고 그거만 하면되겠죠 본인의 로직이 존재하지 않는겁니다. 그냥 받은대로 실행 이게 로직이되는거죠.

그래서 정확히 메시지는 서버가 어디에 어떻게 무엇을 하라고 지시하는 모든 내용이 메시지에 담겨있게 작성을 하는게 좋습니다.

즉 메시지 하나하나가 각자 고유의 목적을 가지고 있는 메시지가 되는거죠.

 

네 그럼 다시 돌아와서 직렬화 버퍼의 필요성에 대해서 조금 더 생각을 해봐야 될 것같습니다.

 

아마 구조체 방식의 메시지와 지금의 직렬화 버퍼식의 메시지중 어떤게 더 나을지 고민을 하다보면 결과가 나오지 않을까 싶은데요... 구조체 방식의 메시지는 이전에 했던것 처럼 버퍼를 크게 하나 잡아두고 그 버퍼를 채운다음 포인터를 전달해서 캐스팅후 마샬링 하는 방법으로 사용했었습니다.

이 때 만약 내용이 지금과같이 컨텐츠가 아니라 채팅이라면 어떨까요.

struct ChatMessage
{
    unsigned short len;
    char data[512];
};

struct CharMessage
{
    unsigned short len;
    char data[0];
};

채팅을 하기위한 구조체를 한번 살펴보겠습니다.

일단 구조체로 한다면 위와같은 두가지 방법을 고려해 볼 수 있겠는데요...

하나는 최대 버퍼가 되어버리면 그냥 잘라버린다의 케이스와 남은 하나는 예전에 자주 쓰던 방식인데 data에는 뒤에 추가적인 버퍼의 가능성을 두는 그냥 포인터를 하나 둔다던지... 이런 방법들을 생각해봤을겁니다.

왜냐하면 저 상황에서는 payload의 길이가 가변적으로 변할 수 밖에 없기때문에 그때그때 맞추거나 아니면 아주 크게 잡아버리는 방법밖에 없기떄문입니다.

하지만 직렬화 버퍼의 경우에는 그냥 적절히 버퍼를 하나 잡아두고 그안에서 오버로딩되어있는 함수를 이용해 >>등과같은 쉬프트 연산자를 이용해서 입력하게 된다면... 만약 모자라면 그냥 내부에서 로그를 남기고 리사이징을 해버리면 될겁니다.

그에비해 지금의 상황에서는 단점이 너무 크게 나타나는데요...

먼저 직렬화 버퍼입니다. 얘도 당연히 버퍼기때문에 넣으려면 copy가 일어나야될겁니다. 어차피 이거야뭐 그존에 char배열에 옮겨올때처럼 똑같이 copy가 일어나는 거기때문에 그렇다고 치겠습니다. + 로 함수의 콜이 많이 늘어나게 되구요... 직렬화 버퍼는 매번 패킷을 받을때 보낼때 쓰게될겁니다. 그렇다고치면 어마어마한 콜이 일어나겠죠... 그러니까 만들때 퍼포먼스를 매우 고려해서 만들어야될겁니다. 그리고 마지막으로 구조체의 경우에는 컴파일러에서 순서보장을 해주고있습니다. 하지만 직렬화 버퍼는 그게 아닙니다. 메뉴얼을 서버 개발자들이 만들어서 숙지하고 있어야된다는 점이 아쉬운점입니다.

 

이렇게 보셨듯 지금당장의 수준에서는 코드의 간결성과 퍼포먼스를 바꾸는 격이라서 좀 아쉽긴 합니다만 추후를 생각해본다면 나름 참아볼 만 할정도의 수준이긴합니다.

구조체는 아직 현역입니다만 가변적인 모양이 나오게 되면 직렬화 버퍼외에는 딱히 생각할 부분이 없긴 한건 맞습니다.

 

그럼에도 불구하고 사실 제입장에서는 당장 프로토콜 사이즈가 가변이아니라 고정이기때문에 이렇게 득보다 실이 더큰 상황의 직렬화 버퍼가 잘 이해되지 않는부분이 많습니다... 성능이 제일 중요한 서버에서 성능을 깎아먹으면서까지 편의성을 추구해야되는지도 모르겠습니다.

그럼에도 필드에서 트랜드가 직렬화 버퍼를 사용하는 추세라고하는 것을 보면... 제가 아직 다음 단계를 거치지 못해서 짧게 생각하고 있는것 같습니다. 일단은 아직 제가 배우지 못한 부분이 많아서 그런것 같으니 조금더 지식을 쌓고 이글을 다시한번 보도록 해야겠습니다.

 

 

void PacketProcDeletePlayer(char* rawPacket)
{
    SC_PacketDeleteCharacter* packet = (SC_PacketDeleteCharacter*)rawPacket;
    auto iter = g_ObjectList.begin();
    for (; iter != g_ObjectList.end();)
    {
        if ((*iter)->GetObjectID() == packet->playerID)
        {
            BaseObject* removePlayer = *iter;
            iter = g_ObjectList.erase(iter);
            delete removePlayer;
        }
        else
        {
            ++iter;
        }
    }
}

플레이어가 나가는 경우의 코드를 한번 보겠습니다.

지금까지의 제 코드에서는 스택에 버퍼를 잡아서 거기다가 RingBuffer에서 Peek하고 그 Peek한걸 함수 내부에서 포인터로 캐스팅을 해서 마샬링을 해서 사용했었습니다.

void PacketProcDeletePlayer(Packet& rawPacket)
{
    unsigned int playerID;

    rawPacket >> playerID;
    auto iter = g_ObjectList.begin();
    for (; iter != g_ObjectList.end();)
    {
        if ((*iter)->GetObjectID() == playerID)
        {
            BaseObject* removePlayer = *iter;
            iter = g_ObjectList.erase(iter);
            delete removePlayer;
        }
        else
        {
            ++iter;
        }
    }
}

사실 이경우도 마샬링이랑 크게 생긴걸로는 다르진 않습니다만. 다른점이라곤 Packet이라는 클래스로 한번 래핑을 하고있다는 점 정도일 것 같습니다.

그리고 데이터를 꺼내고 넣을때 쉬프트 연산자를 오버로딩해서 그런 효과를 두는것 처럼 되어있습니다.

사실 직렬화 버퍼만을 놓고(지금의 경우에는 Packet이 직렬화 버퍼의 역할)본다면 큰 이득이 없습니다.

 

사실 다른함수들도 다 마찬가지이기 때문에... 크게 소스를 가지고할 얘기는 없는것 같아보입니다.

 

 

void MakePacketStopMove(PacketHeader& header, CS_PacketMoveStop& packet, BYTE direction, unsigned short x, unsigned short y)
{
	header.code = 0x89;
	header.packetType = dfPACKET_CS_MOVE_STOP;
	header.payloadSize = sizeof(CS_PacketMoveStop);

	packet.direction = direction;
	packet.x = x;
	packet.y = y;
}

void MakePacketStopMove(Packet& packet, BYTE direction, unsigned short x, unsigned short y)
{
	BYTE code = 0x89;
	BYTE payloadSize = 5;
	BYTE packetType = dfPACKET_CS_MOVE_STOP;

	packet << code << payloadSize << packetType << direction << x << y;
}

뭐... 똑같은거지만 이경우에는 클라이언트에서 서버에게 보내는겁니다.

기존의 코드와 변경된 코드입니다.

 

다른점이 크게 없습니다.

오히려 퍼포먼스는 더 떨어질겁니다.

다시한번 말씀드리지만 사용하는 사람의 편의성을 높이기 위한 코드이지 성능을 높히는 코드는 아닙니다.

 

음... 오늘 글은 뭔가 제가 쓴글중에 제일 내용이 부실하고... 근거도 빈약한 글인거 같습니다..ㅠㅠ

사실 이부분은 제가 왜 써야되는지 정확히 이해가 안가다 보니까... 왜써야된다라고 확신있게 말씀을 못드린것 같습니다...

아직 개발 편의성을 추구한다는게 몸으로 와닫지 않는 부분이 커서 그런 것 같습니다...

다음번에 이글은 한번 수정하도록 하겠습니다. 왜 쓰는지 이해가 안가더라도 내용을 확실하게 알아오겠습니다.

그럼 오늘도 긴글 읽어주신 여러분들께 감사드립니다.

그럼 안녕히 계세요

 

 

320x100

'게임서버 > namespace univ_dev' 카테고리의 다른 글

A* Path Finding Algorithm  (0) 2021.12.19
ObjectFreeList  (1) 2021.11.28
WSAAsyncSelect로 게임클라이언트 제작  (0) 2021.11.19
WSAAsyncSelect 그림그리기 클라이언트  (0) 2021.11.13
WSAAsyncSelect  (0) 2021.11.13