클라이언트와 서버는 많은 종류의 패킷으로 통신하게 될 것이다.
각 패킷 ID(OPCODE) 별로 일일이 분기시키고, 반복된 코드를 계속 작성하게 된다면 매우 비효율적일 것이다.
패킷을 보다 효율적으로 관리할 수 있게 해 줄 핸들러 클래스를 작성한다.
1. 클래스 작성
서버 클라 양쪽에 핸들러를 만들어 둘 것이다.
1-1. Client
#pragma once
enum
{
S_TEST = 1
};
class ClientPacketHandler
{
public:
static void HandlePacket(BYTE* buffer, int32 len);
static void Handle_S_TEST(BYTE* buffer, int32 len);
};
ID 열거형의 네임 컨벤션은 아래와 같다.
- S_TEST
- (S)erver에서 "보내는" Test 패킷 - 클라이언트에서 TEMP 패킷을 보낸다면?
- C_TEMP가 될 것이다.
각 함수는 아래와 같은 역할을 한다.
- HandlePacket()
- 패킷 헤더에 있는 ID로 분기해 해당 패킷에 맞는 처리 함수로 보낸다.
- FF14에서 유사한 일을 하는 함수를 IDA를 통 보면 분기가 800개 이상이나 된다. 그렇게 많이 필요한가? - Handle_S_TEST()
- 테스트 패킷을 받아 처리해 줄 함수다.
이제 함수를 구현해 보자.
void ClientPacketHandler::HandlePacket(BYTE* buffer, int32 len)
{
BufferReader br(buffer, len);
PacketHeader header;
br >> header;
switch (header.id)
{
case S_TEST:
Handle_S_TEST(buffer, len);
break;
}
}
버퍼에서 헤더를 분리하고 그 ID를 이용해 분기하도록 한다.
void ClientPacketHandler::Handle_S_TEST(BYTE* buffer, int32 len)
{
BufferReader br(buffer, len);
PacketHeader header;
br >> header;
uint64 id;
uint32 hp;
uint16 attack;
br >> id >> hp >> attack;
cout << "ID: " << id << " HP : " << hp << " ATT : " << attack << endl;
}
순서대로 데이터를 뽑아서 출력하게 했다.
1-2. Server
클라이언트와 차이가 없다고 볼 수 있다.
#pragma once
enum
{
S_TEST = 1
};
class ServerPacketHandler
{
public:
static void HandlePacket(BYTE* buffer, int32 len);
static SendBufferRef Make_S_TEST(uint64 id, uint32 hp, uint16 attack);
};
아주 똑같다.
차이점이라면 서버에선 패킷을 만들어 보내야 하기 때문에 이름과 역할이 조금 다르다는 것 정도.
void ServerPacketHandler::HandlePacket(BYTE* buffer, int32 len)
{
BufferReader br(buffer, len);
PacketHeader header;
br.Peek(&header);
switch (header.id)
{
default:
break; // 클라에서 보내는 패킷을 이작 설계하지 않았음
}
}
별도 분기가 없는 이유는 아직 클라에서 보내는 패킷이 없기 때문이다.
클라에서도 패킷을 보내게 되면 내용이 추가가 될 것이다.
// 자주 사용할 것이므로 하나의 함수의 형태로 둔다
SendBufferRef ServerPacketHandler::Make_S_TEST(uint64 id, uint32 hp, uint16 attack)
{
SendBufferRef sendBuffer = GSendBufferManager->Open(4096);
BufferWriter bw(sendBuffer->Buffer(), sendBuffer->AllocSize());
PacketHeader* header = bw.Reserve<PacketHeader>();
// id(uint64), 체력(uint32), 공격력(uint16)
bw << id << hp << attack;
header->size = bw.WriteSize();
header->id = S_TEST; // 1 : Test Msg
sendBuffer->Close(bw.WriteSize());
return sendBuffer;
}
기존에 패킷을 만드는 데 사용했던 코드를 그대로 재활용했다.
순서대로 데이터를 넣고 포장하고 보낸다.
2. 추가 사항 반영
새로운 클래스가 생겼으므로 이를 기존 코드에 반영해야 한다.
virtual void OnRecvPacket(BYTE* buffer, int32 len) override
{
ClientPacketHandler::HandlePacket(buffer, len);
}
패킷 수신에 대해선 서버도 동일한 처리를 하게 된다.
HandlePacket()이 알아서 Handle_S_TEST() 또는 Make_S_TEST()로 넘겨줄 것이다.
while (true)
{
SendBufferRef sendBuffer = ServerPacketHandler::Make_S_TEST(1001, 100, 10);
GSessionManager.Broadcast(sendBuffer);
this_thread::sleep_for(250ms);
}
서버에서 Broadcast 하는 부분은 위와 같이 정리된다.
기존의 길었던 코드가 Make_S_TEST()하나로 매우 깔끔하게 정리됐다.
동작이 잘 된다면 이전에 숙제로 남겨뒀던 가변 데이터 처리에 대해 고민해 봐야 한다.
3. 가변 데이터 처리
RPG엔 버프라는 개념이 있으니 이걸로 테스트 할 수 있겠다.
버프나 디버프가 얼마나 걸려 있을지 알 수가 없으니 가변 길이의 데이터가 된다.
먼저 버프 정보를 저장할 구조체가 필요하다.
struct BuffData
{
uint64 buffId;
float remainTime;
};
버프의 ID와 남은 시간을 가질 것이다.
이 버프 데이터는 벡터로 한 곳에 모아둘 것이고, 패킷이 버프 데이터들도 같이 보내야 하기 때문에 함수를 수정한다.
// 자주 사용할 것이므로 하나의 함수의 형태로 둔다
SendBufferRef ServerPacketHandler::Make_S_TEST(uint64 id, uint32 hp, uint16 attack, vector<BuffData> buffs)
{
SendBufferRef sendBuffer = GSendBufferManager->Open(4096);
BufferWriter bw(sendBuffer->Buffer(), sendBuffer->AllocSize());
PacketHeader* header = bw.Reserve<PacketHeader>();
// id(uint64), 체력(uint32), 공격력(uint16)
bw << id << hp << attack;
// 가변 데이터
bw << (uint16)buffs.size();
for (BuffData& buff : buffs)
{
bw << buff.buffId << buff.remainTime;
}
header->size = bw.WriteSize();
header->id = S_TEST; // 1 : Test Msg
sendBuffer->Close(bw.WriteSize());
return sendBuffer;
}
버프 데이터의 벡터를 인자로 받도록 하고
앞의 데이터를 다 뽑아 먹었다면 버퍼에 순서대로 버프 데이터를 넣는다. 가장 먼저 버프의 개수를 넣어줬다.
당연히 받는 쪽도 버프 데이터를 받을 수 있게 수정해 주어야 한다.
void ClientPacketHandler::Handle_S_TEST(BYTE* buffer, int32 len)
{
BufferReader br(buffer, len);
PacketHeader header;
br >> header;
uint64 id;
uint32 hp;
uint16 attack;
br >> id >> hp >> attack;
cout << "ID: " << id << " HP : " << hp << " ATT : " << attack << endl;
vector<BuffData> buffs;
uint16 buffCount;
br >> buffCount;
buffs.resize(buffCount);
for (int32 i = 0; i < buffCount; i++)
{
br >> buffs[i].buffId >> buffs[i].remainTime;
}
cout << "BufCount : " << buffCount << endl;
for (int32 i = 0; i < buffCount; i++)
{
cout << "BufInfo : " << buffs[i].buffId << " " << buffs[i].remainTime << endl;
}
}
데이터를 갖고 있을 임시 벡터와 버퍼의 개수를 갖고 있을 변수를 선언했다.
버프의 개수만큼 벡터를 Resize해 줬고, 임시 벡터에 순서대로 데이터를 넣어준다.
그리고 그 데이터들을 출력하게 했다.
이제 서버에서 데이터를 보내는 부분을 수정해야 한다.
while (true)
{
vector<BuffData> buffs{ BuffData {100, 1.5f}, BuffData{200, 2.3f}, BuffData {300, 0.7f } };
SendBufferRef sendBuffer = ServerPacketHandler::Make_S_TEST(1001, 100, 10, buffs);
GSessionManager.Broadcast(sendBuffer);
this_thread::sleep_for(250ms);
}
버프 데이터를 가지는 벡터를 생성하고 그걸 Make_S_TEST()에 넘겨줬다.
4. 실행
지금은 스트레스 테스트를 하지 않을 것이기 때문에 클라 개수를 1개로 고쳐놓았다.
모든 데이터가 정상적으로 전송되고 출력됨을 알 수 있다.
'Study > C++ & C#' 카테고리의 다른 글
[C++] Packet Serialization (0) | 2023.07.12 |
---|---|
[C++] Unicode / Encoding (0) | 2023.07.10 |
[C++] Buffer Helpers (0) | 2023.07.07 |
[C#] PingPlugin (0) | 2023.07.06 |
[C++] Packet Session (0) | 2023.07.04 |