Receiving real-time data from heating or cooling equipment is a must for many customers of HVAC businesses (Heating, Ventilation, and Air Conditioning). A manufacturing plant may need to monitor the cooling units on its production line carefully to ensure the right temperature is always available. Real-time data here doesn’t simply allow to avoid malfunctions, it also allows to predict temperatures and ensure that no energy is wasted over time. You can achieve this transfer of real-time data from machine to control unit with an embedded web server library like Mongoose.

Server Notifications

Let’s talk about server notifications (or server data push). You have a source, data or event and a variable set of clients that are interested in receiving it with low latency. Low latency is key here. If you implement a solution that simply polls the server with GET or POST requests at a regular interval, then the latency is determined by how often poll is performed. Further, if latency requirements are tight and events are relatively rare, large amounts of resources can be wasted on polling the server when there are no events.

The second requirement is that the set of clients is variable. For a static set of clients, the server may simply issue GET or POST requests to the interested parties. But often the set of clients is not known upfront, or it may change often, so reconfiguring the server may become inconvenient.

How it's done

The solution we’ll discuss in this post is based on WebSocket, which is a two-way persistent session-oriented protocol that is backward-compatible with HTTP at the session setup time. You can read more about it at the link above, here we’ll focus on the specifics of how it can be used to implement low latency server data push with Mongoose.

Mongoose supports WebSocket and does not need to be configured in any special way. Our main function is thus pretty standard:

int main(void) {
const char *listen_port = "8080";
struct mg_server *server;
const char *err;
server = mg_create_server(NULL, ev_handler);
err = mg_set_option(server, "listening_port", listen_port);
if (err != NULL) {
fprintf(stderr, "Error setting up listener on %s: %s\n", listen_port, err);
return 1;
}
mg_start_thread(data_producer, NULL);
printf("Listening on %s\n", listen_port);
while (1) {
mg_poll_server(server, 100);
}
return 0;
}

When client sends a WebSocket handshake request (the request to upgrade protocol from HTTP to WebSocket), Mongoose delivers a MG_WS_HANDSHAKE callback. At this point, as the server developer, you have an option to refuse the upgrade by responding with an HTTP error and returning MG_TRUE. If MG_FALSE is returned, Mongoose proceeds to complete the handshake and delivers a MG_WS_CONNECT callback. From this point on, the connection should use WebSocket framing. To facilitate that, Mongoose provides mg_websocket_write and mg_websocket_printf functions, which put the necessary framing around the data sent to the connection. Other than that, there isn’t much to do and our event handler function is pretty simple:

static int ev_handler(struct mg_connection conn, enum mg_event ev) {
switch (ev) {
case MG_AUTH:
return MG_TRUE; /
Authenticated. /
case MG_WS_HANDSHAKE:
return MG_FALSE; /
Let Mongoose complete the handshake. /
case MG_WS_CONNECT:
fprintf(stderr, "%s:%u joined\n", conn->remote_ip, conn->remote_port);
conn->connection_param = calloc(1, sizeof(struct conn_state));
mg_websocket_printf(conn, WEBSOCKET_OPCODE_TEXT, "Hi %p!\n", conn);
maybe_send_data(conn);
return MG_FALSE; /
Keep the connection open. /
case MG_POLL:
maybe_send_data(conn);
return MG_FALSE; /
Keep the connection open. */
case MG_CLOSE:
fprintf(stderr, "%s:%u went away\n", conn->remote_ip, conn->remote_port);
free(conn->connection_param);
conn->connection_param = NULL;
return MG_TRUE;
default:
return MG_FALSE;
}
}

The sequence of case statements here roughly reflects the lifetime of the connection: there is an authentication phase, the WebSocket handshake phase, followed by WebSocket connection establishment, then a number of MG_POLLs while the connection is alive, and eventually an MG_CLOSE when client disconnects.

We will now turn our attention to the data generation and sending part. As was mentioned in the beginning, in our example data generation is completely decoupled from serving and occurs in a separate thread, which runs the data_producer function:

void *data_producer(void *arg) {
(void) arg;
fprintf(stderr, "Data producer running\n");
srand(time(NULL));
while (1) {
pthread_mutex_lock(&s_data_lock);
snprintf(s_data, sizeof(s_data), "The lucky number is %d.", rand() % 100);
s_data_version++;
pthread_mutex_unlock(&s_data_lock);
sleep(1 + rand() % 10);
}
}

As you can see, this is a pretty simple data producer that generates a random number at random intervals. To signal that the data has been updated, its version is incremented. This way each connection is able to tell whether this version needs to be sent to the client.

There is a subtlety here that is worth highlighting: whenever you have two threads accessing the same data, there has to be synchronization around it. Because Mongoose’s event loop and data generation function run in different threads, we use a mutex to synchronize access to the data array and the version number.

Notice that it would also not be ok to iterate connections and push data straight from the data_producer function because that can interfere with handler’s access to connection state. Instead, we rely on regular MG_POLL events to deliver new data to the clients. maybe_send_data function, invoked from the MG_POLL case of the connection event handler, is responsible for that:

void maybe_send_data(struct mg_connection *conn) {
struct conn_state *cs = (struct conn_state ) conn->connection_param;
if (cs == NULL) return; /
Not connected yet. */
pthread_mutex_lock(&s_data_lock);
if (cs->data_version != s_data_version) {
mg_websocket_printf(conn, WEBSOCKET_OPCODE_TEXT, "%s\n", s_data);
cs->data_version = s_data_version;
}
pthread_mutex_unlock(&s_data_lock);
}

As you can see, it’s called maybe_send_data because invoking it does not always results in data being sent to the client: only if the global data has been updated (i.e. if the version that was last sent to the client is different). Remember, that there is no set period of MG_POLL even delivery, but the upper limit is the sleep time passed to mg_poll_server.

In our example, it can take up to 100 ms for change in data to be noticed, but typically that will be only a fraction of that.

Test it yourself

Well, we hope this example was useful. You can download the source code here (look for examples/server_data_push/server_data_push.c) and as a test WebSocket client you can use wscat.

To contact: send us a message or ask on the developer forum.