voidUPendingNetGame::BeginHandshake() { // Kick off the connection handshake UNetConnection* ServerConn = NetDriver->ServerConnection; if (ServerConn->Handler.IsValid()) { ServerConn->Handler->BeginHandshaking( FPacketHandlerHandshakeComplete::CreateUObject(this, &UPendingNetGame::SendInitialJoin)); } else { SendInitialJoin(); } }
在继续分析之前,先来看官方的注释,描述了登录的流程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
Most of the work for handling these control messages are done either in UWorld::NotifyControlMessage, and UPendingNetGame::NotifyControlMessage. Briefly, the flow looks like this:
Client's UPendingNetGame::NotifyControlMessage receives NMT_Challenge, and sends back data in NMT_Login. Server's UWorld::NotifyControlMessage receives NMT_Login, verifies challenge data, and then calls AGameModeBase::PreLogin. If PreLogin doesn't report any errors, Server calls UWorld::WelcomePlayer, which call AGameModeBase::GameWelcomePlayer, and send NMT_Welcome with map information. Client's UPendingNetGame::NotifyControlMessage receives NMT_Welcome, reads the map info(so it can start loading later), and sends an NMT_NetSpeed message with the configured Net Speed of the client. Server's UWorld::NotifyControlMessage receives NMT_NetSpeed, and adjusts the connections Net Speed appropriately.
// message type definitions DEFINE_CONTROL_CHANNEL_MESSAGE(Hello, 0, uint8, uint32, FString, uint16); // initial client connection message DEFINE_CONTROL_CHANNEL_MESSAGE(Welcome, 1, FString, FString, FString); // server tells client they're ok'ed to load the server's level DEFINE_CONTROL_CHANNEL_MESSAGE(Upgrade, 2, uint32, uint16); // server tells client their version is incompatible DEFINE_CONTROL_CHANNEL_MESSAGE(Challenge, 3, FString); // server sends client challenge string to verify integrity DEFINE_CONTROL_CHANNEL_MESSAGE(Netspeed, 4, int32); // client sends requested transfer rate DEFINE_CONTROL_CHANNEL_MESSAGE(Login, 5, FString, FString, FUniqueNetIdRepl, FString); // client requests to be admitted to the game DEFINE_CONTROL_CHANNEL_MESSAGE(Failure, 6, FString); // indicates connection failure DEFINE_CONTROL_CHANNEL_MESSAGE(Join, 9); // final join request (spawns PlayerController) DEFINE_CONTROL_CHANNEL_MESSAGE(JoinSplit, 10, FString, FUniqueNetIdRepl); // child player (splitscreen) join request DEFINE_CONTROL_CHANNEL_MESSAGE(Skip, 12, FGuid); // client request to skip an optional package DEFINE_CONTROL_CHANNEL_MESSAGE(Abort, 13, FGuid); // client informs server that it aborted a not-yet-verified package due to an UNLOAD request DEFINE_CONTROL_CHANNEL_MESSAGE(PCSwap, 15, int32); // client tells server it has completed a swap of its Connection->Actor DEFINE_CONTROL_CHANNEL_MESSAGE(ActorChannelFailure, 16, int32); // client tells server that it failed to open an Actor channel sent by the server (e.g. couldn't serialize Actor archetype) DEFINE_CONTROL_CHANNEL_MESSAGE(DebugText, 17, FString); // debug text sent to all clients or to server DEFINE_CONTROL_CHANNEL_MESSAGE(NetGUIDAssign, 18, FNetworkGUID, FString); // Explicit NetworkGUID assignment. This is rare and only happens if a netguid is only serialized client->server (this msg goes server->client to tell client what ID to use in that case) DEFINE_CONTROL_CHANNEL_MESSAGE(SecurityViolation, 19, FString); // server tells client that it has violated security and has been disconnected DEFINE_CONTROL_CHANNEL_MESSAGE(GameSpecific, 20, uint8, FString); // custom game-specific message routed to UGameInstance for processing DEFINE_CONTROL_CHANNEL_MESSAGE(EncryptionAck, 21); DEFINE_CONTROL_CHANNEL_MESSAGE(DestructionInfo, 22); DEFINE_CONTROL_CHANNEL_MESSAGE(CloseReason, 23, FString); // Reason for client NetConnection Close, for analytics/logging DEFINE_CONTROL_CHANNEL_MESSAGE(NetPing, 24, ENetPingControlMessage /* MessageType */, FString /* MessageStr */);
voidUPendingNetGame::NotifyControlMessage(UNetConnection* Connection, uint8 MessageType, class FInBunch& Bunch) { 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(); } }
// 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 );
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()); } }
break; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
voidAGameModeBase::PreLogin(const FString& Options, const FString& Address, const FUniqueNetIdRepl& UniqueId, FString& ErrorMessage) { // Login unique id must match server expected unique id type OR No unique id could mean game doesn't use them constbool bUniqueIdCheckOk = (!UniqueId.IsValid() || UOnlineEngineInterface::Get()->IsCompatibleUniqueNetId(UniqueId)); if (bUniqueIdCheckOk) { ErrorMessage = GameSession->ApproveLogin(Options); } else { ErrorMessage = TEXT("incompatible_unique_net_id"); }
if (SplitscreenCount > MaxSplitscreensPerConnection) { UE_LOG(LogGameSession, Warning, TEXT("ApproveLogin: A maximum of %i splitscreen players are allowed"), MaxSplitscreensPerConnection); returnTEXT("Maximum splitscreen players"); }
voidUPendingNetGame::NotifyControlMessage(UNetConnection* Connection, uint8 MessageType, class FInBunch& Bunch) { case NMT_Welcome: { // Server accepted connection. FString GameName; FString RedirectURL;
if (FNetControlMessage<NMT_Welcome>::Receive(Bunch, URL.Map, GameName, RedirectURL)) { // 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; } }
NMT_Netspeed
这个就是双方对网速,用于之后限流。但默认不开。
1 2 3 4 5 6
TAutoConsoleVariable<int32> CVarNetEnableCongestionControl(TEXT("net.EnableCongestionControl"), 0, TEXT("Enables congestion control module.")); if (FNetControlMessage<NMT_Netspeed>::Receive(Bunch, Rate)) { Connection->CurrentNetSpeed = FMath::Clamp(Rate, 1800, NetDriver->MaxClientRate); }
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()); } }
用完 PendingNetGame 发完 NMT_Join,就不需要它了。
1 2 3 4 5 6 7 8 9 10 11
voidUPendingNetGame::TravelCompleted(UEngine* Engine, FWorldContext& Context) { // Show connecting message, cause precaching to occur. Engine->TransitionType = ETransitionType::Connecting;
voidUWorld::NotifyControlMessage(UNetConnection* Connection, uint8 MessageType, class FInBunch& Bunch) { case NMT_Join: { if (Connection->PlayerController == NULL) { // Spawn the player-actor for this network player. FString ErrorMsg;
// 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(); } elseif (!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); }
classFOutBunch : public FNetBitWriter { public: FOutBunch * Next; UChannel * Channel; double Time; int32 ChIndex; FName ChName; int32 ChSequence; int32 PacketId; uint8 ReceivedAck:1; uint8 bOpen:1; uint8 bClose:1; uint8 bReliable:1; uint8 bPartial:1; // Not a complete bunch uint8 bPartialInitial:1; // The first bunch of a partial bunch uint8 bPartialFinal:1; // The final bunch of a partial bunch uint8 bHasPackageMapExports:1; // This bunch has networkGUID name/id pairs uint8 bHasMustBeMappedGUIDs:1; // This bunch has guids that must be mapped before we can process this bunch
EChannelCloseReason CloseReason;
TArray< FNetworkGUID > ExportNetGUIDs; // List of GUIDs that went out on this bunch TArray< uint64 > NetFieldExports; };
其实就是带一些所属 Channel 信息,是否为开启 Channel 或 关闭 Channel,Packet 信息(因为依赖于 Packet发送)需要处理丢包的情况,和分包信息,因为 UDP 超过一定大小会直接丢包,需要分包处理。至于 ExportNetGUIDs 和 NetFieldExports 可以先不管,这是属性同步时,同步 Actor 用的,现在这还处于登录状态,根本没有 Actor 需要同步。
classFInBunch : public FNetBitReader { public: int32 PacketId; // Note this must stay as first member variable in FInBunch for FInBunch(FInBunch, bool) to work FInBunch * Next; UNetConnection * Connection; int32 ChIndex; FName ChName; int32 ChSequence; uint8 bOpen:1; uint8 bClose:1; uint8 bReliable:1; uint8 bPartial:1; // Not a complete bunch uint8 bPartialInitial:1; // The first bunch of a partial bunch uint8 bPartialFinal:1; // The final bunch of a partial bunch uint8 bHasPackageMapExports:1; // This bunch has networkGUID name/id pairs uint8 bHasMustBeMappedGUIDs:1; // This bunch has guids that must be mapped before we can process this bunch uint8 bIgnoreRPCs:1;
classUChannel : public UObject { GENERATED_BODY() public: uint32 OpenAcked:1; // If OpenedLocally is true, this means we have acknowledged the packet we sent the bOpen bunch on. Otherwise, it means we have received the bOpen bunch from the server. uint32 Closing:1; // State of the channel. uint32 Dormant:1; // Channel is going dormant (it will close but the client will not destroy uint32 OpenTemporary:1; // Opened temporarily. uint32 Broken:1; // Has encountered errors and is ignoring subsequent packets. uint32 bTornOff:1; // Actor associated with this channel was torn off uint32 bPendingDormancy:1; // Channel wants to go dormant (it will check during tick if it can go dormant) uint32 bIsInDormancyHysteresis:1; // Channel wants to go dormant, and is otherwise ready to become dormant, but is waiting for a timeout before doing so. uint32 bPausedUntilReliableACK:1; // Unreliable property replication is paused until all reliables are ack'd. uint32 SentClosingBunch:1; // Set when sending closing bunch to avoid recursion in send-failure-close case. uint32 bPooled:1; // Set when placed in the actor channel pool uint32 OpenedLocally:1; // Whether channel was opened locally or by remote. uint32 bOpenedForCheckpoint:1; // Whether channel was opened by replay checkpoint recording int32 ChIndex; // Index of this channel. FPacketIdRange OpenPacketId; // If OpenedLocally is true, this is the packet we sent the bOpen bunch on. Otherwise, it's the packet we received the bOpen bunch on. FName ChName; // Name of the type of this channel. int32 NumInRec; // Number of packets in InRec. int32 NumOutRec; // Number of packets in OutRec. classFInBunch* InRec; // Incoming data with queued dependencies. classFOutBunch* OutRec; // Outgoing reliable unacked data. classFInBunch* InPartialBunch; // Partial bunch we are receiving (incoming partial bunches are appended to this) };
if ( Merge && Connection->LastOut.ChIndex == Bunch->ChIndex && Connection->LastOut.bReliable == Bunch->bReliable // Don't merge bunches of different reliability, since for example a reliable RPC can cause a bunch with properties to become reliable, introducing unnecessary latency for the properties. && Connection->AllowMerge && Connection->LastEnd.GetNumBits() && Connection->LastEnd.GetNumBits()==Connection->SendBuffer.GetNumBits() && Connection->LastOut.GetNumBits() + Bunch->GetNumBits() <= MAX_SINGLE_BUNCH_SIZE_BITS ) { // Merge. PreExistingBits = Connection->LastOut.GetNumBits(); Connection->LastOut.SerializeBits( Bunch->GetData(), Bunch->GetNumBits() ); Connection->LastOut.bOpen |= Bunch->bOpen; Connection->LastOut.bClose |= Bunch->bClose;
int32 GCVarNetPartialBunchReliableThreshold = 8; FAutoConsoleVariableRef CVarNetPartialBunchReliableThreshold( TEXT("net.PartialBunchReliableThreshold"), GCVarNetPartialBunchReliableThreshold, TEXT("If a bunch is broken up into this many partial bunches are more, we will send it reliable even if the original bunch was not reliable. Partial bunches are atonmic and must all make it over to be used"));
if ( !NextBunch->bHasPackageMapExports ) { NextBunch->bHasMustBeMappedGUIDs |= Bunch->bHasMustBeMappedGUIDs; }
if (OutgoingBunches.Num() > 1) { NextBunch->bPartial = 1; NextBunch->bPartialInitial = (PartialNum == 0 ? 1: 0); NextBunch->bPartialFinal = (PartialNum == OutgoingBunches.Num() - 1 ? 1: 0); NextBunch->bOpen &= (PartialNum == 0); // Only the first bunch should have the bOpen bit set NextBunch->bClose = (Bunch->bClose && (OutgoingBunches.Num()-1 == PartialNum)); // Only last bunch should have bClose bit set }
FOutBunch *ThisOutBunch = PrepBunch(NextBunch, OutBunch, Merge); // This handles queuing reliable bunches into the ack list