Skip to main content
Table of Contents

Broadcasting messages

A WebSocket server earns its keep by pushing data: a price tick to every dashboard, a chat line to everyone in a room, a notification to one user. TTMSFNCWebSocketServer offers two families of send methods — broadcasts that reach every connected client, and selector-based sends that reach exactly the clients a function picks — each in a text and a binary variant. It also receives the clients' own messages through events and can close selected clients. This chapter covers broadcasting, targeted sending, relaying messages between clients, splitting large messages into frames, and removing clients. Reach for the selector-based methods as soon as not every client should see every message.

Broadcasting to all clients

BroadcastMessage sends a text message and BroadcastData a binary message to every client whose handshake is complete. Clients that are still connecting are skipped.

Broadcast to every connected clientPascalC++
procedure TForm1.BroadcastBtnClick(Sender: TObject);
begin
  // Text message to every connected client.
  TMSFNCWebSocketServer1.BroadcastMessage('Hello client!');
end;

procedure TForm1.PublishSnapshot(const AImage: TBytes);
begin
  // Binary message to every connected client.
  TMSFNCWebSocketServer1.BroadcastData(AImage);
end;
void __fastcall TForm1::BroadcastBtnClick(TObject *Sender)
{
  // Text message to every connected client.
  TMSFNCWebSocketServer1->BroadcastMessage("Hello client!");
}

void __fastcall TForm1::PublishSnapshot(const TBytes AImage)
{
  // Binary message to every connected client.
  TMSFNCWebSocketServer1->BroadcastData(AImage);
}

Receiving client messages

Handle the event that matches the message type. OnMessageReceived fires for text messages and OnBinaryDataReceived for binary messages. AConnection identifies the client that sent the message; on the server it is always a TTMSFNCWebSocketServerConnection. As on the client, twsoFrameByFrame in Options delivers fragmented messages one frame at a time, with AFrameType marking the first and last frame.

Relaying messages between clients

SendMessageTo (text) and SendDataTo (binary) take a selector of type TTMSFNCWebSocketSendToCallBack: an anonymous function that receives each connected client and returns True for the clients that should get the message. Comparing against the sender produces the classic chat relay — every client except the one the message came from.

Relay a received message to other clientsPascalC++
procedure TForm1.TMSFNCWebSocketServer1MessageReceived(Sender: TObject;
  AConnection: TTMSFNCWebSocketConnection; const AMessage: string);
begin
  // The selector runs once per connected client; only clients for which it
  // returns True receive the message. Here: everyone except the sender.
  TMSFNCWebSocketServer1.SendMessageTo(AMessage,
    function(AClientConnection: TTMSFNCWebSocketServerConnection): Boolean
    begin
      Result := AClientConnection <> AConnection;
    end);
end;
// The anonymous method of the Delphi API is an interface in C++Builder.
class TExceptSenderSelector : public TCppInterfacedObject<TTMSFNCWebSocketSendToCallBack>
{
public:
  TExceptSenderSelector(TTMSFNCWebSocketConnection *ASender) : FSender(ASender) {}
  bool __fastcall Invoke(TTMSFNCWebSocketServerConnection *AClientConnection)
  {
    return static_cast<TTMSFNCWebSocketConnection*>(AClientConnection) != FSender;
  }
private:
  TTMSFNCWebSocketConnection *FSender;
};

void __fastcall TForm1::TMSFNCWebSocketServer1MessageReceived(TObject *Sender,
  TTMSFNCWebSocketConnection *AConnection, const System::UnicodeString AMessage)
{
  // The selector runs once per connected client; only clients for which it
  // returns True receive the message. Here: everyone except the sender.
  TMSFNCWebSocketServer1->SendMessageTo(AMessage,
    _di_TTMSFNCWebSocketSendToCallBack(new TExceptSenderSelector(AConnection)));
}

Targeting specific clients

Because the selector sees the whole connection object, it can route on anything the connection carries: the requested path or query string in HandshakeRequest, the peer address, or — most usefully — your own data in UserData. Store a user name, room, or subscription list on each connection (see Custom connection data) and the selector becomes a one-line filter.

Sending in multiple frames

By default each message goes out in one frame. Set SplitMessage to True to cut messages into frames of at most FrameSize bytes, headers included. The setting applies to the broadcast and the selector-based methods alike.

Removing clients

DisconnectClient uses the same kind of selector and sends a close frame to every client it picks, starting the closing handshake. Use it to kick a user, to close idle clients, or to empty a room.

Combining targeted sends, frames, and client removal

The example keeps a user name and room on every connection and uses them to send split-frame updates to one room and to disconnect one user.

Route messages to a room and remove a clientPascalC++
type
  TChatUser = class
  public
    UserName: string;
    Room: string;
  end;

procedure TForm1.SendToRoom(const ARoom, AMessage: string);
begin
  // Large room updates go out in frames of at most 4 KB.
  TMSFNCWebSocketServer1.SplitMessage := True;
  TMSFNCWebSocketServer1.FrameSize := 4096;

  TMSFNCWebSocketServer1.SendMessageTo(AMessage,
    function(AConnection: TTMSFNCWebSocketServerConnection): Boolean
    begin
      Result := Assigned(AConnection.UserData) and
        (TChatUser(AConnection.UserData).Room = ARoom);
    end);
end;

procedure TForm1.KickUser(const AUserName: string);
begin
  // Sends a close frame to every client the selector picks.
  TMSFNCWebSocketServer1.DisconnectClient(
    function(AConnection: TTMSFNCWebSocketServerConnection): Boolean
    begin
      Result := Assigned(AConnection.UserData) and
        SameText(TChatUser(AConnection.UserData).UserName, AUserName);
    end);
end;
class TChatUser : public TObject
{
public:
  System::UnicodeString UserName;
  System::UnicodeString Room;
};

// The anonymous methods of the Delphi API are interfaces in C++Builder.
class TRoomSelector : public TCppInterfacedObject<TTMSFNCWebSocketSendToCallBack>
{
public:
  TRoomSelector(const System::UnicodeString ARoom) : FRoom(ARoom) {}
  bool __fastcall Invoke(TTMSFNCWebSocketServerConnection *AConnection)
  {
    return (AConnection->UserData != nullptr) &&
      (static_cast<TChatUser*>(AConnection->UserData)->Room == FRoom);
  }
private:
  System::UnicodeString FRoom;
};

class TUserNameSelector : public TCppInterfacedObject<TTMSFNCWebSocketSendToCallBack>
{
public:
  TUserNameSelector(const System::UnicodeString AUserName) : FUserName(AUserName) {}
  bool __fastcall Invoke(TTMSFNCWebSocketServerConnection *AConnection)
  {
    return (AConnection->UserData != nullptr) &&
      SameText(static_cast<TChatUser*>(AConnection->UserData)->UserName, FUserName);
  }
private:
  System::UnicodeString FUserName;
};

void __fastcall TForm1::SendToRoom(const System::UnicodeString ARoom, const System::UnicodeString AMessage)
{
  // Large room updates go out in frames of at most 4 KB.
  TMSFNCWebSocketServer1->SplitMessage = true;
  TMSFNCWebSocketServer1->FrameSize = 4096;

  TMSFNCWebSocketServer1->SendMessageTo(AMessage,
    _di_TTMSFNCWebSocketSendToCallBack(new TRoomSelector(ARoom)));
}

void __fastcall TForm1::KickUser(const System::UnicodeString AUserName)
{
  // Sends a close frame to every client the selector picks.
  TMSFNCWebSocketServer1->DisconnectClient(
    _di_TTMSFNCWebSocketSendToCallBack(new TUserNameSelector(AUserName)));
}

Pitfalls

  • The selector runs for every client on every send. Keep it cheap; look up data you stored on the connection rather than doing I/O.
  • Sends skip clients that are still connecting. A client is only reachable after OnHandshakeResponseSent.
  • Do not store connection references. A connection object is freed after its client disconnects; route with selectors over the live connections instead.
  • Set FrameSize when splitting. Its default is 0; give it a real size whenever SplitMessage is True.
  • Use the binary methods for binary payloads. Encoding files as text wastes bandwidth; BroadcastData and SendDataTo send them as they are.

See also