🇬🇧 English | 🇺🇦 Українська
6. Binary Data¶
Publish and Data from the previous chapter are great for text — JSON, plain strings.
But not everything you need to send is text: images, compressed data, serialized structs,
audio. For this, the plugin has a parallel set of operations that works with exact bytes —
TArray<uint8> instead of FString.
Why not just Publish¶
FString in Unreal is text, and somewhere along the Data/Publish path it's treated
exactly as text (UTF-8 encoding on send, decoding on receive). For ordinary strings this is
invisible and correct. But an arbitrary binary blob — a compressed file, an image, a
serialized struct — isn't necessarily valid UTF-8: representing such data directly as text
byte-for-byte can lose or corrupt part of the content.
The rule is simple: if the data was authored by a human in a language (JSON, a plain
string, XML), Publish/Data is right. If the data is the output of serialization,
compression, or a file from disk, use Publish Bytes/Payload.
Sending: Publish Bytes¶
Publish Bytes
Subject : "game.state.snapshot"
Payload : (byte array)
Reply To : (empty)
TArray<uint8> Bytes = SerializeGameState();
Nats->PublishBytes(TEXT("game.state.snapshot"), Bytes);
Every byte goes onto the wire exactly as it was in the array — including zero bytes inside
the data. The signature is identical to the text Publish, just with Data replaced by
Payload.
Publish Bytes With Headers¶
The same binary path, but with headers — for example, to indicate the content format alongside the bytes themselves:
Make Map (String → String)
["Content-Type"] = "application/octet-stream"
│
└─► Publish Bytes With Headers
Subject : "game.state.snapshot"
Payload : (byte array)
Headers : (the map above)
Receiving: the Payload field¶
Every received message (both in On Message Received and in a request's result) already
contains the bytes — nothing needs converting, Payload is always filled in regardless of
which method was used to publish the data:
On Message Received
│
└─► Break Nats Message
│
└─ Payload ──► (use as TArray<uint8>)
void AMyActor::HandleMessage(const FNatsMessage& Message)
{
if (Message.Subject == TEXT("game.state.snapshot"))
{
ApplyGameState(Message.Payload);
}
}
Data is filled in too — with the same content, decoded as UTF-8 text. For truly binary
data this field doesn't make sense (a text representation of arbitrary bytes is usually
unreadable or even partially lost) — just ignore it and use Payload.
Text ↔ bytes manually¶
Sometimes you need to explicitly convert a string to bytes (for example, to put text JSON alongside a binary blob in a single message of your own format) or the other way around. Two pure nodes in the plugin's library:
"Hello" ──► String To UTF-8 Bytes ──► (TArray<uint8>)
(TArray<uint8>) ──► UTF-8 Bytes To String ──► "Hello"
TArray<uint8> Bytes = UNatsBlueprintLibrary::StringToUtf8Bytes(TEXT("Hello"));
FString Text = UNatsBlueprintLibrary::Utf8BytesToString(Bytes);
Both work correctly with non-Latin characters: each Cyrillic character in UTF-8 takes two bytes, and the conversion is exact in both directions — what you wrote is what you read back.
Request/Reply with binary data¶
The same request/reply pattern as in 5. Core Messaging,
but for binary content — the reply is returned as the full FNatsMessage struct rather than
just text, since the reply's meaning lives in Payload:
Request Bytes Async
Subject : "service.avatar.generate"
Payload : (input parameters as bytes)
Timeout Seconds : 10.0
│
Callback ──► (bSuccess, Response)
│
├─ true → Response → Payload (the generated image)
└─ false → Response → Data (the reason: "Timeout", etc.)
Nats->RequestBytesAsync(TEXT("service.avatar.generate"), RequestBytes, 10.0f,
[](bool bSuccess, const FNatsMessage& Response)
{
if (bSuccess)
{
SaveAvatarTexture(Response.Payload);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("Failed: %s"), *Response.Data);
}
});
On failure (timeout, no connection), Response.Data holds the reason as text, and
Response.Payload is empty — consistent with how Request Async returns the reason in
Response when bSuccess = false.
Practical examples¶
Send a save game¶
TArray<uint8> SaveData;
UGameplayStatics::SaveGameToMemory(SaveGameObject, SaveData);
Nats->PublishBytes(TEXT("player.save.upload"), SaveData);
Send a texture (e.g. a screenshot)¶
Export To Bytes (Render Target)
│
└─► Publish Bytes
Subject : "screenshots.upload"
Payload : (from Export To Bytes)
Pack several values into one binary blob (C++)¶
When you need to send several fields more compactly than JSON — your own binary
serialization via FMemoryWriter/FMemoryReader, the standard Unreal way:
// Sender
TArray<uint8> Bytes;
FMemoryWriter Writer(Bytes);
int32 PlayerId = 42;
FVector Position = GetActorLocation();
Writer << PlayerId;
Writer << Position;
Nats->PublishBytes(TEXT("game.player.position"), Bytes);
// Receiver
void AMyActor::HandleMessage(const FNatsMessage& Message)
{
if (Message.Subject == TEXT("game.player.position"))
{
FMemoryReader Reader(Message.Payload);
int32 PlayerId;
FVector Position;
Reader << PlayerId;
Reader << Position;
}
}
The field order on write and on read must match exactly — this is low-level byte-for-byte serialization, with no self-describing format.
JetStream and Key-Value with binary data¶
The same "a Bytes variant alongside the text one" principle applies to JetStream too:
- JetStream Publisher —
Publish Bytes,Publish Bytes With Headers,Publish Bytes With Message ID,Publish Bytes With Expected Sequence— details in 8. JetStream: Publishing. - Key-Value —
KV Put Bytes,KV Put Bytes With Revision,KV Create Bytes, and a value you read back is returned as both text (Value) and bytes (ValueBytes) at once — details in 10. JetStream: Key-Value.
Next: 7. JetStream: Streams