在 Unreal Engine 5 的 C++ 项目中,实现一个具备消息监听、心跳检测和断线重连功能的 TCP 客户端,可以参考以下完整示例。
准备工作
1、模块依赖
在 YourModule.Build.cs
文件中,添加对 Sockets
和 Networking
模块的依赖:
PublicDependencyModuleNames.AddRange(new string[] {
"Core",
"CoreUObject",
"Engine",
"Sockets",
"Networking"
});
2、包含头文件
在相关的 .h
文件中,包含必要的头文件:
#include "Networking.h"
#include "Sockets.h"
#include "SocketSubsystem.h"
#include "IPAddress.h"
#include "HAL/Runnable.h"
#include "HAL/RunnableThread.h"
#include "TimerManager.h"
TcpClient.h
#pragma once
#include "CoreMinimal.h"
#include "HAL/Runnable.h"
#include "HAL/RunnableThread.h"
#include "Sockets.h"
#include "SocketSubsystem.h"
#include "TimerManager.h"
DECLARE_DELEGATE_OneParam(FOnTcpConnected, bool /*bSuccess*/);
DECLARE_DELEGATE_OneParam(FOnTcpMessage, const TArray<uint8>& /*Data*/);
DECLARE_DELEGATE(FOnTcpDisconnected);
class FTcpClient : public FRunnable
{
public:
FTcpClient(const FString& InIp, int32 InPort, float InHeartbeatInterval = 5.0f);
virtual ~FTcpClient();
void Start();
void StopClient();
bool Send(const TArray<uint8>& Data);
FOnTcpConnected OnConnected;
FOnTcpMessage OnMessage;
FOnTcpDisconnected OnDisconnected;
virtual bool Init() override;
virtual uint32 Run() override;
virtual void Stop() override;
virtual void Exit() override;
private:
bool TryConnect();
void SendHeartbeat();
void StartHeartbeat();
void StopHeartbeat();
FString ServerIp;
int32 ServerPort;
float HeartbeatInterval;
FSocket* Socket;
FRunnableThread* Thread;
FThreadSafeBool bRunThread;
FThreadSafeBool bConnected;
FTimerHandle HeartbeatTimerHandle;
};
TcpClient.cpp
#include "TcpClient.h"
#include "HAL/PlatformProcess.h"
#include "Async/Async.h"
#include "Engine/World.h"
#include "TimerManager.h"
FTcpClient::FTcpClient(const FString& InIp, int32 InPort, float InHeartbeatInterval)
: ServerIp(InIp)
, ServerPort(InPort)
, HeartbeatInterval(InHeartbeatInterval)
, Socket(nullptr)
, Thread(nullptr)
, bRunThread(false)
, bConnected(false)
{
}
FTcpClient::~FTcpClient()
{
StopClient();
}
void FTcpClient::Start()
{
if (Thread == nullptr)
{
bRunThread = true;
Thread = FRunnableThread::Create(this, TEXT("TcpClientThread"));
}
}
void FTcpClient::StopClient()
{
bRunThread = false;
StopHeartbeat();
if (Thread)
{
Thread->Kill(true);
delete Thread;
Thread = nullptr;
}
if (Socket)
{
Socket->Close();
ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->DestroySocket(Socket);
Socket = nullptr;
}
}
bool FTcpClient::Init()
{
return TryConnect();
}
uint32 FTcpClient::Run()
{
TArray<uint8> RecvBuffer;
RecvBuffer.SetNumUninitialized(1024);
while (bRunThread)
{
if (bConnected)
{
int32 BytesRead = 0;
if (Socket->Recv(RecvBuffer.GetData(), RecvBuffer.Num(), BytesRead))
{
if (BytesRead > 0)
{
TArray<uint8> Data;
Data.Append(RecvBuffer.GetData(), BytesRead);
OnMessage.ExecuteIfBound(Data);
}
}
else
{
bConnected = false;
OnDisconnected.ExecuteIfBound();
Socket->Close();
ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->DestroySocket(Socket);
Socket = nullptr;
StopHeartbeat();
}
}
else
{
FPlatformProcess::Sleep(3.0f);
if (TryConnect())
{
bConnected = true;
OnConnected.ExecuteIfBound(true);
StartHeartbeat();
}
}
}
return 0;
}
void FTcpClient::Stop()
{
bRunThread = false;
}
void FTcpClient::Exit()
{
}
bool FTcpClient::TryConnect()
{
Socket = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)
->CreateSocket(NAME_Stream, TEXT("TcpClientSocket"), false);
Socket->SetNonBlocking(true);
TSharedRef<FInternetAddr> Addr = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)
->CreateInternetAddr();
bool bIsValid;
Addr->SetIp(*ServerIp, bIsValid);
Addr->SetPort(ServerPort);
if (!bIsValid) return false;
bool bOk = Socket->Connect(*Addr);
if (bOk)
{
bConnected = true;
OnConnected.ExecuteIfBound(true);
StartHeartbeat();
}
return bOk;
}
bool FTcpClient::Send(const TArray<uint8>& Data)
{
if (bConnected && Socket)
{
int32 BytesSent = 0;
return Socket->Send(Data.GetData(), Data.Num(), BytesSent);
}
return false;
}
void FTcpClient::SendHeartbeat()
{
FString HeartbeatMsg = TEXT("HEARTBEAT");
TArray<uint8> Data;
Data.Append((uint8*)TCHAR_TO_UTF8(*HeartbeatMsg), HeartbeatMsg.Len());
Send(Data);
}
void FTcpClient::StartHeartbeat()
{
if (GWorld)
{
GWorld->GetTimerManager().SetTimer(HeartbeatTimerHandle, [this]()
{
SendHeartbeat();
}, HeartbeatInterval, true);
}
}
void FTcpClient::StopHeartbeat()
{
if (GWorld)
{
GWorld->GetTimerManager().ClearTimer(HeartbeatTimerHandle);
}
}
使用示例
在某个 Actor 中使用:
// .h
TUniquePtr<FTcpClient> TcpClient;
// .cpp BeginPlay
TcpClient = MakeUnique<FTcpClient>(TEXT("127.0.0.1"), 7777);
TcpClient->OnConnected.BindLambda([](bool bOk){
UE_LOG(LogTemp, Log, TEXT("Connected: %s"), bOk ? TEXT("成功") : TEXT("失败"));
});
TcpClient->OnMessage.BindLambda([](const TArray<uint8>& Data){
FString Msg(UTF8_TO_TCHAR(Data.GetData()));
UE_LOG(LogTemp, Log, TEXT("Received: %s"), *Msg);
});
TcpClient->OnDisconnected.BindLambda([](){
UE_LOG(LogTemp, Warning, TEXT("已断线,正在重连…"));
});
TcpClient->Start();
// 发送消息
TArray<uint8> Out;
FString ToSend = TEXT("Hello UE5");
Out.Append((uint8*)TCHAR_TO_UTF8(*ToSend), ToSend.Len());
TcpClient->Send(Out);
这样,你就拥有了一个在独立线程中运行、自动重连、带有心跳检测,并通过委托通知主线程的 TCP 客户端类,可以直接嵌入到 UE5 项目中使用。
注意
在 Unreal Engine 中,FSocket::GetConnectionState()
并不是通过底层 TCP 协议实时探测对端是否已关闭连接,而只是返回之前记录的“连接状态”(ESocketConnectionState
)。如果服务器在另一端直接关闭(例如进程退出或调用 Close()
),而客户端未主动调用 Disconnect()
、也未在套接字上执行任何 I/O 操作,那么 GetConnectionState()
会继续报告 SCS_Connected
。这是因为:
TCP 的连接关闭需要通过 四次挥手(FIN/ACK)握手 完成,若一端不正确地发起或响应 FIN,另一端的状态不会变成 CLOSED。
UE 中的
GetConnectionState()
并不会自动触发 I/O,也不依赖 OS 的 keep-alive 机制,它只反映最初的连接结果。要真正探测断线,需要在套接字上执行一次
Recv()
(或Send()
)才会返回错误或 0 字节读取。