지난번에는 언리얼에서 패킷 세션과 네트워크 워커 클래스들을 만들어 언리얼에서 제공하는 멀티스레드 기능을 이용해 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());
}

 

좌 서버 우 언리얼 클라이언트

지금의 패킷 송수신 과정을 설명하자면 이렇다

  1. 서버가 클라로 S_CHAT 패킷으로 HelloWorld! 보냄
  2. 클라의 recvworker 스레드가 패킷을 받아서 1개 패킷 분량으로 잘라서 RecvPacketQueue에 넣음
  3. 메인 스레드에서 틱마다 RecvPacketQueue 를 확인. 들어온게 있으면 꺼내고(지금은 쓰지는 않음) C_CHAT 패킷을 만들어 ServerPacketHandler::MakeSendBuffer함수로 직렬화 해 SendPacketQueue에 넣음.
  4. 클라의 sendworker가 돌고있다가 SendPacketQueue가 차있으면 꺼내서 서버로 보냄.
  5. 서버는 클라가 보낸 데이터를 받아서 패킷에 담긴 메시지를 출력함 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

 

+ Recent posts