UE4
基
础
:
客
户
端
服
务
器
连
接
流
程
FLYING TREE 2019-03-29
技术文章 › 虚幻4
| #UE4
客户端和服务器启动时,首先会在 FEngineLoop::Init 中创建 GEngine 对象,并调用 GEngine->Init 进行初始化,这里和网络相关的初始化有以下
调用静态函数 FURL::StaticInit 从配置文件 xxxEngine.ini 中初始化默认的服务器信息,如
Protocol, Name, Host, Port
,这里的 Port 可以通过命令行
-Port=n
指定
创建 GameInstance 对象并调用 InitializeStandalone 进行初始化
创建 GameInstance 对应的 WorldContext
创建临时的 DummyWorld
调用 GEngine->Start 启动游戏,默认的内部的逻辑只是调用 GameInstance->StartGameInstance()
从配置文件中读取默认的地图名参数 UGameMapsSettings
调用 UEngine::Browse 加载默认的地图,客户端连接服务器,服务器启动监听都是发生在这个函数里
客户端调用 UEngine::SetClientTravel 发起地图切换,这个函数里只是把这个请求记录下
在引擎的每帧的 UEngine::TickWorldTravel 里,会去检查是否有服务器或客户端的地图切换请求,如果有就用 URL 参数构造一个 FURL 对象,并调用 UEngine::Browse
进行切换,FURL 的构造函数里会去解析服务器地址,端口,参数等信息
UEngine::Browse 里对地图切换会有个判断,如果是 URL.IsLocalInternal() 也就是服务器地址是空的,那就直接调用 UEngine::LoadMap 直接加载本地地图,如果是
URL.IsInternal() && GIsClient 那就需要发起和服务器的连接流程
调用 UEngine::CancelPending 关闭已有的 PendingNetGame 对象
调用 UEngine::ShutdownWorldNetDriver 销毁当前 World 的 NetDriver 和 DemoNetDriver
调用 NewObject<UPendingNetGame>() 新建一个 PendingNetGame 对象
初始化新的 PendingNetGame 对象并调用 UPendingNetGame::InitNetDriver 初始化 NetDriver
调用 GEngine->CreateNamedNetDriver 创建一个具名 NetDriver 对象,这个函数有两个参数,名称参数为 NAME_PendingNetDriver("PendingNetDriver")
定义参数为 NAME_GameNetDriver("GameNetDriver") 。名称参数是之后用来查找 NetDriver 用的。定义参数是用来创建 NetDriver 对象时,根据这个字符串
在 Engine->NetDriverDefinitions 中查找对应的 FNetDriverDefinition 对象,然后根据查找出来的定义对象中
DriverClassName/DriverClassNameFallback 两个变量确定 NetDriver 对象的类名,最后用这个类调用 NewObject 创建 NetDriver 对象。默认的定义有
两个配置在 xxxEngine.ini 中,分别是 GameNetDriver 对应的 NetDriver 类是 /Script/OnlineSubsystemUtils.IpNetDriver;
DemoNetDriver 对应的 NetDriver 类是 /Script/Engine.DemoNetDriver
在创建出来的 NetDriver 对象(这里的 NetDriver 对象一般都是 UIpNetDriver )上调用 InitConnect 初始化连接
调用 UIpNetDriver::InitBase 进行基础信息初始化和 Socket 初始化
加载 NetConnectionClass ,配置在 xxxEngine.ini 文件的 NetConnectionClassName 中
注册 FNetworkNotify 类型的回调
调用 ISocketSubsystem::CreateSocket 创建 FSocket 对象,协议为 UDP。然后对这个对象设置一些 socket 属性,比如是否支持广播,是否支
持 IP 端口重用,设置接收/发送缓冲区大小,绑定端口,设置是否阻塞
根据 CVarNetIpNetDriverUseReceiveThread 和 SocketSubsystem->IsSocketWaitSupported 来决定是否创建单独的接收线程
根据 NetConnectionClass 创建客户端与服务器的连接对象 ServerConnection (这个对象一般都是 UIpConnection ),并调用
UNetConnection::InitLocalConnection 进行初始化,此时的连接状态是 EConnectionState::USOCK_Pending (等待连接)
调用 UIpConnection::InitBase 进行基础信息初始化
初始化一些状态信息
调用 UNetConnection::InitHandler 初始化 PacketHandler 和 StatelessConnectHandlerComponent
创建 VoiceChannel
初始化服务器地址对象 RemoteAddr
初始化发送缓冲区
对 ServerConnection 对象创建控制通道 ControlChannel
连接初始化成功后,调用 PacketHandler::BeginHandshaking 开始握手流程,最终会调用 StatelessConnectHandlerComponent::NotifyHandshakeBegin (这个
StatelessConnectHandlerComponent 是专门用来处理握手包的)发送握手包,握手包的大小为 195 位 ( HANDSHAKE_PACKET_SIZE_BITS + 1),格式为 1 为握
手包标志位 + 1 位 SecretId + 4字节时间戳 + 20字节哈希值(cookie) + 1 位包结束符标志位。不过客户端这时发的握手包时间戳和哈希值都为0
接着客户端会收到服务器的握手回包 ConnectChallenge,此次回包中的时间戳会大于0,然后向服务器发送 ChallengeResponse,也是握手包格式,只不过
此时时间戳和哈希值填的值是复制的服务器返回的握手包里的值,并且将 StatelessConnectHandlerComponent 的状态设置为
Handler::Component::State::InitializedOnLocal 表示本端已初始化,并且根据 cookie 计算 LastServerSequence/LastClientSequence 做为后续的网络包
初始序列号
接着客户端会再次收到服务器的握手回包 ConnectChallengeAck,不过此次的回包中的时间戳值小于0 (实际为 -1),表示握手流程结束,将 State 设置为
Handler::Component::State::Initialized 表示双端都已完成初始化。同时会调用 HandlerComponent::Initialized 表示自己完成初始化,这个函数里会通知
所属的 PacketHandler 去检查是否所有的 HandlerComponent 都完成初始化,如果是的话就调用 PacketHandler::HandlerInitialized ,在其中调用执行
代理 HandshakeCompleteDel ,这个代理就是之前在 UPendingNetGame::InitNetDriver 里调用 ServerConn->Handler->BeginHandshaking 时传入的成员函数
UPendingNetGame::SendInitialJoin
SendInitialJoin 就是通过 ControlChannel 向服务器发送 NMT_Hello 包,参数为大小端标志,网络版本信息CRC32哈希值,EncryptionToken 值
如果版本信息的CRC32一致的话,客户端接着会收到服务器的 NMT_Challenge 包(否则就是 NMT_Upgrade ),在这里,客户端会通过 ULocalPlayer 收集昵称,
游戏选项等信息用来拼出正式的地图URL,然后向服务器发送登录包 NMT_Login ,登录包有 4 个参数,分别是 ClientResponse 字符串,值固定为 “0”; URL
字符串, LocalPlayer->GetPreferredUniqueNetId 值, UGameInstance::GetOnlinePlatformName 值
接着会收到服务器的 NMT_Welcome 包,表示服务器允许登录,这个包里有3个参数,分别是地图名,GameMode类名,RedirectURL,用这些可以构造出一个正
式的 URL 赋值给 UPendingNetGame::URL ,同时设置 UPendingNetGame::bSuccessfullyConnected 为 true, 标志连接流程结束,客户端可以开始加载地图
了。最后向服务器发送 NMT_Netspeed 包通知客户端当前的网络速率
回到 UEngine::TickWorldTravel 这个函数里,这个函数除了会处理地图切换请求外,还会检查是否 PendingNetGame 对象,如果有这个对象,会调用它的 Tick 函
数驱动整个握手和控制通道消息流程,同时也会每帧检查 PendingNetGame->bSuccessfullyConnected 标志和 PendingNetGame->URL.Map ,如果都合法就调用
LoadMap 开始本地加载地图,加载地图的时候会调用 UEngine::MovePendingLevel 将 PendingNetGame 中的 NetDriver 对象赋值给新的 UWorld 的 NetDriver
同时将 NetDriver 重命名为 NAME_GameNetDriver
调用 UNetDriver::SetWorld 设置 NetDriver 对象的 World 成员变量为新 World
FNetworkNotify 回调也改为新 World
绑定成员函数到 World 的几个 Tick 代理上( OnTickDispatch/OnPostTickDispatch/OnTickFlush/OnPostTickFlush )
将新 World 中的网络相关的 Actor 加到 NetDriver 中
上一步的 UEngine::LoadMap 成功之后,会调用 UPendingNetGame::LoadMapCompleted 这个函数里会做如下事情,然后置空 Context.PendingNetGame 之后服务器就
会向客户端同步 PlayerController 等信息,整个连接流程到这里结束。
调用 UPendingNetGame::SendJoin 向服务器发送 NMT_Join 包,无参数
将 Context.PendingNetGame->NetDriver 置空
引
擎
与
网络
相
关
的
初
始
化
步
骤
#
客
户
端
流
程
#
服
务
器
流
程
#
菜单
返回顶部