本文主要剖析 UE5 中客户端是如何与DS建立连接,构建基础 Channel,以及无状态握手流程。
连接建立
服务端监听流程
UGameInstance::EnableListenServer
调用 UWorld::Listen
去创建 UNetDriver 对象。
UNetDriver 默认有两个子类, IpNetDriver 和 DemoNetDriver 后面一个用于回放。
根据要创建的 NetDriver 名, 查找并构造该 Driver, 并存入 World->Context.ActiveNetDrivers,这里面很多地方会用到,比如需要设置某个 Actor 冻结时,就需要通知各个 NetDriver。
1 | UNetDriver* CreateNetDriver_Local(UEngine* Engine, FWorldContext& Context, FName NetDriverDefinition, FName InNetDriverName) |
随后将所有的 NetActor 加入到相同 NetDriverName 的几个集合里去,
1 | void UNetDriver::SetWorld(class UWorld* InWorld) |
开始监听端口 NetDriver->InitListen( this, InURL, bReuseAddressAndPort, Error )
此处使用的是 IpNetDriver::InitListen。
1 | bool UIpNetDriver::InitListen( FNetworkNotify* InNotify, FURL& LocalURL, bool bReuseAddressAndPort, FString& Error ) |
NetDriver 的 InitBase 仅仅只是处理一下参数,创建 NetConnectionClass 对象(注意这里不是NetConnection实例),在此处是IpConnectionClass,以及若有 ReplicationDrvier 则将其构造出来,Replication Graph 插件就是基于此实现的。
1 | bool UIpNetDriver::InitBase( bool bInitAsClient, FNetworkNotify* InNotify, const FURL& URL, bool bReuseAddressAndPort, FString& Error ) |
绑定端口的时候会不断递增端口号,直到绑定成功,然后将 Socket 存储到BoundSockets
,注意服务端只能存在一个 绑定的Socket。
重要的是 InitConnectionlessHandler
,创建 PacketHandler,这个 Handler 能添加一系列的 Handler,所有 Packet 都会先进这个 handler 过一遍筛子,里面默认添加了一个 StatelessConnectHandlerComponent
用于无状态网络连接。
1 | void UNetDriver::InitConnectionlessHandler() |
StatelessConnectComponent.Pin()->SetDriver(this);
此处会触发 UpdateSecret
操作,更新两个 Secret,用于后续握手,当然这个值是会随着时间更新的,之所以需要两个,是因为更新很频繁,需要存一下旧值来比对,主要是为了避免重放攻击。
1 | void StatelessConnectHandlerComponent::UpdateSecret() |
到此为止,服务端的监听网络流程结束,简单来说就是根据平台创建 NetDriver,准备好 NetConnectionClass 但没有用它创建实例,因为此时还没有客户端连上来,创建 Socket,然后创建 PacketHandler 并为它添加 StatelessConnectHandlerComponent 用于无状态网络连接,避免重放攻击。
调用栈如下:
1 | UWorld::Listen() |
客户端连接流程
UEngine::Browse
处理网络连接的基础内容,包括对 URL 的处理,最后会创建一个 UPendingNetGame
对象。
1 | EBrowseReturnVal::Type UEngine::Browse( FWorldContext& WorldContext, FURL URL, FString& Error ) |
它也会和服务端一样创建一个 NetDriver,只不过它的名字叫 PendingNetDriver
而不是 GameNetDriver
,接着初始化连接。
1 | void UPendingNetGame::InitNetDriver() |
这一部分逻辑和服务端几乎一样,根据URL设置变量,创建 Socket,以及准备好 NetConnectionClass,但它是立刻根据 NetConnectionClass 来构造一个 UIpConnection
对象,而服务端是等到握手认证通过后才创建。 CreateInitialClientChannels
还会初始化 Channels 数据,包括可靠传输之类的内容,一条连接的 Channel 个数最多为 DefaultMaxChannelSize(32767)
。
InitLocalConnection
会一路调用到 UNetConnection::InitBase
中,构造 PacketHandler、StatelessConnectHandlerComponent 。
1 | bool UIpNetDriver::InitConnect( FNetworkNotify* InNotify, const FURL& ConnectURL, FString& Error ) |
这里还要注意一点,每条 Connection 都会创建一个 PackageMapClient,用于序列化 Actor,主要功能是把 GUID 和 指针绑定起来。
1 | void UNetConnection::InitBase(UNetDriver* InDriver,class FSocket* InSocket, const FURL& InURL, EConnectionState InState, int32 InMaxPacket, int32 InPacketOverhead) |
创建客户端 Channels,需要注意 UE5 多了个 DataStream Channel,用于 Iris 新的网络功能。
1 | [/Script/Engine.NetDriver] |
1 | void UNetDriver::CreateInitialClientChannels() |
初始化完连接,此时就可以开始握手了,
1 | void UPendingNetGame::BeginHandshake() |
调用栈如下:
1 | UEngine::Browse() |
客户端总体流程也是先创建 NetDriver,这里叫 PendingNetDriver,和服务端不同的是会立即创建 IpConnection,毕竟客户端不需要省资源,创建 PacketHandler 和 StatelessConnectHandlerComponent 存于 Connection(而服务端存于 NetDriver) 用于和服务端无状态连接,同时创建必要的 Channels,目前是 Control、Voice、DataStream 三个Channel,最后开始握手。
握手
服务端、客户端建立好 Socket 之后,就要开始握手了,握手包有以下几类。
1 | enum class EHandshakePacketType : uint8 |
PacketHandler::BeginHandshaking
最终会通知到其下的各个 Handler Component 去执行握手,在这里当然只有 StatelessConnectHandlerComponent
。
1 | void StatelessConnectHandlerComponent::NotifyHandshakeBegin() |
InitialPacket
1 | void StatelessConnectHandlerComponent::SendInitialPacket(EHandshakeVersion HandshakeVersion) |
设置 RawSend
避免被 Handler 处理。
1 | void StatelessConnectHandlerComponent::SendToServer(EHandshakeVersion HandshakeVersion, EHandshakePacketType PacketType, FBitWriter& Packet) |
此处发送 InitialPacket
握手包时,用的也是 UDP,因此存在丢失的风险,解决办法是通过StatelessConnectHandlerComponent::Tick
每帧都发一次握手包,后续握手流程都是通过这种方式进行。
1 | void StatelessConnectHandlerComponent::Tick(float DeltaTime) |
轮到服务端接受握手包,服务端接受 Packet
都在 UIpNetDriver::TickDispatch
中。
1 | void UIpNetDriver::TickDispatch(float DeltaTime) |
ProcessConnectionlessPacket
是用于处理还未有连接的 Packet 包,因为只有完全握手通过之后才会为客户端创建连接。 FPacketIterator
也只是一个简单的迭代器,用于从 Socket 读取内容。
服务端所有收到的握手包都会到 StatelessConnectHandlerComponent::IncomingConnectionless
Challenge
服务端收到 InitialPacket
后 就会发送 Challenge
,此时才会使用服务端的 HandshakeSecret
用当前的时间戳和客户端地址生成一个 Cookie,发送给客户端。
1 | void StatelessConnectHandlerComponent::SendConnectChallenge(FCommonSendToClientParams CommonParams, uint8 ClientSentHandshakePacketCount) |
客户端的读取流程也类似,不过要记住 客户端此时是有连接的,所以会直接进入
1 | void UIpNetDriver::TickDispatch(float DeltaTime) |
1 | void UNetConnection::ReceivedRawPacket( void* InData, int32 Count ) |
在握手阶段 bConnectionlessPacket
为 true 为服务端流程,为 false 为客户端流程。而握手结束后,双方都会走 Incoming
流程。
1 | EIncomingResult PacketHandler::Incoming_Internal(FReceivedPacketView& PacketView) |
Challenge 的 Ack 也是 Challenge 类型的握手包,因此区分方式是通过判断 timestamp 是否小于等于 0 判断是否为 Ack。
ChallengeResponse
ChallengeResponse 也只是将服务端发过来的数据重新发回去,让服务端校验。Secret 过期时间为 40秒。
服务端收到 ChallengeResponse 后 若验证 Cookie 通过则保存该 Cookie,用于后续断线重连的校验,同时从 Cookie 中算出 发送和接收序列号,序列号是第一篇讲的用于 Packet 可靠传输的ID,随机化是为了避免攻击。
1 | void StatelessConnectHandlerComponent::IncomingConnectionless(FIncomingPacketRef PacketRef) |
服务端此时已经通过了握手,为客户端创建连接。
ChallengeAck
客户端收到 ChallengeAck后,也是设置好发送和接收的序列号,用于后续 Packet 可靠传输。
1 | void StatelessConnectHandlerComponent::Incoming(FBitReader& Packet) |
总结
服务端和客户端都会在启动之后创建 NetDriver,只不过服务端不会创建 NetConnection 因为担心被重放攻击,而客户端无所谓,创建了 NetConnection,之后彼此通过 PacketHandler 中的 StatelessConnectHandlerComponent
进行握手,握手方式主要是通过生成 Cookie,同时由于是 UDP 传输,可能丢包,彼此会通过 tick 不断重发握手包,当彼此握手通过之后,双端都会将 Cookie 保存下来,同时服务端为该客户端创建连接,同时对齐两端的 Packet 发送接收序列号,用于未来的可靠传输。
断线重连
当客户端出现切换网络时,可能会带着新的地址连接至服务端,服务端发现无法根据该地址找到连接,因此会下发重新握手的请求。
1 | void StatelessConnectHandlerComponent::IncomingConnectionless(FIncomingPacketRef PacketRef) |