참고 링크

더보기

기본 개념

Packet

  • UDP에서 사용하는 Packet과 동한 개념
  • 여러 개의 Bunch로 구성되어 있음.

Bunch

  • 보통은 하나의 RPC나 Property Replication와 대응
    • Packet 최대 크기를 초과하면 여러 개로 쪼개진다.
    • 반대로 크기가 충분히 작으면 하나로 합쳐지기도 한다.
  • 정보를 수신할 때에는 FInBunch로 수신하고, 송신할 때에는 FOutBunch로 송신한다.

ACK/NAK

ACK

  • 일반적인 Network에서 사용하는 ACK와 동일한 개념
    • 상대방이 Packet을 성공적으로 수신 했을 때 수신 됨.
  • Ack 역시 Bunch의 형태를 띈다. 
    • 한 Packet에 여러 Bunch가 존재할 수 있듯, 한 Packet에 ACK Bunch 또한 여러 개 존재할 수 있다.

NAK

  • ACK의 반대되는 개념
    • 상대방이 Packet을 성공적으로 수신하지 못했을 때 수신 됨.
  • 실제로 구현 상 특정 Packet이 전달되지 못함을 정확하게 판단하는 것은 불가능하다.
    • 때문에 송신한 쪽에서 다른 Packet의 ACK를 통해 자체적으로 NAK 판단을 내린다.
    • 모든 Packet(ACK 포함)에는 PacketId가 입력되기 때문에 가능한 동작.
      • 예를 들어 PacketId 1, 2, 4가 수신되면 3에 해당하는 Packet은 NAK판정을 낸다.
      • 이 3번에 해당하는 Packet은 뒤늦게 수신하더라도 처리하지 않는다.
  • NAK에 대한 처리는 다음 코드를 통해 대략적으로 확인이 가능하다.
void UNetConnection::ReceivedPacket( FBitReader& Reader, bool bIsReinjectedPacket, bool bDispatchPacket )
{
/*-------Skip------*/
    // Process acks
    // Lambda to dispatch delivery notifications, 
    auto HandlePacketNotification = [&Header, &ChannelsToClose, this](FNetPacketNotify::SequenceNumberT AckedSequence, bool bDelivered)
    {
        // Increase LastNotifiedPacketId, this is a full packet Id
        ++LastNotifiedPacketId;
        ++OutTotalNotifiedPackets;
        Driver->IncreaseOutTotalNotifiedPackets();

        // Sanity check
        if (FNetPacketNotify::SequenceNumberT(LastNotifiedPacketId) != AckedSequence)
        {
            UE_LOG(LogNet, Warning, TEXT("LastNotifiedPacketId != AckedSequence"));

            Close(ENetCloseResult::AckSequenceMismatch);

            return;
        }

        if (bDelivered)
        {
            ReceivedAck(LastNotifiedPacketId, ChannelsToClose);
        }
        else
        {
            ReceivedNak(LastNotifiedPacketId);
        };
    };

    // Update incoming sequence data and deliver packet notifications
    // Packet is only accepted if both the incoming sequence number and incoming ack data are valid		
    const int32 UpdatedPacketSequenceDelta = PacketNotify.Update(Header, HandlePacketNotification);
/*-------Skip------*/
}

template<class Functor>
FNetPacketNotify::SequenceNumberT::DifferenceT FNetPacketNotify::Update(const FNotificationHeader& NotificationData, Functor&& InFunc)
{
	const SequenceNumberT::DifferenceT InSeqDelta = GetSequenceDelta(NotificationData);

	if (InSeqDelta > 0)
	{
		UE_LOG_PACKET_NOTIFY(TEXT("FNetPacketNotify::Update - Seq %u, InSeq %u"), NotificationData.Seq.Get(), InSeq.Get());
	
		ProcessReceivedAcks(NotificationData, InFunc);

		return InternalUpdate(NotificationData, InSeqDelta);
	}
	else
	{
		return 0;
	}
}

template<class Functor>
void FNetPacketNotify::ProcessReceivedAcks(const FNotificationHeader& NotificationData, Functor&& InFunc)
{
	if (NotificationData.AckedSeq > OutAckSeq)
	{
		SequenceNumberT::DifferenceT AckCount = SequenceNumberT::Diff(NotificationData.AckedSeq, OutAckSeq);

		UE_LOG_PACKET_NOTIFY(TEXT("Notification::ProcessReceivedAcks - AckedSeq: %u, OutAckSeq: %u AckCount: %u"), NotificationData.AckedSeq.Get(), OutAckSeq.Get(), AckCount);

		// Update InAckSeqAck used to track the needed number of bits to transmit our ack history
		// Note: As we might reset sequence history we need to check if we already have advanced the InAckSeqAck
		const SequenceNumberT NewInAckSeqAck = UpdateInAckSeqAck(AckCount, NotificationData.AckedSeq);
		if (NewInAckSeqAck > InAckSeqAck)
		{
			InAckSeqAck = NewInAckSeqAck;
		}
		
		// ExpectedAck = OutAckSeq + 1
		SequenceNumberT CurrentAck(OutAckSeq);
		++CurrentAck;

		// Make sure that we only look at the sequence history bit included in the notification data as the sequence history might have been reset, 
		// in which case we might not receive the max size history even though the ack-count is bigger than the history
		const SequenceNumberT::DifferenceT HistoryBits = NotificationData.HistoryWordCount * SequenceHistoryT::BitsPerWord;

		// Warn if the received sequence number is greater than our history buffer, since if that is the case we have to treat the data as lost
		// Note: This should normally not be a problem since we try to flush the sequence history before allowing an overshoot of the sequence history window on the sending side.
		// If this occurs with no hitches on server or client, there might be reason to investigate if too much data is being sent in which case the the size sequence history might have to be increased.
		if (AckCount > (SequenceNumberT::DifferenceT)(SequenceHistoryT::Size))
		{
			UE_LOG_PACKET_NOTIFY_WARNING(TEXT("FNetPacketNotify::ProcessReceivedAcks - Missed Acks: AckedSeq: %u, OutAckSeq: %u, FirstMissingSeq: %u Count: %u"), NotificationData.AckedSeq.Get(), OutAckSeq.Get(), CurrentAck.Get(), AckCount - (SequenceNumberT::DifferenceT)(SequenceHistoryT::Size));
		}

		// Everything not found in the history buffer is treated as lost
		while (AckCount > HistoryBits)
		{
			--AckCount;
			InFunc(CurrentAck, false);
			++CurrentAck;
		}

		// For sequence numbers contained in the history we lookup the delivery status from the history
		while (AckCount > 0)
		{
			--AckCount;
			UE_LOG_PACKET_NOTIFY(TEXT("Notification::ProcessReceivedAcks Seq: %u - IsAck: %u HistoryIndex: %u"), CurrentAck.Get(), NotificationData.History.IsDelivered(AckCount) ? 1u : 0u, AckCount);
			InFunc(CurrentAck, NotificationData.History.IsDelivered(AckCount));
			++CurrentAck;
		}
		OutAckSeq = NotificationData.AckedSeq;

		// Are we done waiting for an reset of the ack history?
		if (OutAckSeq > WaitingForFlushSeqAck)
		{
			WaitingForFlushSeqAck = OutAckSeq;
		}
	}
}

 

NetGUID

  • Object Pointer를 Network로 Replicate하기 위해 발급하는 ID
    • 여기서 Object는 Actor 뿐 아니라 Component도 포함된다.
    • 정확히는 UObject 단위로 구분하는 편이 적절하다.
  • Network에서 Object 식별이나 Replicate 여부를 판단하는 기준으로 사용된다.

대략적인 Unreal Network Architecture를 표현 한 그림

NetDriver

  • Game에서 Network를 총괄하는 객체
  • 크게 2가지 정보를 관리.
    • NetConnection
      • Server에서는 ClientConnection으로, Client에서는 ServerConnection으로 관리한다.
    • Connection별로 관리가 필요 없는 정보들

특징

연결 지향

  • NetConnection을 통해 연결이 되어 있어야 통신 가능.
  • 만약 비연결 프로토콜을 사용하면 Timeout 등을 처리하는 로직이 구현되어 있어야 한다.

Packet 형식

  • Stream 형식이 아닌 Packet 형식
  • 모든 데이터가 MAX_PACKET_SIZE 크기로 쪼개져서 독립된 Packet으로 저장된다.

비신뢰성

  • 신뢰성을 보장하는 패킷 송수신은 NteDriver에서 알 수 없는 고수준 계층에서 동작한다.

무결성

  • Packet의 데이터가 변조되지 않는다.

Connection별 관리가 필요 없는 정보들

FNetworkObjectInfo

  • Network에서 Replicate되는 Object에 대한 정보 및 포인터를 포함한 구조체
  • Replicated Actor의 현재 상태를 추적, 관리한다.

FReplicationChangelistMgr

  • 각각의 Replicated Object가 Replicate될 때까지의 변경사항들을 관리한다.
  • List가 가득 차게 된다면, 가장 먼저 들어온 것이 그 다음 것과 Merge 된다.
  • 이 구조체는 모든 Connection에서 사용된다.
    • 각 Connection에 어떤 정보를 전송해야 할지 결정하기 위한 비교 작업을 공유한다.
  • Connection은 마지막으로 Connection이 확인된 이후의 모든 변경사항을 전송한다.

FRepChangedPropertyTracker

  • Connection들이 공유하는 Property에 대한 메타 데이터를 저장.
    • 조건, 활성화 여부, Replay에 필요할 수 있는 외부 데이터와 무관하게 모든 메타데이터가 저장된다.
  • Object가 Replicate 될 때 어느 Property가 변경 되었는지 파악해 최적의 Network Data Packet을 구성한다.

NetConnection

Client가 Server에 접속을 요청할 때 NetConnection의 상호작용 구조

  • Game이 실질적으로 네트워크 통신을 하는 Host의 주체
  • Replicated Actor의 연결을 담당하는 (Actor)Channel을 관리한다.
    • PlayerController, GameMode 등도 이 Actor Channel을 통해 Replicate된다.
  • 때문에 하나의 Connection은 하나의 Server/Client로 취급된다.
    • Client의 경우 접속한 Server에 대응하는 NetConnection을 가진다.
    • Server의 경우 자신에게 접속한 Client에 대응하는 NetConnection들을 가진다.
      • 즉, NetConnection의 수가 Client의 수와 일치한다.

Channel

  • 실질적으로 Packet을 송/수신하는 주체
  • 각 Channel은 타겟팅하는 Object의 정보를 소유하고 있다.
  • 각 Connection들이 서로 다른 Channel을 가진다.
    • 예를 들어 4개의 Client가 연결되어 있고 Replicated Object가 200개라면,
      Client에 대응하는 NetConnection들은 각각 고유한 Actor Channel을 200개씩 가지게 된다.

ControlChannel

  • Client가 Server에 연결될 때 생성되는 Handshake용 Channel
  • 연결 설정, 인증, 상태 업데이트 등의 Control Message를 처리한다.

ActorChannel

  • Actor의 상태, Event를 Replicate하는 Channel
  • Actor 내 RepNotify, RPC이 발생하면 그 Actor의 ActorChannel을 통해 Client로 전달된다.

VoiceChannel

  • 음성 데이터를 송/수신 하는 Channel
  • Game 내 음성채팅 기능 구현에 사용된다.

Replication 판단 순서

Replicated Actor의 변경사항이 발생했을 때 Client로 Replicate되는 구조

  • Replicate 판단 여부는 UNetDriver::ServerReplicateActors에서 이루어진다.
  • Replicate가 결정된 Actor들은 UActorChannel::ReplicateActor 함수에서 세부 사항을 처리해 Replicte 된다.

NetUpdateFrequency

  • NetPriority를 통해 Replicate 여부를 판단

PreReplication

  • Replication 조건을 검사하거나 변수를 Update

bOnlyRelevantToOwner

  • NetRelevancy를 통해 Replicate 여부 판단
  • Owner에게만 Replicate 되어야 하는지 여부 결정
  • IsRelevancyOwnerFor, IsNetRelevantFor의 호출 조건을 결정

IsRelevancyOwnerFor

  • bOnlyRelevantToOwner가 True일 때에만 호출
  • bOnlyRelevantToOwner가 true일 때, 대상 Client와 Relevant한지 판단

IsNetRelevantFor

  • bOnlyRelevantToOwner가 false일 때, 어느 Client에 Relevant한지 판단

NetDormancy

  • Actor의 Dormancy(휴면) 여부를 관리
    • Dormant인 경우 Actor가 List에 등록되지 않아 Iterate 부하를 크게 줄여준다.
  • Network Traffic과 상태 변화의 빈도에 따라 조정된다.

'UE5 > Network' 카테고리의 다른 글

[Network] Property Replication  (1) 2024.06.28
[Network] Network Property  (0) 2024.06.25
[Network] DemoNetDriver 및 Streamer  (0) 2024.06.17
[Network] Beacon  (0) 2024.06.17
[Network] Replication Graph  (1) 2024.06.13

+ Recent posts