Study/TIL(Today I Learned)

24.10.29 VV, UE5, DB

에린_1 2024. 10. 29. 19:22
728x90

VV

  • 서버에서 로그인 과정을 거쳤으니 그 결과를 다시 클라이언트에 보내줘야 한다. 나는 결과에 따라서 다른 프로토콜을 쓰는게 아니라 같은 프로토콜로 대신에 attribute에 다른 값으로 성공했는지 실패했는지 알아볼 수 있도록 구성했다.
bool OnDataHandle(IDataBuffer* pDataBuffer, int32_t nConnID)
{
    if (pDataBuffer == nullptr)
    {
        CLog::GetInstancePtr()->LogError("Received null data buffer in OnDataHandle.");
        return false;
    }

    PacketHeader* pHeader = (PacketHeader*)pDataBuffer->GetBuffer();
    if (pHeader == nullptr)
    {
        CLog::GetInstancePtr()->LogError("Invalid packet header.");
        return false;
    }

    int32_t MsgID = pHeader->MsgID;
    switch (MsgID)
    {
    case EMessageID::LOGIN_REQUEST_MSG_ID:
    {
        string ReceiveData(pDataBuffer->GetData(), pDataBuffer->GetTotalLenth());
        vector<string> ParsedData;

        size_t pos = 0;
        string token;
        while ((pos = ReceiveData.find('|')) != string::npos)
        {
            token = ReceiveData.substr(0, pos);
            ParsedData.push_back(token);
            ReceiveData.erase(0, pos + 1);
        }
        ParsedData.push_back(ReceiveData);
        
        if (ParsedData.size() == 2)
        {
            string ID = ParsedData[0];
            string PW = ParsedData[1];

            bool bIsValid = ValidateUserCredentials(ID, PW);
         
            // TODO: Send success response to client
            hbServerEngine::PacketHeader ResponseHeader;
            ResponseHeader.MsgID = EMessageID::LOGIN_RESPONSE_MSG_ID;
            ResponseHeader.TotalSize = sizeof(hbServerEngine::PacketHeader);
            ResponseHeader.Attribute = bIsValid ? 0 : 1;

            std::vector<uint8_t> ResponsePacket(sizeof(hbServerEngine::PacketHeader));
            memcpy(ResponsePacket.data(), &ResponseHeader, sizeof(hbServerEngine::PacketHeader));

            IDataBuffer* ResponseBuffer = CBufferAllocator::GetInstancePtr()->AllocDataBuff(ResponsePacket.size());
            memcpy(ResponseBuffer->GetBuffer(), ResponsePacket.data(), ResponsePacket.size());
            ResponseBuffer->SetTotalLenth(ResponsePacket.size());

            CConnection* Connection = CConnectionMgr::GetInstancePtr()->GetConnectionByID(nConnID);
            if (Connection)
            {
                Connection->SendBuffer(ResponseBuffer);
                CNetManager::GetInstancePtr()->PostSendOperation(Connection);
            }
         
        }
        break;
    }
    default:
        CLog::GetInstancePtr()->LogWarn("Unhandled MsgID: %d", MsgID);
        break;
    }
  • 서버쪽 코드를 좀 수정했다. 처음에는 if문으로 bIsValid에 따라 확인했는데, 다른 방법을 사용해서 구현했다.
void UNetworkManager::ProcessResponse(const TArray<uint8>& ReceiveData)
{
	if (ReceiveData.Num() < sizeof(hbServerEngine::PacketHeader))
	{
		return;
	}

	hbServerEngine::PacketHeader* Header = (hbServerEngine::PacketHeader*)ReceiveData.GetData();
	switch (Header->MsgID)
	{
	case EMessageID::LOGIN_RESPONSE_MSG_ID:
	{
		if (Header->Attribute == 0)
		{
			UE_LOG(LogTemp, Log, TEXT("Login successful!"));
			//TODO 로그인 성공
		}
		else if (Header->Attribute == 1)
		{
			UE_LOG(LogTemp, Warning, TEXT("Login failed. Incorrect ID or Password."));
			//TODO 로그인 실패 로직 처리
		}
		StopReceiving();
		break;
	}

	default:
		UE_LOG(LogTemp, Warning, TEXT("Unhandled MsgID: %d"), Header->MsgID);
		break;
	}
}

void UNetworkManager::ReceiveData()
{
	if (ClientSocket)
	{
		ReceiveBuffer.SetNumUninitialized(8192);
		int32 ReceivedDataSize = 0;

		bool bReceive = ClientSocket->Recv(ReceiveBuffer.GetData(), ReceiveBuffer.Num(), ReceivedDataSize,
			ESocketReceiveFlags::None);

		if (bReceive && ReceivedDataSize > 0)
		{
			ReceiveBuffer.SetNum(ReceivedDataSize);
			ProcessResponse(ReceiveBuffer);
		}
		else if (bReceive && ReceivedDataSize == 0)
		{
			// 서버에서 데이터를 아직 보내지 않았거나 수신할 데이터가 없는 경우.
			UE_LOG(LogTemp, Log, TEXT("No data received, but connection is still active."));
		}
		else if (!bReceive)
		{
			// 데이터가 수신되지 않았거나 오류가 발생한 경우
			if (ClientSocket->GetConnectionState() == SCS_Connected)
			{
				// 연결은 유지되고 있지만 데이터가 아직 도착하지 않은 경우
				UE_LOG(LogTemp, Log, TEXT("No data received yet, connection is still valid."));
			}
			else
			{
				// 연결이 끊어진 경우
				UE_LOG(LogTemp, Error, TEXT("Connection lost with server."));
			}
		}
	}
	else
	{
		UE_LOG(LogTemp, Warning, TEXT("ClientSocket is null, cannot receive data."));
	}
	
}

void UNetworkManager::StartReceiving()
{
	if (GetWorld())
	{
		GetWorld()->GetTimerManager().SetTimer(ReceiveTimerHandle, this, &UNetworkManager::ReceiveData, 0.1f, true);
	}
}

void UNetworkManager::StopReceiving()
{
	if (GetWorld())
	{
		GetWorld()->GetTimerManager().ClearTimer(ReceiveTimerHandle);
	}
}
  • NetworkManager.cpp에 새롭게 Receive코드를 구현해주었다.

결과

성공하는 것을 볼 수 있다.(야호)

다음은 클라이언트 움직임을 구현해보도록하겠다.

UE5

패킷 데이터 직렬화(버퍼 생성)

TArray<uint> PacketData;
PacketData.SetNumUninitialized(Header.TotalSize);
  • TArray<uint> 형식의 배열을 생성하여 패킷 전체 크기만큼 메모리를 할당한다.
  • SetNumUninitialized() 는 초기화되지 않은 상태로 메모리를 할당하는 함수로, 패킷 데이터를 저장할 공간을 확보한다.

SetNumUnititialized()

  • 배열의 크기르 설정하면서, 불필요한 초기화 비용을 피하고 성능을 최적화 시키기 위해 사용한다.
  • 장점
    1. 성능 최적화
      • 배열의 요소를 초기화하지 않기 때문에 불필요한 초기화 과정에 소요되는 CPU 자원을 절약할 수 있다.
      • 원시 데이터 타입(uint8, int, float 등)에서 초기화는 종종 성능 저하의 원인이 될 수 있기 때문에 초기화를 건너뛰는 것이 빠른 성능을 보장할 수 있다.
    2. 메모리 할당 효율성
      • 초기화가 필요 없는 경우 굳이 SetNum() 을 사용해서 메모리 초기화 까지 할 필요가 없다.
      • SetNum() 을 사용하면 메모리 할당뿐만 아니라 할당된 모든 원소에 기본값을 할당하므로, 데이터가 곧바로 덮어씌워질 경우에 불필요한 작업이 된다.
    3. 유연한 데이터 처리
      • SetNumUnitialized() 를 통해 설정된 배열에 대해서 직접 데이터를 채워 넣기 때문에 네트워크와 같은 시스템에서 데이터를 버퍼로 읽어 들이는 작업에 최적화되어 있다.
      • 특히, 네트워크 패킷이나 파일 입출력 같은 경우 ,이미 외부로부터 받아오는 데이터가 유효하고 초기화 작업이 필요 없기 때문에 유연하게 사용할 수 있다.

DB

쿼리 문자열 연결 에러

  • std::string 을 사용해 쿼리를 구성하는 과정에서 문자열이 제대로 연결되지 않을 수 있다.
  • 예를 들어, ID나 PW가 빈 문자열이거나 ‘ 와 같은 특수 문자를 포함하고 있을 경우 쿼리 문법 오류를 유발할 수 있다.
    • 특히 입력 값에 ‘ 가 포함되면 쿼리에서 문자열을 제대로 닫지 못하고 문법 오류를 일으킬 수 있다.
    • 이 경우, SQL 인젝션 등의 보안 문제도 발생할 수 있다. 이를 방지하기 위해 입력 값을 escape 처리하거나, MySQL의 prepared statement 기능을 사용하는 것이 좋다.

Escape 처리

  • Escape처리는 쿼리 문자열에 있는 특별한 문자를 이스케이프해서 쿼리가 의도한 대로 작동하도록 하는 방법이다. 예를 들어, 데이터베이스에 문자열을 삽입할 때, 문자열에 있는 작은 따옴표(’) 같은 특수 문자가 SQL 구문으로 인식되지 않도록 하는 것이 중요하다.
  • SQL에서 다음과 같은 특수 문자들은 문제가 될 수 있다.
    • \
    • NULL

mysql_real_escape_string

  • mysql_real_escape_string 함수는 escape 처리를 안전하게 자동으로 수행해주는 함수이다. MySQL의 C API에서 제공하는 함수로, 주로 사용자로부터 입력된 데이터를 데이터베이스에 삽입할 때 사용한다.
  • 사용법
    • 입력 문자열의 특수 문자를 이스케이프하여 MySQL에서 안전하게 사용할 수 있도록 한다.
    • 작은 따옴표(’), 큰 따옴표(”), 역슬래시(\), NULL 문자를 이스케이프한다.
    • MySQL 서버에 연결된 상태에서만 사용할 수 있으며, 서버의 문자 집합 정보를 사용하여 올바른 이스케이프 처리를 한다.

Prepared Statement와 비교

  • mysql_real_escape_string 는 수동으로 쿼리 문자열을 빌드하면서 이스케이프 처리를 하지만, Prepared Statement는 별도의 이스케이프 처리가 필요 없이 쿼리를 안전하게 구성할 수 있도록 도와준다. Prepared StateMent는 MySQL의 클라이언트 API뿐만 아니라, 현대적인 데이터베이스 라이브러리나 ORM에서도 지원된다.
  • Prepared Statement를 사용하는 것이 더 안전하고 실수를 줄일 수 있으며, 직접 문자열을 처리하는 것보다 유지 보수 측면에서도 유리하다.
728x90

'Study > TIL(Today I Learned)' 카테고리의 다른 글

24.11.04 VV. 장애물, UI 구현, 점수 구현  (0) 2024.11.04
24.10.30 VV, UE5  (3) 2024.10.30
24.10.28 VV  (0) 2024.10.29
24.10.23 VV  (0) 2024.10.23
24.10.22 VV, UE5  (3) 2024.10.23