지난번에는 언리얼에서 패킷 세션과 네트워크 워커 클래스들을 만들어 언리얼에서 제공하는 멀티스레드 기능을 이용해 recv를 해봤다. 이번엔 send를 해보자. 이를 위해서는 서버패킷핸들러의 MakeSendBuffer를 써야하는데, 지금은 서버패킷핸들러를 언리얼에서 쓸 수 없는 상태이니 이것도 쓸 수 있게 수정해보자.
패킷핸들러 되살리기
지금은 서버패킷핸들러를 사용할 수 없는 상태다. MakeSendBuffer를 사용해야 Protobuf 로 만든 패킷을 편리하게 직렬화 할 수 있으므로 이것부터 부활시켜보자.
우선 패킷 핸들러 탬플릿을 수정한다.
언리얼에서는 Types, Session, SendBuffer와 같은 헤더를 안쓰기 때문에 (있지도 않음), 이를 대체할 기능들을 넣어놓은 Client.h 를 include 하도록 매크로 조건문을 추가해준다. 서버에서는 쉐어드포인터를 만들 때 make_shared를 쓰고, 언리얼에서는 자체 쉐어드포인터를 만들 때 MakeShared를 써야함으로 이 부분도 매크로 조건문으로 언리얼일때와 서버일때를 구분시켜준다. 추가로 function<> 을 쓸 때 std::도 명시해주도록 변경했다.
#pragma once
#if UE_BUILD_DEBUG + UE_BUILD_DEVELOPMENT + UE_BUILD_TEST + UE_BUILD_SHIPPING >= 1
#include "Client.h"
#else
#include "Types.h"
#include "Session.h"
#include "SendBuffer.h"
#include <memory>
#endif
#include "Protocol.pb.h"
#include <functional>
extern std::function<bool(std::function<void()>&, PacketSessionRef&, BYTE*, int32)> GPacketHandler[UINT16_MAX];
enum : uint16
{
{%- for pkt in parser.total_pkt %}
PKT_{{pkt.name}} = {{pkt.id}},
{%- endfor %}
};
bool Handle_INVALID(std::function<void()>& outFunc, PacketSessionRef& session, BYTE* buffer, int32 len);
{%- for pkt in parser.recv_pkt %}
void Handle_{{pkt.name}}(const PacketSessionRef& session, const Protocol::{{pkt.name}}& pkt);
{%- endfor %}
class {{ output }}
{
public:
static void Init()
{
for (int i = 0; i < UINT16_MAX; ++i)
GPacketHandler[i] = Handle_INVALID;
{%- for pkt in parser.recv_pkt %}
GPacketHandler[PKT_{{pkt.name}}] = [](std::function<void()>& outFunc, PacketSessionRef& session, BYTE* buffer, int32 len) {
return GetCallback<Protocol::{{pkt.name}}>(outFunc, Handle_{{pkt.name}}, session, buffer, len);
};
{%- endfor %}
}
static bool PacketHandler(std::function<void()>& outFunc, PacketSessionRef session, BYTE* buffer, int32 len)
{
PacketHeader* header = reinterpret_cast<PacketHeader*>(buffer);
return GPacketHandler[header->id](outFunc, session, buffer, len);
}
{%- for pkt in parser.send_pkt %}
static SendBufferRef MakeSendBuffer(Protocol::{{pkt.name}}& pkt) { return MakeSendBuffer(pkt, PKT_{{pkt.name}}); }
{%- endfor %}
private:
template<typename PacketType, typename ProcessFunc>
static bool GetCallback(std::function<void()>& outFunc, ProcessFunc func, PacketSessionRef session, BYTE* buffer, int32 len)
{
PacketType pkt;
if (false == pkt.ParseFromArray(buffer + sizeof(PacketHeader), len - sizeof(PacketHeader)))
return false;
outFunc = [func, session, pkt](){ func(session, pkt); };
return true;
}
template<typename T>
static SendBufferRef MakeSendBuffer(T& pkt, uint16 pktId)
{
int headerSize = sizeof(PacketHeader);
int pktSize = pkt.ByteSizeLong();
#if UE_BUILD_DEBUG + UE_BUILD_DEVELOPMENT + UE_BUILD_TEST + UE_BUILD_SHIPPING >= 1
SendBufferRef sendBuffer = MakeShared<SendBuffer>(headerSize + pktSize);
#else
SendBufferRef sendBuffer = make_shared<SendBuffer>(headerSize + pktSize);
#endif
PacketHeader header;
header.id = pktId;
header.size = headerSize + pktSize;
sendBuffer->AppendBuffer(reinterpret_cast<BYTE*>(&header), headerSize);
if (pktSize > 0)
{
pkt.SerializeToArray(sendBuffer->WritePos(), pktSize);
sendBuffer->OnWrite(pktSize);
}
return sendBuffer;
}
};
수정을 마쳤으면 GenPacket.bat을 실행해준다.
언리얼에서 서버패킷핸들러를 못쓰고있었던 이유가 언리얼쪽에 PacketSessionRef, SendBufferRef가 정의되어있지 않았기 때문인데, PacketSession은 클래스를 따로 만들었고, SendBuffer도 Client.h, cpp에 만들어줬다. 그리고 ~Ref 도 언리얼용 쉐어드 포인터로 Client.h에서 정의해줬기 때문에 이제 ServerPacketHandler.h 를 쓸 수 있다.
지금은 MakeSendBuffer 만 사용할 것이기 때문에 일단 ServerPacketHandler.cpp는 주석만 해제해서 컴파일이 되는지만 확인해보자.
ServerPacketHandler.cpp
#include "ServerPacketHandler.h"
std::function<bool(std::function<void()>&, PacketSessionRef&, BYTE*, int32)> GPacketHandler[UINT16_MAX];
bool Handle_INVALID(std::function<void()>& outFunc, PacketSessionRef& session, BYTE* buffer, int32 len)
{
return true;
}
void Handle_S_LOGIN(const PacketSessionRef& session, const Protocol::S_LOGIN& pkt)
{
}
void Handle_S_ENTER_ROOM(const PacketSessionRef& session, const Protocol::S_ENTER_ROOM& pkt)
{
}
void Handle_S_CHAT(const PacketSessionRef& session, const Protocol::S_CHAT& pkt)
{
}
컴파일을 해보면 잘 된다.
송신 테스트
지난번에 서버에서 보낸 S_CHAT을 수신해서 직렬화된 버퍼를 큐에 집어넣는 것 까지 수신테스트를 해봤었다.
이번에는 송신이 잘 되는지 확인해보기 위해 S_CHAT을 받아서 RecvPacketQueue 가 차면, 게임인스턴스의 HandleRecvPackets를 호출해 recv 패킷 큐에서 꺼내고, C_CHAT 패킷을 만들어 서버로 보내보자. 그리고 언리얼 클라가 보낸 패킷을 서버에서 잘 받는지 확인하기 위해 받은 패킷의 내용을 출력해보자.
패킷 세션과 SendWorker는 바로 전 글에서 다 만들었으니 조금만 수정해주면 된다.
void PacketSession::HandleRecvPackets()
{
while (true)
{
TArray<uint8> Packet;
if (RecvPacketQueue.Dequeue(OUT Packet) == false)
break;
// TODO
//ServerPacketHandler::HandlePacket(Packet);
Protocol::C_CHAT pkt;
pkt.set_msg("hi i am unreal client!");
SendPacket(ServerPacketHandler::MakeSendBuffer(pkt));
}
}
원래는 ServerPacketHandler를 사용해서 S_CHAT이 들어왔을경우 적절한 처리를 하도록 만들지만, 우선 송신이 잘 되는지만 확인하면 되니까 C_CHAT을 SendBuffer로 만들어 보냈다.
이렇게 하고, GameInstance에서 HandleRecvPackets를 언리얼 블루프린트에서 사용할 수 있게 만들어 매 틱마다 호출하도록 만들어보자.
#pragma once
#include "CoreMinimal.h"
#include "Client.h"
#include "Engine/GameInstance.h"
#include "ClientGameInstance.generated.h"
class FSocket;
class PacketSession;
UCLASS()
class CLIENT_API UClientGameInstance : public UGameInstance
{
GENERATED_BODY()
public:
/*...*/
UFUNCTION(BlueprintCallable)
void HandleRecvPackets();
/*...*/
public:
/*...*/
TSharedPtr<PacketSession> GameServerSession;
};
void UClientGameInstance::HandleRecvPackets()
{
if (Socket == nullptr || GameServerSession == nullptr)
return;
GameServerSession->HandleRecvPackets();
}

만들고보니 패킷 세션 멤버함수를 호출할 일이 없는 것 같아서 아래와같이 수정도 해줬다.
class CLIENT_API PacketSession : public TSharedFromThis<PacketSession>
{
public:
/*...*/
//UFUNCTION(blueprintCallable) 블루프린트에서 호출할 일 없어서 이부분 제거함
void HandleRecvPackets();
/*...*/
};
서버에서는 기존에는 C_CHAT이 들어오면 해당 유저가 속한 Room에 서버가 받은 메시지를 뿌려줬는데, 지금은 Room에 입장도 안하므로 그냥 받은 메시지를 출력해보도록 수정해줬다.
void Handle_C_CHAT(const PacketSessionRef& session, const Protocol::C_CHAT& pkt)
{
Utils::LockPrint("recv C_CHAT packet! msg: " + pkt.msg()); // 이 부분 추가함
GameSessionRef gameSession = static_pointer_cast<GameSession>(session);
if (gameSession->_player == nullptr)
return;
gameSession->_player->ChatTest(pkt.msg());
}


지금의 패킷 송수신 과정을 설명하자면 이렇다
- 서버가 클라로 S_CHAT 패킷으로 HelloWorld! 보냄
- 클라의 recvworker 스레드가 패킷을 받아서 1개 패킷 분량으로 잘라서 RecvPacketQueue에 넣음
- 메인 스레드에서 틱마다 RecvPacketQueue 를 확인. 들어온게 있으면 꺼내고(지금은 쓰지는 않음) C_CHAT 패킷을 만들어 ServerPacketHandler::MakeSendBuffer함수로 직렬화 해 SendPacketQueue에 넣음.
- 클라의 sendworker가 돌고있다가 SendPacketQueue가 차있으면 꺼내서 서버로 보냄.
- 서버는 클라가 보낸 데이터를 받아서 패킷에 담긴 메시지를 출력함 recv C_CHAT packet! msg: hi i am unreal client!
현재까지의 git 버전
언리얼 클라이언트
Feat: 서버패킷핸들러 부활 · Dodontak/Project_Island_Client@c08333e
- Client.h에 서버패킷핸들러를 돌리기 위한 요소들을 추가함. - 서버패킷핸들러를 언리얼 클라이언트용으로 수정함 (탬플릿 파일에 매크로 조건문을 추가함) 테스트용 - 언리얼 에디터에서 레벨
github.com
게임 서버
Feat: 패킷핸들러 탬플릿파일 수정 (언리얼용) · Dodontak/Project_Island_GameServer@b527f5d
언리얼 클라이언트에서 사용하기 위한 버전을 만들어주기 위해 패킷핸들러 탬플릿을 수정함.
github.com
'프로젝트 > Project_Island' 카테고리의 다른 글
| 51. 언리얼 클라에서 OpenSSL 사용하기 (0) | 2026.05.01 |
|---|---|
| 50. 언리얼 클라에서 패킷핸들러 사용 (0) | 2026.04.30 |
| 48. 언리얼 클라이언트 패킷 수신 (0) | 2026.04.27 |
| 47. 클라와 서버 연결하기 (0) | 2026.04.26 |
| 46. 언리얼 클라이언트에 Protobuf 추가하기 (0) | 2026.04.21 |