Skip to main content
Table of Contents

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.

Send and receive messagesPascalC++
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.

Receive a fragmented binary message frame by framePascalC++
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).

Poll for incoming messages manuallyPascalC++
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.

Handle messages on the background threadPascalC++
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.

Send masked messages in multiple framesPascalC++
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 string always sends a text message; convert to TBytes when 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 Send to SendMasked.
  • Options cannot change while connected. Set twsoFrameByFrame and the checking properties before Connect.
  • Frame-by-frame text arrives in pieces. With twsoFrameByFrame, one text message can raise OnMessageReceived several times; buffer the pieces until the message is complete.
  • Background events cannot touch controls. With AutoSyncEvents = False, queue or synchronize every user interface update.
  • FrameSize must be set when splitting. Its default is 0; give it a real size whenever SplitMessage is True.

See also