UE4 Connection关闭+重连

1,935 阅读5分钟

UE关闭Connection

玩家关闭链接, DS销毁Connection

客户端切换地图, 发送Close消息

客户端关闭链接, 并发送给DS关闭消息, 其中关键是切换地图, 切换地图时会清理NetDriver, 清理NetDriver会发送Close消息.

客户端执行disconnect命令, 主动断开连接. 关键函数:UGameInstance.HandleDisconnectCommand, 流程:

首先调用UEngine::SetClientTravel, 并将NextURL设置为?closed.

在下次tick时, 执行UEngine.TickWorldTravel时可以清晰看到, Context中, TravelURL为?Closed.

调用堆栈:

UEngine.TickWorldTravel 
--UEngine.Browse 
 |--UEngine.LoadMap
 | |--ShutdownWorldNetDriver(WorldContext.World());
 | | |--World->SetNetDriver(NULL);
 | | |--DestroyNamedNetDriver(World, NetDriver->NetDriverName);
 | | | |--DestroyNamedNetDriver_Local( GetWorldContextFromWorldChecked(InWorld), NetDriverName);
 | | | | |--NetDriver->SetWorld(NULL);
 | | | | |--NetDriver->Shutdown();
 | | | | | |-- // 关闭所有OpenChannel
 | | | | | |--for (UChannel* Channel : ServerConnection->OpenChannels)
 | | | | | |--    ActorChannel->CleanupReplicators();
 | | | | | |--
 | | | | | |--// 关闭Connection
 | | | | | |--ServerConnection->Close();
 | | | | | | |-- // 通过Channels[0]进行Close
 | | | | | | |--Channels[0]->Close(EChannelCloseReason::Destroyed);
 | | | | | | |--
 | | | | | | |--// 设置网络状态为USOCK_Closed
 | | | | | | |--State = USOCK_Closed;
 | | | | | | |--// 发送给DS, 注意, 这里不保证能到达.
 | | | | | | |--FlushNet();
 | | | | | |--ServerConnection->FlushNet();
 | | | | |--NetDriver->LowLevelDestroy();
 | | | | |--Context.World()->ClearNetDriver(NetDriver);

发送Close消息关键代码UNetConnection::Close:

Channel填充CloseBunch.

DS收到Close消息, 关闭Channel

堆栈:

发现为Close, 则进行清理.

由于关闭了Channels[0], 则证明整个Connection也需要随之关闭.

Connection关闭逻辑, 注意, 这里将UNetConnection.State设置为USOCK_Closed, 后面还要根据它对Connection进行清理. 关闭和清理是分阶段进行的, 即先关闭再清理.

Connection关闭后, 进行清理.

UIpNetDriver.TickDispatch.for.if.if 
--if ((Connection->State != USOCK_Open) || (!AllowPlayerPortUnreach))
--    Connection->CleanUp(); // 链接状态已关闭,主动清理Connection
 |--UIpConnection.CleanupResolutionSockets //清理Socket
 |--UNetConnection.CleanUp
 | |--// 清理所有ChildConnection
 | |--for (int32 i = 0; i < Children.Num(); i++)
 | |--{
 | |--  Children[i]->CleanUp();
 | |--}
 | |--Children.Empty();
 | |--
 | |--// 清理所有Channel
 | |--for (int32 i = OpenChannels.Num() - 1; i >= 0; i--)
 | |--{
 | |--  UChannel* OpenChannel = OpenChannels[i];
 | |--  if (OpenChannel != NULL)
 | |--  {
 | |--      OpenChannel->ConditionalCleanUp(true, EChannelCloseReason::Destroyed);
 | |--  }
 | |--}
 | |--
 | |--// 如果一个Channel即将别清理, 但是当前还有一些Bunch没有处理完成, 则放入UNetConnection.KeepProcessingActorChannelBunchesMap中,
 | |--// 等待发送完成后回收(ConditionalCleanUp). 
 | |--for (const TPair<FNetworkGUID, TArray<UActorChannel*>>& MapKeyValuePair : KeepProcessingActorChannelBunchesMap)
 | |--{
 | |--  for (UActorChannel* CurChannel : MapKeyValuePair.Value)
 | |--  {
 | |--      CurChannel->ConditionalCleanUp(true, EChannelCloseReason::Destroyed);
 | |--  }
 | |--}
 | |--
 | |--CleanupDormantActorState();
 | |--SetClientLoginState(EClientLoginState::CleanedUp);

ECONNRESET(WSAECONNRESET)

客户端只发了一个Close消息, 为什么后续又跟着一个消息, 该消息大小为0, Error类型为SE_ECONNRESET.

ECONNRESET(WSAECONNRESET)
对于TCP
    远程主机已强制关闭,发送数据,远程主机protocol stack回应RST
对于UDP
    在Windows系统上,双方正在进udp数据交互,另一端关闭了,发送方会收到“ICMP Port Unreached",protocol向上报WSAECONNRESET。这时应用层一般不做关闭动作(除非有特殊的需求),因为这仅仅另外一端的 UDP socket不存在了,本端的udp socket还是完全合法的。
    有一点要注意的是,在Linux上,应用层不会得到ECONNRESET

综上, DS在Window上时, 收到客户端发来的Close消息后, 紧跟着又收到了Window额外的Unreached消息, 对Connection进行CleanUp.

DS端超时, 主动关闭Connection

DS设置连接保活时间:UNetDriver.ConnectionTimeout.

UNetConnection处理Timeout, 关键函数:UNetConnection::HandleConnectionTimeout. 广播时间, 并关闭自身.

注意, 当前时间和上次收包时间LastReceiveRealtime的差.

  1. 对于客户端而言, 如果长时间没收到DS的包, 则抛出Timeout事件.
  2. 对于DS而言, 如果长时间没有收到客户端的包则抛出Timeout事件.

设置连接超时时间

[/Script/OnlineSubsystemUtils.IpNetDriver]
;ReplicationDriverClassName="/Script/ReplicationGraph.BasicReplicationGraph"
ReplicationDriverClassName="/Script/ALSV.BaseReplicationGraph"
NetConnectionClassName="/Script/ALSV.BaseConnection"
NetServerMaxTickRate=1000
; 连接超时时间
ConnectionTimeout=600.0

注意, 编辑器下是NoTimeout的, 所以要触发Timeout, 需要把如下代码注掉. 关键函数UNetDriver::PostInitProperties, 注释掉关于UNetDriver.bNoTimeouts的代码.

关键函数UNetConnection.HandleConnectionTimeout

关键函数UNetConnection.HandleConnectionTimeout, 逻辑上要用的.

UIpConnection处理Timeout, 关键函数UIpConnection::HandleConnectionTimeout.

项目逻辑处理Timeout. 在项目中, 如果某个Connection发生timeout, 则要抛出事件给playerController进行处理.

本地测试

// 客户端输入指令`net pktloss=100`, 则DS会触发Timeout.
net pktloss=100
​
// DS输入指令ServerCmd "net pktloss=100", 则客户端会触发Timeout.
ServerCmd "net pktloss=100"

DS主动踢掉玩家

我们一般会有这样的需求, 因为某些情况, 需要将某些玩家主动踢下线. 比如当游戏结束后, 踢掉所有人. 人物死亡后, 清理玩家, 主动踢下线.

将玩家踢下线, 需要清理其Connection, PlayerController, Pawn等信息, 直接借用UE本身Connection销毁逻辑.

DS端结束, 主动清理所有链接

Driver都要销毁了, 自然要清理所有Connection. 关键函数UNetDriver::FinishDestroy, 这里会主动清理所有Connection.

断线重连

当Connection销毁时候, 会附带销毁对应的PlayerController. 如果需要断线重连, 在Connection销毁的时候可以将PlayerController保留, 等待Connection重新建立, 再进行重新使用. 关键函数:APlayerController::OnNetCleanup.

关键函数:OnNetCleanup

调用堆栈:

这里需要改成查找对应的PlayerController. 关键函数AGameModeBase::Login.

PlayerController关联UPlayer(Connection)

关键函数:UWorld.SpawnPlayActor. 调用逻辑:

PlayerController和Player(Connection)互相关联, 关键函数:APlayerController::SetPlayer.

实现重连逻辑

Connection关闭时, 不再销毁PlayerController, Pawn等Actor. 重写APlayerController::OnNetCleanup. 在ABasePlayerController::OnNetCleanup中, 针对默认清理情况, 只设置PlayerNetConnection为空.

PlayerConnection重用. 优先查找, 此处使用Name进行查找, 其实应该使用UniqueId.

重连命令

输入命令: open 127.0.0.1:7777?PlayerName=001
如果第一次, 则创建连接, 其中PlayerName为001.
如果不是第一次,则创建连接, 重新使用PlayerName为001的PlayerController和PlayerState等信息.

在函数UPendingNetGame.NotifyControlMessage中额外处理URL参数:PlayerName.

在PlayerController销毁时, 是怎么连带Pawn一起销毁的?

APlayerController::Destroyed代码中可以看出, 在PlayerController销毁时, 如果Player(即Connection)为空并且当前为DS, 则连带Pawn一同销毁.

针对超时情况, 那如果长时间没发消息, 会被动关闭Connection?

不会. 如果超过一定时间没有发送过任何消息, 则会调用UNetConnection.FlushNet. 关键变量UNetDriver.KeepAliveTime(默认0.2s)

即使空包(有PacketHeader, 但是PacketBody中没有Bunch), 也得发.

如果没有ECONNRESET消息, DS怎么清理Connection呢?

修改源码, 不处理SE_ECONNRESET

DS在UNetDriver.TickDispatch时, 遍历所有ClientConnection, 然后关闭其中USOCK_Closed状态的.

如何使用自定义Connection

UIpNetDriver在创建Connection时, 使用ClassUNetDriver.NetConnectionClass. 关键函数UIpNetDriver::InitConnect.

而其来自UNetDriver.NetConnectionClassName, 关键函数UNetDriver::InitConnectionClass.

所以, 我们只要配置UNetDriver.NetConnectionClassName即可. 在DefaultEngine.ini中配置.

[/Script/OnlineSubsystemUtils.IpNetDriver]
;ReplicationDriverClassName="/Script/ReplicationGraph.BasicReplicationGraph"
ReplicationDriverClassName="/Script/ALSV.BaseReplicationGraph"
NetConnectionClassName="/Script/ALSV.BaseConnection"
NetServerMaxTickRate=1000
ConnectionTimeout=600.0