本篇内容和源码均参考 UE5。
# Unreal 握手 && 登录流程
# 握手包结构
先来了解一下握手包的结构:
主要由 5 个部分组成:
- MagicHeader:暂未使用的空包头。
- SessionID && ClientID:历史会话 ID(用于重连)及客户端 ID。
- 包体:握手包的数据块,其内又可以分为三个部分:
- 基础数据块:主要包含基本的包信息如包类型、客户端版本、客户端包编号等。
- 密钥 ID:加密使用的服务器密钥编号。
- 包时间戳和 Cookie 信息:Cookie 是通过密钥 ID 对应的密钥对时间戳 + 客户端 ip 端口计算得到的密文。
- 随机数据:单纯填充用,可以让包大小不是恒定。
- 终止符:包结束位,如果整个包不是 8 bit 整数倍,还会在其末尾填充至 8bit 整数倍。
# 四次握手流程
整个握手流程主要分为四个步骤:
- Init
- Challenge
- Response
- Ack
# Init 流程
客户端在触发 UEngine::Browse 打开地图文件的时候会初始化对应的 Driver 对象,并尝试和服务器建立无状态链接。为了容忍丢包会进行 1s / 次 的重复发包,如果超过一定时间仍然未收到回包的情况下则结束握手流程。
# Challenge 流程
服务器收到 InitPacket 后,服务器会根据当前正在生效的密钥用时间戳 + 客户端 IP 端口进行加密,得到 Cookie 信息。并连同密钥 ID 和 Cookie 信息一起发送回客户端。由于服务器是无状态的,因此会多次处理客户的的 InitPacket。
# Response 流程
由于每个密钥的有效时间是 15 s,客户端收到 ChallengePacket 后,会检查 Cookie 的时效性(当前时间 - 上次发包时间),确认有效后会把服务器的密钥 ID 和 Cookie 信息留存起来,并再次发送回服务器,同时更新客户端连接状态由 UnInitialized → InitializedOnLocal 。
如果发现密钥失效,则会返回 Init 流程并重置客户端状态为 UnInitialized,并在 Tick 过程重新发送 InitPacket 。
由于服务器无状态,这里可能会接收到多个服务器的 Challenge 包,客户端都会进行处理,并保留最新的密钥 ID 和 Cookie 信息。并且为了容忍丢包会进行 1s / 次 的重复发包(使用最新的密钥 ID 和 Cookie 信息)
# Ack 流程
服务器收到 ResponsePacket 后,会先检查包的时效性,由于服务器存储了 2 个密钥(密钥 0 && 密钥 1),每间隔 5 s 检查一次密钥有效性,密钥激活 15 s 后会被轮替,因此理论上一个密钥的激活时间最长为 20 s。当密钥失活后依旧会保留 15 s,并在 5 s 一次的检查中被清理,失活状态下的密钥依旧有效因此一个密钥最长有效时间为 40 s。
在确认密钥有效的情况下,根据密钥对 Cookie 进行校验比对。成功后服务器便认为客户端连接,又再次发回给客户端 Cookie 信息,密钥 ID 为 1 是默认值,该字段后面不会被用到。之后服务器会初始化一个客户端的 UNetConnect 对象,根据 Cookie 信息得到 2 个随机序列,一个用于客户端一个用于服务器。并且忽略后续的握手包。
客户端收到 AckPacket 后,同样会根据 Cookie 信息初始化客户端和服务器包序列,保留 Cookie 信息并设置连接状态由 InitializedOnLocal → Initialized ,至此整个握手流程完成,可以开始通讯。
# 登录流程
客户端收到 Ack Packet 后,就会进入登录流程,触发的方式是注册握手完成后的回调函数:
// ========================== 注册回调 ========================== | |
void UPendingNetGame::InitNetDriver() | |
{ | |
if (!GDisallowNetworkTravel) | |
{ | |
if( NetDriver->InitConnect( this, URL, ConnectionError ) ) | |
{ | |
if (ServerConn->Handler.IsValid()) | |
{ | |
ServerConn->Handler->BeginHandshaking( | |
FPacketHandlerHandshakeComplete::CreateUObject(this, &UPendingNetGame::SendInitialJoin)); | |
} | |
} | |
} | |
} | |
void PacketHandler::BeginHandshaking(FPacketHandlerHandshakeComplete InHandshakeDel/*=FPacketHandlerHandshakeComplete()*/) | |
{ | |
bBeganHandshaking = true; | |
HandshakeCompleteDel = InHandshakeDel; | |
} | |
// ========================== 握手完成后触发回调 ========================== | |
void StatelessConnectHandlerComponent::Incoming(FBitReader& Packet) | |
{ | |
// 收到握手包 | |
else if (HandshakeData.HandshakePacketType == EHandshakePacketType::Ack && HandshakeData.Timestamp < 0.0) | |
{ | |
Initialized(); | |
} | |
} | |
void HandlerComponent::Initialized() | |
{ | |
bInitialized = true; | |
Handler->HandlerComponentInitialized(this); | |
} | |
void PacketHandler::HandlerComponentInitialized(HandlerComponent* InComponent) | |
{ | |
if (State != Handler::State::Initialized) | |
{ | |
if (bAllInitialized) | |
{ | |
HandlerInitialized(); | |
} | |
} | |
} | |
void PacketHandler::HandlerInitialized() | |
{ | |
if (bBeganHandshaking) | |
{ | |
// 执行回调 | |
HandshakeCompleteDel.ExecuteIfBound(); | |
} | |
} |
SendInitialJoin 最终会发送 Hello 消息给服务器:
void UPendingNetGame::SendInitialJoin() | |
{ | |
if (NetDriver != nullptr) | |
{ | |
UNetConnection* ServerConn = NetDriver->ServerConnection; | |
if (ServerConn != nullptr) | |
{ | |
if (!bEncryptionRequirementsFailure) | |
{ | |
EEngineNetworkRuntimeFeatures LocalNetworkFeatures = NetDriver->GetNetworkRuntimeFeatures(); | |
FNetControlMessage<NMT_Hello>::Send(ServerConn, IsLittleEndian, LocalNetworkVersion, EncryptionToken, LocalNetworkFeatures); | |
ServerConn->FlushNet(); | |
} | |
} | |
} | |
} |
# Hello Packet
FNetControlMessage<NMT_Hello> 在 Unreal 中是通过宏定义实现的:
#define DEFINE_CONTROL_CHANNEL_MESSAGE(Name, Index, ...) \ | |
enum { NMT_##Name = Index }; \ | |
template<> class FNetControlMessage<Index> \ | |
{ \ | |
public: \ | |
static uint8 Initialize() \ | |
{ \ | |
FNetControlMessageInfo::SetName(Index, TEXT(#Name)); \ | |
return 0; \ | |
} \ | |
/** sends a message of this type on the specified connection's control channel \ | |
* @note: const not used only because of the FArchive interface; the parameters are not modified \ | |
*/ \ | |
template<typename... ParamTypes> \ | |
static void Send(UNetConnection* Conn, ParamTypes&... Params) \ | |
{ \ | |
static_assert(Index < FNetControlMessageInfo::MaxNames, "Control channel message must be a byte."); \ | |
checkSlow(!Conn->IsA(UChildConnection::StaticClass())); /** control channel messages can only be sent on the parent connection */ \ | |
if (Conn->Channels[0] != NULL && !Conn->Channels[0]->Closing) \ | |
{ \ | |
FControlChannelOutBunch Bunch(Conn->Channels[0], false); \ | |
uint8 MessageType = Index; \ | |
Bunch << MessageType; \ | |
FNetControlMessageInfo::SendParams(Bunch, Params...); \ | |
Conn->Channels[0]->SendBunch(&Bunch, true); \ | |
} \ | |
} \ | |
/** receives a message of this type from the passed in bunch */ \ | |
template<typename... ParamTypes> \ | |
UE_NODISCARD static bool Receive(FInBunch& Bunch, ParamTypes&... Params) \ | |
{ \ | |
FNetControlMessageInfo::ReceiveParams(Bunch, Params...); \ | |
return !Bunch.IsError(); \ | |
} \ | |
/** throws away a message of this type from the passed in bunch */ \ | |
static void Discard(FInBunch& Bunch) \ | |
{ \ | |
TTuple<__VA_ARGS__> Params; \ | |
VisitTupleElements([&Bunch](auto& Param) \ | |
{ \ | |
Bunch << Param; \ | |
}, \ | |
Params); \ | |
} \ | |
}; | |
DEFINE_CONTROL_CHANNEL_MESSAGE(Hello, 0, uint8, uint32, FString, uint16); |
宏展开后长下面这个样子:
enum | |
{ | |
NMT_Hello = 0 | |
}; | |
template <> | |
class FNetControlMessage<0> | |
{ | |
public: | |
static uint8 Initialize() | |
{ | |
FNetControlMessageInfo::SetName(0, TEXT("Hello")); | |
return 0; | |
} | |
template <typename... ParamTypes> | |
static void Send(UNetConnection* Conn, ParamTypes&... Params) | |
{ | |
static_assert(0 < FNetControlMessageInfo::MaxNames, "Control channel message must be a byte."); | |
checkSlow(!Conn->IsA(UChildConnection::StaticClass())); | |
if (Conn->Channels[0] != NULL && !Conn->Channels[0]->Closing) | |
{ | |
FControlChannelOutBunch Bunch(Conn->Channels[0], false); | |
uint8 MessageType = 0; | |
Bunch << MessageType; | |
FNetControlMessageInfo::SendParams(Bunch, Params...); | |
Conn->Channels[0]->SendBunch(&Bunch, true); | |
} | |
} | |
template <typename... ParamTypes> | |
UE_NODISCARD static bool Receive(FInBunch& Bunch, ParamTypes&... Params) | |
{ | |
FNetControlMessageInfo::ReceiveParams(Bunch, Params...); | |
return !Bunch.IsError(); | |
} | |
static void Discard(FInBunch& Bunch) | |
{ | |
TTuple<uint8, uint32, FString, uint16> Params; | |
VisitTupleElements([&Bunch](auto& Param) { Bunch << Param; }, Params); | |
} | |
};; |
FNetControlMessageInfo::SendParams(Bunch, Params...);
负责对参数进行打包,实现还是比较有意思的,用的模板的模式匹配 + 递归:
template<typename... ParamTypes> | |
static void SendParams(FControlChannelOutBunch& Bunch, ParamTypes&... Params) {} | |
template<typename FirstParamType, typename... ParamTypes> | |
static void SendParams(FControlChannelOutBunch& Bunch, FirstParamType& FirstParam, ParamTypes&... Params) | |
{ | |
Bunch << FirstParam; | |
SendParams(Bunch, Params...); | |
} |
# 其他协议包定义:
// 【c2s.1】Hello | |
DEFINE_CONTROL_CHANNEL_MESSAGE(Hello, 0, uint8, uint32, FString, uint16); | |
// 【s2c.1】Challenge | |
DEFINE_CONTROL_CHANNEL_MESSAGE(Challenge, 3, FString); | |
// 【c2s.2】Login | |
DEFINE_CONTROL_CHANNEL_MESSAGE(Login, 5, FString, FString, FUniqueNetIdRepl, FString); | |
// 【s2c.2】Welcome | |
DEFINE_CONTROL_CHANNEL_MESSAGE(Welcome, 1, FString, FString, FString); | |
// 【c2s.3】Netspeed | |
DEFINE_CONTROL_CHANNEL_MESSAGE(Netspeed, 4, int32); | |
// 【c2s.4】Join | |
DEFINE_CONTROL_CHANNEL_MESSAGE(Join, 9); |
# 登录包定义
主要可以分成三个部分:
- PacketBaseInfo:里面主要包含包序列号、ACK 信息、网络抖动等信息。
- BunchHeader:包含一些标志信息如创建 or 关闭 Channel、是否可靠、是否分包等。值得注意的是 FName 字段的打包规则比较特殊:由于 Unreal 内部会优化字符串,硬编码会通过一个全局字符表存储,然后直接通过编号索引来指代。
- BunchData:BunchData 里就是各个业务的包了,在登录流程中前 8 bit 会被用来解析为 MessageType,后续的内容则根据业务自行处理压包和解包。
# 登录流程总览图
整个登录流程会接着握手包的 Ack 继续执行。
- 客户端会先发起 Hello 请求,里面可以选择携带相关的密文摘要信息。
- 服务器收到 Hello 包后,如果没有密文摘要信息,则直接回复 Challenge 包。
- 客户端收到 Challenge 包后,会发送 Login 包,并携带角色 ID 和 URL。
- 服务器收到 Login 包后,会发送 Welcome 包,并携带当前 World 的 PersistentLevelName 和 GameModeName。
- 客户端收到 Welcome 包后会同步给服务器当前的 NetSpeed。并根据 PersistentLevelName 加载地图、完成 World 的切换。
- 当客户端地图加载完毕后,会回复客户端 Join 包。
- 服务器收到 Join 包后会在地图内创建玩家并且同步各项内容。之后业务就可以正常基于网络交互通讯了。
# 加密通信
Unreal 本身提供了三种加密:
- FAESHandlerComponent
- FAESGCMHandlerComponent
- FDTLSHandlerComponent
这里以 LyraGame 中的 DTLS 为例进行简单介绍:
// 加密结构 | |
struct FEncryptionData | |
{ | |
/** Encryption key */ | |
TArray<uint8> Key; | |
/** Encryption fingerprint */ | |
TArray<uint8> Fingerprint; | |
/** Encryption identifier */ | |
FString Identifier; | |
}; |
加密开启流程在客户端发送 Hello 包之后(Hello 及 Hello 包之前都是明文传输) :
- Hello 包可以携带一个
EncryptionToken
用来唯一标识一个连接。 - 服务器收到
EncryptionToken
后会调用XXXGameInstance::ReceivedNetworkEncryptionToken
函数,为该标识生成一个独有的服务器证书 Cert(有效时长 4 小时) - 证书生成完成后会发送
EncryptionAck
包,通知客户端服务器开启加密 - 客户端收到
EncryptionAck
包后会调用XXXGameInstance::ReceivedNetworkEncryptionAck
同样设置客户端证书。 - 其中证书内的加密信息
FEncryptionData
内容如下:FEncryptionData::Key
一般是通过AES256
生成的密钥做对称加密。FEncryptionData::Fingerprint
需要从特定的安全服务器提取的指纹信息。FEncryptionData::Identifier
对应客户端的EncryptionToken
。一般是用客户端的 UUID 作为EncryptionToken
。
# 加密解密
上述的三种加密组件: FAESHandlerComponent
、 FAESGCMHandlerComponent
、 FDTLSHandlerComponent
可以注册到 PacketHandler
上。
- 在
FDTLSHandlerComponent::Incoming
处理解密。 - 在
FDTLSHandlerComponent::Outgoing
处理加密。
FDTLSHandlerComponent
会在原本包体的前面新增 3 个 bit 用来标识加密是否开启,是否为握手(加密的握手,非建立连接的握手),以及一个终止位。
# 开启加密流程
在 Config/DefaultEngine.ini 中新增配置:
[PacketHandlerComponents] | |
; Options can be set in this section of DefaultEngine.ini to enable different types of network packet encruption plugins | |
EncryptionComponent=DTLSHandlerComponent |
并且在 URL 中加入 EncryptionToken 后缀,这里方便测试直接修改了 Hello 包的默认值:
void UPendingNetGame::SendInitialJoin()
{
if (NetDriver != nullptr)
{
UNetConnection* ServerConn = NetDriver->ServerConnection;
if (ServerConn != nullptr)
{
uint8 IsLittleEndian = uint8(PLATFORM_LITTLE_ENDIAN);
check(IsLittleEndian == !!IsLittleEndian); // should only be one or zero
const int32 AllowEncryption = CVarNetAllowEncryption.GetValueOnGameThread();
FString EncryptionToken;
if (AllowEncryption != 0)
{
//EncryptionToken = URL.GetOption(TEXT("EncryptionToken="), TEXT(""));
EncryptionToken = URL.GetOption(TEXT("EncryptionToken="), TEXT("1"));
}
}
}
}
然后就可以开始调试了,在 FDTLSHandlerComponent::TickHandshake
中会定时检查状态,并发送握手包。当状态为 InternalState = EDTLSHandlerState::Encrypted
时标识开始进行加密传输。
# 可靠性
由于登陆包是通过 ControlChannel 进行处理的,见上面的消息宏展开,使用的是 FControlChannelOutBunch
传输,其中 bReliable
始终是 true。因此登录包以及后续玩家相关包都是可靠。
FControlChannelOutBunch::FControlChannelOutBunch(UChannel* InChannel, bool bClose) | |
: FOutBunch(InChannel, bClose) | |
{ | |
checkSlow(Cast<UControlChannel>(InChannel) != nullptr); | |
// control channel bunches contain critical handshaking/synchronization and should always be reliable | |
bReliable = true; | |
} |
另外每个 Channel 的 Close 包必定是 bReliable 的。
# 重传
发送过的 Bunch 会根据 bReliable 字段存储在 OutRec 中用于重传。重传机制主要通过两种方式保证消息必达(如果消息超过消息缓冲区队列大小,会直接断开 Connect 连接):
- ControlChannel 的重传机制(通过 Tick 进行触发):如果是 Open 包,即开启一个 ControlChannel 通道的包,这个包的重传间隔是 1s,另外就是 ControlChannel 创建成功后的中间数据包,这部分包会存储在 QueuedMessages 用于重传。
- 通用的 Channel 重传机制(通过 ReplicateActor 进行触发):这里重传是基于 SendBunch 触发的,每次 SendBunch 的时候并不一定会发送当前需要发送的包,而是把该可靠包放入队尾,然后取出最早没有 Ack 的多个包进行发送。
void UControlChannel::QueueMessage(const FOutBunch* Bunch) | |
{ | |
LLM_SCOPE_BYTAG(NetChannel); | |
if (QueuedMessages.Num() >= MAX_QUEUED_CONTROL_MESSAGES) | |
{ | |
// we're out of room in our extra buffer as well, so kill the connection | |
UE_LOG(LogNet, Log, TEXT("Overflowed control channel message queue, disconnecting client")); | |
// intentionally directly setting State as the messaging in Close() is not going to work in this case | |
Connection->SetConnectionState(USOCK_Closed); | |
} | |
} | |
void UControlChannel::Tick() | |
{ | |
Super::Tick(); | |
if( !OpenAcked ) | |
{ | |
int32 Count = 0; | |
for (FOutBunch* Out = OutRec; Out; Out = Out->Next) | |
{ | |
if (!Out->ReceivedAck) | |
{ | |
Count++; | |
} | |
} | |
if (Count > 8) | |
{ | |
return; | |
} | |
// Resend any pending packets if we didn't get the appropriate acks. | |
for( FOutBunch* Out=OutRec; Out; Out=Out->Next ) | |
{ | |
if( !Out->ReceivedAck ) | |
{ | |
const double Wait = Connection->Driver->GetElapsedTime() - Out->Time; | |
checkSlow(Wait >= 0.0); | |
if (Wait > 1.0) | |
{ | |
UE_LOG(LogNetTraffic, Log, TEXT("Channel %i ack timeout); resending %i..."), ChIndex, Out->ChSequence ); | |
check(Out->bReliable); | |
Connection->SendRawBunch( *Out, 0 ); | |
} | |
} | |
} | |
} | |
else | |
{ | |
// attempt to send queued messages | |
while (QueuedMessages.Num() > 0 && !Closing) | |
{ | |
FControlChannelOutBunch Bunch(this, 0); | |
if (Bunch.IsError()) | |
{ | |
break; | |
} | |
else | |
{ | |
Bunch.bReliable = 1; | |
Bunch.SerializeBits(QueuedMessages[0].Data.GetData(), QueuedMessages[0].CountBits); | |
if (!Bunch.IsError()) | |
{ | |
Super::SendBunch(&Bunch, 1); | |
QueuedMessages.RemoveAt(0, 1); | |
} | |
else | |
{ | |
// an error here most likely indicates an unfixable error, such as the text using more than the maximum packet size | |
// so there is no point in queueing it as it will just fail again | |
ensureMsgf(false, TEXT("Control channel queued bunch overflowed")); | |
UE_LOG(LogNet, Error, TEXT("Control channel queued bunch overflowed")); | |
Connection->Close(ENetCloseResult::ControlChannelQueueBunchOverflowed); | |
break; | |
} | |
} | |
} | |
} | |
} |
FPacketIdRange UChannel::SendBunch( FOutBunch* Bunch, bool Merge ) | |
{ | |
//----------------------------------------------------- | |
// Contemplate merging or Possibly split large bunch into list of smaller partial bunches | |
//----------------------------------------------------- | |
//----------------------------------------------------- | |
// OverflowsReliable | |
//----------------------------------------------------- | |
const bool bOverflowsReliable = (NumOutRec + OutgoingBunches.Num() >= RELIABLE_BUFFER + Bunch->bClose); | |
if ((GCVarNetPartialBunchReliableThreshold > 0) && (OutgoingBunches.Num() >= GCVarNetPartialBunchReliableThreshold) && !Connection->IsInternalAck()) | |
{ | |
if (!bOverflowsReliable) | |
{ | |
UE_LOG(LogNetPartialBunch, Log, TEXT(" OutgoingBunches.Num (%d) exceeds reliable threashold (%d). Making bunches reliable. Property replication will be paused on this channel until these are ACK'd."), OutgoingBunches.Num(), GCVarNetPartialBunchReliableThreshold); | |
Bunch->bReliable = true; | |
bPausedUntilReliableACK = true; | |
} | |
else | |
{ | |
// The threshold was hit, but making these reliable would overflow the reliable buffer. This is a problem: there is just too much data. | |
UE_LOG(LogNetPartialBunch, Warning, TEXT(" OutgoingBunches.Num (%d) exceeds reliable threashold (%d) but this would overflow the reliable buffer! Consider sending less stuff. Channel: %s"), OutgoingBunches.Num(), GCVarNetPartialBunchReliableThreshold, *Describe()); | |
} | |
} | |
if (Bunch->bReliable && bOverflowsReliable) | |
{ | |
UE_LOG(LogNetPartialBunch, Warning, TEXT("SendBunch: Reliable partial bunch overflows reliable buffer! %s"), *Describe() ); | |
UE_LOG(LogNetPartialBunch, Warning, TEXT(" Num OutgoingBunches: %d. NumOutRec: %d"), OutgoingBunches.Num(), NumOutRec ); | |
PrintReliableBunchBuffer(); | |
// Bail out, we can't recover from this (without increasing RELIABLE_BUFFER) | |
FString ErrorMsg = NSLOCTEXT("NetworkErrors", "ClientReliableBufferOverflow", "Outgoing reliable buffer overflow").ToString(); | |
Connection->SendCloseReason(ENetCloseResult::ReliableBufferOverflow); | |
FNetControlMessage<NMT_Failure>::Send(Connection, ErrorMsg); | |
Connection->FlushNet(true); | |
Connection->Close(ENetCloseResult::ReliableBufferOverflow); | |
return PacketIdRange; | |
} | |
//----------------------------------------------------- | |
// Send all the bunches we need to | |
// Note: this is done all at once. We could queue this up somewhere else before sending to Out. | |
//----------------------------------------------------- | |
for( int32 PartialNum = 0; PartialNum < OutgoingBunches.Num(); ++PartialNum) | |
{ | |
FOutBunch * NextBunch = OutgoingBunches[PartialNum]; | |
NextBunch->bReliable = Bunch->bReliable; | |
NextBunch->bOpen = Bunch->bOpen; | |
NextBunch->bClose = Bunch->bClose; | |
NextBunch->CloseReason = Bunch->CloseReason; | |
NextBunch->bIsReplicationPaused = Bunch->bIsReplicationPaused; | |
NextBunch->ChIndex = Bunch->ChIndex; | |
NextBunch->ChName = Bunch->ChName; | |
if ( !NextBunch->bHasPackageMapExports ) | |
{ | |
NextBunch->bHasMustBeMappedGUIDs |= Bunch->bHasMustBeMappedGUIDs; | |
} | |
if (OutgoingBunches.Num() > 1) | |
{ | |
NextBunch->bPartial = 1; | |
NextBunch->bPartialInitial = (PartialNum == 0 ? 1: 0); | |
NextBunch->bPartialFinal = (PartialNum == OutgoingBunches.Num() - 1 ? 1: 0); | |
NextBunch->bOpen &= (PartialNum == 0); // Only the first bunch should have the bOpen bit set | |
NextBunch->bClose = (Bunch->bClose && (OutgoingBunches.Num()-1 == PartialNum)); // Only last bunch should have bClose bit set | |
} | |
// 这里通过 PrepBunch 获取到需要重传的起始包 | |
FOutBunch *ThisOutBunch = PrepBunch(NextBunch, OutBunch, Merge); // This handles queuing reliable bunches into the ack list | |
// Update Packet Range | |
int32 PacketId = SendRawBunch(ThisOutBunch, Merge, GetTraceCollector(*NextBunch)); | |
if (PartialNum == 0) | |
{ | |
PacketIdRange = FPacketIdRange(PacketId); | |
} | |
else | |
{ | |
PacketIdRange.Last = PacketId; | |
} | |
// Update channel sequence count. | |
Connection->LastOut = *ThisOutBunch; | |
Connection->LastEnd = FBitWriterMark( Connection->SendBuffer ); | |
} | |
// Update open range if necessary | |
if (Bunch->bOpen && (Connection->ResendAllDataState == EResendAllDataState::None)) | |
{ | |
OpenPacketId = PacketIdRange; | |
} | |
// Destroy outgoing bunches now that they are sent, except the one that was passed into ::SendBunch | |
// This is because the one passed in ::SendBunch is the responsibility of the caller, the other bunches in OutgoingBunches | |
// were either allocated in this function for partial bunches, or taken from the package map, which expects us to destroy them. | |
for (auto It = OutgoingBunches.CreateIterator(); It; ++It) | |
{ | |
FOutBunch *DeleteBunch = *It; | |
if (DeleteBunch != Bunch) | |
delete DeleteBunch; | |
} | |
return PacketIdRange; | |
} |
# 参考文档
- UE4 UDP 是如何进行可靠传输的
- 虚幻引擎中如何使用数据包处理程序组件启用加密
- 在 Unreal 引擎中应用 TLS 加密