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的差.
- 对于客户端而言, 如果长时间没收到DS的包, 则抛出Timeout事件.
- 对于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中, 针对默认清理情况, 只设置Player和NetConnection为空.
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