[Unreal Engine] Network DeepDive 2 - NetConnection

이매·2026년 4월 28일

Unreal Network & Multiplay

목록 보기
14/16
post-thumbnail

1. NetConnection

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

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

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

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 단위에서 네트워크 상태를 업데이트하고, 채널 처리 및 패킷 송신 여부를 결정하는 핵심 루프이다.

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()를 호출하여 실제 네트워크로 송신하는 단계이다.

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 데이터를 입력으로 받아, 이를 엔진 내부에서 처리 가능한 형태로 변환하는 역할을 수행한다.

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

profile
언리얼 엔진 주니어(신입) 개발자

0개의 댓글