Unreal Engine 的玩家流程管理
端口监听
在UE中网络相关功能由网络驱动类型UNetDriver来提供,这个UNetDriver是一个虚基类,提供了各种网络收发和管理功能。实际被使用的主要是其子类IpNetDriver或者DemoNetDriver, IpNetDriver负责处理正常的游戏,而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系统同时支持了UDP和TCP的通信,但是这里的UIpNetDriver默认使用的是基于UDP的Socket通信:
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负责发起执行服务端URL的DNS查询,同时这里的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通信,所以有可能出现消息丢失的问题。为了处理可能的握手消息丢失的情况,StatelessConnectHandlerComponent的Tick函数里会检查之前发送的握手包是不是超时了,如果超时了则再次执行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的地址相比较,如果相等则会直接使用这个Connection的ReceivedRawPacket来处理当前的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();
}
}
而这个HandshakeCompleteDel在UPendingNetGame::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里,这个函数会检查PendingGame的URL是否被设置了,
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所绑定的Notify从PendingNetGame切换为当前的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里玩家下线有只有一个入口,就是客户端对应的UNetConnection的CleanUp函数。这个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]发送一个关闭包,这个关闭包会触发ControlChannel的Close函数,然后调用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的销毁,只是将当前APawn的Controller设置为nullptr,并通知当前GameInstance当前Pawn的Controller变化为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的子类APlayerController在Destroyed函数里会通过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);
}
}
综上,如果客户端的连接关闭了,对应的APlayerController、APlayerState、APawn三个对象都会被强制销毁。如果想在断线之后保留APawn,则需要在子类里重载APlayerController::PawnLeavingGame。
/** Clean up when a Pawn's player is leaving a game. Base implementation destroys the pawn. */
virtual void PawnLeavingGame();
断线重连
当前UE实现的断线重连支持两种情况:
- 一种是在
UNetConnection没有关闭情况下的断线重连,此时常见于客户端的网络切换导致的ip:port变化 - 一种是
UNetConnection关闭了,但是玩家的PlayerState、APawn等对象还没有被销毁时的重新登录,此时常见客户端的重启以及客户端设备的更换
客户端重启之后的重新登录这种情况最为简单,当服务端接收到这个新客户端的登录请求时,会在AGameMode::PostLogin里利用FindInactivePlayer来检查当前新登录玩家的UniqueId或者PlayerName是否匹配上了有之前存储的已经断开连接的PlayerState,如果匹配上了,就会将这个新的UNetConnection绑定到之前的PlayerState、APawn等对象上:
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;
}
如果我们的APlayerController在PawnLeavingGame的时候没有销毁对应的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,然后接着两个都是1的bit,表示这是一个重启握手包,然后通过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
}
}
当客户端接收到这个握手包的时候,发现这个bRestartHandshake为1,此时会认为这是一个重启握手包,然后会通过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包的时候,会发现此时bRestartHandshake为1,因此会认为这是一个重启握手包,此时会利用客户端发送过来的老的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来发送消息了。整个过程可以简化为下面这个流程图:
