UE4 TCP协议连接服务器与客户端

一、TCP原理简介

       TCP是传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793  定义。

       TCP旨在适应支持多网络应用的分层协议层次结构。 连接到不同但互连的计算机通信网络的主计算机中的成对进程之间依靠TCP提供可靠的通信服务。TCP假设它可以从较低级别的协议获得简单的,可能不可靠的数据报服务。 原则上,TCP应该能够在从硬线连接到分组交换或电路交换网络的各种通信系统之上操作。

主要特点是:

       TCP是一种面向广域网的通信协议,目的是在跨越多个网络通信时,为两个通信端点之间提供一条具有下列特点的通信方式: 

(1)基于流的方式;

(2)面向连接;

(3)可靠通信方式;

(4)在网络状况不佳的时候尽量降低系统由于重传带来的带宽开销;

(5)通信连接维护是面向通信的两个端点的,而不考虑中间网段和节点。

工作方式:TCP是因特网中的传输层协议,使用三次握手协议建立连接。当主动方发出SYN连接请求后,等待对方回答SYN+ACK,并最终对对方的 SYN 执行 ACK 确认。这种建立连接的方法可以防止产生错误的连接,TCP使用的流量控制协议是可变大小的滑动窗口协议。 

二、UE4多线程原理

       线程:是操作系统能够进行运行调度的最小单位。大部分情况下,它被包含在进程之中,是进程中的实际运作单位。一条线程指的的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

       UE4是跨平台的引擎,对各个平台线程实现进行了封装,抽象出了FRunable。引擎中大部分的需要多线程执行逻辑都是继承这个类实现的多线程。

     UE4的FRunable多线程执行任务逻辑逻辑如下:

#include "HAL/Runnable.h"
UCLASS()
class TCPPROJECT_API USocketRSThread : public UObject,public FRunnable
{
	GENERATED_BODY()
public:
	virtual bool Init() override { return true; }
	virtual uint32 Run() override  { return 0; }
	virtual void Stop() override {}
	virtual void Exit() override {}
}

调用顺序是 Init()Run()Exit()。Runnable对象初始化操作在 Init() 函数中完成,并通过返回值确定是否成功。初始化失败,则该线程停止执行,并返回一个错误码;成功,则会执行 Run() ;执行完毕后,则会调用 Exit() 执行清理操作。

三、案例介绍    

 本次案例使用的是UE4引擎的Sockets模块和Networking模块,利用Socket进行通信,利用UE4的多线程来处理分发任务。

前期准备,在你的项目的build.cs中添加两个模块,

第一步:先创建一个Object类,继承自UObject,FRunable,命名为SocketRSThread

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "Runtime/Sockets/Public/Sockets.h"
#include "HAL/Runnable.h"
#include "Sockets/Public/SocketSubsystem.h"
#include "SocketRSThread.generated.h"

/**
 * 
 */

//声明两个带参数的动态多播代理
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FReceiveSocketDataDelegate, FString, Data);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FLostConnectionDelegate, USocketRSThread*, Thread);


UCLASS()
class TCPPROJECT_API USocketRSThread : public UObject,public FRunnable
{
	GENERATED_BODY()
public:
	virtual bool Init() override { return true; }
	virtual uint32 Run() override;
	virtual void Stop() override;
	virtual void Exit() override {}

	void Start(FSocket* Socket, uint32 SendDataSize, uint32 RecDataSize);
	//发送数据
	void Send(FString Message);
	//接收数据的代理通知
	FReceiveSocketDataDelegate ReceiveSocketDataDelegate;
	//断开连接的代理通知
	FLostConnectionDelegate	LostConnectionDelegate;

protected:
	FSocket* ConnectSocket;
	uint32 SendDataSize1;
	uint32 RecDataSize1;
	TArray<uint8> ReceiveData;
	/** 线程相关 */
	FRunnableThread* pThread;
	bool bThreadStop;

};
// Fill out your copyright notice in the Description page of Project Settings.


#include "SocketRSThread.h"

uint32 USocketRSThread::Run()
{
	while (!bThreadStop)
	{
		//这个地方是之前将socket设置为阻塞模式 在这里5s内判断是否断开连接
		uint32 Size;
		bool LostConnect = false;
		ConnectSocket->HasPendingConnection(LostConnect);
		ConnectSocket->Wait(ESocketWaitConditions::WaitForReadOrWrite, FTimespan(0, 0, 5));
		if (LostConnect)
		{
			//UE_LOG(LogTemp, Warning, TEXT(" doesn't Connect "));
			Stop();
			LostConnectionDelegate.Broadcast(this);
			continue;
		}

		/** 处理接收数据 */
		if (ConnectSocket && ConnectSocket->HasPendingData(Size))
		{
			ReceiveData.Init(0, FMath::Min(Size, RecDataSize1));
			int32 Readed;
			ConnectSocket->Recv(ReceiveData.GetData(), RecDataSize1, Readed);
			FString ReceivedString = FString(ANSI_TO_TCHAR(reinterpret_cast<const char*>(ReceiveData.GetData())));
			ReceiveSocketDataDelegate.Broadcast(ReceivedString);
		}
	}
	return 0;
}

void USocketRSThread::Stop()
{
	bThreadStop = true;
	ConnectSocket->Close();
	ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->DestroySocket(ConnectSocket);
	ConnectSocket = nullptr;
}

void USocketRSThread::Start(FSocket* Socket, uint32 SendDataSize, uint32 RecDataSize)
{
	this->ConnectSocket = Socket;
	this->SendDataSize1 = SendDataSize;
	this->RecDataSize1 = RecDataSize;
	FRunnableThread::Create(this, TEXT("Receive Threald"));
}

void USocketRSThread::Send(FString Message)
{
	///** 处理发送数据 */
	TCHAR* SendMessage = Message.GetCharArray().GetData();
	int32 size = FCString::Strlen(SendMessage) + 1;
	int32 sent = 0;
	if (size >= (int32)SendDataSize1)
	{
		UE_LOG(LogTemp, Error, TEXT("Send Data Size is Larger than Max Size for set"));
	}
	else
	{
		if (ConnectSocket && ConnectSocket->Send((uint8*)TCHAR_TO_UTF8(SendMessage), size, sent))
		{
			UE_LOG(LogTemp, Warning, TEXT("___Send Succeed!"));
			
		}
		else
		{
			UE_LOG(LogTemp, Error, TEXT("___Send Failed!"));
		}
	}
}

 第二步:创建TCPServerActor

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include"SocketRSThread.h"
#include"TimerManager.h"
#include "Runtime/Sockets/Public/Sockets.h"
#include "TCPServer.generated.h"



DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FServerSocketCreateDelegate, bool, bSuccess);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FConnectReceiveDelegate, FString, RemoteIP, int32, RemotePort);

UCLASS(BlueprintType, Blueprintable)
class TCPPROJECT_API ATCPServer : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	ATCPServer();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;
	virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;

	//发送和接受的数据大小
	int32 SendDataSize;
	int32 RecDataDize;
	//服务器Socket
	FSocket* serverSocket;

	UPROPERTY(BlueprintAssignable, VisibleAnywhere, Category = Network)
		FServerSocketCreateDelegate SocketCreateDelegate;
	UPROPERTY(BlueprintAssignable, VisibleAnywhere, Category = Network)
		FConnectReceiveDelegate ConnectReceiveDelegate;
	UPROPERTY(BlueprintAssignable, VisibleAnywhere, Category = Network)
		FReceiveSocketDataDelegate ReceiveSocketDataDelegate;

	FTimerHandle ConnectCheckHandler;
	TArray<USocketRSThread*> RecThreads;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;
	//创建服务器
	UFUNCTION(BlueprintCallable, Category = Network)
		void CreateServer(const FString& ServerIP, int32 ServerPort, int32 ReceiveBufferSize = 1024, int32 SendBufferSize = 1024);
	/** 关闭Server */
	UFUNCTION(BlueprintCallable, Category = Network)
		void CloseServer();
	/** 检测是否有客户端连入 */
	void ConnectCheck();
	//发送数据到客户端
	UFUNCTION(BlueprintCallable, Category = Network)
		void SendToClient(FString Message);
	//客户端断开连接
	UFUNCTION(Category = Network)
		void OnClientDisconnect(class USocketRSThread* pThread);

};
// Fill out your copyright notice in the Description page of Project Settings.


#include "TCPServer.h"
#include "SocketRSThread.h"
#include "Sockets/Public/SocketSubsystem.h"
#include "Networking/Public/Interfaces/IPv4/IPv4Address.h"
#include "Runtime/Networking/Public/Common/TcpSocketBuilder.h"


// Sets default values
ATCPServer::ATCPServer()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

}

// Called when the game starts or when spawned
void ATCPServer::BeginPlay()
{
	Super::BeginPlay();
	
}

void ATCPServer::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
	CloseServer();
	Super::EndPlay(EndPlayReason);
}

// Called every frame
void ATCPServer::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

void ATCPServer::CreateServer(const FString& ServerIP, int32 ServerPort, int32 ReceiveBufferSize, int32 SendBufferSize)
{
	this->RecDataDize = ReceiveBufferSize;
	this->SendDataSize = SendBufferSize;
	FIPv4Address ServerAddr;
	if (!FIPv4Address::Parse(ServerIP, ServerAddr))
	{
		UE_LOG(LogTemp, Error, TEXT("Server Ip %s is illegal"), *ServerIP);
	}
	serverSocket = FTcpSocketBuilder(TEXT("Socket Listener"))
		.AsReusable()
		.AsBlocking()
		.BoundToAddress(ServerAddr)
		.BoundToPort(ServerPort)
		.Listening(8)
		.WithReceiveBufferSize(ReceiveBufferSize)
		.WithSendBufferSize(SendBufferSize);
	if (serverSocket)
	{
		UE_LOG(LogTemp, Warning, TEXT("Server Create Success!"), *ServerIP);
		SocketCreateDelegate.Broadcast(true);
		GetWorld()->GetTimerManager().SetTimer(ConnectCheckHandler, this, &ATCPServer::ConnectCheck, 1, true);
	}
}

void ATCPServer::CloseServer()
{
	if(serverSocket)
	{
		serverSocket->Close();
		for (auto RecThreald : RecThreads)
		{
			RecThreald->Stop();
		}
		ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->DestroySocket(serverSocket);
		UE_LOG(LogTemp, Warning, TEXT("Close is Succeed"));
	}
}

void ATCPServer::ConnectCheck()
{
	bool bPending = false;
	if (serverSocket->HasPendingConnection(bPending) && bPending)
	{
		//有新的socket连接进来
		TSharedRef<FInternetAddr> RemoteAddress = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateInternetAddr();
		FSocket* RecSocket = serverSocket->Accept(*RemoteAddress, TEXT("Receive Socket"));
		USocketRSThread* RSThread = NewObject<USocketRSThread>();
		RecThreads.Add(RSThread);
		RSThread->ReceiveSocketDataDelegate = ReceiveSocketDataDelegate;
		RSThread->LostConnectionDelegate.AddDynamic(this, &ATCPServer::OnClientDisconnect);
		RSThread->Start(RecSocket, SendDataSize, RecDataDize);
		ConnectReceiveDelegate.Broadcast(RemoteAddress->ToString(false), RemoteAddress->GetPort());
	}
}

void ATCPServer::SendToClient(FString Message)
{
	for (auto SocketThread : RecThreads)
	{
		SocketThread->Send(Message);
	}

}

void ATCPServer::OnClientDisconnect(USocketRSThread* pThread)
{
	UE_LOG(LogTemp, Warning, TEXT("Client lost"));
	RecThreads.Remove(pThread);
}

第三步:创建TCPClientActor

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "SocketRSThread.h"
#include "Runtime/Sockets/Public/Sockets.h"
#include "TCPClient.generated.h"

UCLASS(BlueprintType, Blueprintable)
class TCPPROJECT_API ATCPClient : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	ATCPClient();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;
	virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;

	UFUNCTION()
		void OnServerDisconnect(class USocketRSThread* pThread);
public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;



	UFUNCTION(BlueprintCallable, Category = Network)
		void CreateClientAndConnect(FString ServerIP, int32 Port, int32 ReceiveSize = 1024, int32 SendSize = 1024);

	UFUNCTION(Category = Network)
		bool ConnectServer(FString ServerIP, int32 Port);

	UFUNCTION(BlueprintCallable, Category = Network)
		void SendToServer(FString Message);

	UPROPERTY(BlueprintAssignable, VisibleAnywhere, Category = Network)
		FReceiveSocketDataDelegate ReceiveSocketDataDelegate;

protected:
	//客户端Sokcet
	class FSocket* ClientSocket;
	//发送的数据大小
	int32 SendDataSize;
	//接收的数据大小
	int32 RecDataDize;
	//多线程
	TArray<class USocketRSThread*> RecThreads;

};
// Fill out your copyright notice in the Description page of Project Settings.


#include "TCPClient.h"
#include "Runtime/Networking/Public/Common/TcpSocketBuilder.h"


// Sets default values
ATCPClient::ATCPClient()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

}

// Called when the game starts or when spawned
void ATCPClient::BeginPlay()
{
	Super::BeginPlay();
	
}

// Called every frame
void ATCPClient::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}
void ATCPClient::CreateClientAndConnect(FString ServerIP, int32 Port, int32 ReceiveSize, int32 SendSize)
{
	this->SendDataSize = SendSize;
	this->RecDataDize = ReceiveSize;

	ClientSocket = FTcpSocketBuilder(TEXT("Client Socket"))
		.AsReusable()
		.AsBlocking()
		.WithReceiveBufferSize(ReceiveSize)
		.WithSendBufferSize(SendSize);

	if (!ClientSocket)
	{
		UE_LOG(LogTemp, Error, TEXT("Create Client Socket Error!"));
	}
	else
	{
		ConnectServer(ServerIP, Port);
	}

}
bool ATCPClient::ConnectServer(FString ServerIP, int32 Port)
{
	FIPv4Endpoint ServerEndpoint;
	FIPv4Endpoint::Parse(ServerIP, ServerEndpoint);
	TSharedPtr<FInternetAddr> addr = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateInternetAddr();
	bool Success = true;
	addr->SetIp(*ServerIP, Success);
	if (!Success)
	{
		return false;
	}
	addr->SetPort(Port);

	if (ClientSocket->Connect(*addr))
	{
		USocketRSThread* RSThread = NewObject<USocketRSThread>();
		RecThreads.Add(RSThread);
		RSThread->ReceiveSocketDataDelegate = ReceiveSocketDataDelegate;
		RSThread->LostConnectionDelegate.AddDynamic(this, &ATCPClient::OnServerDisconnect);
		RSThread->Start(ClientSocket, SendDataSize, RecDataDize);
		UE_LOG(LogTemp, Warning, TEXT("Client Connect Success"));
		return true;
	}
	else
	{
		ESocketErrors LastErr = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->GetLastErrorCode();

		UE_LOG(LogTemp, Warning, TEXT("Connect failed with error code (%d) error (%s)"), LastErr, ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->GetSocketError(LastErr));
		ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->DestroySocket(ClientSocket);
	}
	return false;
}
void ATCPClient::SendToServer(FString Message)
{
	for (auto SocketThread : RecThreads)
	{
		SocketThread->Send(Message);
	}
}

void ATCPClient::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
	//Super::EndPlay(EndPlayReason);
	if (ClientSocket)
	{
		for (auto RecThreald : RecThreads)
		{
			RecThreald->Stop();
		}
	}
}

void ATCPClient::OnServerDisconnect(USocketRSThread* pThread)
{
	UE_LOG(LogTemp, Warning, TEXT("Server lost"));
	RecThreads.Remove(pThread);
}

第四步:创建基于TCPServer,TCPClient蓝图

 

 第五步:打开蓝图,绑定通知

 

第六步:创建UMG,ServerMain,ClientMain

 

第七步:打开关卡蓝图,创建widget显示到屏幕上

 第八步:打包测试,我这里复制了一份项目,一个作为客户端,一个作为服务器

服务器设置:场景中加入BP_TCPServer.,关卡蓝图widget为ServerMain

 

 客户端设置:场景中加入BP_TCPClient.,关卡蓝图widget为ClientMain

 

测试结果如下:先运行服务器,在开启客户端

服务器创建成功

客户端连接成功

 

 客户端向服务器发送数据

 服务器向客户端发送数据

 服务器关闭

 

客户端显示服务器丢失 

 

 服务器显示客户端丢失

 

猜你喜欢

转载自blog.csdn.net/qq_43021038/article/details/126519570