Blog

ENGINEERING NOTE

[Unreal Engine] Network DeepDive 2 - NetConnection

1. NetConnection 이전 단계에서는 UNetDriver가 리플리케이션 시스템을 통해 송신할 데이터를 구성하고, 네트워크 전송을 위한 준비를 수행하는 과정을 살펴보았다.

Unreal Engine

1. NetConnection

이전 단계에서는 UNetDriver가 리플리케이션 시스템을 통해 송신할 데이터를 구성하고, 네트워크 전송을 위한 준비를 수행하는 과정을 살펴보았다.

이 과정에서 NetDriver는 다음과 같이 각 Connection을 중심으로 동작한다.

cpp
ServerConnection->PreTickDispatch();
ServerConnection->PostTickDispatch();

for ( int32 i=0; i < ClientConnections.Num(); i++ )
{
	...
    
	Connection->ExecuteOnAllDormantReplicators(ValidateFunction);
}

여기서 서버와 클라이언트는 UNetConnection을 통해 실제로 데이터를 주고받게 된다. 정리해보자면 NetDriver가 전체 네트워크 흐름을 관리하는 상위 관리자라면, NetConnection은 각 클라이언트와의 개별 통신을 담당하는 실질적인 네트워크 단위라고 볼 수 있다.

1-1. NetConnection의 역할

UNetConnection은 다음과 같은 역할을 수행한다.

  • 특정 클라이언트(또는 서버)와의 1:1 네트워크 상태 관리
  • 패킷 송수신 타이밍 및 상태 관리 (LastSendTime, LastReceiveTime 등)
  • 리플리케이션 데이터를 전송하기 위한 채널(Channel) 관리
  • RPC, Actor, SubObject 등의 네트워크 객체 전송 컨텍스트 유지
  • 패킷 처리 파이프라인 (PacketHandler, Handshake, FaultRecovery 등) 관리

NetConnection::InitBase

cpp
void UNetConnection::InitBase(...)
{
	// Connection 식별 및 기본 설정
	Driver = InDriver;
	SetConnectionHandle(...);
	SetConnectionState(InState);
	URL = InURL;
    
    ...

	// 전송을 위한 기반 구성
	InitChannelData();      // Channel 생성
	InitHandler();          // 패킷 처리 파이프라인
	MaxPacket = InMaxPacket;
	PacketOverhead = InPacketOverhead;
    
    ...

	// 네트워크 객체 식별 및 매핑
	PackageMap = NewObject<UPackageMapClient>(this, PackageMapClass);
	PackageMap->Initialize(this, Driver->GuidCache);

	...

	// Iris Replication 연결
	if (UReplicationSystem* ReplicationSystem = Driver->GetReplicationSystem())
	{
		ReplicationSystem->AddConnection(...);
		ReplicationSystem->SetConnectionUserData(..., this);
	}
}

InitBase는 Connection이 네트워크 상에서 동작하기 위한 모든 초기 상태를 구성하는 단계로, 이후 Tick과 Flush 과정에서 사용할 모든 네트워크 컨텍스트를 준비한다.

1-2. Tick

Tick()은 매 프레임마다 호출되며, Connection 단위에서 네트워크 상태를 업데이트하고, 채널 처리 및 패킷 송신 여부를 결정하는 핵심 루프이다.

cpp
void UNetConnection::Tick(float DeltaSeconds)
{
	SCOPE_CYCLE_COUNTER(Stat_NetConnectionTick);

	AssertValid();

	// 1. 프레임 시간 및 Tick Rate 계산
	const double CurrentRealtimeSeconds = FPlatformTime::Seconds();

	...

	if (FrameTime < MinNetFrameTime)
	{
		return; // Tick Rate 제한 (과도한 네트워크 처리 방지)
	}

	...

	// 2. 통계 및 상태 업데이트
	if ( CurrentRealtimeSeconds - StatUpdateTime > StatPeriod )
	{
		...
	}

	// 3. 연결 종료 / 타임아웃 처리
	if (bConnectionPendingCloseDueToSocketSendFailure)
	{
		Close(ENetCloseResult::SocketSendFailure);
		return;
	}

	...

	if (bReceiveTimedOut || bGracefulCloseTimedOut)
	{
		HandleConnectionTimeout(Error);
		return;
	}
	else
	{
		// 4. Channel 업데이트
        // 이 단계에서 ActorChannel / DataStreamChannel이 처리되며, 
        // 리플리케이션 결과가 실제 전송 가능한 형태(Bunch / DataStream)로 준비됨.
		checkf(ChannelsToTick.Num() <= OpenChannels.Num(), ...);

		// ActorChannel / DataStreamChannel Tick 수행
		for (int32 i = ChannelsToTick.Num()-1; i>=0; i--)
		{
			ChannelsToTick[i]->Tick();

			if (ChannelsToTick[i]->CanStopTicking())
			{
				ChannelsToTick.RemoveAt(i);
			}
		}

		// 아직 처리되지 않은 ActorChannel Bunch 처리
		for (auto ProcessingActorMapIter = KeepProcessingActorChannelBunchesMap.CreateIterator(); ProcessingActorMapIter; ++ProcessingActorMapIter)
		{
			...
			if (CurChannel->ProcessQueuedBunches())
			{
				CurChannel->ConditionalCleanUp(false, CurChannel->QueuedCloseReason);
			}
			...
		}

		// Channel 종료 처리
		TryClosePendingGracefulClose();

		// Connection 종료 조건 체크
		if (Channels[0] == nullptr && ...)
		{
			SetConnectionState(EConnectionState::USOCK_Closed);
		}
	}

	// 5. 패킷 송신 트리거
	if ( TimeSensitive || (Driver->GetElapsedTime() - LastSendTime) > Driver->KeepAliveTime)
	{
		bool bHandlerHandshakeComplete = !Handler.IsValid() || Handler->IsFullyInitialized();

		// 실제 패킷 송신 시작 지점
		if (bHandlerHandshakeComplete && HasReceivedClientPacket())
		{
			FlushNet(); // ← Replication 결과가 패킷으로 전송됨
		}
	}


	// 6. PacketHandler 처리 (중간 계층)
	if (Handler.IsValid())
	{
		Handler->Tick(FrameTime);

		// 재전송 패킷 처리 (Reliability 보장)
		BufferedPacket* ResendPacket = Handler->GetQueuedRawPacket();

		while (ResendPacket != nullptr)
		{
			LowLevelSend(ResendPacket->Data, ResendPacket->CountBits, ResendPacket->Traits);
			ResendPacket = Handler->GetQueuedRawPacket();
		}

		// 일반 큐 패킷 전송
		BufferedPacket* QueuedPacket = Handler->GetQueuedPacket();

		while(QueuedPacket != nullptr)
		{
			LowLevelSend(QueuedPacket->Data, QueuedPacket->CountBits, QueuedPacket->Traits);
			delete QueuedPacket;
			QueuedPacket = Handler->GetQueuedPacket();
		}
	}
}

1-3. FlushNet

FlushNet()은 Connection에 누적된 데이터를 하나의 패킷으로 구성하고, 최종적으로 LowLevelSend()를 호출하여 실제 네트워크로 송신하는 단계이다.

cpp
void UNetConnection::FlushNet(bool bIgnoreSimulation)
{
	// 전송 준비 상태 검증
	check(Driver);
	ValidateSendBuffer();

	LastEnd = FBitWriterMark();
	TimeSensitive = 0;

	// 1. 전송 필요 여부 판단
	if (SendBuffer.GetNumBits() || HasDirtyAcks || ...)
	{
		// Handshake 완료 전이면 전송 차단 (보안/동기화)
		if (!HasReceivedClientPacket() && ...)
		{
			Close(ENetCloseResult::PrematureSend);
			InitSendBuffer();
			return;
		}

		FOutPacketTraits Traits;

		// 2. 패킷 헤더 구성
		if (SendBuffer.GetNumBits() == 0)
		{
			// KeepAlive 패킷 생성
			WriteBitsToSendBuffer(NULL, 0);
			Traits.bIsKeepAlive = true;
		}

		// PacketHandler 처리 (암호화 / 압축 등)
		if (Handler.IsValid())
		{
			Handler->OutgoingHigh(SendBuffer);
		}

		const double PacketSentTimeInS = FPlatformTime::Seconds();

		// 패킷 종료 비트 기록
		SendBuffer.WriteBit(1);

		// ACK / Sequence / Header 작성
		if (!IsInternalAck())
		{
			WritePacketHeader(SendBuffer);
			WriteFinalPacketInfo(SendBuffer, PacketSentTimeInS);
		}

		ValidateSendBuffer();

		// 3. 실제 전송 
		if (Driver->IsNetResourceValid())
		{
			// 실제 소켓 송신
			LowLevelSend(SendBuffer.GetData(), SendBuffer.GetNumBits(), Traits);
		}


		// 4. 통계 및 상태 업데이트
		...
		++OutPacketId;
		++OutPackets;
		Driver->OutPackets++;

		LastSendTime = Driver->GetElapsedTime();

		...

		bFlushedNetThisFrame = true;

		// 5. 버퍼 초기화
		InitSendBuffer();
	}
}

1-4. ReceivedRawPacket

ReceivedRawPacket()은 소켓을 통해 전달된 네트워크 데이터를 UNetConnection이 처음으로 처리하는 수신 진입점이다. 여기에서는 Raw Byte 데이터를 입력으로 받아, 이를 엔진 내부에서 처리 가능한 형태로 변환하는 역할을 수행한다.

cpp
void UNetConnection::ReceivedRawPacket( void* InData, int32 Count )
{
	using namespace UE::Net;

	...

	uint8* Data = (uint8*)InData;

	// PacketHandler 처리 (암호화 해제 / 핸드셰이크 / 검증)
	if (Handler.IsValid())
	{
		FReceivedPacketView PacketView;
		PacketView.DataView = {Data, Count, ECountUnits::Bytes};

		EIncomingResult IncomingResult = Handler->Incoming(PacketView);

		if (IncomingResult == EIncomingResult::Success)
		{
			// Handler 처리 후 실제 유효 데이터로 갱신
			Count = PacketView.DataView.NumBytes();

			if (Count > 0)
			{
				Data = PacketView.DataView.GetMutableData();
			}
			else
			{
				return; // Handler에서 완전히 소비된 패킷
			}
		}
		else
		{
			// 패킷 처리 실패 → 연결 종료 가능
			UE_LOG(LogNet, Warning, TEXT("Packet failed PacketHandler processing."));
			Close(...);
			return;
		}

		// Handler 초기화 이전 패킷 무시 (Handshake 미완료 상태)
		if (!Handler->IsFullyInitialized())
		{
			UE_LOG(LogNet, Warning, TEXT("PacketHandler not fully initialized."));
			return;
		}
	}

	...

	// Raw Byte → BitReader 변환
	if (Count > 0)
	{
		uint8 LastByte = Data[Count-1];

		if (LastByte != 0)
		{
			int32 BitSize = (Count * 8) - 1;

			// 실제 유효 Bit 길이 계산
			while (!(LastByte & 0x80))
			{
				LastByte *= 2;
				BitSize--;
			}

			FBitReader Reader(Data, BitSize);

			...

			// PacketHandler 후처리 (High-level)
			if (Handler.IsValid())
			{
				Handler->IncomingHigh(Reader);
			}

			// 4. 실제 패킷 파싱 시작
			if (Reader.GetBitsLeft() > 0)
			{
				ReceivedPacket(Reader); // ← Header / ACK / Bunch 파싱

				// 패킷 순서 보정
				FlushPacketOrderCache();
			}
		}
		else
		{
			// 잘못된 패킷 (마지막 바이트 0)
			UE_LOG(LogNet, Warning, TEXT("Received packet with 0's in last byte"));
			HandleNetResultOrClose(...);
		}
	}
	else 
	{
		// 잘못된 패킷 (크기 0)
		UE_LOG(LogNet, Warning, TEXT("Received zero-size packet"));
		HandleNetResultOrClose(...);
	}
}

1-5. UNetConnection Flow

UNetConnection은 요약하면 매 프레임 다음과 같은 흐름으로 동작한다.

  • InitBase() 단계에서 Connection의 기본 상태를 구성

Channel, PacketHandler, PackageMap 등이 초기화되며, 이후 네트워크 송수신에 필요한 모든 컨텍스트가 준비된다.

  • 매 프레임마다 Tick()이 호출

Connection 단위의 프레임 시간 계산, 타임아웃 검사, 통계 업데이트 네트워크 상태가 갱신되며, 연결이 유지 가능한 상태인지 지속적으로 판단한다.

  • Channel 처리

ActorChannel / DataStreamChannel이 Tick되며, 리플리케이션 데이터가 Bunch 형태로 구성된다.

  • FlushNet()

KeepAlive 시간이 초과되었거나, 전송할 데이터가 존재하는 경우 FlushNet()이 호출된다. FlushNet()에서는 SendBuffer에 누적된 데이터를 하나의 패킷으로 구성하고, 패킷 구성이 완료되면 LowLevelSend()가 호출되어 실제 소켓을 통해 네트워크로 전송한다.

  • ReceivedRawPacket()

ReceivedRawPacket()에서는 PacketHandler를 통해 암호화 해제 및 검증을 수행하고, Raw Byte 데이터를 Bit 단위 스트림으로 변환한 뒤 ReceivedPacket()으로 전달한다.

UNetConnection에서 데이터를 네트워크 패킷으로 변환하고 이를 송수신하는 과정을 살펴보았다.
다음에는 Connection 내부에서 쓰이는 Channel에 대해 살펴보겠다.
참고 자료

https://dev.epicgames.com/documentation/unreal-engine/detailed-actor-replication-flow?application_version=4.27 https://youtu.be/JOJP0CvpB8w?si=TFLhKqHtA0VvLnac