WebSocket Broadcasting
Broadcasting allows sending messages to groups of clients in real time. This is the foundation for creating chats, notifications, and live updates.
Concept
- Channel — a named group of WebSocket connections
- Client — a connection subscribed to a channel
- Identification structure — data for filtering recipients
- Response handler — function for forming the message
Configuration
"websockets": {
"routes": {
"/channel-join": {
"GET": { "file": "handlers/libws.so", "function": "channel_join" }
},
"/channel-leave": {
"GET": { "file": "handlers/libws.so", "function": "channel_leave" }
},
"/channel-send": {
"POST": { "file": "handlers/libws.so", "function": "channel_send" }
}
}
}Basic usage
Joining a channel
#include "websockets.h"
#include "broadcast.h"
void channel_join(wsctx_t* ctx) {
broadcast_add("notifications", ctx->request->connection, NULL, broadcast_send_text);
ctx->response->send_text(ctx->response, "Joined channel");
}Leaving a channel
void channel_leave(wsctx_t* ctx) {
broadcast_remove("notifications", ctx->request->connection);
ctx->response->send_text(ctx->response, "Left channel");
}Sending to everyone in a channel
void channel_send(wsctx_t* ctx) {
char* message = websocketsrequest_payload(ctx->request->protocol);
if (!message) {
ctx->response->send_text(ctx->response, "No message");
return;
}
// Send to everyone except the sender
broadcast_send_all("notifications", ctx->request->connection, message, strlen(message));
free(message);
ctx->response->send_text(ctx->response, "Message sent");
}Response handler
The handler determines how a subscriber receives the message. It is a user-defined function with the signature void(*)(response_t*, const char*, size_t), called separately for each recipient. Typical implementations send a text or binary frame:
void broadcast_send_text(response_t* response, const char* data, size_t size) {
websocketsresponse_t* ws_response = (websocketsresponse_t*)response;
ws_response->send_textn(ws_response, data, size);
}
void broadcast_send_binary(response_t* response, const char* data, size_t size) {
websocketsresponse_t* ws_response = (websocketsresponse_t*)response;
ws_response->send_binaryn(ws_response, data, size);
}Recipient filtering
To send messages to specific clients, use an identification structure. It inherits from broadcast_id_t (always as the first field) and plays two roles: when subscribing a client (broadcast_add) and as a filter when sending (broadcast_send).
Identifier lifetime
These roles have different lifetimes:
- the identifier from
broadcast_addis freed through itsfreehandler when the subscriber is removed — on client disconnect orbroadcast_remove; - the filter identifier from
broadcast_sendis freed by the function itself throughfreeimmediately after distribution — do not free it manually and do not reuse it after the call.
Defining the structure
// broadcasting/chat_broadcast.h
typedef struct chat_broadcast_id {
broadcast_id_t base; // Required base field
int user_id;
int room_id;
} chat_broadcast_id_t;
chat_broadcast_id_t* chat_broadcast_id_create(int user_id, int room_id);
void chat_broadcast_id_free(void* id);// broadcasting/chat_broadcast.c
#include "chat_broadcast.h"
chat_broadcast_id_t* chat_broadcast_id_create(int user_id, int room_id) {
chat_broadcast_id_t* id = malloc(sizeof(chat_broadcast_id_t));
if (!id) return NULL;
id->base.free = chat_broadcast_id_free;
id->user_id = user_id;
id->room_id = room_id;
return id;
}
void chat_broadcast_id_free(void* ptr) {
if (ptr) free(ptr);
}Connecting with identification
#include "websockets.h"
void join_room(wsctx_t* ctx) {
websockets_protocol_resource_t* protocol = (websockets_protocol_resource_t*)ctx->request->protocol;
int ok = 0;
const char* room_id_str = protocol->get_query(protocol, "room", &ok);
const char* user_id_str = protocol->get_query(protocol, "user", &ok);
if (!ok || !room_id_str || !user_id_str) {
ctx->response->send_text(ctx->response, "Missing parameters");
return;
}
int room_id = atoi(room_id_str);
int user_id = atoi(user_id_str);
chat_broadcast_id_t* id = chat_broadcast_id_create(user_id, room_id);
if (!id) {
ctx->response->send_text(ctx->response, "Memory error");
return;
}
broadcast_add("chat", ctx->request->connection, (broadcast_id_t*)id, broadcast_send_text);
ctx->response->send_text(ctx->response, "Joined room");
}Comparison function
int compare_by_room(void* source, void* target) {
chat_broadcast_id_t* src = (chat_broadcast_id_t*)source;
chat_broadcast_id_t* tgt = (chat_broadcast_id_t*)target;
return src->room_id == tgt->room_id;
}
int compare_by_user(void* source, void* target) {
chat_broadcast_id_t* src = (chat_broadcast_id_t*)source;
chat_broadcast_id_t* tgt = (chat_broadcast_id_t*)target;
return src->user_id == tgt->user_id;
}Sending with filtering
#include "websockets.h"
void send_to_room(wsctx_t* ctx) {
websockets_protocol_resource_t* protocol = (websockets_protocol_resource_t*)ctx->request->protocol;
int ok = 0;
const char* room_id_str = protocol->get_query(protocol, "room", &ok);
if (!ok) {
ctx->response->send_text(ctx->response, "Missing room parameter");
return;
}
char* message = websocketsrequest_payload(ctx->request->protocol);
if (!message) {
ctx->response->send_text(ctx->response, "No message");
return;
}
// Create filter for the room.
// Note: broadcast_send frees filter via its free handler — do not free it yourself.
chat_broadcast_id_t* filter = chat_broadcast_id_create(0, atoi(room_id_str));
// Send only to clients in the specified room
broadcast_send("chat", ctx->request->connection, message, strlen(message),
(broadcast_id_t*)filter, compare_by_room);
free(message);
ctx->response->send_text(ctx->response, "Message sent to room");
}Broadcasting API
| Function | Description |
|---|---|
broadcast_add(channel, conn, id, handler) | Subscribe a connection to a channel; the channel is created automatically. Returns 1 on success, 0 on error or if the connection is already subscribed |
broadcast_remove(channel, conn) | Unsubscribe a connection from a channel; an empty channel is destroyed |
broadcast_clear(conn) | Unsubscribe a connection from all channels at once (called automatically when the connection closes) |
broadcast_send_all(channel, sender, data, len) | Send to all channel subscribers except the sender |
broadcast_send(channel, sender, data, len, filter, cmp) | Send to subscribers filtered by filter. If filter or cmp is NULL, the message goes to all (except the sender). The function takes ownership of filter and frees it |
Filtering applies only to recipients
The comparison runs between the subscriber's identifier (the first argument of cmp) and the passed filter (the second argument). Every subscriber for which cmp returns a non-zero value receives the message; the sender is always excluded.
Example: Chat room
Server
#include "websockets.h"
#include "broadcast.h"
#include "json.h"
void ws_join_chat(wsctx_t* ctx) {
websockets_protocol_resource_t* protocol = (websockets_protocol_resource_t*)ctx->request->protocol;
int ok = 0;
const char* username = protocol->get_query(protocol, "username", &ok);
if (!ok) {
ctx->response->send_text(ctx->response, "Missing username parameter");
return;
}
chat_user_t* user = chat_user_create(username);
broadcast_add("global_chat", ctx->request->connection,
(broadcast_id_t*)user, broadcast_send_text);
// Join notification
json_doc_t* doc = json_init();
json_token_t* root = json_create_object(doc);
json_object_set(root, "type", json_create_string(doc, "user_joined"));
json_object_set(root, "username", json_create_string(doc, username));
const char* json = json_stringify(doc);
broadcast_send_all("global_chat", ctx->request->connection, json, strlen(json));
json_free(doc);
ctx->response->send_text(ctx->response, "Welcome to chat");
}
void ws_chat_message(wsctx_t* ctx) {
char* message = websocketsrequest_payload(ctx->request->protocol);
if (!message) return;
broadcast_send_all("global_chat", ctx->request->connection, message, strlen(message));
free(message);
}Client (JavaScript)
const ws = new WebSocket("wss://example.com/wss", "resource");
ws.onopen = () => {
ws.send("GET /join-chat?username=Alice");
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log("Message:", data);
};
// Send message
function sendMessage(text) {
ws.send(`POST /chat-message ${JSON.stringify({ text })}`);
}Important
- The identifier from
broadcast_addis freed automatically when the subscriber is removed (client disconnect orbroadcast_remove), while the filter identifier frombroadcast_sendis freed bybroadcast_senditself right after distribution - A connection can subscribe to a channel only once — a repeated
broadcast_addreturns0and does not add a duplicate - On disconnect, a connection is unsubscribed from all channels via
broadcast_clear, so no explicit cleanup is needed in a close handler - The sender never receives its own message
- Channels are created automatically on the first
broadcast_addand destroyed automatically when the last subscriber leaves