Session Keep-Alive and Duplicate Login Check
This API periodically sends session keep-alive signals to the server and checks for duplicate logins from other devices.
Example Code Note
The example code in this document is provided for reference. The example shows a timer-based implementation using FTimerHandle, but feel free to modify it according to your project's architecture and coding conventions.
URL Confirmation
This API uses the service-account.playnanoo.com domain.
API Information
- URL:
https://service-account.playnanoo.com/api/v20240401/alive - Method:
PUT - Authentication Required: Yes
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| platform | string | Required | Platform (e.g., "aos", "ios") |
| device_id | string | Required | Device unique ID |
| device_model | string | Required | Device model name |
| device_os | string | Required | Device OS |
| device_language | string | Required | Device language (e.g., "KO", "EN") |
DeviceInfo Inheritance
The Req class for this API inherits from DeviceInfo. All properties of DeviceInfo are automatically included.
Response Data
Success Response
| Field | Type | Description |
|---|---|---|
| Status | string | Status (success: "success") |
Error Response
| Error Code | Description |
|---|---|
| 30006 | DuplicatedDeviceException - Already authenticated on another device |
Unreal C++ Implementation
CheckAlive Class
// CheckAlive.h
#pragma once
#include "CoreMinimal.h"
#include "Http.h"
#include "Json.h"
#include "CheckAlive.generated.h"
USTRUCT(BlueprintType)
struct FCheckAliveResponse
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly)
FString Status;
UPROPERTY(BlueprintReadOnly)
FString ErrorCode;
UPROPERTY(BlueprintReadOnly)
FString Message;
};
DECLARE_DELEGATE_OneParam(FOnCheckAliveSuccess, const FCheckAliveResponse&);
DECLARE_DELEGATE_OneParam(FOnCheckAliveError, const FCheckAliveResponse&);
UCLASS()
class YOURPROJECT_API UCheckAlive : public UObject
{
GENERATED_BODY()
public:
void Send(FOnCheckAliveSuccess OnSuccess, FOnCheckAliveError OnError);
private:
static const FString Path;
};
// CheckAlive.cpp
#include "CheckAlive.h"
#include "YourHelper.h"
const FString UCheckAlive::Path = TEXT("https://service-account.playnanoo.com/api/v20240401/alive");
void UCheckAlive::Send(FOnCheckAliveSuccess OnSuccess, FOnCheckAliveError OnError)
{
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request = FHttpModule::Get().CreateRequest();
Request->SetVerb(TEXT("PUT"));
Request->SetURL(Path);
// Set common headers (authentication token required)
FYourHelper::SetCommonHeaders(Request, true);
// Create request body
TSharedPtr<FJsonObject> JsonObject = MakeShareable(new FJsonObject);
JsonObject->SetStringField(TEXT("platform"), FYourHelper::GetPlatform());
JsonObject->SetStringField(TEXT("device_id"), FYourHelper::GetDeviceId());
JsonObject->SetStringField(TEXT("device_model"), FYourHelper::GetDeviceModel());
JsonObject->SetStringField(TEXT("device_os"), FYourHelper::GetDeviceOS());
JsonObject->SetStringField(TEXT("device_language"), FYourHelper::GetDeviceLanguage());
FString RequestBody;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&RequestBody);
FJsonSerializer::Serialize(JsonObject.ToSharedRef(), Writer);
Request->SetContentAsString(RequestBody);
Request->OnProcessRequestComplete().BindLambda(
[OnSuccess, OnError](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bSuccess)
{
FCheckAliveResponse Result;
if (bSuccess && Response.IsValid())
{
TSharedPtr<FJsonObject> JsonObject;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Response->GetContentAsString());
if (FJsonSerializer::Deserialize(Reader, JsonObject) && JsonObject.IsValid())
{
JsonObject->TryGetStringField(TEXT("Status"), Result.Status);
JsonObject->TryGetStringField(TEXT("ErrorCode"), Result.ErrorCode);
JsonObject->TryGetStringField(TEXT("Message"), Result.Message);
if (Result.ErrorCode.IsEmpty())
{
OnSuccess.ExecuteIfBound(Result);
}
else
{
OnError.ExecuteIfBound(Result);
}
}
}
else
{
Result.ErrorCode = TEXT("HTTP_ERROR");
Result.Message = TEXT("Request failed");
OnError.ExecuteIfBound(Result);
}
}
);
Request->ProcessRequest();
}
Add to Singleton Manager Class
Add periodic CheckAlive calls and duplicate login detection to your singleton manager.
// YourSingleton.h
DECLARE_DELEGATE_OneParam(FOnDuplicateDetected, bool);
private:
bool bIsDuplicate = false;
FTimerHandle AliveTimerHandle;
FOnDuplicateDetected DuplicateCallback;
public:
// Start session keep-alive (minimum 300 seconds)
UFUNCTION(BlueprintCallable, Category = "YourProject|Session")
void CheckAliveStart(int32 DelayTime = 300);
// Stop session keep-alive
UFUNCTION(BlueprintCallable, Category = "YourProject|Session")
void CheckAliveStop();
// Register duplicate login check callback
void CheckDuplicate(FOnDuplicateDetected Callback);
// YourSingleton.cpp
void UYourSingleton::CheckAliveStart(int32 DelayTime)
{
if (AliveTimerHandle.IsValid())
{
return;
}
int32 Timer = FMath::Max(DelayTime, 300);
GetWorld()->GetTimerManager().SetTimer(
AliveTimerHandle,
[this]()
{
if (DataManager->GetAccessToken().IsEmpty())
{
return;
}
UCheckAlive* CheckAlive = NewObject<UCheckAlive>();
CheckAlive->Send(
FOnCheckAliveSuccess::CreateLambda([](const FCheckAliveResponse& Response)
{
// Success response - not duplicate
UE_LOG(LogTemp, Log, TEXT("Session alive: %s"), *Response.Status);
}),
FOnCheckAliveError::CreateLambda([this](const FCheckAliveResponse& Response)
{
// 30006: DuplicatedDeviceException - Already authenticated on another device
if (Response.ErrorCode == TEXT("30006"))
{
bIsDuplicate = true;
DuplicateCallback.ExecuteIfBound(true);
}
})
);
},
Timer,
true // Repeat execution
);
}
void UYourSingleton::CheckAliveStop()
{
if (AliveTimerHandle.IsValid())
{
GetWorld()->GetTimerManager().ClearTimer(AliveTimerHandle);
AliveTimerHandle.Invalidate();
}
}
void UYourSingleton::CheckDuplicate(FOnDuplicateDetected Callback)
{
DuplicateCallback = Callback;
}
Usage Example
Basic Usage
void AYourGameMode::BeginPlay()
{
Super::BeginPlay();
// Start session keep-alive after successful login (every 300 seconds)
UYourSingleton::Get()->CheckAliveStart(300);
// Register duplicate login check callback
UYourSingleton::Get()->CheckDuplicate(
FOnDuplicateDetected::CreateLambda([](bool bIsDuplicate)
{
if (bIsDuplicate)
{
UE_LOG(LogTemp, Error, TEXT("Duplicate connection has been detected."));
// Force logout processing
// e.g., Navigate to login screen, delete tokens, etc.
}
})
);
}
Stop Session Keep-Alive on Logout
void AYourGameMode::Logout()
{
// Stop session keep-alive
UYourSingleton::Get()->CheckAliveStop();
// Other logout processing...
}
Flow
- After successful login, call
CheckAliveStart(300) - Every 300 seconds, send alive request to server
- Success response: Session maintained
- Error 30006: Logged in from another device →
bIsDuplicate = true - Callback invoked: When duplicate detected, execute registered callback
- Callback processing: Force logout, show notification, etc.
Caution
Do Not Call Multiple Times
CheckAliveStart() should be called only once in your entire application.
- Do not call it from multiple levels or actors.
- Multiple calls may cause the server to detect abnormal request patterns, resulting in false positive duplicate login errors (30006).
- Use a singleton pattern to manage session keep-alive from a single location.
- While the implementation internally prevents duplicate calls when a timer is already running, it's best practice to maintain a single call point structurally.