ChatClient
WebSocket-based chat client core class. Handles actual server connection and message processing.
Event Codes
| Code | Constant Name | Description |
|---|---|---|
| 0 | EventChannels | Channel list |
| 1 | EventSubscribe | Channel subscribe |
| 2 | EventUnSubscribe | Channel unsubscribe |
| 3 | EventPublicMessage | Public message |
| 4 | EventPrivateMessage | Private message |
| 5 | EventPlayerOnline | Player online status |
| 97 | EventNotifyMessage | Notification message |
| 99 | EventError | Error |
Main Features
| Method | Description |
|---|---|
| Connect | Connect to WebSocket server |
| Disconnect | Disconnect from server |
| Subscribe | Subscribe to channel (can set previous message count) |
| Unsubscribe | Unsubscribe from channel |
| GetChannels | Request channel list |
| GetPlayersOnline | Get player online status |
| SendPublicMessage | Send public message |
| SendPrivateMessage | Send private message |
| SetUserName | Set user name |
| Service | Process main thread callbacks |
Properties
| Property | Type | Description |
|---|---|---|
| IsConnected | bool | Connection status |
| CurrentChannel | FString | Currently subscribed channel |
Unreal C++ Implementation
Header File (ChatClient.h)
// ChatClient.h
#pragma once
#include "CoreMinimal.h"
#include "IWebSocket.h"
#include "ChatModels.h"
class FChatClient
{
public:
// Event codes
static const uint8 EventChannels = 0;
static const uint8 EventSubscribe = 1;
static const uint8 EventUnSubscribe = 2;
static const uint8 EventPublicMessage = 3;
static const uint8 EventPrivateMessage = 4;
static const uint8 EventPlayerOnline = 5;
static const uint8 EventNotifyMessage = 97;
static const uint8 EventError = 99;
FChatClient(IChatListener* InListener, const FString& InUserName = TEXT(""));
~FChatClient();
// Public API
void Connect(const FChatServerItem& Server);
void Disconnect();
void Subscribe(const FString& Channel, int32 PrevMessageCount = 0);
void Unsubscribe(const FString& Channel);
void GetChannels();
void GetPlayersOnline(const TArray<FString>& UserIds);
void SendPublicMessage(const FString& Channel, const FString& Text);
void SendPrivateMessage(const FString& TargetUserId, const FString& Text);
void SetUserName(const FString& InUserName);
// Getters
bool GetIsConnected() const { return bIsConnected; }
FString GetCurrentChannel() const { return CurrentChannel; }
private:
void ConnectToServer(const FString& Url);
FString BuildServerUrl(const FString& BaseUrl);
void Send(uint8 Type, const FString& Channel, const FString& Message = TEXT(""),
const FString& PrivateToUserId = TEXT(""), int32 PrevMessageCount = 0);
void ProcessMessage(const FString& Json);
FString ComputeHash();
FString GetLocalIP();
// WebSocket event handlers
void OnConnected();
void OnConnectionError(const FString& Error);
void OnClosed(int32 StatusCode, const FString& Reason, bool bWasClean);
void OnMessage(const FString& Message);
IChatListener* Listener;
TSharedPtr<IWebSocket> WebSocket;
FString UserUniqueId;
FString UserName;
FString CurrentChannel;
bool bIsConnected;
};
Implementation File (ChatClient.cpp)
OpenSSL Dependency
Uses OpenSSL for HMAC-SHA256 calculation. You must add the "OpenSSL" module to your project's Build.cs file.
// ChatClient.cpp
#include "ChatClient.h"
#include "WebSocketsModule.h"
#include "PlayNANOOAuth.h"
#include "Misc/Base64.h"
#include "SocketSubsystem.h"
#include "IPAddress.h"
#if PLATFORM_WINDOWS
#include "Windows/AllowWindowsPlatformTypes.h"
#endif
#include <openssl/hmac.h>
#include <openssl/sha.h>
#if PLATFORM_WINDOWS
#include "Windows/HideWindowsPlatformTypes.h"
#endif
FChatClient::FChatClient(IChatListener* InListener, const FString& InUserName)
: Listener(InListener)
, UserName(InUserName)
, bIsConnected(false)
{
// UUID is assumed to be retrieved from DataManager
UserUniqueId = UGameDataManager::Get()->GetAccessToken(); // Or separate UUID management
}
FChatClient::~FChatClient()
{
Disconnect();
}
void FChatClient::Connect(const FChatServerItem& Server)
{
if (bIsConnected) return;
FString Protocol = Server.Secure == TEXT("Y") ? TEXT("wss") : TEXT("ws");
FString BaseUrl = FString::Printf(TEXT("%s://%s:%d"), *Protocol, *Server.Addr, Server.Port);
ConnectToServer(BuildServerUrl(BaseUrl));
}
void FChatClient::Disconnect()
{
if (WebSocket.IsValid())
{
bIsConnected = false;
WebSocket->Close();
WebSocket.Reset();
}
}
void FChatClient::Subscribe(const FString& Channel, int32 PrevMessageCount)
{
CurrentChannel = Channel;
Send(EventSubscribe, Channel, TEXT(""), TEXT(""), PrevMessageCount);
}
void FChatClient::Unsubscribe(const FString& Channel)
{
Send(EventUnSubscribe, Channel);
}
void FChatClient::GetChannels()
{
Send(EventChannels, TEXT(""));
}
void FChatClient::GetPlayersOnline(const TArray<FString>& UserIds)
{
if (!WebSocket.IsValid() || !bIsConnected)
{
if (Listener)
Listener->OnError(TEXT("NOT_CONNECTED"), TEXT("WebSocket is not connected"));
return;
}
FChatMessageModel Msg;
Msg.mid = FGuid::NewGuid().ToString();
Msg.type = EventPlayerOnline;
Msg.gameId = FPlayNANOOAuth::GameId;
Msg.serviceKey = FPlayNANOOAuth::ServiceKey;
Msg.userUniqueId = UserUniqueId;
Msg.userName = UserName;
Msg.onlinePlayers = UserIds;
Msg.ipAddr = GetLocalIP();
Msg.createdAt = FDateTime::Now().ToString(TEXT("%Y-%m-%dT%H:%M:%S"));
WebSocket->Send(Msg.ToJson());
}
void FChatClient::SendPublicMessage(const FString& Channel, const FString& Text)
{
Send(EventPublicMessage, Channel, Text);
}
void FChatClient::SendPrivateMessage(const FString& TargetUserId, const FString& Text)
{
Send(EventPrivateMessage, CurrentChannel, Text, TargetUserId);
}
void FChatClient::SetUserName(const FString& InUserName)
{
UserName = InUserName;
}
FString FChatClient::BuildServerUrl(const FString& BaseUrl)
{
FString Hash = ComputeHash();
// Unreal WebSocket has a bug where it requests '//' if there's no path
// Explicitly add '/' path
return FString::Printf(TEXT("%s/?gameId=%s&uuid=%s&hash=%s"),
*BaseUrl,
*FPlayNANOOAuth::GameId,
*UserUniqueId,
*FGenericPlatformHttp::UrlEncode(Hash));
}
void FChatClient::ConnectToServer(const FString& Url)
{
if (!FModuleManager::Get().IsModuleLoaded(TEXT("WebSockets")))
{
FModuleManager::Get().LoadModule(TEXT("WebSockets"));
}
WebSocket = FWebSocketsModule::Get().CreateWebSocket(Url);
WebSocket->OnConnected().AddLambda([this]()
{
OnConnected();
});
WebSocket->OnConnectionError().AddLambda([this](const FString& Error)
{
OnConnectionError(Error);
});
WebSocket->OnClosed().AddLambda([this](int32 StatusCode, const FString& Reason, bool bWasClean)
{
OnClosed(StatusCode, Reason, bWasClean);
});
WebSocket->OnMessage().AddLambda([this](const FString& Message)
{
OnMessage(Message);
});
WebSocket->Connect();
}
void FChatClient::OnConnected()
{
bIsConnected = true;
if (Listener)
Listener->OnConnected();
}
void FChatClient::OnConnectionError(const FString& Error)
{
if (Listener)
Listener->OnError(TEXT("CONNECTION_FAILED"), Error);
}
void FChatClient::OnClosed(int32 StatusCode, const FString& Reason, bool bWasClean)
{
bIsConnected = false;
if (Listener)
Listener->OnDisconnected();
}
void FChatClient::OnMessage(const FString& Message)
{
ProcessMessage(Message);
}
void FChatClient::Send(uint8 Type, const FString& Channel, const FString& Message,
const FString& PrivateToUserId, int32 PrevMessageCount)
{
if (!WebSocket.IsValid() || !bIsConnected)
{
if (Listener)
Listener->OnError(TEXT("NOT_CONNECTED"), TEXT("WebSocket is not connected"));
return;
}
FChatMessageModel Msg;
Msg.mid = FGuid::NewGuid().ToString();
Msg.type = Type;
Msg.gameId = FPlayNANOOAuth::GameId;
Msg.serviceKey = FPlayNANOOAuth::ServiceKey;
Msg.channelId = Channel;
Msg.userUniqueId = UserUniqueId;
Msg.userName = UserName;
Msg.message = Message;
Msg.privateToUserUniqueId = PrivateToUserId;
Msg.prevMessageCount = PrevMessageCount;
Msg.ipAddr = GetLocalIP();
Msg.createdAt = FDateTime::Now().ToString(TEXT("%Y-%m-%dT%H:%M:%S"));
WebSocket->Send(Msg.ToJson());
}
void FChatClient::ProcessMessage(const FString& Json)
{
UE_LOG(LogTemp, Log, TEXT("[Chat] Received: %s"), *Json);
FChatMessageModel Msg;
if (!Msg.FromJson(Json))
{
UE_LOG(LogTemp, Error, TEXT("[Chat] ProcessMessage error: Failed to parse JSON"));
return;
}
FChatUserInfo User;
User.visitorId = Msg.userUniqueId;
User.visitorName = Msg.userName;
if (!Listener) return;
switch (Msg.type)
{
case EventChannels:
Listener->OnChannels(Msg.channels);
break;
case EventSubscribe:
Listener->OnSubscribed(User);
break;
case EventUnSubscribe:
Listener->OnUnSubscribed(User);
break;
case EventPublicMessage:
Listener->OnPublicMessage(User, Msg.message);
break;
case EventPrivateMessage:
Listener->OnPrivateMessage(User, Msg.message);
break;
case EventPlayerOnline:
Listener->OnPlayerOnline(Msg.players);
break;
case EventNotifyMessage:
if (Msg.userUniqueId != UserUniqueId)
Listener->OnNotifyMessage(User, Msg.message);
break;
case EventError:
Listener->OnError(FString::FromInt(Msg.error), Msg.message);
break;
}
}
FString FChatClient::ComputeHash()
{
// Generate HMAC-SHA256 hash
FString Message = FPlayNANOOAuth::GameId + FPlayNANOOAuth::ServiceKey + UserUniqueId;
// Convert to UTF-8
FTCHARToUTF8 KeyUtf8(*FPlayNANOOAuth::SecretKey);
FTCHARToUTF8 MessageUtf8(*Message);
// Calculate HMAC-SHA256 (OpenSSL)
unsigned char Hash[SHA256_DIGEST_LENGTH];
unsigned int HashLen = 0;
HMAC(EVP_sha256(),
KeyUtf8.Get(), KeyUtf8.Length(),
(const unsigned char*)MessageUtf8.Get(), MessageUtf8.Length(),
Hash, &HashLen);
// Base64 encoding
TArray<uint8> HashArray;
HashArray.Append(Hash, HashLen);
return FBase64::Encode(HashArray);
}
FString FChatClient::GetLocalIP()
{
bool bCanBindAll;
TSharedPtr<FInternetAddr> LocalAddr = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->GetLocalHostAddr(*GLog, bCanBindAll);
if (LocalAddr.IsValid())
{
return LocalAddr->ToString(false);
}
return TEXT("0.0.0.0");
}