Unreal Engine 的玩家流程管理

端口监听

UE中网络相关功能由网络驱动类型UNetDriver来提供,这个UNetDriver是一个虚基类,提供了各种网络收发和管理功能。实际被使用的主要是其子类IpNetDriver或者DemoNetDriverIpNetDriver负责处理正常的游戏,而DemoNetDriver负责处理游戏录像的回放。UNetDriver可以继续扩展,但是每个扩展出来的子类都需要在BaseEngine.ini通过NetDriverDefinitions注册到引擎:

+NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="/Script/OnlineSubsystemUtils.IpNetDriver",DriverClassNameFallback="/Script/OnlineSubsystemUtils.IpNetDriver")
+NetDriverDefinitions=(DefName="BeaconNetDriver",DriverClassName="/Script/OnlineSubsystemUtils.IpNetDriver",DriverClassNameFallback="/Script/OnlineSubsystemUtils.IpNetDriver")
+NetDriverDefinitions=(DefName="DemoNetDriver",DriverClassName="/Script/Engine.DemoNetDriver",DriverClassNameFallback="/Script/Engine.DemoNetDriver")

同时引擎里提供一个CreateNamedNetDriver的接口,来通过传入的DefName来创建对应的NetDriver:

bool UEngine::CreateNamedNetDriver(UWorld *InWorld, FName NetDriverName, FName NetDriverDefinition)
{
	return UE::Private::CreateNamedNetDriver_Local(this, GetWorldContextFromWorldChecked(InWorld), NetDriverName, NetDriverDefinition);
}

UNetDriver* CreateNetDriver_Local(UEngine* Engine, FWorldContext& Context, FName NetDriverDefinition, FName InNetDriverName)
{
	UNetDriver* ReturnVal = nullptr;
	FNetDriverDefinition* Definition = nullptr;
	auto FindNetDriverDefPred =
		[NetDriverDefinition](const FNetDriverDefinition& CurDef)
	{
		return CurDef.DefName == NetDriverDefinition;
	};


	{
		Definition = Engine->NetDriverDefinitions.FindByPredicate(FindNetDriverDefPred);
	}

	if (Definition != nullptr)
	{
		UClass* NetDriverClass = StaticLoadClass(UNetDriver::StaticClass(), nullptr, *Definition->DriverClassName.ToString(), nullptr,
			LOAD_Quiet);

		// if it fails, then fall back to standard fallback
		if (NetDriverClass == nullptr || !NetDriverClass->GetDefaultObject<UNetDriver>()->IsAvailable())
		{
			NetDriverClass = StaticLoadClass(UNetDriver::StaticClass(), nullptr, *Definition->DriverClassNameFallback.ToString(),
				nullptr, LOAD_None);
		}

		if (NetDriverClass != nullptr)
		{
			ReturnVal = NewObject<UNetDriver>(GetTransientPackage(), NetDriverClass);

			check(ReturnVal != nullptr);

			const FName DriverName = InNetDriverName.IsNone() ? ReturnVal->GetFName() : InNetDriverName;
			const bool bInitializeWithIris = Engine->WillNetDriverUseIris(Context, NetDriverDefinition, DriverName);

			ReturnVal->SetNetDriverName(DriverName);
			ReturnVal->SetNetDriverDefinition(NetDriverDefinition);
			ReturnVal->PostCreation(bInitializeWithIris);

			new(Context.ActiveNetDrivers) FNamedNetDriver(ReturnVal, Definition);

			FWorldDelegates::OnNetDriverCreated.Broadcast(Context.World(), ReturnVal);
		}
	}


	if (ReturnVal == nullptr)
	{
		UE_LOG(LogNet, Log, TEXT("CreateNamedNetDriver failed to create driver from definition %s"), *NetDriverDefinition.ToString());
	}

	return ReturnVal;
}

当服务器启动并加载地图之后,就会去调用UWorld::Listen来开启网络的监听,也就是在这里会创建GameNetDriver:

bool UWorld::Listen( FURL& InURL )
{
#if WITH_SERVER_CODE
	LLM_SCOPE(ELLMTag::Networking);

	if( NetDriver )
	{
		GEngine->BroadcastNetworkFailure(this, NetDriver, ENetworkFailure::NetDriverAlreadyExists);
		return false;
	}

	// Create net driver.
	if (GEngine->CreateNamedNetDriver(this, NAME_GameNetDriver, NAME_GameNetDriver))
	{
		NetDriver = GEngine->FindNamedNetDriver(this, NAME_GameNetDriver);
		NetDriver->SetWorld(this);
		FLevelCollection* const SourceCollection = FindCollectionByType(ELevelCollectionType::DynamicSourceLevels);
		if (SourceCollection)
		{
			SourceCollection->SetNetDriver(NetDriver);
		}
		FLevelCollection* const StaticCollection = FindCollectionByType(ELevelCollectionType::StaticLevels);
		if (StaticCollection)
		{
			StaticCollection->SetNetDriver(NetDriver);
		}
	}

	if (NetDriver == nullptr)
	{
		GEngine->BroadcastNetworkFailure(this, NULL, ENetworkFailure::NetDriverCreateFailure);
		return false;
	}

	AWorldSettings* WorldSettings = GetWorldSettings();
	const bool bReuseAddressAndPort = WorldSettings ? WorldSettings->bReuseAddressAndPort : false;

	FString Error;
	if( !NetDriver->InitListen( this, InURL, bReuseAddressAndPort, Error ) )
	{
		// 忽略一些错误处理代码
		return false;
	}
	static const bool bLanPlay = FParse::Param(FCommandLine::Get(),TEXT("lanplay"));
	const bool bLanSpeed = bLanPlay || InURL.HasOption(TEXT("LAN"));
	if ( !bLanSpeed && (NetDriver->MaxInternetClientRate < NetDriver->MaxClientRate) && (NetDriver->MaxInternetClientRate > 2500) )
	{
		NetDriver->MaxClientRate = NetDriver->MaxInternetClientRate;
	}

	NextSwitchCountdown = NetDriver->ServerTravelPause;
	return true;
#else
	return false;
#endif // WITH_SERVER_CODE
}

当一个NetDriver被成功的创建之后,其InitListen接口负责开启指定端口的监听。由于默认配置文件里GameNetDriver使用的是UIpNetDriver,所以这个开启监听的逻辑在UIpNetDriver::InitListen里:

bool UIpNetDriver::InitListen( FNetworkNotify* InNotify, FURL& LocalURL, bool bReuseAddressAndPort, FString& Error )
{
	if( !InitBase( false, InNotify, LocalURL, bReuseAddressAndPort, Error ) )
	{
		UE_LOG(LogNet, Warning, TEXT("Failed to init net driver ListenURL: %s: %s"), *LocalURL.ToString(), *Error);
		return false;
	}

	InitConnectionlessHandler();

	// Update result URL.
	//LocalURL.Host = LocalAddr->ToString(false);
	LocalURL.Port = LocalAddr->GetPort();
	UE_LOG(LogNet, Log, TEXT("%s IpNetDriver listening on port %i"), *GetDescription(), LocalURL.Port );

	return true;
}

这个InitListen函数的重点是InitBase,这个函数里面负责真正的创建Socket并在指定的端口进行监听:

bool UIpNetDriver::InitBase( bool bInitAsClient, FNetworkNotify* InNotify, const FURL& URL, bool bReuseAddressAndPort, FString& Error )
{
	using namespace UE::Net::Private;

	if (!Super::InitBase(bInitAsClient, InNotify, URL, bReuseAddressAndPort, Error))
	{
		return false;
	}

	ISocketSubsystem* SocketSubsystem = GetSocketSubsystem();
	if (SocketSubsystem == nullptr)
	{
		UE_LOG(LogNet, Warning, TEXT("Unable to find socket subsystem"));
		return false;
	}

	const int32 BindPort = bInitAsClient ? GetClientPort() : URL.Port;
	// Increase socket queue size, because we are polling rather than threading
	// and thus we rely on the OS socket to buffer a lot of data.
	const int32 DesiredRecvSize = bInitAsClient ? ClientDesiredSocketReceiveBufferBytes : ServerDesiredSocketReceiveBufferBytes;
	const int32 DesiredSendSize = bInitAsClient ? ClientDesiredSocketSendBufferBytes : ServerDesiredSocketSendBufferBytes;
	const EInitBindSocketsFlags InitBindFlags = bInitAsClient ? EInitBindSocketsFlags::Client : EInitBindSocketsFlags::Server;
	FCreateAndBindSocketFunc CreateAndBindSocketsFunc = [this, BindPort, bReuseAddressAndPort, DesiredRecvSize, DesiredSendSize]
									(TSharedRef<FInternetAddr> BindAddr, FString& Error) -> FUniqueSocket
		{
			return this->CreateAndBindSocket(BindAddr, BindPort, bReuseAddressAndPort, DesiredRecvSize, DesiredSendSize, Error);
		};

	bool bInitBindSocketsSuccess = Resolver->InitBindSockets(MoveTemp(CreateAndBindSocketsFunc), InitBindFlags, SocketSubsystem, Error);

	if (!bInitBindSocketsSuccess)
	{
		UE_LOG(LogNet, Error, TEXT("InitBindSockets failed: %s"), ToCStr(Error));

		return false;
	}

	
	// If the cvar is set and the socket subsystem supports it, create the receive thread.
	if (CVarNetIpNetDriverUseReceiveThread.GetValueOnAnyThread() != 0 && SocketSubsystem->IsSocketWaitSupported())
	{
		SocketReceiveThreadRunnable = MakeUnique<FReceiveThreadRunnable>(this);
		SocketReceiveThread.Reset(FRunnableThread::Create(SocketReceiveThreadRunnable.Get(), *FString::Printf(TEXT("IpNetDriver Receive Thread: %s"), *NetDriverName.ToString())));
	}

	SetSocketAndLocalAddress(Resolver->GetFirstSocket());

	bool bRecvMultiEnabled = CVarNetUseRecvMulti.GetValueOnAnyThread() != 0;
	bool bRecvThreadEnabled = CVarNetIpNetDriverUseReceiveThread.GetValueOnAnyThread() != 0;

	if (bRecvMultiEnabled && !bRecvThreadEnabled)
	{
		// 忽略多线程收发的处理
	}
	else if (bRecvMultiEnabled && bRecvThreadEnabled)
	{
		UE_LOG(LogNet, Warning, TEXT("NetDriver RecvMulti is not yet supported with the Receive Thread enabled."));
	}

	// Success.
	return true;
}

这里的CreateAndBindSocket是最终负责绑定的地方,由于指定的端口可能已经被使用了,所以这里内部会使用BindNextPort来不断递增从而获取下一个可以使用的端口号:


int32 ISocketSubsystem::BindNextPort(FSocket* Socket, FInternetAddr& Addr, int32 PortCount, int32 PortIncrement)
{
	// go until we reach the limit (or we succeed)
	for (int32 Index = 0; Index < PortCount; Index++)
	{
		// try to bind to the current port
		if (Socket->Bind(Addr) == true)
		{
			// if it succeeded, return the port
			if (Addr.GetPort() != 0)
			{
				return Addr.GetPort();
			}
			else
			{
				return Socket->GetPortNo();
			}
		}
		// if the address had no port, we are done
		if( Addr.GetPort() == 0 )
		{
			break;
		}

		// increment to the next port, and loop!
		Addr.SetPort(Addr.GetPort() + PortIncrement);
	}

	return 0;
}

FUniqueSocket UIpNetDriver::CreateAndBindSocket(TSharedRef<FInternetAddr> BindAddr, int32 Port, bool bReuseAddressAndPort, int32 DesiredRecvSize, int32 DesiredSendSize, FString& Error)
{
	ISocketSubsystem* SocketSubsystem = GetSocketSubsystem();
	if (SocketSubsystem == nullptr)
	{
		Error = TEXT("Unable to find socket subsystem");
		return nullptr;
	}

	// Create the socket that we will use to communicate with
	FUniqueSocket NewSocket = CreateSocketForProtocol(BindAddr->GetProtocolType());

	// 忽略一些参数设置代码

	int32 ActualRecvSize(0);
	int32 ActualSendSize(0);
	NewSocket->SetReceiveBufferSize(DesiredRecvSize, ActualRecvSize);
	NewSocket->SetSendBufferSize(DesiredSendSize, ActualSendSize);
	UE_LOG(LogInit, Log, TEXT("%s: Socket queue. Rx: %i (config %i) Tx: %i (config %i)"), SocketSubsystem->GetSocketAPIName(),
		ActualRecvSize, DesiredRecvSize, ActualSendSize, DesiredSendSize);

	// Bind socket to our port.
	BindAddr->SetPort(Port);

	int32 AttemptPort = BindAddr->GetPort();
	int32 BoundPort = SocketSubsystem->BindNextPort(NewSocket.Get(), *BindAddr, MaxPortCountToTry + 1, 1);
	if (BoundPort == 0)
	{
		Error = FString::Printf(TEXT("%s: binding to port %i failed (%i)"), SocketSubsystem->GetSocketAPIName(), AttemptPort,
			(int32)SocketSubsystem->GetLastErrorCode());

		if (bExitOnBindFailure)
		{
			UE_LOG(LogNet, Fatal, TEXT("Fatal error: %s"), *Error);
		}

		return nullptr;
	}

	if (NewSocket->SetNonBlocking() == false)
	{
		Error = FString::Printf(TEXT("%s: SetNonBlocking failed (%i)"), SocketSubsystem->GetSocketAPIName(),
			(int32)SocketSubsystem->GetLastErrorCode());
		return nullptr;
	}

	return NewSocket;
}

虽然UE的底层Socket系统同时支持了UDPTCP的通信,但是这里的UIpNetDriver默认使用的是基于UDPSocket通信:

FUniqueSocket UIpNetDriver::CreateSocketForProtocol(const FName& ProtocolType)
{
	// Create UDP socket and enable broadcasting.
	ISocketSubsystem* SocketSubsystem = GetSocketSubsystem();

	if (SocketSubsystem == NULL)
	{
		UE_LOG(LogNet, Warning, TEXT("UIpNetDriver::CreateSocket: Unable to find socket subsystem"));
		return NULL;
	}

	return SocketSubsystem->CreateUniqueSocket(NAME_DGram, TEXT("Unreal"), ProtocolType);
}

由于UDP是无连接的,所以不像TCP一样有accept连接建立回调,只能通过接收到的数据来区分发送端属于哪个逻辑连接。因此在UIpNetDriver::InitListen里会执行InitConnectionlessHandle来注册消息处理函数PacketHandler来应对这个无状态网络连接的入站消息:

void UNetDriver::InitConnectionlessHandler()
{
	check(!ConnectionlessHandler.IsValid());

#if !UE_BUILD_SHIPPING
	if (!FParse::Param(FCommandLine::Get(), TEXT("NoPacketHandler")))
#endif
	{
		ConnectionlessHandler = MakeUnique<PacketHandler>(&DDoS);

		if (ConnectionlessHandler.IsValid())
		{
			ConnectionlessHandler->NotifyAnalyticsProvider(AnalyticsProvider, AnalyticsAggregator);
			ConnectionlessHandler->Initialize(UE::Handler::Mode::Server, MAX_PACKET_SIZE, true, nullptr, nullptr, NetDriverDefinition);

			// Add handling for the stateless connect handshake, for connectionless packets, as the outermost layer
			TSharedPtr<HandlerComponent> NewComponent =
				ConnectionlessHandler->AddHandler(TEXT("Engine.EngineHandlerComponentFactory(StatelessConnectHandlerComponent)"), true);

			StatelessConnectComponent = StaticCastSharedPtr<StatelessConnectHandlerComponent>(NewComponent);

			if (StatelessConnectComponent.IsValid())
			{
				StatelessConnectComponent.Pin()->SetDriver(this);
			}

			ConnectionlessHandler->InitializeComponents();
		}
	}
}

在这个函数里会注册一个StatelessConnectHandlerComponent来处理逻辑层网络连接的握手。

连接建立

当客户端准备连接到指定服务器的时候,需要以某种途径获取服务器的URL,然后通过UEngine::Browse来触发客户端的NetDriver初始化:

EBrowseReturnVal::Type UEngine::Browse( FWorldContext& WorldContext, FURL URL, FString& Error )
{
	Error = TEXT("");
	WorldContext.TravelURL = TEXT("");

	UE_LOGSTATUS(Log, TEXT("Started Browse: \"%s\""), *URL.ToString());
	// 省略很多代码
	if( URL.IsLocalInternal() )
	{
		// Local map file.
		return LoadMap( WorldContext, URL, NULL, Error ) ? EBrowseReturnVal::Success : EBrowseReturnVal::Failure;
	}
	else if( URL.IsInternal() && GIsClient )
	{
		// Network URL.
		if( WorldContext.PendingNetGame )
		{
			CancelPending(WorldContext);
		}

		// Clean up the netdriver/socket so that the pending level succeeds
		if (WorldContext.World() && ShouldShutdownWorldNetDriver())
		{
			ShutdownWorldNetDriver(WorldContext.World());
		}

		WorldContext.PendingNetGame = NewObject<UPendingNetGame>();
		WorldContext.PendingNetGame->Initialize(URL); //-V595
		WorldContext.PendingNetGame->InitNetDriver(); //-V595
		// 省略很多代码
		return EBrowseReturnVal::Pending;
	}
}

这里会创建一个UPendingNetGame对象来处理客户端到服务端连接正式建立之前的一些握手逻辑,首先就是创建一个GameNetDriver,也就是之前介绍过的UIpNetDriver:

void UPendingNetGame::InitNetDriver()
{
	LLM_SCOPE(ELLMTag::Networking);

	if (!GDisallowNetworkTravel)
	{
		NETWORK_PROFILER(GNetworkProfiler.TrackSessionChange(true, URL));

		// Try to create network driver.
		if (GEngine->CreateNamedNetDriver(this, NAME_PendingNetDriver, NAME_GameNetDriver))
		{
			NetDriver = GEngine->FindNamedNetDriver(this, NAME_PendingNetDriver);
		}

		if (NetDriver == nullptr)
		{
			UE_LOG(LogNet, Warning, TEXT("Error initializing the pending net driver.  Check the configuration of NetDriverDefinitions and make sure module/plugin dependencies are correct."));
			ConnectionError = NSLOCTEXT("Engine", "NetworkDriverInit", "Error creating network driver.").ToString();
			return;
		}

		if( NetDriver->InitConnect( this, URL, ConnectionError ) )
		{
			FNetDelegates::OnPendingNetGameConnectionCreated.Broadcast(this);

			ULocalPlayer* LocalPlayer = GEngine->GetFirstGamePlayer(this);
			if (LocalPlayer)
			{
				LocalPlayer->PreBeginHandshake(ULocalPlayer::FOnPreBeginHandshakeCompleteDelegate::CreateWeakLambda(this,
					[this]()
					{
						BeginHandshake();
					}));
			}
			else
			{
				BeginHandshake();
			}
		}
		else
		{
			// 忽略一些错误处理代码
		}
	}
}

NetDriver被创建之后,接下来使用InitConnect来向服务端URL发起一个连接建立请求,这里的InitBase我们在前面已经介绍过了,会注册一个UDP端口的监听,同时注册StatelessConnectHandlerComponent为消息处理函数:

bool UIpNetDriver::InitConnect( FNetworkNotify* InNotify, const FURL& ConnectURL, FString& Error )
{
	using namespace UE::Net::Private;

	ISocketSubsystem* SocketSubsystem = GetSocketSubsystem();
	if (SocketSubsystem == nullptr)
	{
		UE_LOG(LogNet, Warning, TEXT("Unable to find socket subsystem"));
		return false;
	}

	if( !InitBase( true, InNotify, ConnectURL, false, Error ) )
	{
		UE_LOG(LogNet, Warning, TEXT("Failed to init net driver ConnectURL: %s: %s"), *ConnectURL.ToString(), *Error);
		return false;
	}

	// Create new connection.
	ServerConnection = NewObject<UNetConnection>(GetTransientPackage(), NetConnectionClass);

	ServerConnection->InitLocalConnection(this, SocketPrivate.Get(), ConnectURL, USOCK_Pending);

	Resolver->InitConnect(ServerConnection, SocketSubsystem, GetSocket(), ConnectURL);

	UIpConnection* IpServerConnection = Cast<UIpConnection>(ServerConnection);

	if (FNetConnectionAddressResolution* ConnResolver = FNetDriverAddressResolution::GetConnectionResolver(IpServerConnection))
	{
		if (ConnResolver->IsAddressResolutionEnabled() && !ConnResolver->IsAddressResolutionComplete())
		{
			SocketState = ESocketState::Resolving;
		}
	}
	
	UE_LOG(LogNet, Log, TEXT("Game client on port %i, rate %i"), ConnectURL.Port, ServerConnection->CurrentNetSpeed );
	CreateInitialClientChannels();

	return true;
}

在这里终于看到逻辑层连接对象ServerConnection,这个对象会在InitConnect的时候被创建,创建之后的Resolver->InitConnect负责发起执行服务端URLDNS查询,同时这里的SocketState的状态会被标记为ESocketState::Resolving。最后面的CreateInitialClientChannels会创建一些初始的Channel,每个Channel相当于一个信道,往同一个信道里投递的可靠消息能保证被对端有序接收:

void UNetDriver::CreateInitialClientChannels()
{
	if (ServerConnection != nullptr)
	{
		for (const FChannelDefinition& ChannelDef : ChannelDefinitions)
		{
			if (ChannelDef.bInitialClient && (ChannelDef.ChannelClass != nullptr))
			{
				ServerConnection->CreateChannelByName(ChannelDef.ChannelName, EChannelCreateFlags::OpenedLocally, ChannelDef.StaticChannelIndex);
			}
		}
	}
}

在默认配置里,会配置下面四个Channel:

[/Script/Engine.NetDriver]
+ChannelDefinitions=(ChannelName=Control, ClassName=/Script/Engine.ControlChannel, StaticChannelIndex=0, bTickOnCreate=true, bServerOpen=false, bClientOpen=true, bInitialServer=false, bInitialClient=true)
+ChannelDefinitions=(ChannelName=Voice, ClassName=/Script/Engine.VoiceChannel, StaticChannelIndex=1, bTickOnCreate=true, bServerOpen=true, bClientOpen=true, bInitialServer=true, bInitialClient=true)
+ChannelDefinitions=(ChannelName=DataStream, ClassName=/Script/Engine.DataStreamChannel, StaticChannelIndex=2, bTickOnCreate=true, bServerOpen=true, bClientOpen=true, bInitialServer=true, bInitialClient=true)
+ChannelDefinitions=(ChannelName=Actor, ClassName=/Script/Engine.ActorChannel, StaticChannelIndex=-1, bTickOnCreate=false, bServerOpen=true, bClientOpen=false, bInitialServer=false, bInitialClient=false)

UIpNetDriver::InitConnect结束之后,控制流回到了UPendingNetGame::InitNetDriver,这里开始正式的向服务器发起handshake:

void UPendingNetGame::BeginHandshake()
{
	// Kick off the connection handshake
	UNetConnection* ServerConn = NetDriver->ServerConnection;
	if (ServerConn->Handler.IsValid())
	{
		ServerConn->Handler->BeginHandshaking(
			FPacketHandlerHandshakeComplete::CreateUObject(this, &UPendingNetGame::SendInitialJoin));
	}
	else
	{
		SendInitialJoin();
	}
}

这里的serverConn->Handler对象就是之前的PacketHandler,在PacketHandler::BeginHandShaking里会遍历所有的注册过来的HandlerComponent来执行握手操作:

void PacketHandler::BeginHandshaking(FPacketHandlerHandshakeComplete InHandshakeDel/*=FPacketHandlerHandshakeComplete()*/)
{
	check(!bBeganHandshaking);

	bBeganHandshaking = true;

	HandshakeCompleteDel = InHandshakeDel;

	for (int32 i=HandlerComponents.Num() - 1; i>=0; --i)
	{
		HandlerComponent& CurComponent = *HandlerComponents[i];

		if (CurComponent.RequiresHandshake() && !CurComponent.IsInitialized())
		{
			CurComponent.NotifyHandshakeBegin();
			break;
		}
	}
}

这里我们只需要关心之前创建的StatelessConnectHandlerComponent相关握手逻辑:

void StatelessConnectHandlerComponent::NotifyHandshakeBegin()
{
	using namespace UE::Net;

	SendInitialPacket(static_cast<EHandshakeVersion>(CurrentHandshakeVersion));
}
void StatelessConnectHandlerComponent::SendInitialPacket(EHandshakeVersion HandshakeVersion)
{
	using namespace UE::Net;

	if (Handler->Mode == UE::Handler::Mode::Client)
	{
		UNetConnection* ServerConn = (Driver != nullptr ? ToRawPtr(Driver->ServerConnection) : nullptr);

		if (ServerConn != nullptr)
		{
			const int32 AdjustedSize = GetAdjustedSizeBits(HANDSHAKE_PACKET_SIZE_BITS, HandshakeVersion);
			FBitWriter InitialPacket(AdjustedSize + (BaseRandomDataLengthBytes * 8) + 1 /* Termination bit */);

			BeginHandshakePacket(InitialPacket, EHandshakePacketType::InitialPacket, HandshakeVersion, SentHandshakePacketCount, CachedClientID,
									(bRestartedHandshake ? EHandshakePacketModifier::RestartHandshake : EHandshakePacketModifier::None));

			uint8 SecretIdPad = 0;
			uint8 PacketSizeFiller[28];

			InitialPacket.WriteBit(SecretIdPad);

			FMemory::Memzero(PacketSizeFiller, UE_ARRAY_COUNT(PacketSizeFiller));
			InitialPacket.Serialize(PacketSizeFiller, UE_ARRAY_COUNT(PacketSizeFiller));

			SendToServer(HandshakeVersion, EHandshakePacketType::InitialPacket, InitialPacket);
		}
		else
		{
			UE_LOG(LogHandshake, Error, TEXT("Tried to send handshake connect packet without a server connection."));
		}
	}
}

这里的InitialPacket的具体格式在源代码文件里的注释里写的非常清楚:

 * Handshake Process/Protocol:
 * --------------------------
 *
 * The protocol for the handshake involves the client sending an initial packet to the server,
 * and the server responding with a unique 'Cookie' value, which the client has to respond with.
 *
 * Client - Initial Connect:
 *
 * [?:MagicHeader][2:SessionID][3:ClientID][HandshakeBit][RestartHandshakeBit]
 * [8:MinVersion][8:CurVersion][8:HandshakePacketType][8:SentPacketCount][32:NetworkVersion]
 * [16:NetworkFeatures][SecretIdBit][28:PacketSizeFiller][AlignPad][?:RandomData]

填充好InitialPacket之后,调用SendToServer往服务器端发送这个握手消息,这里会有一个比较特殊的标记SetRawSend,作用是发送消息的时候直接走底层的发送接口,不要被上层的Handler托管:

void StatelessConnectHandlerComponent::SendToServer(EHandshakeVersion HandshakeVersion, EHandshakePacketType PacketType, FBitWriter& Packet)
{
	if (UNetConnection* ServerConn = (Driver != nullptr ? Driver->ServerConnection : nullptr))
	{
		CapHandshakePacket(Packet, HandshakeVersion);


		// Disable PacketHandler parsing, and send the raw packet
		Handler->SetRawSend(true);

		{
			if (Driver->IsNetResourceValid())
			{
				FOutPacketTraits Traits;

				Driver->ServerConnection->LowLevelSend(Packet.GetData(), Packet.GetNumBits(), Traits);
			}
		}

		Handler->SetRawSend(false);

		LastClientSendTimestamp = FPlatformTime::Seconds();
	}
}

/**
 * Sets whether or not outgoing packets should bypass this handler - used when raw packet sends are necessary
 * (such as for the stateless handshake)
 *
 * @param bInEnabled	Whether or not raw sends are enabled
 */
FORCEINLINE void SetRawSend(bool bInEnabled)
{
	bRawSend = bInEnabled;
}

由于当前的UIpNetDriver使用的是UDP通信,所以有可能出现消息丢失的问题。为了处理可能的握手消息丢失的情况,StatelessConnectHandlerComponentTick函数里会检查之前发送的握手包是不是超时了,如果超时了则再次执行SendInitialPacket:

void StatelessConnectHandlerComponent::Tick(float DeltaTime)
{
	using namespace UE::Net;

	if (Handler->Mode == UE::Handler::Mode::Client)
	{
		if (State != UE::Handler::Component::State::Initialized && LastClientSendTimestamp != 0.0)
		{
			double LastSendTimeDiff = FPlatformTime::Seconds() - LastClientSendTimestamp;

			if (LastSendTimeDiff > UE::Net::HandshakeResendInterval)
			{
				const bool bRestartChallenge = Driver != nullptr && ((Driver->GetElapsedTime() - LastChallengeTimestamp) > MIN_COOKIE_LIFETIME);

				if (bRestartChallenge)
				{
					SetState(UE::Handler::Component::State::UnInitialized);
				}

				if (State == UE::Handler::Component::State::UnInitialized)
				{
					UE_LOG(LogHandshake, Verbose, TEXT("Initial handshake packet timeout - resending."));

					EHandshakeVersion ResendVersion = static_cast<EHandshakeVersion>(CurrentHandshakeVersion);

					// 忽略一些无关代码

					SendInitialPacket(ResendVersion);
				}
				else if (State == UE::Handler::Component::State::InitializedOnLocal && LastTimestamp != 0.0)
				{
					UE_LOG(LogHandshake, Verbose, TEXT("Challenge response packet timeout - resending."));

					SendChallengeResponse(LastRemoteHandshakeVersion, LastSecretId, LastTimestamp, LastCookie);
				}
			}
		}
	}
	else
	{
		// 省略服务器相关逻辑
	}
}

UE接收网络数据的位置在UIpNetDriver::TickDispatch里,这个函数每帧都会被调用,这个函数里处理的逻辑太多了,这里就先考虑服务端处理新的客户端连接相关内容:

void UIpNetDriver::TickDispatch(float DeltaTime)
{
	LLM_SCOPE_BYTAG(NetDriver);

	Super::TickDispatch( DeltaTime );

	// Process all incoming packets
	for (FPacketIterator It(this); It; ++It)
	{
		FReceivedPacketView ReceivedPacket;
		FInPacketTraits& ReceivedTraits = ReceivedPacket.Traits;
		bool bOk = It.GetCurrentPacket(ReceivedPacket);
		const TSharedRef<const FInternetAddr> FromAddr = ReceivedPacket.Address.ToSharedRef();
		UNetConnection* Connection = nullptr;
		UIpConnection* const MyServerConnection = GetServerConnection();
		bool bIgnorePacket = false;

		// If we didn't find a client connection, maybe create a new one.
		if (Connection == nullptr)
		{
			
			// Determine if allowing for client/server connections
			const bool bAcceptingConnection = Notify != nullptr && Notify->NotifyAcceptingConnection() == EAcceptConnection::Accept;

			if (bAcceptingConnection)
			{
				if (!DDoS.CheckLogRestrictions() && !bExceededIPAggregationLimit)
				{
					TrackAndLogNewIP(FromAddr.Get());
				}

				FPacketBufferView WorkingBuffer = It.GetWorkingBuffer();

				Connection = ProcessConnectionlessPacket(ReceivedPacket, WorkingBuffer);
				bIgnorePacket = ReceivedPacket.DataView.NumBytes() == 0;
			}
		}
	}
}

这个函数会使用FPacketIterator来遍历当前Socket上接收到的网络数据,业务层数据包的最小粒度就是Packet。如果这个Packet的客户端地址并没有绑定好的UNetConnection,则可以认为这个Packet一定是来自于新客户端的相关数据包,这里会使用ProcessConnectionlessPacket来处理这些包:

UNetConnection* UIpNetDriver::ProcessConnectionlessPacket(FReceivedPacketView& PacketRef, const FPacketBufferView& WorkingBuffer)
{
	UNetConnection* ReturnVal = nullptr;
	TSharedPtr<StatelessConnectHandlerComponent> StatelessConnect;
	const TSharedPtr<const FInternetAddr>& Address = PacketRef.Address;
	FString IncomingAddress = Address->ToString(true);
	bool bPassedChallenge = false;
	bool bRestartedHandshake = false;
	bool bIgnorePacket = true;

	if (Notify != nullptr && ConnectionlessHandler.IsValid() && StatelessConnectComponent.IsValid())
	{
		StatelessConnect = StatelessConnectComponent.Pin();

		EIncomingResult Result = ConnectionlessHandler->IncomingConnectionless(PacketRef);
		// 省略后续所有代码
	}
	// 省略后续所有代码
}

每个包都会通过ConnectionlessHandler->IncomingConnectionless的处理,这个函数会最终调用到StatelessConnectHandlerComponent::IncomingConnectionless,这个函数里会重点处理handshake包逻辑:

void StatelessConnectHandlerComponent::IncomingConnectionless(FIncomingPacketRef PacketRef)
{
	using namespace UE::Net;

	FBitReader& Packet = PacketRef.Packet;
	const TSharedPtr<const FInternetAddr> Address = PacketRef.Address;

	if (MagicHeader.Num() > 0)
	{
		uint32 ReadMagic = 0;

		Packet.SerializeBits(&ReadMagic, MagicHeader.Num());

		if (GVerifyMagicHeader && ReadMagic != MagicHeaderUint)
		{
#if !UE_BUILD_SHIPPING
			UE_CLOG(TrackValidationLogs(), LogNet, Log, TEXT("Rejecting packet with invalid magic header '%08X' vs '%08X' (%i bits)"),
					ReadMagic, MagicHeaderUint, MagicHeader.Num());
#endif

			Packet.SetError();

			return;
		}
	}


	bool bHasValidSessionID = true;
	uint8 SessionID = 0;
	uint8 ClientID = 0;

	if (CurrentHandshakeVersion >= static_cast<uint8>(EHandshakeVersion::SessionClientId))
	{
		Packet.SerializeBits(&SessionID, SessionIDSizeBits);
		Packet.SerializeBits(&ClientID, ClientIDSizeBits);

		bHasValidSessionID = GVerifyNetSessionID == 0 || (SessionID == CachedGlobalNetTravelCount && !Packet.IsError());

		// No ClientID validation until connected
	}

	const bool bHandshakePacket = !!Packet.ReadBit() && !Packet.IsError();

	LastChallengeSuccessAddress = nullptr;

	// 忽略一些容错代码

	FParsedHandshakeData HandshakeData;
	const bool bValidHandshakePacket = ParseHandshakePacket(Packet, HandshakeData);

	//忽略bValidHandshakePacket为false的处理的代码
	
	const bool bIsServer = Handler->Mode == UE::Handler::Mode::Server;
	if (UNLIKELY(!bIsServer))
	{
		// Only server can negotiate handshake requests here
		return;
	}

	EHandshakeVersion TargetVersion = EHandshakeVersion::Latest;
	const bool bValidVersion = CheckVersion(HandshakeData, TargetVersion);
	const bool bInitialConnect = HandshakeData.HandshakePacketType == EHandshakePacketType::InitialPacket && HandshakeData.Timestamp == 0.0;
	const double ElapsedTime = Driver ? Driver->GetElapsedTime() : 0.0;

	const bool bIsValidRequest = bValidVersion && (bHasValidSessionID || bInitialConnect);

	// Handle invalid requests
	if (!bIsValidRequest)
	{
		// 忽略错误的包数据的处理
		return;
	}

	if (bInitialConnect)
	{
		SendConnectChallenge(FCommonSendToClientParams(Address, TargetVersion, ClientID), HandshakeData.RemoteSentHandshakePacketCount);
	}
	// 省略后续所有代码
}

上面的函数负责解析和验证一个有效的HandshakePacket,如果这个HandshakePacket是有效的,则会使用SendConnectChallenge来通知客户端:

void StatelessConnectHandlerComponent::SendConnectChallenge(FCommonSendToClientParams CommonParams, uint8 ClientSentHandshakePacketCount)
{
	using namespace UE::Net;

	if (Driver != nullptr)
	{
		const int32 AdjustedSize = GetAdjustedSizeBits(HANDSHAKE_PACKET_SIZE_BITS, CommonParams.HandshakeVersion);
		FBitWriter ChallengePacket(AdjustedSize + (BaseRandomDataLengthBytes * 8) + 1 /* Termination bit */);

		BeginHandshakePacket(ChallengePacket, EHandshakePacketType::Challenge, CommonParams.HandshakeVersion, ClientSentHandshakePacketCount,
								CommonParams.ClientID);

		double Timestamp = Driver->GetElapsedTime();
		uint8 Cookie[COOKIE_BYTE_SIZE];

		GenerateCookie(CommonParams.ClientAddress, ActiveSecret, Timestamp, Cookie);

		ChallengePacket.WriteBit(ActiveSecret);

		ChallengePacket << Timestamp;

		ChallengePacket.Serialize(Cookie, UE_ARRAY_COUNT(Cookie));

		SendToClient(CommonParams, EHandshakePacketType::Challenge, ChallengePacket);
	}
}

这个SendConnectChallenge的作用是生成一个Challenge包,包里会塞入当前时间戳和一个随机生成的与这个ClientAddress所绑定的Cookie,作为临时的Session标识符来使用。这个Packet填充完成之后,使用SendToClient发送回对应的客户端地址,这个SendToClient的具体实现与之前介绍的SendToServer的实现基本一样,都是使用底层的LowLevelSend来跳过PacketHandler直接调用系统接口来执行数据发送。

接下来要考虑的是客户端如何接收这个Challenge包,虽然数据接收的地方依然是在UIpNetDriver::TickDispatch,但是由于此时客户端早就建立好了与服务端之间的网络连接对象,因此走的代码分支是不一样的:

void UIpNetDriver::TickDispatch(float DeltaTime)
{
	LLM_SCOPE_BYTAG(NetDriver);

	Super::TickDispatch( DeltaTime );
	// Process all incoming packets
	for (FPacketIterator It(this); It; ++It)
	{
		FReceivedPacketView ReceivedPacket;
		FInPacketTraits& ReceivedTraits = ReceivedPacket.Traits;
		bool bOk = It.GetCurrentPacket(ReceivedPacket);
		const TSharedRef<const FInternetAddr> FromAddr = ReceivedPacket.Address.ToSharedRef();
		UNetConnection* Connection = nullptr;
		UIpConnection* const MyServerConnection = GetServerConnection();
		// Figure out which socket the received data came from.
		if (MyServerConnection)
		{
			if (MyServerConnection->RemoteAddr->CompareEndpoints(*FromAddr))
			{
				Connection = MyServerConnection;
			}
		}
		// Send the packet to the connection for processing.
		if (Connection != nullptr && !bIgnorePacket)
		{
			if (DDoS.IsDDoSDetectionEnabled())
			{
				DDoS.IncNetConnPacketCounter();
				DDoS.CondCheckNetConnLimits();
			}

			if (bRetrieveTimestamps)
			{
				It.GetCurrentPacketTimestamp(Connection);
			}

			Connection->ReceivedRawPacket((uint8*)ReceivedPacket.DataView.GetData(), ReceivedPacket.DataView.NumBytes());
		}
	}
}

客户端处理消息的时候会将Packet的来源地址与当前记录的服务端连接MyServerConnection的地址相比较,如果相等则会直接使用这个ConnectionReceivedRawPacket来处理当前的ReceivedPacket:

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

	uint8* Data = (uint8*)InData;

	++InTotalHandlerPackets;

	if (Handler.IsValid())
	{
		FReceivedPacketView PacketView;

		PacketView.DataView = {Data, Count, ECountUnits::Bytes};

		EIncomingResult IncomingResult = Handler->Incoming(PacketView);
		// 省略后续代码
	}
	// 省略后续代码
}

这个ReceivedRawPacket又会调用到Handler->Incoming,这个函数是所有包处理的通用接口,这里重点关注Challenge包的逻辑处理,也就是下面的bIsChallengePacket=true的部分:

void StatelessConnectHandlerComponent::Incoming(FBitReader& Packet)
{
	using namespace UE::Net;

	if (MagicHeader.Num() > 0)
	{
		// Don't bother with the expense of verifying the magic header here.
		uint32 ReadMagic = 0;
		Packet.SerializeBits(&ReadMagic, MagicHeader.Num());
	}

	bool bHasValidSessionID = true;
	bool bHasValidClientID = true;
	uint8 SessionID = 0;
	uint8 ClientID = 0;

	if (LastRemoteHandshakeVersion >= EHandshakeVersion::SessionClientId)
	{
		Packet.SerializeBits(&SessionID, SessionIDSizeBits);
		Packet.SerializeBits(&ClientID, ClientIDSizeBits);

		bHasValidSessionID = GVerifyNetSessionID == 0 || (SessionID == CachedGlobalNetTravelCount && !Packet.IsError());
		bHasValidClientID = GVerifyNetClientID == 0 || (ClientID == CachedClientID && !Packet.IsError());
	}

	bool bHandshakePacket = !!Packet.ReadBit() && !Packet.IsError();

	if (bHandshakePacket)
	{
		FParsedHandshakeData HandshakeData;

		bHandshakePacket = ParseHandshakePacket(Packet, HandshakeData);

		if (bHandshakePacket)
		{
			const bool bIsChallengePacket = HandshakeData.HandshakePacketType == EHandshakePacketType::Challenge && HandshakeData.Timestamp > 0.0;
			const bool bIsInitialChallengePacket = bIsChallengePacket && State != UE::Handler::Component::State::Initialized;
			const bool bIsUpgradePacket = HandshakeData.HandshakePacketType == EHandshakePacketType::VersionUpgrade;

			if (Handler->Mode == UE::Handler::Mode::Client && bHasValidClientID && (bHasValidSessionID || bIsInitialChallengePacket || bIsUpgradePacket))
			{
				if (State == UE::Handler::Component::State::UnInitialized || State == UE::Handler::Component::State::InitializedOnLocal)
				{
					if (HandshakeData.bRestartHandshake)
					{
#if !UE_BUILD_SHIPPING
						UE_LOG(LogHandshake, Log, TEXT("Ignoring restart handshake request, while already restarted."));
#endif
					}
					// Receiving challenge
					else if (bIsChallengePacket)
					{
#if !UE_BUILD_SHIPPING
						UE_LOG(LogHandshake, Log, TEXT("Cached server SessionID: %u"), SessionID);
#endif

						CachedGlobalNetTravelCount = SessionID;

						LastChallengeTimestamp = (Driver != nullptr ? Driver->GetElapsedTime() : 0.0);

						SendChallengeResponse(HandshakeData.RemoteCurVersion, HandshakeData.SecretId, HandshakeData.Timestamp, HandshakeData.Cookie);

						// Utilize this state as an intermediary, indicating that the challenge response has been sent
						SetState(UE::Handler::Component::State::InitializedOnLocal);
						// 省略后续所有代码
					}
				}
			}
		}
	}
}

bIsChallengePacket的处理结果就是客户端会通过SendChallengeResponse发送一个ChallengeResponse包到服务器,这里会将服务端Challenge包里的SecretID,Cookie,Timestamp等字段再次打包进去:

void StatelessConnectHandlerComponent::SendChallengeResponse(EHandshakeVersion HandshakeVersion, uint8 InSecretId, double InTimestamp,
																uint8 InCookie[COOKIE_BYTE_SIZE])
{
	using namespace UE::Net;

	UNetConnection* ServerConn = (Driver != nullptr ? ToRawPtr(Driver->ServerConnection) : nullptr);

	if (ServerConn != nullptr)
	{
		const int32 AdjustedSize = GetAdjustedSizeBits((bRestartedHandshake ? RESTART_RESPONSE_SIZE_BITS : HANDSHAKE_PACKET_SIZE_BITS),
														HandshakeVersion);
		FBitWriter ResponsePacket(AdjustedSize + (BaseRandomDataLengthBytes * 8) + 1 /* Termination bit */);
		EHandshakePacketType HandshakePacketType = bRestartedHandshake ? EHandshakePacketType::RestartResponse : EHandshakePacketType::Response;

		BeginHandshakePacket(ResponsePacket, HandshakePacketType, HandshakeVersion, SentHandshakePacketCount, CachedClientID,
								(bRestartedHandshake ? EHandshakePacketModifier::RestartHandshake : EHandshakePacketModifier::None));

		ResponsePacket.WriteBit(InSecretId);

		ResponsePacket << InTimestamp;
		ResponsePacket.Serialize(InCookie, COOKIE_BYTE_SIZE);

		if (bRestartedHandshake)
		{
			ResponsePacket.Serialize(AuthorisedCookie, COOKIE_BYTE_SIZE);
		}

#if !UE_BUILD_SHIPPING
		UE_LOG(LogHandshake, Log, TEXT("SendChallengeResponse. Timestamp: %f, Cookie: %s"), InTimestamp,
				*FString::FromBlob(InCookie, COOKIE_BYTE_SIZE));
#endif

		SendToServer(HandshakeVersion, HandshakePacketType, ResponsePacket);


		int16* CurSequence = (int16*)InCookie;

		LastSecretId = InSecretId;
		LastTimestamp = InTimestamp;
		LastServerSequence = *CurSequence & (MAX_PACKETID - 1);
		LastClientSequence = *(CurSequence + 1) & (MAX_PACKETID - 1);
		LastRemoteHandshakeVersion = HandshakeVersion;

		FMemory::Memcpy(LastCookie, InCookie, UE_ARRAY_COUNT(LastCookie));
	}
	else
	{
		UE_LOG(LogHandshake, Error, TEXT("Tried to send handshake response packet without a server connection."));
	}
}

这里在使用SendToServer发送完数据之后,会将Cookie的头两个int16数据当作服务端客户端通信的包序列号,这样强制转换的作用是为了随机初始化第一个包的序列号。

数据走到服务端之后,仍然会调用到StatelessConnectHandlerComponent::IncomingConnectionless来处理,同样的这里我们只关心ChallengeResponse的部分,也就是bInitialConnect=false的分支:

void StatelessConnectHandlerComponent::IncomingConnectionless(FIncomingPacketRef PacketRef)
{
	using namespace UE::Net;

	FBitReader& Packet = PacketRef.Packet;
	const TSharedPtr<const FInternetAddr> Address = PacketRef.Address;
	// 省略很多代码
	if (bInitialConnect)
	{
		SendConnectChallenge(FCommonSendToClientParams(Address, TargetVersion, ClientID), HandshakeData.RemoteSentHandshakePacketCount);
	}
	else
	{
		// Challenge response
		// NOTE: Allow CookieDelta to be 0.0, as it is possible for a server to send a challenge and receive a response,
		//			during the same tick
		bool bChallengeSuccess = false;
		const double CookieDelta = ElapsedTime - HandshakeData.Timestamp;
		const double SecretDelta = HandshakeData.Timestamp - LastSecretUpdateTimestamp;
		const bool bValidCookieLifetime = CookieDelta >= 0.0 && (MAX_COOKIE_LIFETIME - CookieDelta) > 0.0;
		const bool bValidSecretIdTimestamp = (HandshakeData.SecretId == ActiveSecret) ? (SecretDelta >= 0.0) : (SecretDelta <= 0.0);

		if (bValidCookieLifetime && bValidSecretIdTimestamp)
		{
			// Regenerate the cookie from the packet info, and see if the received cookie matches the regenerated one
			uint8 RegenCookie[COOKIE_BYTE_SIZE];

			GenerateCookie(Address, HandshakeData.SecretId, HandshakeData.Timestamp, RegenCookie);

			bChallengeSuccess = FMemory::Memcmp(HandshakeData.Cookie, RegenCookie, COOKIE_BYTE_SIZE) == 0;

			if (bChallengeSuccess)
			{
				if (HandshakeData.bRestartHandshake)
				{
					FMemory::Memcpy(AuthorisedCookie, HandshakeData.OrigCookie, UE_ARRAY_COUNT(AuthorisedCookie));
				}
				else
				{
					int16* CurSequence = (int16*)HandshakeData.Cookie;

					LastServerSequence = *CurSequence & (MAX_PACKETID - 1);
					LastClientSequence = *(CurSequence + 1) & (MAX_PACKETID - 1);

					FMemory::Memcpy(AuthorisedCookie, HandshakeData.Cookie, UE_ARRAY_COUNT(AuthorisedCookie));
				}

				bRestartedHandshake = HandshakeData.bRestartHandshake;
				LastChallengeSuccessAddress = Address->Clone();
				LastRemoteHandshakeVersion = TargetVersion;
				CachedClientID = ClientID;

				if (TargetVersion < MinClientHandshakeVersion && static_cast<uint8>(TargetVersion) >= MinSupportedHandshakeVersion)
				{
					MinClientHandshakeVersion = TargetVersion;
				}

				// Now ack the challenge response - the cookie is stored in AuthorisedCookie, to enable retries
				SendChallengeAck(FCommonSendToClientParams(Address, TargetVersion, ClientID), HandshakeData.RemoteSentHandshakePacketCount, AuthorisedCookie);
			}
		}
	}
}

在这里会利用时间戳和客户端地址重新生成一次Cookie,如果客户端发送过来的包里携带的Cookie与此时生成的RegenCookie一致,则代表ChallengeResponse被成功接收,此时也会从Cookie的前两个int16转换为客户端与服务端两者通信的包序列号,这样就实现了上行包序列号与下行包序列号的第一次同步。最后再发送一个ChallengeAck包通知到客户端连接建立完成。在StatelessConnectHandlerComponent::IncomingConnectionless处理完成了Challenge的验证之后,其外层调用UIpNetDriver::ProcessConnectionlessPacket会再次检查是否Challenge接收成功,如果成功了则会创建一个对应的客户端连接UIpConnection:

UNetConnection* UIpNetDriver::ProcessConnectionlessPacket(FReceivedPacketView& PacketRef, const FPacketBufferView& WorkingBuffer)
{
	UNetConnection* ReturnVal = nullptr;
	TSharedPtr<StatelessConnectHandlerComponent> StatelessConnect;
	const TSharedPtr<const FInternetAddr>& Address = PacketRef.Address;
	FString IncomingAddress = Address->ToString(true);
	bool bPassedChallenge = false;
	bool bRestartedHandshake = false;
	bool bIgnorePacket = true;

	if (Notify != nullptr && ConnectionlessHandler.IsValid() && StatelessConnectComponent.IsValid())
	{
		StatelessConnect = StatelessConnectComponent.Pin();

		EIncomingResult Result = ConnectionlessHandler->IncomingConnectionless(PacketRef);

		if (Result == EIncomingResult::Success)
		{
			bPassedChallenge = StatelessConnect->HasPassedChallenge(Address, bRestartedHandshake);

			// 省略一些代码
		}
	}
	if (bPassedChallenge)
	{
		if (!bRestartedHandshake)
		{
			SCOPE_CYCLE_COUNTER(Stat_IpNetDriverAddNewConnection);

			UE_LOG(LogNet, Log, TEXT("Server accepting post-challenge connection from: %s"), *IncomingAddress);

			ReturnVal = NewObject<UIpConnection>(GetTransientPackage(), NetConnectionClass);
			check(ReturnVal != nullptr);

			ReturnVal->InitRemoteConnection(this, SocketPrivate.Get(), World ? World->URL : FURL(), *Address, USOCK_Open);

			// Set the initial packet sequence from the handshake data
			if (StatelessConnect.IsValid())
			{
				int32 ServerSequence = 0;
				int32 ClientSequence = 0;

				StatelessConnect->GetChallengeSequence(ServerSequence, ClientSequence);

				ReturnVal->InitSequence(ClientSequence, ServerSequence);
			}

			if (ReturnVal->Handler.IsValid())
			{
				ReturnVal->Handler->BeginHandshaking();
			}

			Notify->NotifyAcceptedConnection(ReturnVal);

			AddClientConnection(ReturnVal);
			RemoveFromNewIPTracking(*Address.Get());
		}

		if (StatelessConnect.IsValid())
		{
			StatelessConnect->ResetChallengeData();
		}
	}
	// 省略后续代码
}

这个ClientConnection被创建之后,会利用StatelessConnect里的数据做一些字段初始化,例如ServerSequence,ClientSequence。初始化完成之后还会通过Notify来通知到当前的World新的客户端连接:

void UWorld::NotifyAcceptedConnection( UNetConnection* Connection )
{
	check(NetDriver!=NULL);
	check(NetDriver->ServerConnection==NULL);
	UE_LOG(LogNet, Log, TEXT("NotifyAcceptedConnection: Name: %s, TimeStamp: %s, %s"), *GetName(), FPlatformTime::StrTimestamp(), *Connection->Describe() );
	NETWORK_PROFILER( GNetworkProfiler.TrackEvent( TEXT( "OPEN" ), *( GetName() + TEXT( " " ) + Connection->LowLevelGetRemoteAddress() ), Connection ) );
}

同时NetDriver里也会收到通知,会加入到所有的客户端连接集合MappedClientConnections里,同通知ReplicationDriver在执行后续的Actor同步的时候考虑这个新连接:

void UNetDriver::AddClientConnection(UNetConnection* NewConnection)
{
	LLM_SCOPE_BYTAG(NetDriver);

	SCOPE_CYCLE_COUNTER(Stat_NetDriverAddClientConnection);

	UE_CLOG(!DDoS.CheckLogRestrictions(), LogNet, Log, TEXT("AddClientConnection: Added client connection: %s"), *NewConnection->Describe());

	ClientConnections.Add(NewConnection);

	TSharedPtr<const FInternetAddr> ConnAddr = NewConnection->GetRemoteAddr();

	if (ConnAddr.IsValid())
	{
		MappedClientConnections.Add(ConnAddr.ToSharedRef(), NewConnection);

		// On the off-chance of the same IP:Port being reused, check RecentlyDisconnectedClients
		int32 RecentDisconnectIdx = RecentlyDisconnectedClients.IndexOfByPredicate(
			[&ConnAddr](const FDisconnectedClient& CurElement)
			{
				return *ConnAddr == *CurElement.Address;
			});

		if (RecentDisconnectIdx != INDEX_NONE)
		{
			RecentlyDisconnectedClients.RemoveAt(RecentDisconnectIdx);
		}
	}

	if (ReplicationDriver)
	{
		ReplicationDriver->AddClientConnection(NewConnection);
	}
	// 省略一些代码
}

至此在UE的服务端,这个客户端连接建立的流程就完整走完了。

前面说过在StatelessConnectHandlerComponent验证完Challenge之后会发送一个ACK包到客户端,客户端处理这个包的代码仍然是StatelessConnectHandlerComponent::Incoming,只不过分支不一样:

void StatelessConnectHandlerComponent::Incoming(FBitReader& Packet)
{
		bool bHandshakePacket = !!Packet.ReadBit() && !Packet.IsError();

	if (bHandshakePacket)
	{
		FParsedHandshakeData HandshakeData;

		bHandshakePacket = ParseHandshakePacket(Packet, HandshakeData);

		if (bHandshakePacket)
		{
			const bool bIsChallengePacket = HandshakeData.HandshakePacketType == EHandshakePacketType::Challenge && HandshakeData.Timestamp > 0.0;
			const bool bIsInitialChallengePacket = bIsChallengePacket && State != UE::Handler::Component::State::Initialized;
			const bool bIsUpgradePacket = HandshakeData.HandshakePacketType == EHandshakePacketType::VersionUpgrade;

			if (Handler->Mode == UE::Handler::Mode::Client && bHasValidClientID && (bHasValidSessionID || bIsInitialChallengePacket || bIsUpgradePacket))
			{
				if (State == UE::Handler::Component::State::UnInitialized || State == UE::Handler::Component::State::InitializedOnLocal)
				{
					if (HandshakeData.bRestartHandshake)
					{
#if !UE_BUILD_SHIPPING
						UE_LOG(LogHandshake, Log, TEXT("Ignoring restart handshake request, while already restarted."));
#endif
					}
					// Receiving challenge
					else if (bIsChallengePacket)
					{
						// 忽略已经介绍过的部分代码
					}
					// Receiving challenge ack, verify the timestamp is < 0.0f
					else if (HandshakeData.HandshakePacketType == EHandshakePacketType::Ack && HandshakeData.Timestamp < 0.0)
					{
						if (!bRestartedHandshake)
						{
							UNetConnection* ServerConn = (Driver != nullptr ? ToRawPtr(Driver->ServerConnection) : nullptr);

							// Extract the initial packet sequence from the random Cookie data
							if (ensure(ServerConn != nullptr))
							{
								int16* CurSequence = (int16*)HandshakeData.Cookie;

								int32 ServerSequence = *CurSequence & (MAX_PACKETID - 1);
								int32 ClientSequence = *(CurSequence + 1) & (MAX_PACKETID - 1);

								ServerConn->InitSequence(ServerSequence, ClientSequence);
							}

							// Save the final authorized cookie
							FMemory::Memcpy(AuthorisedCookie, HandshakeData.Cookie, UE_ARRAY_COUNT(AuthorisedCookie));
						}

						// Now finish initializing the handler - flushing the queued packet buffer in the process.
						SetState(UE::Handler::Component::State::Initialized);
						Initialized();

						bRestartedHandshake = false;

						// Reset packet count clientside, due to how it affects protocol version fallback selection
						SentHandshakePacketCount = 0;
					}
				}
			}
		}
	}
}

在这里又会通过Cookie重新解析出下行包的初始序列号ServerSequence和上行包的初始序列号ClientSequence,并用这两个序列号来初始化服务端连接。同时这个完整的Cookie会被拷贝到AuthorisedCookie,以方便后面的断线重连来使用。

玩家登录

后续的Initialized函数调用会最终触发到PacketHandler::HandlerInitialized,在这个函数的末尾会调用创建PacketHandler的时候传入来的HandshakeCompleteDel:

void PacketHandler::HandlerInitialized()
{
	// 省略一些代码
	SetState(UE::Handler::State::Initialized);

	if (bBeganHandshaking)
	{
		HandshakeCompleteDel.ExecuteIfBound();
	}
}

void UPendingNetGame::BeginHandshake()
{
	// Kick off the connection handshake
	UNetConnection* ServerConn = NetDriver->ServerConnection;
	if (ServerConn->Handler.IsValid())
	{
		ServerConn->Handler->BeginHandshaking(
			FPacketHandlerHandshakeComplete::CreateUObject(this, &UPendingNetGame::SendInitialJoin));
	}
	else
	{
		SendInitialJoin();
	}
}

而这个HandshakeCompleteDelUPendingNetGame::BeginHandshake的时候会设置为SendInitialJoin,在这个函数里会发送一个NMT_Hello消息到服务器端,这个消息里会带上可能的加密密钥EncryptionToken:

void UPendingNetGame::SendInitialJoin()
{
	if (NetDriver != nullptr)
	{
		UNetConnection* ServerConn = NetDriver->ServerConnection;

		if (ServerConn != nullptr)
		{
			uint8 IsLittleEndian = uint8(PLATFORM_LITTLE_ENDIAN);
			check(IsLittleEndian == !!IsLittleEndian); // should only be one or zero

			const int32 AllowEncryption = CVarNetAllowEncryption.GetValueOnGameThread();
			FString EncryptionToken;

			if (AllowEncryption != 0)
			{
				EncryptionToken = URL.GetOption(TEXT("EncryptionToken="), TEXT(""));
			}

			bool bEncryptionRequirementsFailure = false;

			// 忽略加密检查相关代码
			
			if (!bEncryptionRequirementsFailure)
			{
				uint32 LocalNetworkVersion = FNetworkVersion::GetLocalNetworkVersion();

				UE_LOG(LogNet, Log, TEXT("UPendingNetGame::SendInitialJoin: Sending hello. %s"), *ServerConn->Describe());

				EEngineNetworkRuntimeFeatures LocalNetworkFeatures = NetDriver->GetNetworkRuntimeFeatures();
				FNetControlMessage<NMT_Hello>::Send(ServerConn, IsLittleEndian, LocalNetworkVersion, EncryptionToken, LocalNetworkFeatures);


				ServerConn->FlushNet();
			}
			else
			{
				UE_LOG(LogNet, Error, TEXT("UPendingNetGame::SendInitialJoin: EncryptionToken is empty when 'net.AllowEncryption' requires it."));

				ConnectionError = TEXT("EncryptionToken not set.");
			}
		}
	}
}

这个FNetControlMessage会发送一个ControlMessage通过UControllChannel到对端,发送接口调用的是UChannel::SendBunch函数,而不是之前提到的LowLevelSend。通过UControllChannel发送的消息将会带上bReliable标记,如果消息丢失会执行重传。

UE针对接收到的UControllChannel数据会有专门的函数void UControlChannel::ReceivedBunch( FInBunch& Bunch )去处理,对于NMT_Hello这个消息会通过NetDriver上的Notify对象进行转发:

if (Connection->Driver->Notify != nullptr)
{
	// Process control message on client/server connection
	Connection->Driver->Notify->NotifyControlMessage(Connection, MessageType, Bunch);
}

在服务端这个Notify对象就是UWorld,在客户端这个Notify对象就是UPendingNetGame,所以服务端这里会调用到UWorld::NotifyControlMessage:

void UWorld::NotifyControlMessage(UNetConnection* Connection, uint8 MessageType, class FInBunch& Bunch)
{
	if( NetDriver->ServerConnection )
	{
		// 客户端代码 先忽略
	}
	else
	{
		// We are the server.
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
		UE_LOG(LogNet, Verbose, TEXT("Level server received: %s"), FNetControlMessageInfo::GetName(MessageType));
#endif
		if ( !Connection->IsClientMsgTypeValid( MessageType ) )
		{
			// If we get here, either code is mismatched on the client side, or someone could be spoofing the client address
			UE_LOG(LogNet, Error, TEXT( "IsClientMsgTypeValid FAILED (%i): Remote Address = %s" ), (int)MessageType,
					ToCStr(Connection->LowLevelGetRemoteAddress(true)));
			Bunch.SetError();
			return;
		}
		
		switch (MessageType)
		{
			// 暂时省略每个类型的消息处理
		}
	}
}

这里的NotifyControlMessage会根据传入的MessageType来执行各自的逻辑,对于NMT_Hello来说,会首先利用FNetControlMessage来反序列化出来传入的各种字段,检查完网络兼容性和加密Token之后,会通过SendChallengeControlMessage予以回包:

case NMT_Hello:
{
	uint8 IsLittleEndian = 0;
	uint32 RemoteNetworkVersion = 0;
	uint32 LocalNetworkVersion = FNetworkVersion::GetLocalNetworkVersion();
	FString EncryptionToken;

	EEngineNetworkRuntimeFeatures LocalNetworkFeatures = NetDriver->GetNetworkRuntimeFeatures();
	EEngineNetworkRuntimeFeatures RemoteNetworkFeatures = EEngineNetworkRuntimeFeatures::None;

	if (FNetControlMessage<NMT_Hello>::Receive(Bunch, IsLittleEndian, RemoteNetworkVersion, EncryptionToken, RemoteNetworkFeatures))
	{
		const bool bIsNetCLCompatible = FNetworkVersion::IsNetworkCompatible(LocalNetworkVersion, RemoteNetworkVersion);
		const bool bAreNetFeaturesCompatible = FNetworkVersion::AreNetworkRuntimeFeaturesCompatible(LocalNetworkFeatures, RemoteNetworkFeatures);

		if (!bIsNetCLCompatible || !bAreNetFeaturesCompatible)
		{
			// 忽略网络格式不匹配的处理
		}
		else
		{
			if (EncryptionToken.IsEmpty())
			{
				EEncryptionFailureAction FailureResult = EEncryptionFailureAction::Default;
				
				if (FNetDelegates::OnReceivedNetworkEncryptionFailure.IsBound())
				{
					FailureResult = FNetDelegates::OnReceivedNetworkEncryptionFailure.Execute(Connection);
				}

				const bool bGameplayDisableEncryptionCheck = FailureResult == EEncryptionFailureAction::AllowConnection;
				const bool bEncryptionRequired = NetDriver->IsEncryptionRequired() && !bGameplayDisableEncryptionCheck;

				if (!bEncryptionRequired)
				{
					Connection->SendChallengeControlMessage();
				}
				else
				{
					// 忽略强制要求加密的错误处理
				}
			}
		}
	}
}

这个SendChallengeControlMessage又是发送一个FNetControlMessage<NMT_Challenge>的包到客户端,这个包里唯一的参数就是当前服务器的时间:

void UNetConnection::SendChallengeControlMessage()
{
	if (GetConnectionState() != USOCK_Invalid && GetConnectionState() != USOCK_Closed && Driver)
	{
		Challenge = FString::Printf(TEXT("%08X"), FPlatformTime::Cycles());
		SetExpectedClientLoginMsgType(NMT_Login);
		FNetControlMessage<NMT_Challenge>::Send(this, Challenge);
		FlushNet();
	}
	else
	{
		UE_LOG(LogNet, Log, TEXT("UWorld::SendChallengeControlMessage: connection in invalid state. %s"), *Describe());
	}
}

客户端接收这个NMT_Challenge的地方在UPendingNetGame::NotifyControlMessage,解析完Challenge之后,又会构造一个FNetControlMessage<NMT_Login>消息发送到服务端,这个消息里会带上当前客户端玩家的唯一ID、用户名以及一些登录相关的额外信息:

case NMT_Challenge:
{
	// Challenged by server.
	if (FNetControlMessage<NMT_Challenge>::Receive(Bunch, Connection->Challenge))
	{
		FURL PartialURL(URL);
		PartialURL.Host = TEXT("");
		PartialURL.Port = PartialURL.UrlConfig.DefaultPort; // HACK: Need to fix URL parsing 
		PartialURL.Map = TEXT("");

		for (int32 i = URL.Op.Num() - 1; i >= 0; i--)
		{
			if (URL.Op[i].Left(5) == TEXT("game="))
			{
				URL.Op.RemoveAt(i);
			}
		}

		ULocalPlayer* LocalPlayer = GEngine->GetFirstGamePlayer(this);
		if (LocalPlayer)
		{
			// Send the player nickname if available
			FString OverrideName = LocalPlayer->GetNickname();
			if (OverrideName.Len() > 0)
			{
				PartialURL.AddOption(*FString::Printf(TEXT("Name=%s"), *OverrideName));
			}

			// Send any game-specific url options for this player
			FString GameUrlOptions = LocalPlayer->GetGameLoginOptions();
			if (GameUrlOptions.Len() > 0)
			{
				PartialURL.AddOption(*FString::Printf(TEXT("%s"), *GameUrlOptions));
			}

			// Send the player unique Id at login
			Connection->PlayerId = LocalPlayer->GetPreferredUniqueNetId();
		}

		// Send the player's online platform name
		FName OnlinePlatformName = NAME_None;
		if (const FWorldContext* const WorldContext = GEngine->GetWorldContextFromPendingNetGame(this))
		{
			if (WorldContext->OwningGameInstance)
			{
				OnlinePlatformName = WorldContext->OwningGameInstance->GetOnlinePlatformName();
			}
		}

		Connection->ClientResponse = TEXT("0");
		FString URLString(PartialURL.ToString());
		FString OnlinePlatformNameString = OnlinePlatformName.ToString();

		FNetControlMessage<NMT_Login>::Send(Connection, Connection->ClientResponse, URLString, Connection->PlayerId, OnlinePlatformNameString);
		NetDriver->ServerConnection->FlushNet();
	}
	else
	{
		Connection->Challenge.Empty();
	}

	break;
}

当服务端的UWorld收到这个NMT_Login消息之后,先解析出传入参数,然后通知GameMode来检查这个玩家是否被允许登录,如果允许则会调用到UWorld::PreLoginComplete来准备登录:

case NMT_Login:
{
	// Admit or deny the player here.
	FUniqueNetIdRepl UniqueIdRepl;
	FString OnlinePlatformName;
	FString& RequestURL = Connection->RequestURL;

	// Expand the maximum string serialization size, to accommodate extremely large Fortnite join URL's.
	Bunch.ArMaxSerializeSize += (16 * 1024 * 1024);

	bool bReceived = FNetControlMessage<NMT_Login>::Receive(Bunch, Connection->ClientResponse, RequestURL, UniqueIdRepl,
															OnlinePlatformName);

	Bunch.ArMaxSerializeSize -= (16 * 1024 * 1024);

	if (bReceived)
	{
		// Only the options/portal for the URL should be used during join
		const TCHAR* NewRequestURL = *RequestURL;

		for (; *NewRequestURL != '\0' && *NewRequestURL != '?' && *NewRequestURL != '#'; NewRequestURL++){}


		UE_LOG(LogNet, Log, TEXT("Login request: %s userId: %s platform: %s"), NewRequestURL, UniqueIdRepl.IsValid() ? *UniqueIdRepl.ToDebugString() : TEXT("UNKNOWN"), *OnlinePlatformName);

		// Compromise for passing splitscreen playercount through to gameplay login code,
		// without adding a lot of extra unnecessary complexity throughout the login code.
		// NOTE: This code differs from NMT_JoinSplit, by counting + 1 for SplitscreenCount
		//			(since this is the primary connection, not counted in Children)
		FURL InURL( NULL, NewRequestURL, TRAVEL_Absolute );

		if ( !InURL.Valid )
		{
			RequestURL = NewRequestURL;

			UE_LOG( LogNet, Error, TEXT( "NMT_Login: Invalid URL %s" ), *RequestURL );
			Bunch.SetError();
			break;
		}

		int32 SplitscreenCount = FMath::Min(Connection->Children.Num() + 1, 255);

		// Don't allow clients to specify this value
		InURL.RemoveOption(TEXT("SplitscreenCount"));
		InURL.AddOption(*FString::Printf(TEXT("SplitscreenCount=%i"), SplitscreenCount));

		RequestURL = InURL.ToString();

		// skip to the first option in the URL
		const TCHAR* Tmp = *RequestURL;
		for (; *Tmp && *Tmp != '?'; Tmp++);

		// keep track of net id for player associated with remote connection
		Connection->PlayerId = UniqueIdRepl;

		// keep track of the online platform the player associated with this connection is using.
		Connection->SetPlayerOnlinePlatformName(FName(*OnlinePlatformName));

		// ask the game code if this player can join
		AGameModeBase* GameMode = GetAuthGameMode();
		AGameModeBase::FOnPreLoginCompleteDelegate OnComplete = AGameModeBase::FOnPreLoginCompleteDelegate::CreateUObject(
			this, &UWorld::PreLoginComplete, TWeakObjectPtr<UNetConnection>(Connection));
		if (GameMode)
		{
			GameMode->PreLoginAsync(Tmp, Connection->LowLevelGetRemoteAddress(), Connection->PlayerId, OnComplete);
		}
		else
		{
			OnComplete.ExecuteIfBound(FString());
		}
	}
	else
	{
		Connection->ClientResponse.Empty();
		RequestURL.Empty();
	}

	break;
}

由于GameMode验证登录是一个异步操作,所以这里使用的是连接对象弱指针,如果允许登录,则会执行WelcomePlayer操作:

void UWorld::PreLoginComplete(const FString& ErrorMsg, TWeakObjectPtr<UNetConnection> WeakConnection)
{
	UNetConnection* Connection = WeakConnection.Get();
	if (!PreLoginCheckError(Connection, ErrorMsg))
	{
		return;
	}

	WelcomePlayer(Connection);
}

WelComePlayer内部会发送一个NMT_Welcome的控制消息到客户端,参数里会填充好当前服务器的LevelName,GameName,以通知客户端去加载指定的地图:

void UWorld::WelcomePlayer(UNetConnection* Connection)
{
#if !WITH_EDITORONLY_DATA
	ULevel* CurrentLevel = PersistentLevel;
#endif

	check(CurrentLevel);

	FString LevelName;

	const FSeamlessTravelHandler& SeamlessTravelHandler = GEngine->SeamlessTravelHandlerForWorld(this);
	if (SeamlessTravelHandler.IsInTransition())
	{
		// Tell the client to go to the destination map
		LevelName = SeamlessTravelHandler.GetDestinationMapName();
		Connection->SetClientWorldPackageName(NAME_None);
	}
	else
	{
		LevelName = CurrentLevel->GetOutermost()->GetName();
		Connection->SetClientWorldPackageName(CurrentLevel->GetOutermost()->GetFName());
	}
	if (UGameInstance* GameInst = GetGameInstance())
	{
		GameInst->ModifyClientTravelLevelURL(LevelName);
	}

	FString GameName;
	FString RedirectURL;
	if (AuthorityGameMode != NULL)
	{
		GameName = AuthorityGameMode->GetClass()->GetPathName();
		AuthorityGameMode->GameWelcomePlayer(Connection, RedirectURL);
	}

	FNetControlMessage<NMT_Welcome>::Send(Connection, LevelName, GameName, RedirectURL);

	// 忽略一些代码


	Connection->FlushNet();
	// don't count initial join data for netspeed throttling
	// as it's unnecessary, since connection won't be fully open until it all gets received, and this prevents later gameplay data from being delayed to "catch up"
	Connection->QueuedBits = 0;
	Connection->SetClientLoginState( EClientLoginState::Welcomed );		// Client has been told to load the map, will respond via SendJoin
}

客户端收到这个消息的额时候并不会立即就加载地图,而是将地图信息填充到UPendingGame::URL里,最后再给服务器发送一个NMT_Netspeed控制消息来通知当前的网速限制:

case NMT_Welcome:
{
	// Server accepted connection.
	FString GameName;
	FString RedirectURL;

	if (FNetControlMessage<NMT_Welcome>::Receive(Bunch, URL.Map, GameName, RedirectURL))
	{
		//GEngine->NetworkRemapPath(this, URL.Map);

		UE_LOG(LogNet, Log, TEXT("Welcomed by server (Level: %s, Game: %s)"), *URL.Map, *GameName);

		// extract map name and options
		{
			FURL DefaultURL;
			FURL TempURL(&DefaultURL, *URL.Map, TRAVEL_Partial);
			URL.Map = TempURL.Map;
			URL.RedirectURL = RedirectURL;
			URL.Op.Append(TempURL.Op);
		}

		if (GameName.Len() > 0)
		{
			URL.AddOption(*FString::Printf(TEXT("game=%s"), *GameName));
		}

		// Send out netspeed now that we're connected
		FNetControlMessage<NMT_Netspeed>::Send(Connection, Connection->CurrentNetSpeed);

		// We have successfully connected
		// TickWorldTravel will load the map and call LoadMapCompleted which eventually calls SendJoin
		bSuccessfullyConnected = true;
	}
	else
	{
		URL.Map.Empty();
	}

	break;
}

客户端地图加载的逻辑则是在UEngine::TickWorldTravel里,这个函数会检查PendingGameURL是否被设置了,

void UEngine::TickWorldTravel(FWorldContext& Context, float DeltaSeconds)
{
	// Handle seamless traveling
	if (Context.SeamlessTravelHandler.IsInTransition())
	{
		// Note: SeamlessTravelHandler.Tick may automatically update Context.World and GWorld internally
		Context.SeamlessTravelHandler.Tick();
	}

	// 忽略服务端加载地图的逻辑

	// Handle client traveling.
	// 忽略一些其他分支的代码

	if( Context.PendingNetGame )
	{
		Context.PendingNetGame->Tick( DeltaSeconds );
		if ( Context.PendingNetGame && Context.PendingNetGame->ConnectionError.Len() > 0 )
		{
			BroadcastNetworkFailure(NULL, Context.PendingNetGame->NetDriver, ENetworkFailure::PendingConnectionFailure, Context.PendingNetGame->ConnectionError);
			CancelPending(Context);
		}
		else if (Context.PendingNetGame && Context.PendingNetGame->bSuccessfullyConnected && !Context.PendingNetGame->bSentJoinRequest && !Context.PendingNetGame->bLoadedMapSuccessfully && (Context.OwningGameInstance == NULL || !Context.OwningGameInstance->DelayPendingNetGameTravel()))
		{
			if (Context.PendingNetGame->HasFailedTravel())
			{
				BrowseToDefaultMap(Context);
				BroadcastTravelFailure(Context.World(), ETravelFailure::TravelFailure, TEXT("Travel failed for unknown reason"));
			}
			else if (!MakeSureMapNameIsValid(Context.PendingNetGame->URL.Map))
			{
				BrowseToDefaultMap(Context);
				BroadcastTravelFailure(Context.World(), ETravelFailure::PackageMissing, Context.PendingNetGame->URL.Map);
			}
			else if (!Context.PendingNetGame->bLoadedMapSuccessfully)
			{
				// Attempt to load the map.
				FString Error;

				const bool bLoadedMapSuccessfully = LoadMap(Context, Context.PendingNetGame->URL, Context.PendingNetGame, Error);

				if (Context.PendingNetGame != nullptr)
				{
					if (!Context.PendingNetGame->LoadMapCompleted(this, Context, bLoadedMapSuccessfully, Error))
					{
						BrowseToDefaultMap(Context);
						BroadcastTravelFailure(Context.World(), ETravelFailure::LoadMapFailure, Error);
					}
				}
				else
				{
					BrowseToDefaultMap(Context);
					BroadcastTravelFailure(Context.World(), ETravelFailure::TravelFailure, Error);
				}
			}
		}
		
		// 省略一些代码
	}
	else if (TransitionType == ETransitionType::WaitingToConnect)
	{
		TransitionType = ETransitionType::None;
	}

	return;
}

LoadMap内部会通过MovePendingLevel这个接口将当前NetDriver所绑定的NotifyPendingNetGame切换为当前的Uworld,这样后续的ControlChannel的消息回调都会通过UWorld来处理了,与服务器一样:

void UEngine::MovePendingLevel(FWorldContext &Context)
{
	check(Context.World());
	check(Context.PendingNetGame);

	Context.World()->SetNetDriver(Context.PendingNetGame->NetDriver);

	UNetDriver* NetDriver = Context.PendingNetGame->NetDriver;
	if (NetDriver)
	{
		// The pending net driver is renamed to the current "game net driver"
		NetDriver->SetNetDriverName(NAME_GameNetDriver);
		NetDriver->SetWorld(Context.World());

		FLevelCollection& SourceLevels = Context.World()->FindOrAddCollectionByType(ELevelCollectionType::DynamicSourceLevels);
		SourceLevels.SetNetDriver(NetDriver);

		if (FLevelCollection* StaticLevels = Context.World()->FindCollectionByType(ELevelCollectionType::StaticLevels))
		{
			StaticLevels->SetNetDriver(NetDriver);
		}
	}

	// Attach the DemoNetDriver to the world if there is one
	if (UDemoNetDriver* DemoNetDriver = Context.PendingNetGame->GetDemoNetDriver())
	{
		DemoNetDriver->SetWorld(Context.World());
		Context.World()->SetDemoNetDriver(DemoNetDriver);

		FLevelCollection& MainLevels = Context.World()->FindOrAddCollectionByType(ELevelCollectionType::DynamicSourceLevels);
		MainLevels.SetDemoNetDriver(DemoNetDriver);
	}

	// Reset the Navigation System
	Context.World()->SetNavigationSystem(nullptr);
}

由于LoadMap是一个异步的过程,所以加载完成的检查依然需要在UEngine::TickWorldTravel里去做,当地图加载完成之后,PendingNetGame->TravelCompleted就会被调用到,在这个函数里会发送一个NMT_Join控制消息来通知服务器客户端已经加载完了地图,可以进入服务器地图了:

if (Context.PendingNetGame && Context.PendingNetGame->bLoadedMapSuccessfully && (Context.OwningGameInstance == NULL || !Context.OwningGameInstance->DelayCompletionOfPendingNetGameTravel()))
{
	if (!Context.PendingNetGame->HasFailedTravel() )
	{
		Context.PendingNetGame->TravelCompleted(this, Context);
		Context.PendingNetGame = nullptr;
	}
	else
	{
		CancelPending(Context);
		BrowseToDefaultMap(Context);
		BroadcastTravelFailure(Context.World(), ETravelFailure::LoadMapFailure, TEXT("Travel failed for unknown reason"));
	}
}

void UPendingNetGame::TravelCompleted(UEngine* Engine, FWorldContext& Context)
{
	// Show connecting message, cause precaching to occur.
	Engine->TransitionType = ETransitionType::Connecting;

	Engine->RedrawViewports(false);

	// Send join.
	Context.PendingNetGame->SendJoin();
	Context.PendingNetGame->NetDriver = NULL;

	UE_LOGSTATUS(Log, TEXT("Pending net game travel completed"));
}

void UPendingNetGame::SendJoin()
{
	bSentJoinRequest = true;

	FNetControlMessage<NMT_Join>::Send(NetDriver->ServerConnection);
	NetDriver->ServerConnection->FlushNet(true);
}

这里将Context.PendingNetGame设置为nullptr的目的就是彻底消除对PendingNetGame的引用,这样在后续的GC过程中可以回收这个对象。

当服务端的UWorld接收到这个NMT_Join控制消息之后,开始为这个客户端连接创建对应的PlayerController,同时利用这个PlayerController来调用ClientTravel来跳转到最终的关卡:

case NMT_Join:
{
	if (Connection->PlayerController == NULL)
	{
		// Spawn the player-actor for this network player.
		FString ErrorMsg;
		UE_LOG(LogNet, Log, TEXT("Join request: %s"), *Connection->RequestURL);

		FURL InURL( NULL, *Connection->RequestURL, TRAVEL_Absolute );

		if ( !InURL.Valid )
		{
			UE_LOG( LogNet, Error, TEXT( "NMT_Login: Invalid URL %s" ), *Connection->RequestURL );
			Bunch.SetError();
			break;
		}

		Connection->PlayerController = SpawnPlayActor( Connection, ROLE_AutonomousProxy, InURL, Connection->PlayerId, ErrorMsg );
		if (Connection->PlayerController == NULL)
		{
			// 忽略错误处理
		}
		else
		{
			// Successfully in game.
			UE_LOG(LogNet, Log, TEXT("Join succeeded: %s"), *Connection->PlayerController->PlayerState->GetPlayerName());
			NETWORK_PROFILER(GNetworkProfiler.TrackEvent(TEXT("JOIN"), *Connection->PlayerController->PlayerState->GetPlayerName(), Connection));

			Connection->SetClientLoginState(EClientLoginState::ReceivedJoin);

			// if we're in the middle of a transition or the client is in the wrong world, tell it to travel
			FString LevelName;
			FSeamlessTravelHandler &SeamlessTravelHandler = GEngine->SeamlessTravelHandlerForWorld( this );

			if (SeamlessTravelHandler.IsInTransition())
			{
				// tell the client to go to the destination map
				LevelName = SeamlessTravelHandler.GetDestinationMapName();
			}
			else if (!Connection->PlayerController->HasClientLoadedCurrentWorld())
			{
				// tell the client to go to our current map
				FString NewLevelName = GetOutermost()->GetName();
				UE_LOG(LogNet, Log, TEXT("Client joined but was sent to another level. Asking client to travel to: '%s'"), *NewLevelName);
				LevelName = NewLevelName;
			}
			if (LevelName != TEXT(""))
			{
				Connection->PlayerController->ClientTravel(LevelName, TRAVEL_Relative, true);
			}

			// @TODO FIXME - TEMP HACK? - clear queue on join
			Connection->QueuedBits = 0;
		}
	}
	break;
}

在这个UWorld::SpawnPlayActor里会通过GameMode::Login来创建指定类型的PlayerController对象,同时设置好对应的Role:

APlayerController* UWorld::SpawnPlayActor(UPlayer* NewPlayer, ENetRole RemoteRole, const FURL& InURL, const FUniqueNetIdRepl& UniqueId, FString& Error, uint8 InNetPlayerIndex)
{
	Error = TEXT("");

	// Make the option string.
	FString Options;
	for (int32 i = 0; i < InURL.Op.Num(); i++)
	{
		Options += TEXT('?');
		Options += InURL.Op[i];
	}

	if (AGameModeBase* const GameMode = GetAuthGameMode())
	{
		// Give the GameMode a chance to accept the login
		APlayerController* const NewPlayerController = GameMode->Login(NewPlayer, RemoteRole, *InURL.Portal, Options, UniqueId, Error);
		if (NewPlayerController == NULL)
		{
			UE_LOG(LogSpawn, Warning, TEXT("Login failed: %s"), *Error);
			return NULL;
		}

		UE_LOG(LogSpawn, Log, TEXT("%s got player %s [%s]"), *NewPlayerController->GetName(), *NewPlayer->GetName(), UniqueId.IsValid() ? *UniqueId->ToString() : TEXT("Invalid"));

		// Possess the newly-spawned player.
		NewPlayerController->NetPlayerIndex = InNetPlayerIndex;
		NewPlayerController->SetRole(ROLE_Authority);
		NewPlayerController->SetReplicates(RemoteRole != ROLE_None);
		if (RemoteRole == ROLE_AutonomousProxy)
		{
			NewPlayerController->SetAutonomousProxy(true);
		}
		NewPlayerController->SetPlayer(NewPlayer);
		GameMode->PostLogin(NewPlayerController);
		return NewPlayerController;
	}

	UE_LOG(LogSpawn, Warning, TEXT("Login failed: No game mode set."));
	return nullptr;
}

在最后的GameMode->PostLogin里,还会建立这个客户端玩家对应的Pawn对象,并尝试去开启当前的Match:

void AGameModeBase::PostLogin(APlayerController* NewPlayer)
{
	// Runs shared initialization that can happen during seamless travel as well

	GenericPlayerInitialization(NewPlayer);

	// Perform initialization that only happens on initially joining a server

	UWorld* World = GetWorld();

	NewPlayer->ClientCapBandwidth(NewPlayer->Player->CurrentNetSpeed);

	// 忽略观战相关的代码

	if (GameSession)
	{
		GameSession->PostLogin(NewPlayer);
	}

	DispatchPostLogin(NewPlayer);

	// Now that initialization is done, try to spawn the player's pawn and start match
	HandleStartingNewPlayer(NewPlayer);
}

void AGameMode::HandleStartingNewPlayer_Implementation(APlayerController* NewPlayer)
{
	// If players should start as spectators, leave them in the spectator state
	if (!bStartPlayersAsSpectators && !MustSpectate(NewPlayer))
	{
		// If match is in progress, start the player
		if (IsMatchInProgress() && PlayerCanRestart(NewPlayer))
		{
			RestartPlayer(NewPlayer);
		}
		// Check to see if we should start right away, avoids a one frame lag in single player games
		else if (GetMatchState() == MatchState::WaitingToStart)
		{
			// Check to see if we should start the match
			if (ReadyToStartMatch())
			{
				StartMatch();
			}
		}
	}
}

StartMatch里会为每一个PlayerController分配一个出生点,并在这个出生点使用GameModeBase::DefaultPawnClass这个配置来创建初始的客户端对应的Pawn

APawn* AGameModeBase::SpawnDefaultPawnFor_Implementation(AController* NewPlayer, AActor* StartSpot)
{
	// Don't allow pawn to be spawned with any pitch or roll
	FRotator StartRotation(ForceInit);
	StartRotation.Yaw = StartSpot->GetActorRotation().Yaw;
	FVector StartLocation = StartSpot->GetActorLocation();

	FTransform Transform = FTransform(StartRotation, StartLocation);
	return SpawnDefaultPawnAtTransform(NewPlayer, Transform);
}

APawn* AGameModeBase::SpawnDefaultPawnAtTransform_Implementation(AController* NewPlayer, const FTransform& SpawnTransform)
{
	FActorSpawnParameters SpawnInfo;
	SpawnInfo.Instigator = GetInstigator();
	SpawnInfo.ObjectFlags |= RF_Transient;	// We never want to save default player pawns into a map
	UClass* PawnClass = GetDefaultPawnClassForController(NewPlayer);
	APawn* ResultPawn = GetWorld()->SpawnActor<APawn>(PawnClass, SpawnTransform, SpawnInfo);
	if (!ResultPawn)
	{
		UE_LOG(LogGameMode, Warning, TEXT("SpawnDefaultPawnAtTransform: Couldn't spawn Pawn of type %s at %s"), *GetNameSafe(PawnClass), *SpawnTransform.ToHumanReadableString());
	}
	return ResultPawn;
}

对应的还有一个与玩家绑定的重要的类型PlayerState,会在PlayerController的初始化函数里创建:

void AController::InitPlayerState()
{
	if ( GetNetMode() != NM_Client )
	{
		UWorld* const World = GetWorld();
		const AGameModeBase* GameMode = World ? World->GetAuthGameMode() : NULL;

		// If the GameMode is null, this might be a network client that's trying to
		// record a replay. Try to use the default game mode in this case so that
		// we can still spawn a PlayerState.
		if (GameMode == NULL)
		{
			const AGameStateBase* const GameState = World ? World->GetGameState() : NULL;
			GameMode = GameState ? GameState->GetDefaultGameMode() : NULL;
		}

		if (GameMode != NULL)
		{
			FActorSpawnParameters SpawnInfo;
			SpawnInfo.Owner = this;
			SpawnInfo.Instigator = GetInstigator();
			SpawnInfo.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
			SpawnInfo.ObjectFlags |= RF_Transient;	// We never want player states to save into a map

			TSubclassOf<APlayerState> PlayerStateClassToSpawn = GameMode->PlayerStateClass;
			if (PlayerStateClassToSpawn.Get() == nullptr)
			{
				UE_LOG(LogPlayerController, Log, TEXT("AController::InitPlayerState: the PlayerStateClass of game mode %s is null, falling back to APlayerState."), *GameMode->GetName());
				PlayerStateClassToSpawn = APlayerState::StaticClass();
			}

			SetPlayerState(World->SpawnActor<APlayerState>(PlayerStateClassToSpawn, SpawnInfo));
	
			// force a default player name if necessary
			if (PlayerState && PlayerState->GetPlayerName().IsEmpty())
			{
				// don't call SetPlayerName() as that will broadcast entry messages but the GameMode hasn't had a chance
				// to potentially apply a player/bot name yet
				
				PlayerState->SetPlayerNameInternal(GameMode->DefaultPlayerName.ToString());
			}
		}
	}
}

至此,一个客户端玩家对应的PlayerController, PlayerState, PlayerPawn三个Actor都在登录成功之后创建出来了。

玩家下线

UE里玩家下线有只有一个入口,就是客户端对应的UNetConnectionCleanUp函数。这个UNetConnection::CleanUp函数的调用时机有很多,包括但不限于:客户端主动登出、客户端进程退出、网络异常以及服务器主动断开等。

void UNetConnection::CleanUp()
{
	// Remove UChildConnection(s)
	for (int32 i = 0; i < Children.Num(); i++)
	{
		Children[i]->CleanUp();
	}
	Children.Empty();

	if ( State != USOCK_Closed )
	{
		UE_LOG( LogNet, Log, TEXT( "UNetConnection::Cleanup: Closing open connection. %s" ), *Describe() );
	}

	Close();

	if (Driver != nullptr)
	{
		// Remove from driver.
		if (Driver->ServerConnection)
		{
			check(Driver->ServerConnection == this);
			Driver->ServerConnection = NULL;
		}
		else
		{
			check(Driver->ServerConnection == NULL);
			Driver->RemoveClientConnection(this);
		}
	}

	// 省略一些关于netchannel清理的代码

	if (GIsRunning)
	{
		DestroyOwningActor();
	}

	CleanupDormantActorState();

	Handler.Reset(NULL);

	SetClientLoginState(EClientLoginState::CleanedUp);

	Driver = nullptr;
}

在这个UNetConnection::CleanUp函数里,会调用Close函数,这个函数里会往当前的ControlChannel也就是Channels[0]发送一个关闭包,这个关闭包会触发ControlChannelClose函数,然后调用FlushNet来将所有未发送的包都发送出去,这样对端就知道当前连接已经不在可用了,:

void UNetConnection::Close()
{
	if (IsInternalAck())
	{
		SetReserveDestroyedChannels(false);
		SetIgnoreReservedChannels(false);
	}

	if (Driver != nullptr && State != USOCK_Closed)
	{
		if (Channels[0] != nullptr)
		{
			Channels[0]->Close(EChannelCloseReason::Destroyed);
		}
		State = USOCK_Closed;

		if ((Handler == nullptr || Handler->IsFullyInitialized()) && HasReceivedClientPacket())
		{
			FlushNet();
		}

		// 省略一些代码
	}

	LogCallLastTime		= 0;
	LogCallCount		= 0;
	LogSustainedCount	= 0;
}

在执行完成Close操作之后,接下来会调用DestroyOwningActor函数来销毁当前UNetConnection对应的Actor,这个Actor就是PlayerController

void UNetConnection::DestroyOwningActor()
{
	if (OwningActor != nullptr)
	{
		// Cleanup/Destroy the connection actor & controller
		if (!OwningActor->HasAnyFlags(RF_BeginDestroyed | RF_FinishDestroyed))
		{
			// UNetConnection::CleanUp can be called from UNetDriver::FinishDestroyed that is called from GC.
			OwningActor->OnNetCleanup(this);
		}
		OwningActor = nullptr;
		PlayerController = nullptr;
	}
	else
	{
		if (ClientLoginState < EClientLoginState::ReceivedJoin)
		{
			UE_LOG(LogNet, Log, TEXT("UNetConnection::PendingConnectionLost. %s bPendingDestroy=%d "), *Describe(), bPendingDestroy);
			FGameDelegates::Get().GetPendingConnectionLostDelegate().Broadcast(PlayerId);
		}
	}
}

默认的Actor::OnNetCleanup的实现是空的,没有任何逻辑,只是为了方便子类重写。当前的APlayerController::OnNetCleanup函数会通过Destroy函数来强行销毁自己:

void APlayerController::OnNetCleanup(UNetConnection* Connection)
{
	UWorld* World = GetWorld();
	// destroy the PC that was waiting for a swap, if it exists
	if (World != NULL)
	{
		World->DestroySwappedPC(Connection);
	}

	check(UNetConnection::GNetConnectionBeingCleanedUp == NULL);
	UNetConnection::GNetConnectionBeingCleanedUp = Connection;
	//@note: if we ever implement support for splitscreen players leaving a match without the primary player leaving, we'll need to insert
	// a call to ClearOnlineDelegates() here so that PlayerController.ClearOnlineDelegates can use the correct ControllerId (which lives
	// in ULocalPlayer)
	Player = NULL;
	NetConnection = NULL;	
	Destroy( true );
	UNetConnection::GNetConnectionBeingCleanedUp = NULL;
}

PlayerController在销毁之后会触发AController::Destroyed函数,这个函数会通知当前的GameMode执行Logout函数,:

void AController::Destroyed()
{
	if (GetLocalRole() == ROLE_Authority && PlayerState != NULL)
	{
		// if we are a player, log out
		AGameModeBase* const GameMode = GetWorld()->GetAuthGameMode();
		if (GameMode)
		{
			GameMode->Logout(this);
		}

		CleanupPlayerState();
	}

	UnPossess();
	GetWorld()->RemoveController( this );
	Super::Destroyed();
}

这个AGameModeBase::Logout会执行这个玩家下线的事件广播FGameModeEvents::GameModeLogoutEvent,然后通知GameSession执行NotifyLogout函数,这个函数会通知所有玩家当前玩家下线了,并一路通知到当前的OnlineSubsystem::UnregisterPlayer

void AGameModeBase::Logout(AController* Exiting)
{
	APlayerController* PC = Cast<APlayerController>(Exiting);
	if (PC != nullptr)
	{
		FGameModeEvents::GameModeLogoutEvent.Broadcast(this, Exiting);
		K2_OnLogout(Exiting);

		if (GameSession)
		{
			GameSession->NotifyLogout(PC);
		}
	}
}
void AGameSession::NotifyLogout(const APlayerController* PC)
{
	// Unregister the player from the online layer
	UnregisterPlayer(PC);
}
void AGameSession::UnregisterPlayer(const APlayerController* ExitingPlayer)
{
	if (GetNetMode() != NM_Standalone &&
		ExitingPlayer != NULL &&
		ExitingPlayer->PlayerState &&
		ExitingPlayer->PlayerState->GetUniqueId().IsValid())
	{
		UnregisterPlayer(ExitingPlayer->PlayerState->SessionName, ExitingPlayer->PlayerState->GetUniqueId());
	}
}
void AGameSession::UnregisterPlayer(FName InSessionName, const FUniqueNetIdRepl& UniqueId)
{
	UWorld* World = GetWorld();
	if (GetNetMode() != NM_Standalone &&
		UniqueId.IsValid() &&
		UniqueId->IsValid())
	{
		// Remove the player from the session
		UOnlineEngineInterface::Get()->UnregisterPlayer(World, InSessionName, *UniqueId);
	}
}

此外在AGameMode这个子类里还有额外的Logout逻辑,在这个子类的Logout重载里,会通过AddInactivePlayer函数构造当前PlayerState的一个副本,复制所有的属性字段,然后将这个副本添加到InactivePlayers数组中,这个数组会在后续的断线重连里使用:

void AGameMode::Logout( AController* Exiting )
{
	APlayerController* PC = Cast<APlayerController>(Exiting);
	if ( PC != nullptr )
	{
		RemovePlayerControllerFromPlayerCount(PC);
		AddInactivePlayer(PC->PlayerState, PC);
	}

	Super::Logout(Exiting);
}

void AGameMode::AddInactivePlayer(APlayerState* PlayerState, APlayerController* PC)
{
	check(PlayerState)
	UWorld* LocalWorld = GetWorld();
	// don't store if it's an old PlayerState from the previous level or if it's a spectator... or if we are shutting down
	if (!PlayerState->IsFromPreviousLevel() && !MustSpectate(PC) && !LocalWorld->bIsTearingDown)
	{
		APlayerState* const NewPlayerState = PlayerState->Duplicate();
		if (NewPlayerState)
		{
			// Side effect of Duplicate() adding PlayerState to PlayerArray (see APlayerState::PostInitializeComponents)
			GameState->RemovePlayerState(NewPlayerState);

			// make PlayerState inactive
			NewPlayerState->SetReplicates(false);

			// delete after some time
			NewPlayerState->SetLifeSpan(InactivePlayerStateLifeSpan);

			// On console, we have to check the unique net id as network address isn't valid
			const bool bIsConsole = !PLATFORM_DESKTOP;
			// Assume valid unique ids means comparison should be via this method
			const bool bHasValidUniqueId = NewPlayerState->GetUniqueId().IsValid();
			// Don't accidentally compare empty network addresses (already issue with two clients on same machine during development)
			const bool bHasValidNetworkAddress = !NewPlayerState->SavedNetworkAddress.IsEmpty();
			const bool bUseUniqueIdCheck = bIsConsole || bHasValidUniqueId;
			
			// make sure no duplicates
			// 省略一些容错代码
			InactivePlayerArray.Add(NewPlayerState);

			// 省略一些容错代码
		}
	}
}

这个PlayerState->Duplicate会调用到APlayerState::CopyProperties,这里会复制当前PlayerState的所有属性字段到新创建的PlayerState中,如果创建了子类,则需要重载这个函数来增加子类属性的复制:

void APlayerState::CopyProperties(APlayerState* PlayerState)
{
	PlayerState->SetScore(GetScore());
	PlayerState->SetPing(GetPing());
	PlayerState->ExactPing = ExactPing;
	PlayerState->SetPlayerId(GetPlayerId());
	PlayerState->SetUniqueId(GetUniqueId().GetUniqueNetId());
	PlayerState->SetPlayerNameInternal(GetPlayerName());
	PlayerState->SetStartTime(GetStartTime());
	PlayerState->SavedNetworkAddress = SavedNetworkAddress;
}

当然这个离线的PlayerState并不是永久保存在InactivePlayerArray中的,而是会在InactivePlayerStateLifeSpan时间之后被销毁,目前这个时间默认是300秒,也就是五分钟,同时这个InactivePlayerArray有最大容量MaxInactivePlayers限制,默认为16个:

AGameMode::AGameMode(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	bDelayedStart = false;

	// One-time initialization
	PrimaryActorTick.bCanEverTick = true;
	PrimaryActorTick.TickGroup = TG_PrePhysics;
	MatchState = MatchState::EnteringMap;
	EngineMessageClass = UEngineMessage::StaticClass();
	GameStateClass = AGameState::StaticClass();
	MinRespawnDelay = 1.0f;
	InactivePlayerStateLifeSpan = 300.f;
	MaxInactivePlayers = 16;
}

完成了GameMode->Logout之后,还会调用CleanupPlayerState函数来销毁当前玩家的PlayerState

void AController::CleanupPlayerState()
{
	PlayerState->Destroy();
	PlayerState = NULL;
}

然后调用AController::UnPossess来取消控制当前玩家的Pawn

void AController::UnPossess()
{
	APawn* CurrentPawn = GetPawn();

	// No need to notify if we don't have a pawn
	if (CurrentPawn == nullptr)
	{
		return;
	}

	OnUnPossess();

	// Notify only when pawn has been successfully unpossessed by the native class.
	APawn* NewPawn = GetPawn();
	if (NewPawn != CurrentPawn)
	{
		ReceiveUnPossess(CurrentPawn);
		OnNewPawn.Broadcast(NewPawn);
	}
}

void AController::OnUnPossess()
{
	// Should not be called when Pawn is null but since OnUnPossess could be overridden
	// the derived class could have already cleared the pawn and then call its base class.
	if ( Pawn != NULL )
	{
		Pawn->UnPossessed();
		SetPawn(NULL);
	}
}

值得注意的是APawn::UnPossessed内部并不会执行当前APawn的销毁,只是将当前APawnController设置为nullptr,并通知当前GameInstance当前PawnController变化为nullptr

void APawn::UnPossessed()
{
	AController* const OldController = Controller;

	ForceNetUpdate();

	SetPlayerState(nullptr);
	SetOwner(nullptr);
	Controller = nullptr;

	// Unregister input component if we created one
	DestroyPlayerInputComponent();

	// dispatch Blueprint event if necessary
	if (OldController)
	{
		ReceiveUnpossessed(OldController);
	}

	if (UGameInstance* GameInstance = GetGameInstance())
	{
		GameInstance->GetOnPawnControllerChanged().Broadcast(this, nullptr);
	}

	ConsumeMovementInputVector();
}

但是AController的子类APlayerControllerDestroyed函数里会通过PawnLeavingGame来销毁当前玩家的APawn

void APlayerController::Destroyed()
{
	if (GetPawn() != NULL)
	{
		// Handle players leaving the game
		if (Player == NULL && GetLocalRole() == ROLE_Authority)
		{
			PawnLeavingGame();
		}
		else
		{
			UnPossess();
		}
	}

	if (GetSpectatorPawn() != NULL)
	{
		DestroySpectatorPawn();
	}
	// 省略一些代码
}
void APlayerController::PawnLeavingGame()
{
	if (GetPawn() != NULL)
	{
		GetPawn()->Destroy();
		SetPawn(NULL);
	}
}

综上,如果客户端的连接关闭了,对应的APlayerControllerAPlayerStateAPawn三个对象都会被强制销毁。如果想在断线之后保留APawn,则需要在子类里重载APlayerController::PawnLeavingGame

	/** Clean up when a Pawn's player is leaving a game. Base implementation destroys the pawn. */
	virtual void PawnLeavingGame();

断线重连

当前UE实现的断线重连支持两种情况:

  1. 一种是在UNetConnection没有关闭情况下的断线重连,此时常见于客户端的网络切换导致的ip:port变化
  2. 一种是UNetConnection关闭了,但是玩家的PlayerStateAPawn等对象还没有被销毁时的重新登录,此时常见客户端的重启以及客户端设备的更换

客户端重启之后的重新登录这种情况最为简单,当服务端接收到这个新客户端的登录请求时,会在AGameMode::PostLogin里利用FindInactivePlayer来检查当前新登录玩家的UniqueId或者PlayerName是否匹配上了有之前存储的已经断开连接的PlayerState,如果匹配上了,就会将这个新的UNetConnection绑定到之前的PlayerStateAPawn等对象上:

void AGameMode::PostLogin( APlayerController* NewPlayer )
{
	UWorld* World = GetWorld();

	// 省略一些无关代码

	// save network address for re-associating with reconnecting player, after stripping out port number
	FString Address = NewPlayer->GetPlayerNetworkAddress();
	int32 pos = Address.Find(TEXT(":"), ESearchCase::CaseSensitive);
	NewPlayer->PlayerState->SavedNetworkAddress = (pos > 0) ? Address.Left(pos) : Address;

	// check if this player is reconnecting and already has PlayerState
	FindInactivePlayer(NewPlayer);

	Super::PostLogin(NewPlayer);
}

bool AGameMode::FindInactivePlayer(APlayerController* PC)
{
	check(PC && PC->PlayerState);
	// don't bother for spectators
	if (MustSpectate(PC))
	{
		return false;
	}

	// On console, we have to check the unique net id as network address isn't valid
	const bool bIsConsole = !PLATFORM_DESKTOP;
	// Assume valid unique ids means comparison should be via this method
	const bool bHasValidUniqueId = PC->PlayerState->GetUniqueId().IsValid();
	// Don't accidentally compare empty network addresses (already issue with two clients on same machine during development)
	const bool bHasValidNetworkAddress = !PC->PlayerState->SavedNetworkAddress.IsEmpty();
	const bool bUseUniqueIdCheck = bIsConsole || bHasValidUniqueId;

	const FString NewNetworkAddress = PC->PlayerState->SavedNetworkAddress;
	const FString NewName = PC->PlayerState->GetPlayerName();
	for (int32 i=0; i < InactivePlayerArray.Num(); i++)
	{
		APlayerState* CurrentPlayerState = InactivePlayerArray[i];
		if ( (CurrentPlayerState == nullptr) || CurrentPlayerState->IsPendingKill() )
		{
			InactivePlayerArray.RemoveAt(i,1);
			i--;
		}
		else if ((bUseUniqueIdCheck && (CurrentPlayerState->GetUniqueId() == PC->PlayerState->GetUniqueId())) ||
				 (!bUseUniqueIdCheck && bHasValidNetworkAddress && (FCString::Stricmp(*CurrentPlayerState->SavedNetworkAddress, *NewNetworkAddress) == 0) && (FCString::Stricmp(*CurrentPlayerState->GetPlayerName(), *NewName) == 0)))
		{
			// found it!
			APlayerState* OldPlayerState = PC->PlayerState;
			PC->PlayerState = CurrentPlayerState;
			PC->PlayerState->SetOwner(PC);
			PC->PlayerState->SetReplicates(true);
			PC->PlayerState->SetLifeSpan(0.0f);
			OverridePlayerState(PC, OldPlayerState);
			GameState->AddPlayerState(PC->PlayerState);
			InactivePlayerArray.RemoveAt(i, 1);
			OldPlayerState->SetIsInactive(true);
			// Set the uniqueId to nullptr so it will not kill the player's registration 
			// in UnregisterPlayerWithSession()
			OldPlayerState->SetUniqueId(nullptr);
			OldPlayerState->Destroy();
			PC->PlayerState->OnReactivated();
			return true;
		}
		
	}
	return false;
}

如果我们的APlayerControllerPawnLeavingGame的时候没有销毁对应的APawn,那么在这里找到老的PlayerState之后,还需要自己做一下逻辑来执行重新Posses

UNetConnection没有关闭情况下的断线重连则复杂一些,涉及到一个重新握手的过程。但是此时客户端与服务器都不知道当前客户端的ip:port发生了改变,仍然以之前的UNetConnection来进行通信。此时服务端发现这个新的数据包来自于一个未知的ip:port,因此会使用StatelessConnectHandlerComponent来处理这个包。当发现这个数据包并不是请求连接建立的握手包,此时会通过StatelessConnectHandlerComponent::SendRestartHandshakeRequest发送一个回包,提示客户端重新执行身份验证来绑定之前的UNetConnection:

void StatelessConnectHandlerComponent::IncomingConnectionless(FIncomingPacketRef PacketRef)
{
	FBitReader& Packet = PacketRef.Packet;
	const TSharedPtr<const FInternetAddr> Address = PacketRef.Address;

	if (MagicHeader.Num() > 0)
	{
		// Don't bother with the expense of verifying the magic header here.
		uint32 ReadMagic = 0;
		Packet.SerializeBits(&ReadMagic, MagicHeader.Num());
	}

	bool bHandshakePacket = !!Packet.ReadBit() && !Packet.IsError();

	LastChallengeSuccessAddress = nullptr;

	if (bHandshakePacket)
	{
		// 省略正常的代码
	}
#if !UE_BUILD_SHIPPING
	else if (Packet.IsError())
	{
		UE_LOG(LogHandshake, Log, TEXT("IncomingConnectionless: Error reading handshake bit from packet."));
	}
#endif
	// Late packets from recently disconnected clients may incorrectly trigger this code path, so detect and exclude those packets
	else if (!Packet.IsError() && !PacketRef.Traits.bFromRecentlyDisconnected)
	{
		// The packet was fine but not a handshake packet - an existing client might suddenly be communicating on a different address.
		// If we get them to resend their cookie, we can update the connection's info with their new address.
		SendRestartHandshakeRequest(Address);
	}
}

SendRestartHandshakeRequest里会构造一个RestartPacket,这个RestartPacket开头会先填充MagicHeader,然后接着两个都是1bit,表示这是一个重启握手包,然后通过Driver->LowLevelSend发送回这个客户端:

void StatelessConnectHandlerComponent::SendRestartHandshakeRequest(const TSharedPtr<const FInternetAddr> ClientAddress)
{
	if (Driver != nullptr)
	{
		FBitWriter RestartPacket(GetAdjustedSizeBits(RESTART_HANDSHAKE_PACKET_SIZE_BITS) + 1 /* Termination bit */);
		uint8 bHandshakePacket = 1;
		uint8 bRestartHandshake = 1;

		if (MagicHeader.Num() > 0)
		{
			RestartPacket.SerializeBits(MagicHeader.GetData(), MagicHeader.Num());
		}

		RestartPacket.WriteBit(bHandshakePacket);
		RestartPacket.WriteBit(bRestartHandshake);

		CapHandshakePacket(RestartPacket);

		
		// Disable PacketHandler parsing, and send the raw packet
		PacketHandler* ConnectionlessHandler = Driver->ConnectionlessHandler.Get();

		if (ConnectionlessHandler != nullptr)
		{
			ConnectionlessHandler->SetRawSend(true);
		}
		{
			if (Driver->IsNetResourceValid())
			{
				FOutPacketTraits Traits;

				Driver->LowLevelSend(ClientAddress, RestartPacket.GetData(), RestartPacket.GetNumBits(), Traits);
			}
		}


		if (ConnectionlessHandler != nullptr)
		{
			ConnectionlessHandler->SetRawSend(false);
		}
	}
	else
	{
#if !UE_BUILD_SHIPPING
		UE_LOG(LogHandshake, Error, TEXT("Tried to send restart handshake packet without a net driver."));
#endif
	}
}

当客户端接收到这个握手包的时候,发现这个bRestartHandshake1,此时会认为这是一个重启握手包,然后会通过NotifyHandshakeBegin重新执行身份验证,此时内部的bRestartedHandshake字段会被设置为true:

void StatelessConnectHandlerComponent::Incoming(FBitReader& Packet)
{
	if (MagicHeader.Num() > 0)
	{
		// Don't bother with the expense of verifying the magic header here.
		uint32 ReadMagic = 0;
		Packet.SerializeBits(&ReadMagic, MagicHeader.Num());
	}

	bool bHandshakePacket = !!Packet.ReadBit() && !Packet.IsError();

	if (bHandshakePacket)
	{
		bool bRestartHandshake = false;
		uint8 SecretId = 0;
		double Timestamp = 1.;
		uint8 Cookie[COOKIE_BYTE_SIZE];
		uint8 OrigCookie[COOKIE_BYTE_SIZE];

		bHandshakePacket = ParseHandshakePacket(Packet, bRestartHandshake, SecretId, Timestamp, Cookie, OrigCookie);

		if (bHandshakePacket)
		{
			if (Handler->Mode == Handler::Mode::Client)
			{
				if (State == Handler::Component::State::UnInitialized || State == Handler::Component::State::InitializedOnLocal)
				{
					// 忽略正常分支的处理
				}
				else if (bRestartHandshake)
				{
					uint8 ZeroCookie[COOKIE_BYTE_SIZE] = {0};
					bool bValidAuthCookie = FMemory::Memcmp(AuthorisedCookie, ZeroCookie, COOKIE_BYTE_SIZE) != 0;

					// The server has requested us to restart the handshake process - this is because
					// it has received traffic from us on a different address than before.
					if (ensure(bValidAuthCookie))
					{
						bool bPassedDelayCheck = false;
						bool bPassedDualIPCheck = false;
						double CurrentTime = FPlatformTime::Seconds();;

						if (!bRestartedHandshake)
						{
							// 省略一些检查逻辑 内部会设置 bPassedDelayCheck, bPassedDualIPCheck
						}

						LastRestartPacketTimestamp = CurrentTime;

						
						if (!bRestartedHandshake && bPassedDelayCheck && bPassedDualIPCheck)
						{
							UE_LOG(LogHandshake, Log, TEXT("Beginning restart handshake process."));

							bRestartedHandshake = true;

							SetState(Handler::Component::State::UnInitialized);
							NotifyHandshakeBegin();
						}
					}
				}
			}
		}
	}
}

然后在NotifyHandshakeComplete中会构造一个新的握手包,此时bRestartHandshake对应的bit会被设置为1,然后往ServerConnection发送这个新的握手包:

void StatelessConnectHandlerComponent::NotifyHandshakeBegin()
{
	if (Handler->Mode == Handler::Mode::Client)
	{
		UNetConnection* ServerConn = (Driver != nullptr ? Driver->ServerConnection : nullptr);

		if (ServerConn != nullptr)
		{
			FBitWriter InitialPacket(GetAdjustedSizeBits(HANDSHAKE_PACKET_SIZE_BITS) + 1 /* Termination bit */);
			uint8 bHandshakePacket = 1;

			if (MagicHeader.Num() > 0)
			{
				InitialPacket.SerializeBits(MagicHeader.GetData(), MagicHeader.Num());
			}

			InitialPacket.WriteBit(bHandshakePacket);


			// In order to prevent DRDoS reflection amplification attacks, clients must pad the packet to match server packet size
			uint8 bRestartHandshake = bRestartedHandshake ? 1 : 0;
			uint8 SecretIdPad = 0;
			uint8 PacketSizeFiller[28];

			InitialPacket.WriteBit(bRestartHandshake);
			InitialPacket.WriteBit(SecretIdPad);

			FMemory::Memzero(PacketSizeFiller, UE_ARRAY_COUNT(PacketSizeFiller));
			InitialPacket.Serialize(PacketSizeFiller, UE_ARRAY_COUNT(PacketSizeFiller));



			CapHandshakePacket(InitialPacket);


			// Disable PacketHandler parsing, and send the raw packet
			Handler->SetRawSend(true);

			{
				if (ServerConn->Driver->IsNetResourceValid())
				{
					FOutPacketTraits Traits;

					ServerConn->LowLevelSend(InitialPacket.GetData(), InitialPacket.GetNumBits(), Traits);
				}
			}

			Handler->SetRawSend(false);

			LastClientSendTimestamp = FPlatformTime::Seconds();
		}
		else
		{
			UE_LOG(LogHandshake, Error, TEXT("Tried to send handshake connect packet without a server connection."));
		}
	}
}

然后当服务端收到这个新的握手包的时候,对应的处理函数依然是StatelessConnectHandlerComponent::IncomingConnectionless,但是此时会暂时忽视解析出来的bRestartHandshake,而是把这个当作一个初始握手包来看待,此时会利用SendConnectChallenge来构造一个新的Cookie往下发一个Chanllenge包:

void StatelessConnectHandlerComponent::IncomingConnectionless(FIncomingPacketRef PacketRef)
{
	FBitReader& Packet = PacketRef.Packet;
	const TSharedPtr<const FInternetAddr> Address = PacketRef.Address;

	if (MagicHeader.Num() > 0)
	{
		// Don't bother with the expense of verifying the magic header here.
		uint32 ReadMagic = 0;
		Packet.SerializeBits(&ReadMagic, MagicHeader.Num());
	}

	bool bHandshakePacket = !!Packet.ReadBit() && !Packet.IsError();

	LastChallengeSuccessAddress = nullptr;

	if (bHandshakePacket)
	{
		bool bRestartHandshake = false;
		uint8 SecretId = 0;
		double Timestamp = 1.0;
		uint8 Cookie[COOKIE_BYTE_SIZE];
		uint8 OrigCookie[COOKIE_BYTE_SIZE];

		bHandshakePacket = ParseHandshakePacket(Packet, bRestartHandshake, SecretId, Timestamp, Cookie, OrigCookie);

		if (bHandshakePacket)
		{
			if (Handler->Mode == Handler::Mode::Server)
			{
				const bool bInitialConnect = Timestamp == 0.0;

				if (bInitialConnect)
				{
					SendConnectChallenge(Address);
				}
				// 省略后续代码
			}
		}
	}
}

当客户端收到这个Challenge包的时候,会利用SendChallengeResponse来构造一个回包,不过此时发现当前自己已经记录了正在重连的状态,因此会额外附加之前已经商定的AuthorisedCookie:

void StatelessConnectHandlerComponent::SendChallengeResponse(uint8 InSecretId, double InTimestamp, uint8 InCookie[COOKIE_BYTE_SIZE])
{
	UNetConnection* ServerConn = (Driver != nullptr ? Driver->ServerConnection : nullptr);

	if (ServerConn != nullptr)
	{
		int32 RestartHandshakeResponseSize = RESTART_RESPONSE_SIZE_BITS;

#if RESTART_HANDSHAKE_DIAGNOSTICS && !DISABLE_SEND_HANDSHAKE_DIAGNOSTICS
		bool bEnableDiagnostics = bRestartedHandshake && !!CVarNetRestartHandshakeDiagnostics.GetValueOnAnyThread();

		RestartHandshakeResponseSize = bEnableDiagnostics ? RESTART_RESPONSE_DIAGNOSTICS_SIZE_BITS : RestartHandshakeResponseSize;
#endif

		const int32 BaseSize = GetAdjustedSizeBits(bRestartedHandshake ? RestartHandshakeResponseSize : HANDSHAKE_PACKET_SIZE_BITS);
		FBitWriter ResponsePacket(BaseSize + 1 /* Termination bit */);
		uint8 bHandshakePacket = 1;
		uint8 bRestartHandshake = (bRestartedHandshake ? 1 : 0);

		if (MagicHeader.Num() > 0)
		{
			ResponsePacket.SerializeBits(MagicHeader.GetData(), MagicHeader.Num());
		}

		ResponsePacket.WriteBit(bHandshakePacket);
		ResponsePacket.WriteBit(bRestartHandshake);
		ResponsePacket.WriteBit(InSecretId);

		ResponsePacket << InTimestamp;
		ResponsePacket.Serialize(InCookie, COOKIE_BYTE_SIZE);

		if (bRestartedHandshake)
		{
			ResponsePacket.Serialize(AuthorisedCookie, COOKIE_BYTE_SIZE);

#if RESTART_HANDSHAKE_DIAGNOSTICS && !DISABLE_SEND_HANDSHAKE_DIAGNOSTICS
			if (bEnableDiagnostics)
			{
				ResponsePacket << HandshakeDiagnostics;
			}
#endif
		}
	}

	// 省略后续代码
}

当服务端收到这个ChallengeResponse包的时候,会发现此时bRestartHandshake1,因此会认为这是一个重启握手包,此时会利用客户端发送过来的老的OrigCookie来填充AuthorisedCookie,而不是用新Challenge时构造的Cookie:

if (bValidCookieLifetime && bValidSecretIdTimestamp)
{
	// Regenerate the cookie from the packet info, and see if the received cookie matches the regenerated one
	uint8 RegenCookie[COOKIE_BYTE_SIZE];

	GenerateCookie(Address, SecretId, Timestamp, RegenCookie);

	bChallengeSuccess = FMemory::Memcmp(Cookie, RegenCookie, COOKIE_BYTE_SIZE) == 0;

	if (bChallengeSuccess)
	{
		if (bRestartHandshake)
		{
			FMemory::Memcpy(AuthorisedCookie, OrigCookie, UE_ARRAY_COUNT(AuthorisedCookie));
		}
		else
		{
			int16* CurSequence = (int16*)Cookie;

			LastServerSequence = *CurSequence & (MAX_PACKETID - 1);
			LastClientSequence = *(CurSequence + 1) & (MAX_PACKETID - 1);

			FMemory::Memcpy(AuthorisedCookie, Cookie, UE_ARRAY_COUNT(AuthorisedCookie));
		}

		bRestartedHandshake = bRestartHandshake;
		LastChallengeSuccessAddress = Address->Clone();


		// Now ack the challenge response - the cookie is stored in AuthorisedCookie, to enable retries
		SendChallengeAck(Address, AuthorisedCookie);
	}
}

同时外层处理函数UIpNetDriver::ProcessConnectionlessPacket发现此时重新握手成功之后,会执行客户端地址与之前UNetConnection的重新绑定,重点就是更新MappedClientConnections这个映射表:

UNetConnection* UIpNetDriver::ProcessConnectionlessPacket(FReceivedPacketView& PacketRef, const FPacketBufferView& WorkingBuffer)
{
	UNetConnection* ReturnVal = nullptr;
	TSharedPtr<StatelessConnectHandlerComponent> StatelessConnect;
	const TSharedPtr<const FInternetAddr>& Address = PacketRef.Address;
	FString IncomingAddress = Address->ToString(true);
	bool bPassedChallenge = false;
	bool bRestartedHandshake = false;
	bool bIgnorePacket = true;

	if (ConnectionlessHandler.IsValid() && StatelessConnectComponent.IsValid())
	{
		StatelessConnect = StatelessConnectComponent.Pin();

		EIncomingResult Result = ConnectionlessHandler->IncomingConnectionless(PacketRef);

		if (Result == EIncomingResult::Success)
		{
			bPassedChallenge = StatelessConnect->HasPassedChallenge(Address, bRestartedHandshake);

			if (bPassedChallenge)
			{
				if (bRestartedHandshake)
				{
					UE_LOG(LogNet, Log, TEXT("Finding connection to update to new address: %s"), *IncomingAddress);

					TSharedPtr<StatelessConnectHandlerComponent> CurComp;
					UIpConnection* FoundConn = nullptr;

					for (UNetConnection* const CurConn : ClientConnections)
					{
						CurComp = CurConn != nullptr ? CurConn->StatelessConnectComponent.Pin() : nullptr;

						if (CurComp.IsValid() && StatelessConnect->DoesRestartedHandshakeMatch(*CurComp))
						{
							FoundConn = Cast<UIpConnection>(CurConn);
							break;
						}
					}

					if (FoundConn != nullptr)
					{
						UNetConnection* RemovedConn = nullptr;
						TSharedRef<FInternetAddr> RemoteAddrRef = FoundConn->RemoteAddr.ToSharedRef();

						verify(MappedClientConnections.RemoveAndCopyValue(RemoteAddrRef, RemovedConn) && RemovedConn == FoundConn);


						// @todo: There needs to be a proper/standardized copy API for this. Also in IpConnection.cpp
						bool bIsValid = false;

						const FString OldAddress = RemoteAddrRef->ToString(true);

						RemoteAddrRef->SetIp(*Address->ToString(false), bIsValid);
						RemoteAddrRef->SetPort(Address->GetPort());


						MappedClientConnections.Add(RemoteAddrRef, FoundConn);


						// Make sure we didn't just invalidate a RecentlyDisconnectedClients entry, with the same address
						int32 RecentDisconnectIdx = RecentlyDisconnectedClients.IndexOfByPredicate(
							[&RemoteAddrRef](const FDisconnectedClient& CurElement)
							{
								return *RemoteAddrRef == *CurElement.Address;
							});

						if (RecentDisconnectIdx != INDEX_NONE)
						{
							RecentlyDisconnectedClients.RemoveAt(RecentDisconnectIdx);
						}


						ReturnVal = FoundConn;

						// We shouldn't need to log IncomingAddress, as the UNetConnection should dump it with it's description.
						UE_LOG(LogNet, Log, TEXT("Updated IP address for connection. Connection = %s, Old Address = %s"), *FoundConn->Describe(), *OldAddress);
					}
					else
					{
						UE_LOG(LogNet, Log, TEXT("Failed to find an existing connection with a matching cookie. Restarted Handshake failed."));
					}
				}


				// 省略无关代码
			}
		}
	}
	// 省略其他分支的代码
}

当客户端收到这个ChallengeAck包的时候,就可以认为连接已经重建好了,可以继续利用之前的UNetConnection来发送消息了。整个过程可以简化为下面这个流程图:

ue断线重连流程图