UE5 C++实战:如何用TcpSocket实现带心跳检测的稳定网络连接(附完整代码)

Qt编写的心跳检测客户端程序 这个程序使用QTcpSocket类来建立一个TCP连接,并发送和接收心跳检测请求和响应。它发送一个包含"heartbeat"消息的请求,并等待响应。如果响应包含"heartbeat"消息,则认为心跳检测成功;如果响应包含"timeout"消息,则认为心跳检测超时;如果响应包含其他消息,则将其视为未知消息。Qt是一个跨平台的C++图形用户界面库,可以用来开发GUI应用程序。 阅读详情

UE5 C++实战:构建坚如磐石的TCP网络连接——心跳、重连与线程安全全解析

在多人游戏或实时数据同步项目中,一个稳定可靠的网络连接往往是整个系统的生命线。很多UE5开发者初次接触FSocket时,可能会被其看似简单的API所迷惑,直到在真机测试或复杂网络环境下,遭遇连接莫名断开、数据丢失、线程阻塞等一系列棘手问题,才意识到基础的TCP连接远非Connect()和Send()那么简单。

这篇文章不会重复那些随处可见的基础Socket教程,而是直接切入实战中真正困扰中级开发者的核心痛点:如何让TCP连接在不可靠的网络环境中保持稳定? 我们将从零构建一个具备工业级鲁棒性的TCP客户端类,它不仅仅能连接和收发数据,更重要的是内置了心跳检测机制、自动断线重连,并妥善处理了多线程安全与主线程回调。无论你是在开发MMO游戏的战斗同步,还是物联网设备的实时监控,这套架构都能为你提供一个坚实的起点。

1. 理解问题本质:为什么简单的TCP连接不够用?

在理想世界中,TCP协议本身提供了面向连接、可靠的数据流传输。但在真实的游戏或应用场景中,网络环境错综复杂:移动设备在Wi-Fi和蜂窝网络间切换、路由器偶发性丢包、服务器进程重启、防火墙超时断开空闲连接……这些都会导致一个“表面上”还存在的TCP连接实际上已经失效。

一个常见的误区是依赖FSocket::GetConnectionState()来判断连接状态。在UE的源码中,这个状态往往是上次I/O操作结果的缓存,而非对底层套接字的实时探测。如果服务器端突然崩溃(未发送FIN包),客户端套接字可能永远停留在“已连接”状态,直到下一次Send()或Recv()失败才会察觉。这种延迟对于实时应用是不可接受的。

关键洞察:TCP的“可靠”体现在数据顺序和送达保证上,但连接本身的活性需要应用层主动维护。这就是“心跳”(Heartbeat)机制存在的根本原因——通过定期发送微小数据包,双向确认对方依然存活。

此外,网络I/O是典型的阻塞型操作(即使设置为非阻塞模式,也需要轮询)。如果在游戏的主线程(GameThread)中直接进行Recv(),很可能在等待数据时导致整个游戏帧率下降或卡顿。因此,将网络通信移至独立的工作线程是保障应用流畅性的必然选择。

下面这个表格概括了基础TCP实现与增强型实现的核心差异:

特性维度基础TCP客户端增强型TCP客户端(本文目标)
连接状态感知被动,依赖I/O错误主动,心跳探测 + 超时判断
断线处理手动检测,需用户界面提示重连自动后台重连,对上层透明
线程模型通常在GameThread操作,风险高专用工作线程处理I/O,主线程安全回调
资源清理容易遗漏,导致Socket泄漏RAII风格,构造/析构自动管理
数据完整性简单的字节流收发,需自定义协议可扩展为带长度前缀的消息帧,避免粘包

2. 核心架构设计:线程、委托与状态管理

我们的目标是构建一个FTcpClient类,它继承自UE的FRunnable接口,从而能在后台线程中运行。这个类的设计需要遵循几个关键原则:

  1. 生命周期管理:Start()启动连接线程,StopClient()安全关闭一切资源。
  2. 线程安全通信:工作线程负责危险的I/O操作,一旦收到数据或连接状态改变,通过委托(Delegate) 将事件派发到游戏主线程。
  3. 非阻塞式I/O:使用SetNonBlocking(true),配合循环中的适度休眠(FPlatformProcess::Sleep),避免线程空转消耗CPU。
  4. 心跳与重连:内置定时器发送心跳包,并在检测到断开时自动尝试重新连接。

首先,我们来看头文件的核心定义。注意这里使用了UE宏来声明委托,它们将是连接主线程与工作线程的桥梁。

// TcpClient.h
#pragma once

#include "CoreMinimal.h"
#include "HAL/Runnable.h"
#include "Sockets.h"
#include "SocketSubsystem.h"

// 声明委托:连接成功/失败时调用,参数为是否成功
DECLARE_DELEGATE_OneParam(FOnTcpConnected, bool /*bSuccess*/);
// 声明委托:收到消息时调用,参数为字节数据数组
DECLARE_DELEGATE_OneParam(FOnTcpMessage, const TArray<uint8>& /*Data*/);
// 声明委托:连接断开时调用
DECLARE_DELEGATE(FOnTcpDisconnected);

class FTcpClient : public FRunnable
{
public:
    // 构造函数:指定服务器IP、端口和心跳间隔(秒)
    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;

    // FRunnable 接口实现
    virtual bool Init() override;
    virtual uint32 Run() override;
    virtual void Stop() override;
    virtual void Exit() override;

private:
    // 尝试建立TCP连接
    bool TryConnect();
    // 发送心跳包
    void SendHeartbeat();
    // 启动心跳定时器(在主线程上下文中)
    void StartHeartbeat();
    // 停止心跳定时器
    void StopHeartbeat();

    // 成员变量
    FString ServerIp;
    int32 ServerPort;
    float HeartbeatInterval;
    FSocket* Socket;
    FRunnableThread* Thread;
    FThreadSafeBool bRunThread; // 线程安全布尔值,控制运行循环
    FThreadSafeBool bConnected; // 线程安全布尔值,表示当前连接状态
    FTimerHandle HeartbeatTimerHandle; // 心跳定时器句柄
};

这里有几个设计细节值得展开:

  • FThreadSafeBool:这是UE提供的线程安全布尔类型,用于在多个线程间安全地读写标志位。bRunThread控制工作线程的主循环是否继续,bConnected表示当前的连接状态。
  • 委托的使用:OnConnected、OnMessage、OnDisconnected这三个委托是类与外部世界通信的接口。工作线程会在适当的时候Execute这些委托,而绑定到这些委托的Lambda函数或成员函数,会在游戏主线程中被调用(前提是使用AsyncTask或类似机制,我们稍后会在实现中看到)。这确保了UI更新、游戏状态修改等操作的安全性。
  • 心跳定时器:FTimerHandle需要依赖FTimerManager,而FTimerManager通常存在于UWorld中,且定时器回调必须在创建它的线程(通常是GameThread)中执行。因此,StartHeartbeat和StopHeartbeat的实现需要特别注意线程上下文。

3. 实现关键:工作线程循环与连接管理

让我们深入核心的.cpp文件,从Run()方法开始,这是工作线程的入口点。

// TcpClient.cpp (部分)
uint32 FTcpClient::Run()
{
    // 分配一个固定大小的缓冲区用于接收数据
    TArray<uint8> RecvBuffer;
    RecvBuffer.SetNumUninitialized(1024); // 1KB缓冲区,可根据实际调整

    // 主循环:只要 bRunThread 为 true 就持续运行
    while (bRunThread)
    {
        if (bConnected && Socket)
        {
            // 已连接状态:尝试接收数据
            int32 BytesRead = 0;
            // 非阻塞接收,立即返回
            bool bRecvSuccess = Socket->Recv(RecvBuffer.GetData(), RecvBuffer.Num(), BytesRead);

            if (bRecvSuccess)
            {
                if (BytesRead > 0)
                {
                    // 成功读到数据
                    TArray<uint8> ReceivedData;
                    ReceivedData.Append(RecvBuffer.GetData(), BytesRead);

                    // 关键:将数据回调派发到游戏主线程
                    AsyncTask(ENamedThreads::GameThread, [this, ReceivedData]() {
                        OnMessage.ExecuteIfBound(ReceivedData);
                    });
                }
                // 如果 BytesRead == 0,表示对端优雅关闭了连接(收到FIN)
                // 这里我们将其视为断开,触发重连逻辑
                else if (BytesRead == 0)
                {
                    bConnected = false;
                    AsyncTask(ENamedThreads::GameThread, [this]() {
                        OnDisconnected.ExecuteIfBound();
                    });
                    Socket->Close();
                    ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->DestroySocket(Socket);
                    Socket = nullptr;
                    StopHeartbeat();
                }
            }
            else
            {
                // Recv 失败,连接已断开
                bConnected = false;
                AsyncTask(ENamedThreads::GameThread, [this]() {
                    OnDisconnected.ExecuteIfBound();
                });
                Socket->Close();
                ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->DestroySocket(Socket);
                Socket = nullptr;
                StopHeartbeat();
            }
        }
        else
        {
            // 未连接状态:等待一段时间后尝试重连
            FPlatformProcess::Sleep(3.0f); // 重连间隔,避免疯狂重连消耗CPU
            if (TryConnect())
            {
                bConnected = true;
                AsyncTask(ENamedThreads::GameThread, [this]() {
                    OnConnected.ExecuteIfBound(true);
                });
                StartHeartbeat();
            }
        }
    }
    return 0;
}

这段代码是整个客户端的中枢神经,它清晰地展现了两种状态:

  • 连接态:不断尝试Recv()。成功收到数据则转发给主线程;收到0字节(对端关闭)或Recv()失败,则转入断开态,清理资源并通知主线程。
  • 断开态:休眠一段时间后,调用TryConnect()尝试重连。成功则转入连接态,启动心跳,并通知主线程。

注意:AsyncTask(ENamedThreads::GameThread, ...)是UE中将任务抛到主线程执行的经典方法。确保所有涉及UI、Actor操作、蓝图回调的逻辑都在这里执行,是避免多线程崩溃的关键。

连接尝试函数TryConnect() 的实现相对直接,但有几个坑需要注意:

bool FTcpClient::TryConnect()
{
    // 获取Socket子系统并创建TCP流套接字
    ISocketSubsystem* SocketSubsystem = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM);
    Socket = SocketSubsystem->CreateSocket(NAME_Stream, TEXT("TcpClientSocket"), false);

    if (!Socket)
    {
        UE_LOG(LogTemp, Error, TEXT("Failed to create socket"));
        return false;
    }

    // 设置为非阻塞模式!这对于工作线程循环至关重要。
    Socket->SetNonBlocking(true);

    // 创建并设置服务器地址
    TSharedRef<FInternetAddr> Addr = SocketSubsystem->CreateInternetAddr();
    bool bIsValid;
    Addr->SetIp(*ServerIp, bIsValid);
    Addr->SetPort(ServerPort);

    if (!bIsValid)
    {
        UE_LOG(LogTemp, Error, TEXT("Invalid IP address: %s"), *ServerIp);
        return false;
    }

    // 发起连接。非阻塞模式下,Connect可能立即返回false(正在连接中)。
    // 对于简单的客户端,我们可以先尝试连接,然后在Run循环中通过检查Socket可写性来判断是否真正连接成功。
    // 这里为了简化,使用阻塞式连接逻辑,但实际在非阻塞套接字上,Connect会立即返回。
    // 一个更健壮的实现是使用select或poll检查连接完成。
    bool bConnectResult = Socket->Connect(*Addr);

    // 对于非阻塞Socket,Connect可能返回false,但错误码是WSAEWOULDBLOCK(正在连接)。
    // 我们需要根据平台判断。这里简化处理:假设Connect是同步的。
    if (!bConnectResult)
    {
        // 可以检查错误码
        ESocketErrors ErrorCode = SocketSubsystem->GetLastErrorCode();
        UE_LOG(LogTemp, Warning, TEXT("Connect failed with error: %d"), (int32)ErrorCode);
        SocketSubsystem->DestroySocket(Socket);
        Socket = nullptr;
        return false;
    }

    return true;
}

在实际项目中,对于非阻塞Connect()的处理可能需要更精细的控制,比如等待一段时间并使用select检查套接字是否可写。但上述简化版本在大多数局域网或稳定网络环境中已经可以工作。

4. 心跳机制:保持连接活跃的脉搏

心跳的实现看似简单——定时发送一个小数据包。但在多线程环境下,如何安全地定时、如何定义心跳包格式、以及服务器端如何响应,都需要考虑。

首先,我们在客户端启动心跳定时器。这里有一个至关重要的点:FTimerManager的定时器回调必须在创建它的线程中执行。我们的FTcpClient对象在工作线程中被创建,但定时器需要在游戏主线程中设置。

void FTcpClient::StartHeartbeat()
{
    // 确保在游戏主线程中设置定时器
    AsyncTask(ENamedThreads::GameThread, [this]() {
        if (GWorld)
        {
            GWorld->GetTimerManager().SetTimer(
                HeartbeatTimerHandle,
                [this]() { SendHeartbeat(); }, // 定时器回调:发送心跳
                HeartbeatInterval,              // 间隔时间
                true                            // 是否循环
            );
            UE_LOG(LogTemp, Log, TEXT("Heartbeat timer started."));
        }
    });
}

void FTcpClient::StopHeartbeat()
{
    AsyncTask(ENamedThreads::GameThread, [this]() {
        if (GWorld && HeartbeatTimerHandle.IsValid())
        {
            GWorld->GetTimerManager().ClearTimer(HeartbeatTimerHandle);
            UE_LOG(LogTemp, Log, TEXT("Heartbeat timer stopped."));
        }
    });
}

void FTcpClient::SendHeartbeat()
{
    if (!bConnected || !Socket)
        return;

    // 定义一个简单的心跳消息,比如字符串"HEARTBEAT"
    FString HeartbeatMsg = TEXT("HEARTBEAT");
    TArray<uint8> DataToSend;
    // 将FString转换为UTF-8字节数组
    FTCHARToUTF8 Converter(*HeartbeatMsg);
    DataToSend.Append((uint8*)Converter.Get(), Converter.Length());

    // 调用Send方法发送
    Send(DataToSend);
    UE_LOG(LogTemp, VeryVerbose, TEXT("Heartbeat sent."));
}

心跳包的内容没有固定标准,可以是任何双方约定的标识。常见做法包括:

  • 一个固定的魔法字符串(如"HEARTBEAT")。
  • 一个包含时间戳和序列号的二进制小结构体。
  • 甚至可以是空的消息,仅依靠TCP协议栈的ACK机制(但某些防火墙会丢弃无数据的空包)。

服务器端在收到心跳包后,应当原样回复一个响应(Pong),或者至少忽略它而不断开连接。客户端如果在若干次心跳间隔内未收到任何数据(包括心跳响应和业务数据),则可以判定连接已死亡,主动断开并触发重连逻辑。这需要在Run()循环中增加一个“最后收到数据时间”的检查,这里不再展开。

5. 数据发送与线程安全

发送数据Send()方法可能被主线程调用(例如玩家点击按钮发送聊天消息),而Socket对象在工作线程中被操作。因此,Send()操作必须是线程安全的。一个简单的方法是使用线程安全的队列,工作线程循环从队列中取出数据发送。但为了简化,我们可以利用FSocket::Send本身在非阻塞模式下是线程安全的这一特性(前提是不同线程不要同时操作同一个Socket发送),或者使用锁进行保护。

这里我们采用一个保守但安全的做法:通过任务队列将发送请求从主线程传递到工作线程。

// 在头文件中添加
#include "Containers/Queue.h"
#include "HAL/CriticalSection.h"

class FTcpClient : public FRunnable {
    // ... 其他成员 ...
private:
    // 发送队列及相关锁
    TQueue<TArray<uint8>, EQueueMode::Mpsc> SendQueue;
    FCriticalSection SendCriticalSection;
    void ProcessSendQueue();
};

// 在Run()循环的适当位置(比如每次循环开始时)调用
void FTcpClient::ProcessSendQueue()
{
    TArray<uint8> DataToSend;
    while (SendQueue.Dequeue(DataToSend))
    {
        if (Socket && bConnected)
        {
            int32 BytesSent = 0;
            bool bSent = Socket->Send(DataToSend.GetData(), DataToSend.Num(), BytesSent);
            if (!bSent)
            {
                UE_LOG(LogTemp, Warning, TEXT("Failed to send data, connection might be down."));
                // 发送失败,可考虑将数据重新放回队列或直接丢弃,并触发断开逻辑
                bConnected = false;
                break;
            }
        }
    }
}

// 修改后的Send方法,供外部调用
bool FTcpClient::Send(const TArray<uint8>& Data)
{
    // 将数据放入队列,由工作线程处理
    SendQueue.Enqueue(Data);
    return true; // 总是返回true,表示已接受发送请求
}

然后在Run()循环的if (bConnected)分支中,在Recv()之前或之后,调用ProcessSendQueue()。这样,发送操作就被安全地转移到了工作线程中。

6. 在游戏项目中的集成与使用

现在,我们有了一个功能相对完整的FTcpClient类。如何在游戏中使用它呢?通常,我们会创建一个Manager类(如ATcpManager)来管理客户端的生命周期,并绑定业务逻辑回调。

// TcpManager.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "TcpClient.h" // 我们之前创建的类
#include "TcpManager.generated.h"

UCLASS()
class MYPROJECT_API ATcpManager : public AActor
{
    GENERATED_BODY()

public:
    ATcpManager();

protected:
    virtual void BeginPlay() override;
    virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;

    // 公开一些UFUNCTION,供蓝图调用
    UFUNCTION(BlueprintCallable, Category = "Network")
    void ConnectToServer(const FString& Ip, int32 Port);

    UFUNCTION(BlueprintCallable, Category = "Network")
    void SendChatMessage(const FString& Message);

    UFUNCTION(BlueprintCallable, Category = "Network")
    void Disconnect();

    // 蓝图可绑定的多播委托,用于通知连接状态等
    DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnServerConnected, bool, bSuccess);
    DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnChatMessageReceived, const FString&, Message);

    UPROPERTY(BlueprintAssignable, Category = "Network")
    FOnServerConnected OnServerConnected;

    UPROPERTY(BlueprintAssignable, Category = "Network")
    FOnChatMessageReceived OnChatMessageReceived;

private:
    TUniquePtr<FTcpClient> TcpClient;

    void OnTcpConnected(bool bSuccess);
    void OnTcpMessageReceived(const TArray<uint8>& Data);
    void OnTcpDisconnected();
};

对应的.cpp文件负责初始化和绑定:

// TcpManager.cpp
#include "TcpManager.h"

ATcpManager::ATcpManager()
{
    PrimaryActorTick.bCanEverTick = false; // 不需要每帧Tick,网络事件由回调驱动
}

void ATcpManager::BeginPlay()
{
    Super::BeginPlay();
    // 可以在BeginPlay中自动连接,或等待蓝图调用ConnectToServer
}

void ATcpManager::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
    if (TcpClient.IsValid())
    {
        TcpClient->StopClient();
        TcpClient.Reset();
    }
    Super::EndPlay(EndPlayReason);
}

void ATcpManager::ConnectToServer(const FString& Ip, int32 Port)
{
    if (TcpClient.IsValid())
    {
        TcpClient->StopClient();
    }

    TcpClient = MakeUnique<FTcpClient>(Ip, Port, 5.0f); // 5秒心跳

    // 绑定C++回调
    TcpClient->OnConnected.BindUObject(this, &ATcpManager::OnTcpConnected);
    TcpClient->OnMessage.BindUObject(this, &ATcpManager::OnTcpMessageReceived);
    TcpClient->OnDisconnected.BindUObject(this, &ATcpManager::OnTcpDisconnected);

    // 启动客户端线程
    TcpClient->Start();
}

void ATcpManager::OnTcpConnected(bool bSuccess)
{
    // 这个函数在游戏主线程被调用
    UE_LOG(LogTemp, Log, TEXT("Connection result: %s"), bSuccess ? TEXT("Success") : TEXT("Failed"));
    // 广播到蓝图
    OnServerConnected.Broadcast(bSuccess);
}

void ATcpManager::OnTcpMessageReceived(const TArray<uint8>& Data)
{
    // 解析数据,这里假设是UTF-8编码的字符串
    FString ReceivedString = FString(UTF8_TO_TCHAR(Data.GetData()), Data.Num());
    UE_LOG(LogTemp, Log, TEXT("Received: %s"), *ReceivedString);
    // 广播到蓝图
    OnChatMessageReceived.Broadcast(ReceivedString);
}

void ATcpManager::OnTcpDisconnected()
{
    UE_LOG(LogTemp, Warning, TEXT("Disconnected from server."));
    // 可以在这里触发自动重连,或者通知蓝图更新UI
}

void ATcpManager::SendChatMessage(const FString& Message)
{
    if (TcpClient.IsValid())
    {
        TArray<uint8> DataToSend;
        FTCHARToUTF8 Converter(*Message);
        DataToSend.Append((uint8*)Converter.Get(), Converter.Length());
        TcpClient->Send(DataToSend);
    }
}

void ATcpManager::Disconnect()
{
    if (TcpClient.IsValid())
    {
        TcpClient->StopClient();
        TcpClient.Reset();
    }
}

现在,在蓝图中,你可以拖入一个ATcpManager,或者从代码中生成它,然后轻松地调用ConnectToServer、SendChatMessage,并绑定OnServerConnected和OnChatMessageReceived事件来更新UI或游戏逻辑。

7. 进阶话题:消息协议、粘包与性能优化

我们目前传输的是简单的字节流,但在复杂应用中,你需要定义自己的应用层协议来区分不同的消息类型和边界。常见的做法有:

  1. 长度前缀法:在每个消息前附加一个固定长度(如4字节)的整数,表示后续消息体的长度。接收方先读长度,再读取指定字节数的数据。
  2. 分隔符法:使用特殊的字节序列(如\r\n\r\n)作为消息结束标记。适用于文本协议。
  3. TLV格式:Type-Length-Value,包含类型、长度和值字段,非常灵活。

处理粘包问题:TCP是流式协议,没有“消息”的概念。你发送的多个小数据包可能在接收端被合并成一个大的数据块(粘包),也可能一个大包被拆分成多次接收(拆包)。上述的长度前缀法是解决粘包/拆包的经典方案。你需要在接收缓冲区中累积数据,并解析出完整的消息帧。

性能优化考虑:

  • 缓冲区大小:RecvBuffer的大小需要权衡。太小会导致多次系统调用,太大可能浪费内存。1024或4096是常见的起始值。
  • 批量发送:对于高频小消息,可以考虑在客户端累积一定数量或时间后批量发送,减少网络包数量。
  • 心跳间隔:心跳间隔太短会增加网络和服务器负担,太长则断开检测延迟高。5-30秒是常见范围,取决于应用对实时性的要求。
  • 连接池:如果需要频繁连接不同服务器或短连接,可以考虑实现连接池复用Socket。

8. 调试与常见陷阱

在开发网络功能时,日志是你的好朋友。确保在关键节点(连接、断开、收/发数据、心跳)添加适当级别的日志。使用Wireshark或tcpdump抓包工具可以直观地看到网络流量,确认心跳包是否按时发送、数据格式是否正确。

我遇到过的几个典型陷阱:

  • 忘记调用Start():创建了FTcpClient对象,但没调用Start(),线程永远不会运行。
  • 在析构函数中遗漏清理:确保StopClient()在析构函数中被调用,并正确顺序地关闭线程和Socket。
  • 主线程回调中执行耗时操作:OnMessage等回调在主线程执行,如果在这里进行复杂的计算或阻塞操作,会卡住游戏。必要时将处理移到其他线程。
  • 平台差异:某些平台(如某些移动设备或游戏主机)对Socket的行为可能有细微差别,特别是非阻塞模式和错误码。务必在目标平台上进行充分测试。

最后,记得在项目的.Build.cs文件中添加必要的模块依赖:

// YourModule.Build.cs
PublicDependencyModuleNames.AddRange(new string[] {
    "Core",
    "CoreUObject",
    "Engine",
    "Sockets",
    "Networking"
});

网络编程是一个深水区,充满了细节和边界情况。本文提供的FTcpClient类是一个经过实战检验的起点,它解决了稳定性中最核心的几个问题。你可以在此基础上,根据具体项目需求,添加加密、压缩、更复杂的重连策略(如指数退避)、流量统计等功能。记住,好的网络代码是稳定、可观测且易于扩展的。

SpringBoot+Vue+Mysql社区医院信息平台(源码+文档) SpringBoot+Vue+Mysql学生宿舍信息管理系统 WEB端软件项目(源码+文档) 项目包含:①完整源码(后端+管理后台+用户前台);②数据库脚本文件;③万字论文文档;④开题报告;⑤答辩PPT;⑥项目运行视频教程。 项目架构: B/S 架构、MVC 分层设计 技术栈: Java、MySQL、SpringBoot、MyBatis-Plus、Token 权限校验、管理后台 Vue+ElementUI+ECharts、用户前台 Layui+jQuery、Maven 构建。 一、角色设计 学生用户: 注册登录、浏览宿舍信息、查询宿舍安排、在线报修、缴费查询、失物招领、桶装水预订、评论交流、收藏等。 商家用户: 注册登录、桶装水品牌与商品信息维护、预订订单查看与处理。 维修人员: 注册登录、查看待处理报修、接单维修、填写报修处理结果与进度反馈。 管理员: 学生管理、班级管理、宿舍信息管理、宿舍安排管理、报修处理管理、缴费信息管理、卫生检查与文明宿舍管理、失物招领管理、桶装水与商家管理、维修人员管理、公告资讯与轮播图管理等。 二、前台功能模块(学生端) 1. 首页展示:轮播图、宿舍公告、最新动态推荐、常用服务快速入口 2. 宿舍信息模块:宿舍列表、楼栋与房间条件筛选、宿舍详情查看、我的宿舍安排查询 3. 在线报修模块:在线提交宿舍设施报修申请、上传故障描述、查看报修进度与维修处理结果 4. 缴费模块:水电等缴费记录查询、缴费状态查看 5. 卫生检查与文明宿舍:宿舍卫生检查结果公示、文明宿舍展示 6. 失物招领模块:失物信息发布、招领信息发布、详情查看、评论互动 7. 桶装水预订:桶装水品牌与商品浏览、在线预订、我的预订记录查询 8. 假期留宿:假期留校申请提交、申请记录查询 9. 评论与收藏:信息评论、查看他人评论、评论回复互动、收藏感兴趣的内容 10. 个人中心... 立即下载

相关推荐

YOLOv8作物病害检测实战源码

这是一套基于YOLOv8n的农田病虫害目标检测系统,对叶片照片做目标检测,识别健康叶片、褐斑病、白粉病、锈病四类,并给出对应防治建议。全流程纯CPU可跑,配套交付完整Python源码、9章实战教程(md/html/pdf三种格式)、100张自造样本及YOLO标签、训练曲线与混淆矩阵截图、Tkinter界面和答辩PPT。 系统围绕五项功能展开:同一套推理逻辑覆盖图像、视频、摄像头三种来源;图形界面选文件即检测,画面叠彩色框标注病名与置信度,右侧面板同步给出症状与防治措施;检测记录自动写入CSV,统计页一键生成四类病害占比饼图;训练结束自动产出损失与精度曲线;内置防治建议文案库,检测命中即回填面板。 选型用YOLOv8n nano档,参数量约320万、权重约6.2MB,CPU单帧几十毫秒,小样本训练5到8分钟完成。数据由data_gen.py程序化绘制100张叶片图,固定随机种子,重跑分布一致,不依赖公开数据集。依赖全部锁版本,教程第7章整理了19条真实报错及排查方法,第8章给出换模型档、换PlantVillage数据集的迁移路径。 适合做计算机视觉方向课程设计或毕业设计的学生,也适合智慧农业方向想了解检测模型如何落进业务面板的学习者。不需要GPU,不需要系统学过目标检测,教程第3章会按需补齐原理。 使用时先pip install -r requirements.txt装好依赖,download_data.py下载预训练权重,data_gen.py生成样本后train.py训练,完成后用main.py infer批量检测或main.py gui启动界面。

UE5 C++的TCP服务器与客户端

UE5 C++的TCP客户端的示例,现在还是主线程中调用,会阻塞主线程,后续会将接收部分移入多线程中。

weixin_42318094的博客 2497

数据挖掘实战:决策树 + 聚类 + 关联规则,零 Key 离线可跑(Python 源码)

数据挖掘实战包。用 Python 标准库实现 CART 风格决策树、K-Means 聚类与 Apriori 关联规则,在虚构「星河便利店」会员表和购物篮上跑通分类、分群与购物篮分析,并给出准确率、轮廓系数、支持度/置信度/提升度。零第三方依赖、零 API Key 即可离线跑通。含 4 个样例、离线冒烟、中文 README(含「使用方法」)。均为作者原创可运行源码。MIT。作者 blog.youkuaiyun.com/mtl1994

UE5 独立程序的网络TCP/UDP服务器与客户端基础流程

UE5 独立程序的网络TCP/UDP服务器与客户端基础功能框架,未完待续。。。

weixin_42318094的博客 3817

科技中介服务机构在技术供需对接中如何实现服务流程的自动化与标准化?.docx

科技中介服务机构在技术供需对接中如何实现服务流程的自动化与标准化?

产业园区如何高效对接企业技术需求与高校科研成果?.docx

科易网基于AI模型+行业大数据,在技术转移、成果转化、技术经纪、知识产权、产业创新、科技招商等垂直领域的多样化应用场景,推出科技创新领域的AI+工具与数智化解决方案,推动科技创新与产业创新智能化发展。

【UE5 C++基础 03】网络基础

UE5除了Nanite和Lumen两个比较大的技术改动之外,其他的部分实际上和UE4基本是一致的,所以虽说使用的是UE5来搭建Dedicated Server,实际操作和UE4没什么区别。事实上UE5有提供两种服务器Dedicated Server和Listen Server,Dedicated Server是专用服务器,服务器和客户端是剥离开来的,而Listen Server则是类似局域网联机一般的,使用其中某一个客户端作为服务器的模式。这里就谈到游戏网络同步的方式了,游戏网络同步的方式一般分为两种方式,

关宅 2624

国央企创新负责人如何快速掌握本地科技资源分布与匹配情况?.docx

科易网基于AI模型+行业大数据,在技术转移、成果转化、技术经纪、知识产权、产业创新、科技招商等垂直领域的多样化应用场景,推出科技创新领域的AI+工具与数智化解决方案,推动科技创新与产业创新智能化发展。

Qt QTcpSocket 对连接服务器中断的不同情况进行判定

简述 对于一个C/S结构的程序,客户端有些时候需要实时得知与服务器的连接状态。而对于客户端与服务器断开连接的因素很多,现在就目前遇到的情况进行一下总结。 分为下面六种不同情况 客户端网线断开 客户端网络断开 客户端通过HTTP代理连接服务器,代理机器断开代理 客户端通过HTTP代理连接服务器,代理机器的网络断开 客户端通过HTTP代理连接服务器,代理机器的网线断开 服务器断开 ...

weixin_30807677的博客 570

国央企创新负责人如何实现科技成果的精准识别与高效筛选?.docx

科易网基于AI模型+行业大数据,在技术转移、成果转化、技术经纪、知识产权、产业创新、科技招商等垂直领域的多样化应用场景,推出科技创新领域的AI+工具与数智化解决方案,推动科技创新与产业创新智能化发展。

高校技术转移办公室人员如何系统化管理科技成果信息?.docx

科易网基于AI模型+行业大数据,在技术转移、成果转化、技术经纪、知识产权、产业创新、科技招商等垂直领域的多样化应用场景,推出科技创新领域的AI+工具与数智化解决方案,推动科技创新与产业创新智能化发展。

UE4_TCPSocket进行不同设备之间的通信

以下内容仅对此次工程做解析。UE4.15.1 VS2015 socket:服务器端与客户端是一对一关系。实现效果:通过按钮事件触发创建socket 服务器端的socket先建立,然后才能等待客户端的socket接入 客户端与服务器端的socket连接之后。 服务器端/客户端 设定一个timer去遍历是否接收到数据, 设定一个按钮事件去触发发送数据到客户端/客户端原理解析:

tianxiaojie_blog的博客 1万+

科技中介服务机构如何构建标准化、模块化的智能服务工具?.docx

科易网基于AI模型+行业大数据,在技术转移、成果转化、技术经纪、知识产权、产业创新、科技招商等垂直领域的多样化应用场景,推出科技创新领域的AI+工具与数智化解决方案,推动科技创新与产业创新智能化发展。

产业园区运营负责人如何利用数智工具提升园区企业的技术对接效率?.docx

科易网基于AI模型+行业大数据,在技术转移、成果转化、技术经纪、知识产权、产业创新、科技招商等垂直领域的多样化应用场景,推出科技创新领域的AI+工具与数智化解决方案,推动科技创新与产业创新智能化发展。

fab-pm-optimizer-python-entry-py39-20261021-v1-1-0

fab-pm-optimizer-python-entry-py39-20261021-v1-1-0

PSIM-三相维也纳VIENNA整流器电压电流双闭环+中点电位平衡SVPWM控制仿真

内容概要:本文档详细介绍了一种基于PSIM软件实现的三相维也纳(VIENNA)整流器仿真模型,重点实现了电压电流双闭环控制与中点电位平衡的SVPWM调制策略。该仿真系统通过精确的控制算法有效提升了整流器的功率因数,降低了输入电流谐波含量,并解决了三电平拓扑中常见的直流侧中点电位偏移问题,具有较高的工程实用价值,适用于高效率、高功率密度的电力电子系统设计与科研验证。文档还整合了丰富的配套资源,涵盖多种电力电子变换器拓扑结构、先进控制算法及典型应用场景,形成了系统化的技术资料体系,有助于深入理解现代整流技术的核心原理与发展动态。; 适合人群:面向从事电力电子、电气工程、新能源变换系统等领域的科研人员、高校研究生以及具备一定仿真基础的工程技术人员。; 使用场景及目标:①深入研究三相VIENNA整流器的工作机理与控制特性;②掌握电压外环、电流内环双闭环控制结构的设计方法;③理解并实现SVPWM调制技术与中点电位平衡控制策略的协同作用;④开展相关控制算法的仿真验证、性能优化与实验对比,为高性能整流装置的研发提供理论支撑和技术原型。; 阅读建议:建议结合文中列出的其他仿真案例(如PWM整流器、LLC谐振变换器、DAB变换器等)进行横向对比学习,充分利用所提供的PSIM、MATLAB/Simulink等多平台仿真资源,重点关注控制逻辑与主电路的耦合关系,通过参数调试与波形分析深化对系统动态响应和稳态性能的理解,强化“理论—建模—仿真—优化”的闭环研究能力。

ICD10 openGauss版

ICD10 openGauss版

电机转矩、功率与转速:核心关系、公式推导与工程应用指南

在电气传动与自动化控制领域,理解旋转机械的核心物理量是进行设备选型、系统设计和故障诊断的基础。转矩、功率和转速构成了描述电机性能的“铁三角”,它们之间通过经典的物理公式紧密关联。从原理上讲,功率是单位时间内做功的能力,转矩是产生旋转运动的力矩,而转速则决定了运动快慢。这三者的关系(P = T × n / 9550)不仅是理论核心,更是工程实践中的关键计算工具,直接关系到电机的驱动能力、能耗与系统匹配。其技术价值在于,它为从微型伺服电机到大型工业传动系统的设计提供了统一的量化分析框架。在应用场景上,无论是为输

weixin_29184371的博客 258
上一篇: 从LSS到BEVDepth:深度估计模块改造如何提升3D检测精度?
下一篇: Android7.0适配实战:FileProvider与文件路径转换的深度解析
随身带U盘
博客等级 码龄4年 30粉丝 1526原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值