👯 Unreal 5 多人网络同步源码梳理(基础)
2025-06-08
笔者撰写本文使用的 UE 版本为 5.5.4
UE 网络同步系统
NetDriver
,
NetConnections
和
Channels
-
Game NetDriver :负责正常的游戏网络通信 -
Demo NetDriver : 负责记录或回放记录的游戏数据,这也是提供回放功能的地方 -
Beacon NetDriver :负责正常游戏外网络通信
-
Control Channel :用于发送监控当前连接的数据,比如当前连接是否需要关闭等 -
Voice Channel :用于在客户端和服务端之间发送声音数据 -
Unique Actor Channel 在每个从服务端复制到客户端的 Actor 都存在
Game Net Drivers
,
Net Connections
, 和
Channels
初始化连接 / 握手流程(Initiating Connections / Handshaking Flow)
开始设置与握手(Startup and Handshaking)
UWorld / UPendingNetGame / AGameModeBase Startup and Handshaking
使用挑战-响应(challenge-response)机制
-
客户端的 UPendingNetGame::SendInitialJoin 发送 NMT_Hello -
服务器的 UWorld::NotifyControlMessage 接收 NMT_Hello ,并发送 NMT_Challenge -
客户端的 UPendingNetGame::NotifyControlMessage 接收 NMT_Challenge 并在 NMT_Login 中发回数据 -
服务器的 UWorld::NotifyControlMessage 接收 NMT_Login ,验证质询数据(challenge data),并调用 AGameModeBase::PreLogin -
如果 PreLogin 并没有报任何错误,服务器会调用 UWorld::WelcomePlayer ,这个函数会调用 AGameModeBase::GameWelcomePlayer 并发送含有地图信息的 NMT_Welcome -
客户端的 UPendingNetGame::NotifyControlMessage 接收 NMT_Welcome ,并读取地图信息(以便于后续加载),然后发送 NMT_NetSpeed 信息,这里包含了客户端配置了的网络速度(Net Speed) -
服务器的 UWorld::NotifyControlMessage 接收 NMT_NetSpeed ,然后调整到正确的网络速度
重新建立丢失的连接(Reestablishing Lost Connections)
-
initiated:发起
-
albeit:尽管
数据传输(Data Transmission)
-
客户端调用 Server_RPC -
这个请求将会通过 NetDriver 和 NetConnection 被转发(forwarded)到对应 Actor 的 Actor Channel -
这个 Actor Channel 会序列化 RPC 标识符和参数到一个带有这个 Actor Channel 的 ID 的束中 -
Actor Channel 向 NetConnection 请求发送束 -
接着, NetConnection 将会组合这些数据到一个包中,被发送到服务器 -
在服务器中,包会被 NetDriver 接收 -
NetDriver 讲话检查包的源地址,并将包交给适当的 NetConnection -
对应的 NetConnection 会把包拆称束 -
NetConnection 会使用束包含的 Actor Channel ID 来把束发送到对应的 Actor Channel、 -
Actor Channel 会拆开束,查看其包含的 RPC 数据,然后使用 RPC ID 和被序列化的参数去调用 Actor 的函数
可靠性和重新传输(Reliability and Retransmission)
接收侧检测丢包(Detecting Incoming Dropped Packets)
发出侧检测丢包(Detecting Outgoing Dropped Packets)
重新发送丢失数据(Resending Missing Data)
重要概念
源代码梳理 - Tick 阶段
-
ServerConnection 与服务端的连接,为 nullptr 说明当前运行在服务器上 -
ClientConnections 与客户端的连接列表 -
MappedClientConnections IP 到连接的表
-
World::Tick -
OnTickDispatch -
PrePhysics -
ActorTick -
OnPostTickDispatch -
OnPreTickFlush -
OnTickFlush -
OnPostTickFlush
-
OnTickDispatch -
OnPostTickDispatch -
OnTickFlush -
OnPostTickFlush
// NetDriver.cpp
void UNetDriver::RegisterTickEvents(class UWorld* InWorld)
{
if (InWorld)
{
TickDispatchDelegateHandle = InWorld->OnTickDispatch().AddUObject(this, &UNetDriver::InternalTickDispatch);
PostTickDispatchDelegateHandle = InWorld->OnPostTickDispatch().AddUObject(this, &UNetDriver::PostTickDispatch);
TickFlushDelegateHandle = InWorld->OnTickFlush().AddUObject(this, &UNetDriver::InternalTickFlush);
PostTickFlushDelegateHandle = InWorld->OnPostTickFlush().AddUObject(this, &UNetDriver::PostTickFlush);
}
}
调用阶段 | 主要函数(UNetDriver) | 备注 |
OnTickDispatch | TickDispatch | 通过 InternalTickDispatch 调用,Internal 中间层目的是防止非法递归调用 |
OnPostTickDispatch | PostTickDispatch | |
OnTickFlush | TickFlush | 通过 InternalTickFlush 调用,Internal 作用如上<br> |
OnPostTickFlush | PostTickFlush | |
下文会按顺序详细介绍上述四个函数。 |
TickDispatch
// NetDriver.cpp/UNetDriver::TickDispatch
void UNetDriver::TickDispatch( float DeltaTime )
{
SendCycles=0;
// Manage realtime values
{
const double CurrentRealtime = FPlatformTime::Seconds();
LastTickDispatchRealtime = CurrentRealtime;
// Check to see if too much time is passing between ticks
// Setting this to somewhat large value for now, but small enough to catch blocking calls that are causing timeouts
constexpr float TickLogThreshold = 5.0f;
const float DeltaRealtime = CurrentRealtime - LastTickDispatchRealtime;
bDidHitchLastFrame = (DeltaTime > TickLogThreshold || DeltaRealtime > TickLogThreshold);
if (bDidHitchLastFrame)
{
UE_LOG( LogNet, Log, TEXT( "UNetDriver::TickDispatch: Very long time between ticks. DeltaTime: %2.2f, Realtime: %2.2f. %s" ), DeltaTime, DeltaRealtime, *GetName() );
}
}
// Get new time.
ElapsedTime += DeltaTime;
IncomingBunchProcessingElapsedFrameTimeMS = 0.0f;
// Checks for standby cheats if enabled
UpdateStandbyCheatStatus();
ResetNetworkMetrics();
-
ResetNetworkMetrics 负责重置网络指标
if (ServerConnection == nullptr)
{
// Delete any straggler connections
{
QUICK_SCOPE_CYCLE_COUNTER(UNetDriver_TickDispatch_CheckClientConnectionCleanup)
for (int32 ConnIdx=ClientConnections.Num()-1; ConnIdx>=0; ConnIdx--)
{
UNetConnection* CurConn = ClientConnections[ConnIdx];
if (IsValid(CurConn))
{
if (CurConn->GetConnectionState() == USOCK_Closed)
{
CurConn->CleanUp();
}
else
{
CurConn->PreTickDispatch();
}
}
}
}
// Clean up recently disconnected client tracking
if (RecentlyDisconnectedClients.Num() > 0)
{
int32 NumToRemove = 0;
for (const FDisconnectedClient& CurElement : RecentlyDisconnectedClients)
{
if ((LastTickDispatchRealtime - CurElement.DisconnectTime) >= RecentlyDisconnectedTrackingTime)
{
verify(MappedClientConnections.Remove(CurElement.Address) == 1);
NumToRemove++;
}
else
{
break;
}
}
if (NumToRemove > 0)
{
RecentlyDisconnectedClients.RemoveAt(0, NumToRemove);
}
}
}
else if (IsValid(ServerConnection))
{
ServerConnection->PreTickDispatch();
}
#if RPC_CSV_TRACKER
GReceiveRPCTimingEnabled = (NetDriverName == NAME_GameNetDriver && ShouldEnableScopeSecondsTimers()) && (ServerConnection==nullptr);
#endif
}
void UIpNetDriver::TickDispatch(float DeltaTime)
{
LLM_SCOPE_BYTAG(NetDriver);
Super::TickDispatch( DeltaTime );
const bool bUsingReceiveThread = SocketReceiveThreadRunnable.IsValid();
if (bUsingReceiveThread)
{
SocketReceiveThreadRunnable->PumpOwnerEventQueue();
}
#if !UE_BUILD_SHIPPING
...
#endif
...
// 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();
if (bOk)
{
// Immediately stop processing (continuing to next receive), for empty packets (usually a DDoS)
if (ReceivedPacket.DataView.NumBits() == 0)
{
DDoS.IncBadPacketCounter();
continue;
}
FPacketAudit::NotifyLowLevelReceive((uint8*)ReceivedPacket.DataView.GetData(), ReceivedPacket.DataView.NumBytes());
}
-
bOk 代表当前数据包是否可被处理
else
{
if (IsRecvFailBlocking(ReceivedPacket.Error))
{
break;
}
else if (ReceivedPacket.Error != SE_ECONNRESET && ReceivedPacket.Error != SE_UDP_ERR_PORT_UNREACH)
{
//...log
continue;
}
}
if (MyServerConnection)
{
if (MyServerConnection->RemoteAddr->CompareEndpoints(*FromAddr))
{
Connection = MyServerConnection;
}
else //...log
}
if (Connection == nullptr) { ... }
if( bOk == false ) { if( Connection ) { ... } }
else {
bool bIgnorePacket = false;
// If we didn't find a client connection, maybe create a new one.
if (Connection == nullptr) { ... }
}
}
if (NewIPHashes.Num() > 0) TickNewIPTracking(DeltaTime);
DDoS.PostFrameReceive();
}
PostTickDispatch
void UNetDriver::PostTickDispatch()
{
// Flush out of order packet caches for connections that did not receive the missing packets during TickDispatch
if (ServerConnection != nullptr)
{
if (IsValid(ServerConnection))
{
ServerConnection->PostTickDispatch();
}
}
TArray<UNetConnection*> ClientConnCopy = ClientConnections;
for (UNetConnection* CurConn : ClientConnCopy)
{
if (IsValid(CurConn))
{
CurConn->PostTickDispatch();
}
}
#if UE_WITH_IRIS
PostDispatchSendUpdate();
#endif
if (ReplicationDriver)
{
ReplicationDriver->PostTickDispatch();
}
-
ReplicationDriver->PostTickDispatch() 是一个空实现,暂时没有任何行为
if (GReceiveRPCTimingEnabled)
{
GRPCCSVTracker.EndTickDispatch();
GReceiveRPCTimingEnabled = false;
}
if (bPendingDestruction)
{
if (World)
{
GEngine->DestroyNamedNetDriver(World, NetDriverName);
}
else
{
UE_LOG(LogNet, Error, TEXT("NetDriver %s pending destruction without valid world."), *NetDriverName.ToString());
}
bPendingDestruction = false;
}
}
TickFlush
void UNetDriver::TickFlush(float DeltaSeconds)
{
// ...trace
bool bEnableTimer = (NetDriverName == NAME_GameNetDriver) && ShouldEnableScopeSecondsTimers();
if (bEnableTimer)
{
GTickFlushGameDriverTimeSeconds = 0.0;
}
FSimpleScopeSecondsCounter ScopedTimer(GTickFlushGameDriverTimeSeconds, bEnableTimer);
if (IsServer() && ClientConnections.Num() > 0 && !bSkipServerReplicateActors)
{
// Update all clients.
#if WITH_SERVER_CODE
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(ServerReplicateActors);
#if UE_WITH_IRIS
...
#endif // UE_WITH_IRIS
{
ServerReplicateActors(DeltaSeconds);
}
#endif // WITH_SERVER_CODE
}
#if UE_WITH_IRIS
...
#endif // UE_WITH_IRIS
ServerReplicateActors()
int32 UNetDriver::ServerReplicateActors(float DeltaSeconds)
{
SCOPE_CYCLE_COUNTER(STAT_NetServerRepActorsTime);
#if WITH_SERVER_CODE
if ( ClientConnections.Num() == 0 ) { return 0; }
GetMetrics()->SetInt(UE::Net::Metric::NumReplicatedActors,0 );
GetMetrics()->SetInt(UE::Net::Metric::NumReplicatedActorBytes, 0);
#if CSV_PROFILER_STATS
FScopedNetDriverStats NetDriverStats(this);
GNumClientConnections = ClientConnections.Num();
#endif
if (ReplicationDriver)
{
return ReplicationDriver->ServerReplicateActors(DeltaSeconds);
}
check( World );
// Bump the ReplicationFrame value to invalidate any properties marked as "unchanged" for this frame.
ReplicationFrame++;
int32 Updated = 0;
const int32 NumClientsToTick = ServerReplicateActors_PrepConnections( DeltaSeconds );
if ( NumClientsToTick == 0 ) { return 0; } // No connections are ready this frame
-
ServerReplicateActors_PrepConnections 有两个主要功能,一是确定本帧要处理的连接的数量,也就是 NumClientsToTick ;二是轮转连接处理顺序,防止有连接不被处理
AWorldSettings* WorldSettings = World->GetWorldSettings();
bool bCPUSaturated = false;
float ServerTickTime = GEngine->GetMaxTickRate( DeltaSeconds );
if ( ServerTickTime == 0.f )
{
ServerTickTime = DeltaSeconds;
}
else
{
ServerTickTime = 1.f/ServerTickTime;
bCPUSaturated = DeltaSeconds > 1.2f * ServerTickTime;
}
TArray<FNetworkObjectInfo*> ConsiderList;
ConsiderList.Reserve( GetNetworkObjectList().GetActiveObjects().Num() );
// Build the consider list (actors that are ready to replicate)
ServerReplicateActors_BuildConsiderList( ConsiderList, ServerTickTime );
TSet<UNetConnection*> ConnectionsToClose;
FMemMark Mark( FMemStack::Get() );
if (OnPreConsiderListUpdateOverride.IsBound())
{
OnPreConsiderListUpdateOverride.Execute({ DeltaSeconds, nullptr, bCPUSaturated }, Updated, ConsiderList);
}
-
ServerTickTime 代表服务器 Tick 的间隔,如果设置了最大 Tick 频率( ServerTickRate ),间隔就是最大 Tick 频率的倒数,用于待同步列表的构建 -
ServerReplicateActors_BuildConsiderList 构建待同步列表,这会剔除掉睡眠的 Actor
for ( int32 i=0; i < ClientConnections.Num(); i++ )
{
UNetConnection* Connection = ClientConnections[i];
check(Connection);
// net.DormancyValidate can be set to 2 to validate all dormant actors against last known state before going dormant
if ( GNetDormancyValidate == 2 )
{
...
}
// if this client shouldn't be ticked this frame
if (i >= NumClientsToTick)
{
// UE_LOG(LogNet, Log, TEXT("skipping update to %s"),*Connection->GetName());
// then mark each considered actor as bPendingNetUpdate so that they will be considered again the next frame when the connection is actually ticked
for (int32 ConsiderIdx = 0; ConsiderIdx < ConsiderList.Num(); ConsiderIdx++)
{
AActor *Actor = ConsiderList[ConsiderIdx]->Actor;
// if the actor hasn't already been flagged by another connection,
if (Actor != NULL && !ConsiderList[ConsiderIdx]->bPendingNetUpdate)
{
// find the channel
UActorChannel *Channel = Connection->FindActorChannelRef(ConsiderList[ConsiderIdx]->WeakActor);
// and if the channel last update time doesn't match the last net update time for the actor
if (Channel != NULL && Channel->LastUpdateTime < ConsiderList[ConsiderIdx]->LastNetUpdateTimestamp)
{
//UE_LOG(LogNet, Log, TEXT("flagging %s for a future update"),*Actor->GetName());
// flag it for a pending update
ConsiderList[ConsiderIdx]->bPendingNetUpdate = true;
}
}
}
// clear the time sensitive flag to avoid sending an extra packet to this connection
Connection->TimeSensitive = false;
}
else if (Connection->ViewTarget)
{
const int32 LocalNumSaturated = GNumSaturatedConnections;
// Make a list of viewers this connection should consider (this connection and children of this connection)
TArray<FNetViewer>& ConnectionViewers = WorldSettings->ReplicationViewers;
ConnectionViewers.Reset();
new( ConnectionViewers )FNetViewer( Connection, DeltaSeconds );
for ( int32 ViewerIndex = 0; ViewerIndex < Connection->Children.Num(); ViewerIndex++ )
{
if ( Connection->Children[ViewerIndex]->ViewTarget != NULL )
{
new( ConnectionViewers )FNetViewer( Connection->Children[ViewerIndex], DeltaSeconds );
}
}
-
ViewTarget 是一个 AActor 指针,通常是当前的 Actor 的控制者,例如 PlayerController ,用于获取当前 Actor 的物理位置 -
Viewer :本质是一个 FNetViewer 结构体,它包含了网络连接指针、视角、位置坐标等,用于快速判断一个物体是否在另一个物体的视野范围内,这些数据通常是关联于某个 Actor 的 -
new (ConnectionViewers) FNetViewer(Connection, DeltaSeconds); 语法在 ConnectionViewers 上分配内存给一个新的 FNetViewer
// send ClientAdjustment if necessary
// we do this here so that we send a maximum of one per packet to that client; there is no value in stacking additional corrections
if ( Connection->PlayerController )
{
Connection->PlayerController->SendClientAdjustment();
}
for ( int32 ChildIdx = 0; ChildIdx < Connection->Children.Num(); ChildIdx++ )
{
if ( Connection->Children[ChildIdx]->PlayerController != NULL )
{
Connection->Children[ChildIdx]->PlayerController->SendClientAdjustment();
}
}
FMemMark RelevantActorMark(FMemStack::Get());
const bool bProcessConsiderListIsBound = OnProcessConsiderListOverride.IsBound();
if (bProcessConsiderListIsBound)
{
OnProcessConsiderListOverride.Execute( { DeltaSeconds, Connection, bCPUSaturated }, Updated, ConsiderList );
}
-
FMemMark 是内存栈的管理标记工具,它通过移动栈顶指针来分配内存,并可以快速释放。具体来说,它会记录当前的栈顶指针,调用 Pop 时,会将栈顶设置回开始记录的栈顶,来实现快速“释放”内存的效果。 -
OnProcessConsiderListOverride 是自定义同步逻辑相关的事件,可以允许开发者自定义 ConsiderList
if (!bProcessConsiderListIsBound)
{
FActorPriority* PriorityList = NULL;
FActorPriority** PriorityActors = NULL;
// Get a sorted list of actors for this connection
const int32 FinalSortedCount = ServerReplicateActors_PrioritizeActors(Connection, ConnectionViewers, ConsiderList, bCPUSaturated, PriorityList, PriorityActors);
// Process the sorted list of actors for this connection
TInterval<int32> ActorsIndexRange(0, FinalSortedCount);
const int32 LastProcessedActor = ServerReplicateActors_ProcessPrioritizedActorsRange(Connection, ConnectionViewers, PriorityActors, ActorsIndexRange, Updated);
ServerReplicateActors_MarkRelevantActors(Connection, ConnectionViewers, LastProcessedActor, FinalSortedCount, PriorityActors);
}
-
ServerReplicateActors_ProcessPrioritizedActorsRange 会处理高优先级的 Actors 并返回没有处理完的 Actors 的数量 -
ServerReplicateActors_MarkRelevantActors 会利用剩下的 Actor 数量,将相关的 Actor 标记为相关,并留到下帧优先处理
-
在每 1.0 + 0.5R 秒内可见的 Actor -
使用 IsActorRelevantToConnection 函数的返回结果
RelevantActorMark.Pop();
ConnectionViewers.Reset();
Connection->LastProcessedFrame = ReplicationFrame;
const bool bWasSaturated = GNumSaturatedConnections > LocalNumSaturated;
Connection->TrackReplicationForAnalytics(bWasSaturated);
}
if (Connection->GetPendingCloseDueToReplicationFailure())
{
ConnectionsToClose.Add(Connection);
}
}
if (OnPostConsiderListUpdateOverride.IsBound())
{
OnPostConsiderListUpdateOverride.ExecuteIfBound( { DeltaSeconds, nullptr, bCPUSaturated }, Updated, ConsiderList );
}
// shuffle the list of connections if not all connections were ticked
if (NumClientsToTick < ClientConnections.Num())
{
int32 NumConnectionsToMove = NumClientsToTick;
while (NumConnectionsToMove > 0)
{
// move all the ticked connections to the end of the list so that the other connections are considered first for the next frame
UNetConnection *Connection = ClientConnections[0];
ClientConnections.RemoveAt(0,1);
ClientConnections.Add(Connection);
NumConnectionsToMove--;
}
}
Mark.Pop();
#if NET_DEBUG_RELEVANT_ACTORS
//...debug
#endif // NET_DEBUG_RELEVANT_ACTORS
for (UNetConnection* ConnectionToClose : ConnectionsToClose)
{
ConnectionToClose->Close();
}
return Updated;
#else
return 0;
#endif // WITH_SERVER_CODE
}
// Reset queued bunch amortization timer
ProcessQueuedBunchesCurrentFrameMilliseconds = 0.0f;
// Poll all sockets.
if( ServerConnection )
{
// Queue client voice packets in the server's voice channel
ProcessLocalClientPackets();
ServerConnection->Tick(DeltaSeconds);
}
else
{
// Queue up any voice packets the server has locally
ProcessLocalServerPackets();
}
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_NetDriver_TickClientConnections)
for (UNetConnection* Connection : ClientConnections)
{
Connection->Tick(DeltaSeconds);
}
}
if (ConnectionlessHandler.IsValid())
{
ConnectionlessHandler->Tick(DeltaSeconds);
FlushHandler();
}
-
ConnectionlessHandler 是一个 TUniquePtr<PacketHandler> ,在 NetDriver::InitConnectionlessHandler() 被初始化,其 Tick 函数会安全地发送其队列里的包 -
FlushHandler 会强制发送所有缓存的无建立连接的包
//... debug
//... Iris
UpdateNetworkStats();
// Send the current values of metrics to all of the metrics listeners.
GetMetrics()->ProcessListeners();
-
UpdateNetworkStats() 主要作用是更新基础统计,如带宽利用率、丢包率等 -
GetMetrics() 会返回 UNetworkMetricsDatabase 的智能指针,这个类管理着所有网络指标监听器(监听者模式),指标变化时会调用监听器,用于执行可视化、性能检测等行为,同时允许开发者使用;在 UNetworkMetricsDatabase.h 可以看到详细的文档注释(包含使用案例)
// Update the lag state
UpdateNetworkLagState();
}
UNetConnection::Tick
if (!IsInternalAck() && MaxNetTickRateFloat < EngineTickRate && DesiredTickRate > 0.0f)
{
const float MinNetFrameTime = 1.0f/DesiredTickRate;
if (FrameTime < MinNetFrameTime)
{
return;
}
}
if (IsInternalAck())
{
...
}
if ( CurrentRealtimeSeconds - StatUpdateTime > StatPeriod )
{
...
// Add TotalPacketsLost to total since InTotalPackets only counts ACK packets
InPacketsLossPercentage.UpdateLoss(InPacketsLost, InTotalPackets + InTotalPacketsLost, StatPeriodCount);
// Using OutTotalNotifiedPackets so we do not count packets that are still in transit.
OutPacketsLossPercentage.UpdateLoss(OutPacketsLost, OutTotalNotifiedPackets, StatPeriodCount);
...
}
if (bConnectionPendingCloseDueToSocketSendFailure)
{
Close(ENetCloseResult::SocketSendFailure);
bConnectionPendingCloseDueToSocketSendFailure = false;
// early out
return;
}
const float Timeout = GetTimeoutValue();
const bool bReceiveTimedOut = (CurrentRealtimeSeconds - LastReceiveRealtime) > Timeout;
const bool bGracefulCloseTimedOut = (GetConnectionState() == USOCK_Closing) && (DriverElapsedTime > bGracefulCloseTimedOut);
if (bReceiveTimedOut || bGracefulCloseTimedOut)
{
...
}
-
数据接收超时( bReceiveTimedOut )的触发条件是当前时间与最后一次收到有效数据包的时间差超过阈值 Timeout ,除了会输出日志外,还会关闭连接和尝试自动重连 -
优雅关闭超时( bGracefulCloseTimedOut )的触发条件是当前连接处于关闭流程中( USOCK_Closing )并且超过了优雅超时时限( bGracefulCloseTimedOut )。这是由正常的关闭流程,不立刻关闭时是为了等待未完成的可靠数据传输等任务完成;如果优雅关闭超时了,则会强制关闭
if (CVarTickAllOpenChannels.GetValueOnAnyThread() == 0) {
for( int32 i=ChannelsToTick.Num()-1; i>=0; i-- ) {
ChannelsToTick[i]->Tick();
if (ChannelsToTick[i]->CanStopTicking())
ChannelsToTick.RemoveAt(i);
}
}
else {
for (int32 i = OpenChannels.Num() - 1; i >= 0; i--) {
if (OpenChannels[i]) OpenChannels[i]->Tick();
else UE_LOG(LogNet, Warning, TEXT("UNetConnection::Tick: null channel in OpenChannels array. %s"), *Describe());
}
}
-
CVarTickAllOpenChannels 配置是否 Tick 所有 Channels
for ( auto ProcessingActorMapIter = KeepProcessingActorChannelBunchesMap.CreateIterator(); ProcessingActorMapIter; ++ProcessingActorMapIter ) {
...
}
-
KeepProcessingActorChannelBunchesMap 是想要被完全关闭的 Channels 的表,这些 Channel 虽然被请求了完全关闭,但是还有数据未处理完成,因此需要在这里继续处理,直到完成后才关闭
if ( TimeSensitive || (Driver->GetElapsedTime() - LastSendTime) > Driver->KeepAliveTime)
{
bool bHandlerHandshakeComplete = !Handler.IsValid() || Handler->IsFullyInitialized();
// Delay any packet sends on the server, until we've verified that a packet has been received from the client.
if (bHandlerHandshakeComplete && HasReceivedClientPacket())
{
FlushNet();
}
}
// Resend any queued up raw packets (these come from the reliability handler)
BufferedPacket* ResendPacket = Handler->GetQueuedRawPacket();
if (ResendPacket && Driver->IsNetResourceValid())
{
Handler->SetRawSend(true);
while (ResendPacket != nullptr)
{
LowLevelSend(ResendPacket->Data, ResendPacket->CountBits, ResendPacket->Traits);
ResendPacket = Handler->GetQueuedRawPacket();
}
Handler->SetRawSend(false);
}
...
PostTickFlush
void UNetDriver::PostTickFlush()
{
#if UE_WITH_IRIS
...
#endif // UE_WITH_IRIS
if (World && !bSkipClearVoicePackets)
{
UOnlineEngineInterface::Get()->ClearVoicePackets(World);
}
if (bPendingDestruction)
{
if (World)
{
GEngine->DestroyNamedNetDriver(World, NetDriverName);
}
else
{
UE_LOG(LogNet, Error, TEXT("NetDriver %s pending destruction without valid world."), *NetDriverName.ToString());
}
bPendingDestruction = false;
}
}
小结
握手
-
客户端的 UPendingNetGame::SendInitialJoin 发送 NMT_Hello -
服务器的 UWorld::NotifyControlMessage 接收 NMT_Hello ,并发送 NMT_Challenge -
客户端的 UPendingNetGame::NotifyControlMessage 接收 NMT_Challenge 并在 NMT_Login 中发回数据 -
服务器的 UWorld::NotifyControlMessage 接收 NMT_Login ,验证质询数据(challenge data),并调用 AGameModeBase::PreLogin -
如果 PreLogin 并没有报任何错误,服务器会调用 UWorld::WelcomePlayer ,这个函数会调用 AGameModeBase::GameWelcomePlayer 并发送含有地图信息的 NMT_Welcome -
客户端的 UPendingNetGame::NotifyControlMessage 接收 NMT_Welcome ,并读取地图信息(以便于后续加载),然后发送 NMT_NetSpeed 信息,这里包含了客户端配置了的网络速度(Net Speed) -
服务器的 UWorld::NotifyControlMessage 接收 NMT_NetSpeed ,然后调整到正确的网络速度
-
一个 URL 是 FURL 类型,它携带了网络协议、地图路径等信息 -
在客户端主动切换关卡时( ClientTravel 或 OpenLevel ),会重新建立连接 -
在服务器主导的关卡切换时,不需要重新建立连接
void UEngine::SetClientTravel( UWorld *InWorld, const TCHAR* NextURL, ETravelType InTravelType )
{
FWorldContext &Context = GetWorldContextFromWorldChecked(InWorld);
Context.TravelURL = NextURL;
Context.TravelType = InTravelType;
...
}
-
UGameInstance::StartGameInstance() :这个函数会使用游戏默认 URL 进行 Browse -
UEngine::TickWorldTravel() :这个函数会获取传入的世界上下文( FWorldContext )的最新的一个 URL 进行 Browse ,如果失败,会尝试使用默认的 URL,也就是下面这条的函数 -
UEngine::BrowseToDefaultMap() :这个函数会在 UEngine::TickWorldTravel() 被调用,试图加载默认地图
WorldContext.PendingNetGame = NewObject<UPendingNetGame>();
WorldContext.PendingNetGame->Initialize(URL); //-V595
WorldContext.PendingNetGame->InitNetDriver(); //-V595
// PendingNetGame.cpp
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();
}
}
...
void UPendingNetGame::BeginHandshake()
{
// Kick off the connection handshake
UNetConnection* ServerConn = NetDriver->ServerConnection;
if (ServerConn->Handler.IsValid()) { ... }
else
{
SendInitialJoin();
}
}
-
UPendingNetGame::NotifyControlMessage 会在 UControlChannel::ReceivedBunch 中被调用,经过层层转发,最顶层的触发时机为 IpNetDriver 的 TickDispatch -
UWorld::NotifyControlMessage 经过层层转发,在顶层也是在 UWorld::OnTickDispatch 中被触发
小结