本篇内容和源码均参考 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 加密