Sending and receiving
Once the handshake is complete, a WebSocket connection carries messages in both directions at any time: the client sends text or binary messages, and the server pushes its own messages whenever it has something to say. Text messages suit JSON or other structured protocols; binary messages suit files, images, and compact encodings that would be wasteful as text. This chapter covers sending both kinds, controlling how messages are cut into frames, receiving messages through events, and choosing how and on which thread incoming data is read. Reach for the frame and threading options only when the defaults — one frame per message, automatic reading, events delivered on the main thread — do not fit your workload.
Sending text and binary messages
Pick the Send overload that matches the payload.
Send with a string sends a text
message (UTF-8 encoded); Send with TBytes sends a binary message. Both
require an open connection, and both raise an exception when the socket turns
out to be closed, so wrap them in try..except wherever the connection can
drop.
procedure TForm1.SendChat(const AText: string);
begin
try
// Send(string) sends a text message.
TMSFNCWebsocketClient1.Send(AText);
except
on E: Exception do
Memo1.Lines.Add('Send failed: ' + E.Message);
end;
end;
procedure TForm1.SendFile(const AFileName: string);
begin
// Send(TBytes) sends a binary message.
TMSFNCWebsocketClient1.Send(TFile.ReadAllBytes(AFileName));
end;
procedure TForm1.TMSFNCWebsocketClient1MessageReceived(Sender: TObject;
AConnection: TTMSFNCWebSocketConnection; const AMessage: string);
begin
// Fires for every text message pushed by the server.
Memo1.Lines.Add(AMessage);
end;
procedure TForm1.TMSFNCWebsocketClient1BinaryDataReceived(Sender: TObject;
AConnection: TTMSFNCWebSocketConnection; const AData: TBytes;
AFrameType: TTMSFNCWebSocketFrameTypes);
begin
// Fires for every binary message pushed by the server.
Memo1.Lines.Add(Format('Received %d bytes', [Length(AData)]));
end;
#include <System.IOUtils.hpp>
void __fastcall TForm1::SendChat(const System::UnicodeString AText)
{
try
{
// Send(string) sends a text message.
TMSFNCWebsocketClient1->Send(AText);
}
catch (Exception &E)
{
Memo1->Lines->Add("Send failed: " + E.Message);
}
}
void __fastcall TForm1::SendFile(const System::UnicodeString AFileName)
{
// Send(TBytes) sends a binary message.
TMSFNCWebsocketClient1->Send(TFile::ReadAllBytes(AFileName));
}
void __fastcall TForm1::TMSFNCWebsocketClient1MessageReceived(TObject *Sender,
TTMSFNCWebSocketConnection *AConnection, const System::UnicodeString AMessage)
{
// Fires for every text message pushed by the server.
Memo1->Lines->Add(AMessage);
}
void __fastcall TForm1::TMSFNCWebsocketClient1BinaryDataReceived(TObject *Sender,
TTMSFNCWebSocketConnection *AConnection, const System::DynamicArray<System::Byte> AData,
TTMSFNCWebSocketFrameTypes AFrameType)
{
// Fires for every binary message pushed by the server.
Memo1->Lines->Add(Format("Received %d bytes", ARRAYOFCONST((AData.Length))));
}
Masking client messages
RFC 6455 requires every frame a client sends to be masked, and many servers
enforce it by closing the connection when an unmasked frame arrives.
SendMasked has the same two
overloads as Send and masks the frames it sends. Send sends unmasked
frames, which TTMSFNCWebSocketServer and other lenient servers accept. When
you connect to a third-party server, use SendMasked. Pings and pongs sent by
the client are always masked.
Sending in multiple frames
By default every message goes out as a single frame. Set
SplitMessage to True to
cut messages into frames of at most
FrameSize bytes; the size
includes the frame headers. Splitting keeps individual writes small for large
uploads and lets the receiver process a message while it is still arriving.
Both settings apply to Send and SendMasked.
Receiving messages
Handle the event that matches the message type.
OnMessageReceived
fires for every text message, with the decoded string in AMessage.
OnBinaryDataReceived
fires for every binary message, with the payload in AData. By default a
message that the server split into several frames is reassembled first, so each
event delivers one complete message.
To process a large message piece by piece instead, add twsoFrameByFrame to
Options. The events then fire
once per frame, and AFrameType in OnBinaryDataReceived tells you where a
frame sits in its message: ftFirst for the opening frame, ftContinuation
for the frames that follow, and ftLast on the final one.
procedure TForm1.PrepareClient;
begin
// Options can only be changed while the client is not connected.
TMSFNCWebsocketClient1.Options := [twsoFrameByFrame];
FDownload := TMemoryStream.Create;
end;
procedure TForm1.TMSFNCWebsocketClient1BinaryDataReceived(Sender: TObject;
AConnection: TTMSFNCWebSocketConnection; const AData: TBytes;
AFrameType: TTMSFNCWebSocketFrameTypes);
begin
if ftFirst in AFrameType then
FDownload.Clear; // first fragment of a new message
if Length(AData) > 0 then
FDownload.WriteBuffer(AData[0], Length(AData));
ProgressLabel.Text := Format('%d KB received', [FDownload.Size div 1024]);
if ftLast in AFrameType then // final fragment: message complete
begin
FDownload.SaveToFile(TPath.Combine(TPath.GetDocumentsPath, 'download.bin'));
FDownload.Clear;
end;
end;
#include <System.IOUtils.hpp>
void __fastcall TForm1::PrepareClient()
{
// Options can only be changed while the client is not connected.
TMSFNCWebsocketClient1->Options = TTMSFNCWebsocketOptions() << TTMSFNCWebsocketOption::twsoFrameByFrame;
FDownload = new TMemoryStream();
}
void __fastcall TForm1::TMSFNCWebsocketClient1BinaryDataReceived(TObject *Sender,
TTMSFNCWebSocketConnection *AConnection, const System::DynamicArray<System::Byte> AData,
TTMSFNCWebSocketFrameTypes AFrameType)
{
if (AFrameType.Contains(TTMSFNCWebSocketFrameType::ftFirst))
FDownload->Clear(); // first fragment of a new message
if (AData.Length > 0)
FDownload->WriteBuffer(&AData[0], AData.Length);
ProgressLabel->Text = Format("%d KB received", ARRAYOFCONST((FDownload->Size / 1024)));
if (AFrameType.Contains(TTMSFNCWebSocketFrameType::ftLast)) // final fragment: message complete
{
FDownload->SaveToFile(System::Ioutils::TPath::Combine(System::Ioutils::TPath::GetDocumentsPath(), "download.bin"));
FDownload->Clear();
}
}
Automatic and manual message checking
With AutoCheckMessages
at its default True, a background driver checks the connection for incoming
data every AutoCheckInterval
milliseconds (default 50) and raises the events. The driver is shared by all
clients in the application and takes its interval from the first client that
starts it.
Turn AutoCheckMessages off when you want to decide yourself when data is
read — in a game loop, a timer, or a worker thread. Call
CheckIncoming, which
waits up to CheckTimeout
milliseconds, reads every pending frame, raises the events on the calling
thread, and returns a
TTMSFNCWebSocketIncomingResult:
irNone when nothing arrived, irOK when data was processed, and irClose
when the server closed the connection (the client has then already
disconnected).
procedure TForm1.FormCreate(Sender: TObject);
begin
// Set these before connecting; they cannot change while Active.
TMSFNCWebsocketClient1.AutoCheckMessages := False;
TMSFNCWebsocketClient1.CheckTimeout := 10; // wait at most 10 ms per check
TMSFNCWebsocketClient1.Connect;
PollTimer.Interval := 100;
PollTimer.Enabled := TMSFNCWebsocketClient1.Active;
end;
procedure TForm1.PollTimerTimer(Sender: TObject);
begin
if not TMSFNCWebsocketClient1.Active then
begin
PollTimer.Enabled := False;
Exit;
end;
// Reads all pending frames on this (main) thread; the message events are raised from here.
case TMSFNCWebsocketClient1.CheckIncoming of
irNone: ; // nothing arrived
irOK: StatusLabel.Text := 'Data received';
irClose:
begin
// The server closed the connection; the client already disconnected.
PollTimer.Enabled := False;
StatusLabel.Text := 'Closed by server';
end;
end;
end;
void __fastcall TForm1::FormCreate(TObject *Sender)
{
// Set these before connecting; they cannot change while Active.
TMSFNCWebsocketClient1->AutoCheckMessages = false;
TMSFNCWebsocketClient1->CheckTimeout = 10; // wait at most 10 ms per check
TMSFNCWebsocketClient1->Connect();
PollTimer->Interval = 100;
PollTimer->Enabled = TMSFNCWebsocketClient1->Active;
}
void __fastcall TForm1::PollTimerTimer(TObject *Sender)
{
if (!TMSFNCWebsocketClient1->Active)
{
PollTimer->Enabled = false;
return;
}
// Reads all pending frames on this (main) thread; the message events are raised from here.
switch (TMSFNCWebsocketClient1->CheckIncoming())
{
case TTMSFNCWebSocketIncomingResult::irNone: // nothing arrived
break;
case TTMSFNCWebSocketIncomingResult::irOK:
StatusLabel->Text = "Data received";
break;
case TTMSFNCWebSocketIncomingResult::irClose:
// The server closed the connection; the client already disconnected.
PollTimer->Enabled = false;
StatusLabel->Text = "Closed by server";
break;
}
}
Choosing the event thread
AutoSyncEvents (public,
default True) queues the message, ping, pong, and disconnect events to the
main thread, so handlers can update controls directly. Set it to False to
receive those events on the thread that reads the socket. That keeps expensive
parsing off the main thread, but every user interface update must then be
queued explicitly.
procedure TForm1.FormCreate(Sender: TObject);
begin
// Events now fire on the thread that reads the socket.
TMSFNCWebsocketClient1.AutoSyncEvents := False;
end;
procedure TForm1.TMSFNCWebsocketClient1MessageReceived(Sender: TObject;
AConnection: TTMSFNCWebSocketConnection; const AMessage: string);
var
Quote: string;
begin
// Heavy work stays off the main thread...
Quote := ParseQuote(AMessage);
// ...and only the control update is queued to the main thread.
TThread.Queue(nil,
procedure
begin
QuoteLabel.Text := Quote;
end);
end;
void __fastcall TForm1::FormCreate(TObject *Sender)
{
// Events now fire on the thread that reads the socket.
TMSFNCWebsocketClient1->AutoSyncEvents = false;
}
void __fastcall TForm1::ShowQuote()
{
QuoteLabel->Text = FQuote;
}
void __fastcall TForm1::TMSFNCWebsocketClient1MessageReceived(TObject *Sender,
TTMSFNCWebSocketConnection *AConnection, const System::UnicodeString AMessage)
{
// Heavy work stays off the main thread...
FQuote = ParseQuote(AMessage);
// ...and only the control update is queued to the main thread.
TThread::Queue(nullptr, ShowQuote);
}
Combining frame splitting with masked sends
Uploading a large payload to a standards-strict server combines two of the features above: split frames keep each write small, and masking keeps the server from rejecting them.
procedure TForm1.ConfigureUpload;
begin
// Every outgoing message is split into frames of at most 16 KB,
// frame headers included.
TMSFNCWebsocketClient1.SplitMessage := True;
TMSFNCWebsocketClient1.FrameSize := 16 * 1024;
end;
procedure TForm1.UploadReport(const AReport: TBytes);
begin
// SendMasked applies the client-to-server masking that RFC 6455 requires.
// SplitMessage and FrameSize apply to SendMasked and Send alike.
TMSFNCWebsocketClient1.SendMasked(AReport);
TMSFNCWebsocketClient1.SendMasked('{"type":"upload-complete"}');
end;
void __fastcall TForm1::ConfigureUpload()
{
// Every outgoing message is split into frames of at most 16 KB,
// frame headers included.
TMSFNCWebsocketClient1->SplitMessage = true;
TMSFNCWebsocketClient1->FrameSize = 16 * 1024;
}
void __fastcall TForm1::UploadReport(const TBytes AReport)
{
// SendMasked applies the client-to-server masking that RFC 6455 requires.
// SplitMessage and FrameSize apply to SendMasked and Send alike.
TMSFNCWebsocketClient1->SendMasked(AReport);
TMSFNCWebsocketClient1->SendMasked("{\"type\":\"upload-complete\"}");
}
Pitfalls
- The binary and text overloads are chosen by type. Passing a
stringalways sends a text message; convert toTByteswhen the server expects binary data. - Strict servers reject unmasked frames. If a third-party server drops the
connection right after the first message, switch from
SendtoSendMasked. - Options cannot change while connected. Set
twsoFrameByFrameand the checking properties beforeConnect. - Frame-by-frame text arrives in pieces. With
twsoFrameByFrame, one text message can raiseOnMessageReceivedseveral times; buffer the pieces until the message is complete. - Background events cannot touch controls. With
AutoSyncEvents = False, queue or synchronize every user interface update. FrameSizemust be set when splitting. Its default is0; give it a real size wheneverSplitMessageisTrue.
Related API
Send,SendMasked,SplitMessage,FrameSizeOnMessageReceived,OnBinaryDataReceived,OptionsAutoCheckMessages,AutoCheckInterval,CheckIncoming,CheckTimeout,AutoSyncEventsTTMSFNCWebSocketFrameTypes,TTMSFNCWebSocketIncomingResult,TTMSFNCWebSocketOption
See also
- Connecting — open the connection first.
- Connection health — ping, pong, and detecting a lost connection.
- Broadcasting messages — how the server sends to its clients.