UE5通过C++实现TcpSocket连接

在 Unreal Engine 5 的 C++ 项目中,实现一个具备消息监听、心跳检测和断线重连功能的 TCP 客户端,可以参考以下完整示例。

准备工作

1、模块依赖

YourModule.Build.cs 文件中,添加对 SocketsNetworking 模块的依赖:

cpp 复制代码
PublicDependencyModuleNames.AddRange(new string[] {
    "Core",
    "CoreUObject",
    "Engine",
    "Sockets",
    "Networking"
});

2、包含头文件

在相关的 .h 文件中,包含必要的头文件:

cpp 复制代码
#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

cpp 复制代码
#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

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 中使用:

cpp 复制代码
// .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。这是因为:

  1. TCP 的连接关闭需要通过 四次挥手(FIN/ACK)握手 完成,若一端不正确地发起或响应 FIN,另一端的状态不会变成 CLOSED。

  2. UE 中的 GetConnectionState() 并不会自动触发 I/O,也不依赖 OS 的 keep-alive 机制,它只反映最初的连接结果。

  3. 要真正探测断线,需要在套接字上执行一次 Recv()(或 Send())才会返回错误或 0 字节读取。

相关推荐
June`27 分钟前
专题三:穷举vs暴搜vs深搜vs回溯vs剪枝(全排列)决策树与递归实现详解
c++·算法·深度优先·剪枝
我叫珂蛋儿吖1 小时前
[redis进阶六]详解redis作为缓存&&分布式锁
运维·c语言·数据库·c++·redis·分布式·缓存
yxc_inspire1 小时前
基于Qt的app开发第七天
开发语言·c++·qt·app
周Echo周2 小时前
20、map和set、unordered_map、un_ordered_set的复现
c语言·开发语言·数据结构·c++·算法·leetcode·list
dot to one3 小时前
Qt 中 QWidget涉及的常用核心属性介绍
开发语言·c++·qt
二年级程序员4 小时前
C++的历史与发展
c++
迷茫不知归路4 小时前
操作系统实验习题解析 上篇
c++·算法·操作系统·实验课设
一个尚在学习的计算机小白4 小时前
文件相关操作
c++
电子云与长程纠缠4 小时前
UE5中制作动态数字Decal
学习·ue5·编辑器·贴图